Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 17b194dd30 | |||
| e1807167d2 | |||
| 8645342666 | |||
| 5e4d78b83d | |||
| ef84eb9a9d | |||
| 3688abb259 | |||
| 8e766b540e | |||
| abcb327278 | |||
| 5d22544df1 | |||
| 0898315910 | |||
| eee067f0ca | |||
| 2941b14b38 | |||
| 888bb2d87b | |||
| 029812c6a4 | |||
| 6a60d67b29 | |||
| b0350abfbe | |||
| ec83b97c25 | |||
| 86b8968dce | |||
| 90a96b4125 | |||
| 64295b689b | |||
| 75198ed00f | |||
| 01e8226c2b | |||
| 4d5090d76c | |||
| 02c6cb680b | |||
| 86f37dde2d | |||
| 9c793bad1b | |||
| ba7b757541 | |||
| edb7eabd18 | |||
| d8349a0936 | |||
| 60867af69c | |||
| 35ba568d97 | |||
| f06968eb46 | |||
| 075f29cde1 | |||
| e8a655d54c | |||
| 6ba49127aa | |||
| 2084acb078 | |||
| d1e45c7686 | |||
| b4574f9df1 | |||
| 8dfe7e8b4a | |||
| bdf3ba2483 | |||
| b98d1f65d3 | |||
| 787f90b5cf | |||
| d2b77d4553 | |||
| eb5b8fbf26 | |||
| 1bb91820d0 | |||
| 9dd4b5f067 | |||
| 5c34816359 | |||
| 5eb2f5220b | |||
| 318075d686 | |||
| ba2e3b9545 | |||
| 508eb8b981 | |||
| cc3d0a8b07 | |||
| 63d486f48d | |||
| 42549eb116 | |||
| 894415e3ac | |||
| e4a1fcfd25 | |||
| 4e80d525db | |||
| 8d30480bdc | |||
| 4cf8543c82 | |||
| bffdf0ec6c |
@@ -0,0 +1,20 @@
|
|||||||
|
# Нормализовать окончания строк: хранить LF в репозитории
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Явно указать текстовые файлы — Git будет применять конвертацию
|
||||||
|
*.go text
|
||||||
|
*.mod text
|
||||||
|
*.sum text
|
||||||
|
*.txt text
|
||||||
|
*.md text
|
||||||
|
*.json text
|
||||||
|
*.yml text
|
||||||
|
*.yaml text
|
||||||
|
|
||||||
|
# Бинарные файлы — не трогать окончания строк
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.zip binary
|
||||||
|
*.exe binary
|
||||||
@@ -1,23 +1,52 @@
|
|||||||
name: Deploy
|
name: Deploy
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches: [main]
|
||||||
- main
|
paths:
|
||||||
|
- 'main_dc/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Pull latest main
|
- uses: actions/checkout@v4
|
||||||
run: |
|
|
||||||
cd /home/gaziz/artefacts/tp
|
|
||||||
git pull gitea main
|
|
||||||
|
|
||||||
- name: Rebuild changed services
|
- name: Deploy
|
||||||
run: |
|
run: |
|
||||||
cd /home/gaziz/artefacts/tp/main_dc
|
cd /home/gaziz/artefacts/tp/main_dc
|
||||||
docker compose build
|
git pull origin main
|
||||||
docker compose up -d --remove-orphans
|
|
||||||
|
|
||||||
- name: Cleanup old images
|
# Если изменился sites.yml — генерируем конфиги
|
||||||
run: docker image prune -f
|
if git diff --name-only HEAD~1 HEAD | grep -q 'main_dc/sites.yml'; then
|
||||||
|
echo "→ sites.yml changed, generating configs..."
|
||||||
|
bash generate-configs.sh
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Авто-детект и пересборка изменённых сервисов
|
||||||
|
echo "→ Detecting changed services..."
|
||||||
|
CHANGED=$(git diff --name-only HEAD~1 HEAD | grep -oP 'main_dc/\K[^/]+' | sort -u)
|
||||||
|
for svc in $CHANGED; do
|
||||||
|
svc_name="$svc"
|
||||||
|
# маппинг директорий на имена compose-сервисов
|
||||||
|
case "$svc" in
|
||||||
|
BB) svc_name="api_bb" ;;
|
||||||
|
valitovgaziz) svc_name="valitovgaziz" ;;
|
||||||
|
nginx|certbot|backup|gitea) svc_name="$svc" ;;
|
||||||
|
api_bb|api_yal|analytics|db) svc_name="$svc" ;;
|
||||||
|
yalarba) svc_name="yalarba" ;;
|
||||||
|
*) svc_name="" ;;
|
||||||
|
esac
|
||||||
|
if [ -n "$svc_name" ] && grep -q "^ $svc_name:" docker-compose.yml; then
|
||||||
|
echo " → Rebuilding $svc_name..."
|
||||||
|
make stop_$svc_name build_$svc_name start_$svc_name || \
|
||||||
|
make stop_$svc build_$svc start_$svc 2>/dev/null || \
|
||||||
|
true
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Nginx всегда перезапускаем если изменились конфиги
|
||||||
|
if echo "$CHANGED" | grep -q 'nginx\|sites.yml'; then
|
||||||
|
echo " → Reloading nginx..."
|
||||||
|
docker compose exec -T nginx nginx -s reload 2>/dev/null || \
|
||||||
|
docker compose restart nginx
|
||||||
|
fi
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
name: Mirror to GitLab
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 */6 * * *'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
mirror:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Clone and push mirror
|
|
||||||
run: |
|
|
||||||
git clone --mirror https://git.yalarba.ru/valitovgaziz/tp.git ./mirror
|
|
||||||
cd ./mirror
|
|
||||||
git remote add gitlab https://oauth2:${{ secrets.GITLAB_MIRROR_TOKEN }}@gitlab.com/yalarba/tp.git
|
|
||||||
git push --mirror gitlab --force
|
|
||||||
@@ -35,5 +35,3 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.vscode/extensions.json
|
.vscode/extensions.json
|
||||||
.gigacode/*
|
|
||||||
.gigacode
|
|
||||||
@@ -1,29 +1,81 @@
|
|||||||
# tp — YalArba Platform
|
# AGENTS.md
|
||||||
|
|
||||||
## Git Remotes
|
## Repo overview
|
||||||
|
|
||||||
| Remote | URL |
|
Docker Compose hosting for 4 websites (yalarba.ru, begushiybashkir.ru, easysite102.ru, valitovgaziz.ru).
|
||||||
|---------|--------------------------------------------------|
|
All infrastructure lives under `main_dc/`. Root `package.json` is vestigial — do not use it.
|
||||||
| gitea | `https://git.yalarba.ru/valitovgaziz/tp.git` |
|
|
||||||
| origin | `git@github.com:valitovgaziz/tp.git` |
|
|
||||||
| gitlab | `https://gitlab.com/yalarba/tp.git` (mirror) |
|
|
||||||
|
|
||||||
## Servers
|
## Directory structure
|
||||||
|
|
||||||
| Host | Role | Path |
|
|
||||||
|----------------|-------------------|-------------------------------|
|
|
||||||
| YalArbaServer | Production | `/home/gaziz/artefacts/tp` |
|
|
||||||
| YalArbaServer | Gitea + Runner | Docker containers |
|
|
||||||
|
|
||||||
## Server Config
|
|
||||||
|
|
||||||
Ветки main и develop трекают remote `gitea`.
|
|
||||||
На сервере должно быть: `git config pull.rebase false` (иначе rebase ломается на конфликтах).
|
|
||||||
Workflow использует `git pull gitea main`.
|
|
||||||
|
|
||||||
## Branches
|
|
||||||
|
|
||||||
```
|
```
|
||||||
develop → разработка (все фичи здесь)
|
main_dc/
|
||||||
main → production (merge из develop при деплое)
|
docker-compose.yml -- single compose file orchestrating everything
|
||||||
|
Makefile -- the primary dev/ops interface; use `make` not raw docker
|
||||||
|
.env -- shared env: domains, email, api_es port
|
||||||
|
BB/api_bb/ -- Go REST API (GORM+Chi), port 7777, DB: db_bb (5433)
|
||||||
|
BB/bbvue/ -- Vue 3 + Vite frontend for begushiybashkir.ru
|
||||||
|
yalarba/api_tp/ -- Go REST API (GORM+Chi), port 8888, DB: db (5432)
|
||||||
|
yalarba/api_es/ -- Go REST API (GORM+Chi), port 8088, DB: db (5432)
|
||||||
|
yalarba/api_yal/ -- Go REST API (GORM+Chi), port 8787, DB: db (5432)
|
||||||
|
yalarba/easySite/ -- Nuxt 4 SPA for easysite102.ru
|
||||||
|
yalarba/yalarba-nuxt/ -- Nuxt 4 SPA for yalarba.ru
|
||||||
|
valitovgaziz/analytics/ -- Node.js (Express) analytics server, port 9999
|
||||||
|
valitovgaziz/html/ -- static HTML for valitovgaziz.ru
|
||||||
|
nginx/ -- nginx with automatic HTTP↔HTTPS switching
|
||||||
|
certbot/ -- Let's Encrypt cert management
|
||||||
|
stubSite/ -- placeholder site while building
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Developer commands (always run from `main_dc/`)
|
||||||
|
|
||||||
|
| Command | What it does |
|
||||||
|
|---|---|
|
||||||
|
| `make all` | Full cycle: down → git pull → build --no-cache → up -d → watch |
|
||||||
|
| `make <svc>` | Full cycle for one service, e.g. `make api_bb`, `make nginx`, `make es`, `make analytics` |
|
||||||
|
| `make bbvue` | Rebuild Vue frontend (calls `npm run build` in `BB/bbvue/`) |
|
||||||
|
| `make vue_bb` | git pull + npm cache clean + bbvue build + watch |
|
||||||
|
| `make wn` | `watch -n2 docker ps` — monitor containers |
|
||||||
|
| `make bb_db` | `psql -U postgres -d bb_db` inside db_bb container |
|
||||||
|
|
||||||
|
All `build_*` targets use `--no-cache`.
|
||||||
|
All full-cycle targets follow: `stop_<svc> → git → build_<svc> → start_<svc> → wn`.
|
||||||
|
|
||||||
|
## Frontend dev (outside compose)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd main_dc/BB/bbvue && npm run dev # Vite dev server
|
||||||
|
cd main_dc/BB/bbvue && npm run lint # ESLint --fix
|
||||||
|
cd main_dc/BB/bbvue && npm run format # Prettier --write src/
|
||||||
|
|
||||||
|
# serv_spa удалён — yalarba работает через yalarba-nuxt (Nuxt SSR)
|
||||||
|
|
||||||
|
cd main_dc/yalarba/easySite && npm run dev # Nuxt dev
|
||||||
|
cd main_dc/yalarba/easySite && npm run build # Nuxt build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service quirks
|
||||||
|
|
||||||
|
- **Nginx SSL**: `switch-config.sh` is all-or-nothing — HTTPS only activates when *every* domain has a cert. Until then, SSL port redirects back to HTTP.
|
||||||
|
- **`yalarba/serv_spa/`**: удалён — был legacy Vue SPA, не использовался.
|
||||||
|
- **`api_yal`** is the only container that runs as non-root. Runs on port 8787.
|
||||||
|
- **`api_es`** port is configurable via `API_ES_APP_PORT` in `.env` (default 8088). All other API ports are hardcoded.
|
||||||
|
- **Databases**: `db` (port 5432) is shared between api_tp, api_es, api_yal. `db_bb` (port 5433) is dedicated to api_bb.
|
||||||
|
- **GORM auto-migration**: All Go APIs use GORM auto-migrate at startup — no manual migration tooling.
|
||||||
|
- **Keycloak** referenced in Makefile targets but absent from docker-compose.yml — likely not deployed.
|
||||||
|
- **`api_yal/testrunner`**: standalone Go test runner binary (not containerized), for running integration test suites.
|
||||||
|
|
||||||
|
## Docs convention
|
||||||
|
|
||||||
|
READMEs and documentation are primarily in Russian. See `documentation/` for Makefile, Docker, restart, and LLM info docs.
|
||||||
|
|
||||||
|
## Server (YalArbaServer)
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| IP | `94.41.23.97` |
|
||||||
|
| User | `gaziz` |
|
||||||
|
| SSH key | `~/.ssh/id_ed25519` (local) |
|
||||||
|
| SSH | `ssh gaziz@94.41.23.97` |
|
||||||
|
| Root password | `sudoowneranduser` |
|
||||||
|
| User `gaziz` password | `sudoowneranduser` |
|
||||||
|
| Repo path | `/home/gaziz/artefacts/tp/main_dc` |
|
||||||
|
|||||||
@@ -1,8 +1,41 @@
|
|||||||
# Hosting by ValitovGaziz's team on docker compose
|
# Hosting by ValitovGaziz's team on docker compose
|
||||||
|
|
||||||
## for yalarba.ru \&\& begushiybashkir.ru
|
## for yalarba.ru && begushiybashkir.ru
|
||||||
|
|
||||||
|
В этом репозитроии собранны все сервисы для работы приложений YalArba. Тае же есть отдельный сайт для ValitovGaziz.ru && BegushiyBashkir.ru. Будет много дополнений и развития поэтому буду стараться поддерживать документацию в валидном состоянии.
|
||||||
|
|
||||||
|
### BackEnd api_bb
|
||||||
|
|
||||||
|
REST API on golang. Frameworks gorm with PostgresQL. Migration on automigrate with gorm into REST API server.
|
||||||
|
|
||||||
|
### FrontEnd vue_bb
|
||||||
|
|
||||||
|
Vue3.js, pinia, axios.
|
||||||
|
|
||||||
|
### product owner Zagir Загир тренер FOR
|
||||||
|
|
||||||
|
### BackEnd api_es
|
||||||
|
|
||||||
|
EasySite102.ru REST API on Golang. Frameworks gorm with PostgresQL, automigraion with gorm and Chi rounting.
|
||||||
|
|
||||||
|
### FrontEnd nuxt_es
|
||||||
|
|
||||||
|
SPA on nuxt.js (vue3.js, axios, pinia).
|
||||||
|
|
||||||
|
### BackEnd api_ya
|
||||||
|
|
||||||
|
yalarba.ru/api/ REST API on Golang. Frameworks gorm with PostgresQL, automigraion with gorm and Chi rounting.
|
||||||
|
|
||||||
|
### FrontEnd vue_ya
|
||||||
|
|
||||||
|
yalarba.ru on vue3.js (pinia) need to redevelop on nuxt.js
|
||||||
|
|
||||||
|
Ближайшие задачи
|
||||||
|
|
||||||
|
|
||||||
|
!!! Need documentation for working REST API and working SPA aps
|
||||||
|
|
||||||
# документация находиться в директории documentation в корне проекта
|
1. Написать документацию к api всех сайтов
|
||||||
|
2. Доработать begushiybashkir.ru && easysite102.rr
|
||||||
|
|
||||||
|
# документация находится в директории documentation в корне проекта
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# LLM Information
|
||||||
|
|
||||||
|
## Current LLM Configuration
|
||||||
|
|
||||||
|
Based on system analysis conducted on 2026-04-16, the following LLM (Large Language Model) is being used:
|
||||||
|
|
||||||
|
### Model Details
|
||||||
|
- **Model Name**: `sourcecraft_model`
|
||||||
|
- **Current Mode**: Architect (`architect`)
|
||||||
|
- **Mode Display Name**: 🏗️ Architect
|
||||||
|
- **System**: SourceCraft Code Assistant Agent
|
||||||
|
|
||||||
|
### Environment Context
|
||||||
|
- **Operating System**: Windows 11
|
||||||
|
- **Default Shell**: C:\WINDOWS\system32\cmd.exe
|
||||||
|
- **Workspace Directory**: d:/artifacts/tp
|
||||||
|
- **User Time Zone**: Asia/Yekaterinburg (UTC+5:00)
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
The SourceCraft Code Assistant Agent is an experienced technical leader with capabilities including:
|
||||||
|
- Information gathering and context analysis
|
||||||
|
- Detailed planning and task breakdown
|
||||||
|
- Code writing and modification
|
||||||
|
- System operations and command execution
|
||||||
|
- File management and editing
|
||||||
|
- Web development and debugging
|
||||||
|
|
||||||
|
### Modes Available
|
||||||
|
The system supports multiple specialized modes:
|
||||||
|
1. **🏗️ Architect** (current) - Planning, design, and strategy
|
||||||
|
2. **💻 Code** - Code writing, modification, and refactoring
|
||||||
|
3. **❓ Ask** - Explanations, documentation, and technical questions
|
||||||
|
4. **🪲 Debug** - Troubleshooting and error diagnosis
|
||||||
|
5. **🪃 Orchestrator** - Complex multi-step project coordination
|
||||||
|
|
||||||
|
### Project Context
|
||||||
|
The current workspace contains a Docker-based hosting solution for multiple websites:
|
||||||
|
- yalarba.ru
|
||||||
|
- begushiybashkir.ru
|
||||||
|
- easysite102.ru
|
||||||
|
- valitovgaziz.ru
|
||||||
|
|
||||||
|
The project includes backend APIs in Go, frontend applications in Vue.js/Nuxt.js, and various supporting services (nginx, certbot).
|
||||||
|
|
||||||
|
### Analysis Method
|
||||||
|
This information was gathered through:
|
||||||
|
1. System environment details inspection
|
||||||
|
2. File structure analysis
|
||||||
|
3. Configuration file review (package.json, README.md)
|
||||||
|
4. Current mode and model identification from system metadata
|
||||||
|
|
||||||
|
### Last Updated
|
||||||
|
2026-04-16T15:25:15.218Z
|
||||||
@@ -1,16 +1,13 @@
|
|||||||
#CERTBOT NGINX VARIABLES
|
|
||||||
|
|
||||||
EMAIL=valitovgaziz@yandex.ru
|
EMAIL=valitovgaziz@yandex.ru
|
||||||
DOMAINS_yalarba=yalarba.ru,www.yalarba.ru
|
#CERTBOT NGINX VARIABLES — авто-сгенерировано, не редактировать вручную
|
||||||
DOMAINS_valitovgaziz=valitovgaziz.ru,www.valitovgaziz.ru
|
|
||||||
DOMAINS_easysite102=easysite102.ru,www.easysite102.ru
|
|
||||||
DOMAINS_begushiybashkir=xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
|
|
||||||
DOMAINS_begushiybashkir_latin=begushiybashkir.ru,www.begushiybashkir.ru
|
|
||||||
ALL_DOMAINS=yalarba.ru,www.yalarba.ru,valitovgaziz.ru,www.valitovgaziz.ru,easysite102.ru,www.easysite102.ru,begushiybashkir.ru,www.begushiybashkir.ru,xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
|
ALL_DOMAINS=yalarba.ru,www.yalarba.ru,valitovgaziz.ru,www.valitovgaziz.ru,easysite102.ru,www.easysite102.ru,begushiybashkir.ru,www.begushiybashkir.ru,xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||||
|
DOMAINS_begushiybashkir=begushiybashkir.ru,www.begushiybashkir.ru
|
||||||
|
DOMAINS_begushiybashkir_idn=xn--80abahjtcfl5d0a8di.xn--p1ai,www.xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||||
|
DOMAINS_easysite102=easysite102.ru,www.easysite102.ru
|
||||||
|
DOMAINS_valitovgaziz=valitovgaziz.ru,www.valitovgaziz.ru
|
||||||
|
DOMAINS_yalarba=yalarba.ru,www.yalarba.ru
|
||||||
|
|
||||||
# keycloak
|
# keycloak
|
||||||
KEYCLOAK_ADMIN_PASSWORD=your_secure_password
|
KEYCLOAK_ADMIN_PASSWORD=your_secure_password
|
||||||
KEYCLOAK_DB_PASSWORD=your_secure_db_password
|
KEYCLOAK_DB_PASSWORD=your_secure_db_password
|
||||||
|
|
||||||
# API_ES port
|
|
||||||
API_ES_APP_PORT=8088
|
|
||||||
@@ -3,13 +3,12 @@ FROM golang:1.26.0-alpine
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Копируем go.mod и go.sum
|
# Копируем весь исходный код
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# Копируем исходный код
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Скачиваем зависимости
|
||||||
|
RUN go mod tidy && go mod download
|
||||||
|
|
||||||
# Компилируем БЕЗ CGO
|
# Компилируем БЕЗ CGO
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
|
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ require (
|
|||||||
github.com/go-chi/chi/v5 v5.2.3
|
github.com/go-chi/chi/v5 v5.2.3
|
||||||
github.com/go-chi/cors v1.2.2
|
github.com/go-chi/cors v1.2.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||||
|
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||||
golang.org/x/crypto v0.43.0
|
golang.org/x/crypto v0.43.0
|
||||||
gorm.io/driver/postgres v1.6.0
|
gorm.io/driver/postgres v1.6.0
|
||||||
gorm.io/gorm v1.31.0
|
gorm.io/gorm v1.31.0
|
||||||
@@ -15,8 +16,12 @@ require (
|
|||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||||
|
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.9 // indirect
|
||||||
github.com/stretchr/testify v1.11.1 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/sys v0.37.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ func (a *App) Initialize() error {
|
|||||||
|
|
||||||
// Инициализация базы данных
|
// Инициализация базы данных
|
||||||
dbConfig := &database.Config{
|
dbConfig := &database.Config{
|
||||||
URL: a.cfg.DatabaseURL,
|
URL: a.cfg.DatabaseURL,
|
||||||
|
Schema: a.cfg.DBSchema,
|
||||||
}
|
}
|
||||||
a.db = database.NewDatabase(dbConfig)
|
a.db = database.NewDatabase(dbConfig)
|
||||||
|
|
||||||
@@ -46,11 +47,6 @@ func (a *App) Initialize() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Выполнение миграций
|
|
||||||
if err := a.db.Migrate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Настройка роутера
|
// Настройка роутера
|
||||||
router := routes.SetupRouter(a.db.DB, a.cfg)
|
router := routes.SetupRouter(a.db.DB, a.cfg)
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
type Config struct {
|
type Config struct {
|
||||||
Port string
|
Port string
|
||||||
DatabaseURL string
|
DatabaseURL string
|
||||||
|
DBSchema string
|
||||||
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
|
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
|
||||||
JWTSecret string `env:"JWT_SECRET,required"`
|
JWTSecret string `env:"JWT_SECRET,required"`
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ func Load() *Config {
|
|||||||
return &Config{
|
return &Config{
|
||||||
Port: port,
|
Port: port,
|
||||||
DatabaseURL: databaseURL,
|
DatabaseURL: databaseURL,
|
||||||
|
DBSchema: getEnv("DB_SCHEMA", "public"),
|
||||||
JWTSecret: jwtSecret,
|
JWTSecret: jwtSecret,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"api_bb/migrations"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang-migrate/migrate/v4"
|
||||||
|
migratepg "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||||
|
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/driver/postgres"
|
gormpg "gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"api_bb/pkg/logger"
|
"api_bb/pkg/logger"
|
||||||
@@ -17,26 +23,34 @@ type Database struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
URL string
|
URL string
|
||||||
|
Schema string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDatabase(cfg *Config) *Database {
|
func NewDatabase(cfg *Config) *Database {
|
||||||
|
if cfg.Schema == "" {
|
||||||
|
cfg.Schema = "public"
|
||||||
|
}
|
||||||
return &Database{
|
return &Database{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect устанавливает соединение с базой данных
|
|
||||||
func (d *Database) Connect() error {
|
func (d *Database) Connect() error {
|
||||||
zapLogger := logger.Get()
|
zapLogger := logger.Get()
|
||||||
|
|
||||||
// Логирование попытки подключения к БД
|
|
||||||
zapLogger.Info("attempting to connect to database",
|
zapLogger.Info("attempting to connect to database",
|
||||||
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
||||||
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
||||||
|
zap.String("schema", d.cfg.Schema),
|
||||||
)
|
)
|
||||||
|
|
||||||
db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{})
|
dsn := d.cfg.URL
|
||||||
|
if d.cfg.Schema != "public" {
|
||||||
|
dsn = dsn + fmt.Sprintf(" search_path=%s", d.cfg.Schema)
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gorm.Open(gormpg.Open(dsn), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zapLogger.Error("failed to connect to database",
|
zapLogger.Error("failed to connect to database",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
@@ -47,7 +61,21 @@ func (d *Database) Connect() error {
|
|||||||
|
|
||||||
d.DB = db
|
d.DB = db
|
||||||
|
|
||||||
// Логирование успешного подключения к БД
|
zapLogger.Info("Configure connection pool")
|
||||||
|
sqlDB, err := d.DB.DB()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxOpenConns(25)
|
||||||
|
sqlDB.SetMaxIdleConns(10)
|
||||||
|
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||||
|
|
||||||
|
zapLogger.Info("Run database migrations")
|
||||||
|
if err := d.runMigrations(sqlDB); err != nil {
|
||||||
|
return fmt.Errorf("failed to run migrations: %w", err)
|
||||||
|
}
|
||||||
|
zapLogger.Info("Migrations completed successfully")
|
||||||
|
|
||||||
zapLogger.Info("successfully connected to database",
|
zapLogger.Info("successfully connected to database",
|
||||||
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
||||||
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
||||||
@@ -56,7 +84,32 @@ func (d *Database) Connect() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping проверяет соединение с базой данных
|
func (d *Database) runMigrations(sqlDB *sql.DB) error {
|
||||||
|
zapLogger := logger.Get()
|
||||||
|
|
||||||
|
source, err := iofs.New(migrations.FS, ".")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migration source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver, err := migratepg.WithInstance(sqlDB, &migratepg.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create postgres driver: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create migrate instance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
||||||
|
zapLogger.Error("Migration error", zap.Error(err))
|
||||||
|
return fmt.Errorf("failed to apply migrations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Database) Ping() error {
|
func (d *Database) Ping() error {
|
||||||
zapLogger := logger.Get()
|
zapLogger := logger.Get()
|
||||||
|
|
||||||
@@ -75,7 +128,6 @@ func (d *Database) Ping() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close закрывает соединение с базой данных
|
|
||||||
func (d *Database) Close() error {
|
func (d *Database) Close() error {
|
||||||
zapLogger := logger.Get()
|
zapLogger := logger.Get()
|
||||||
|
|
||||||
@@ -99,11 +151,7 @@ func (d *Database) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Вспомогательные функции для работы с DSN
|
|
||||||
|
|
||||||
// ExtractHostFromDSN извлекает хост из DSN строки
|
|
||||||
func ExtractHostFromDSN(dsn string) string {
|
func ExtractHostFromDSN(dsn string) string {
|
||||||
// Простая реализация для PostgreSQL DSN
|
|
||||||
parts := strings.Split(dsn, " ")
|
parts := strings.Split(dsn, " ")
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if strings.HasPrefix(part, "host=") {
|
if strings.HasPrefix(part, "host=") {
|
||||||
@@ -113,9 +161,7 @@ func ExtractHostFromDSN(dsn string) string {
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
|
|
||||||
func ExtractDBNameFromDSN(dsn string) string {
|
func ExtractDBNameFromDSN(dsn string) string {
|
||||||
// Простая реализация для PostgreSQL DSN
|
|
||||||
parts := strings.Split(dsn, " ")
|
parts := strings.Split(dsn, " ")
|
||||||
for _, part := range parts {
|
for _, part := range parts {
|
||||||
if strings.HasPrefix(part, "dbname=") {
|
if strings.HasPrefix(part, "dbname=") {
|
||||||
@@ -125,9 +171,7 @@ func ExtractDBNameFromDSN(dsn string) string {
|
|||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
|
|
||||||
func MaskPassword(dsn string) string {
|
func MaskPassword(dsn string) string {
|
||||||
// Простая реализация - заменяет пароль на ***
|
|
||||||
parts := strings.Split(dsn, " ")
|
parts := strings.Split(dsn, " ")
|
||||||
for i, part := range parts {
|
for i, part := range parts {
|
||||||
if strings.HasPrefix(part, "password=") {
|
if strings.HasPrefix(part, "password=") {
|
||||||
@@ -136,4 +180,4 @@ func MaskPassword(dsn string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return strings.Join(parts, " ")
|
return strings.Join(parts, " ")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go.uber.org/zap"
|
|
||||||
|
|
||||||
"api_bb/internal/models"
|
|
||||||
"api_bb/pkg/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Migrate выполняет автоматические миграции для всех моделей
|
|
||||||
func (d *Database) Migrate() error {
|
|
||||||
zapLogger := logger.Get()
|
|
||||||
|
|
||||||
zapLogger.Info("starting database migration")
|
|
||||||
|
|
||||||
// Список всех моделей для миграции
|
|
||||||
models := []interface{}{
|
|
||||||
&models.User{},
|
|
||||||
&models.News{},
|
|
||||||
&models.Comment{},
|
|
||||||
&models.Review{},
|
|
||||||
&models.UserStats{},
|
|
||||||
&models.Workout{},
|
|
||||||
&models.Achievement{},
|
|
||||||
&models.Event{},
|
|
||||||
&models.EventRegistration{},
|
|
||||||
&models.PersonalBest{},
|
|
||||||
&models.TrainingPlan{},
|
|
||||||
&models.EmailVerification{},
|
|
||||||
// Добавьте другие модели здесь
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, model := range models {
|
|
||||||
modelName := getModelName(model)
|
|
||||||
zapLogger.Debug("migrating model", zap.String("model", modelName))
|
|
||||||
|
|
||||||
if err := d.DB.AutoMigrate(model); err != nil {
|
|
||||||
zapLogger.Error("failed to migrate model",
|
|
||||||
zap.String("model", modelName),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zapLogger.Info("database migration completed successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MigrateModels выполняет миграции для конкретных моделей
|
|
||||||
func (d *Database) MigrateModels(models ...interface{}) error {
|
|
||||||
zapLogger := logger.Get()
|
|
||||||
|
|
||||||
zapLogger.Info("starting migration for specific models",
|
|
||||||
zap.Int("model_count", len(models)),
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, model := range models {
|
|
||||||
modelName := getModelName(model)
|
|
||||||
zapLogger.Debug("migrating model", zap.String("model", modelName))
|
|
||||||
|
|
||||||
if err := d.DB.AutoMigrate(model); err != nil {
|
|
||||||
zapLogger.Error("failed to migrate model",
|
|
||||||
zap.String("model", modelName),
|
|
||||||
zap.Error(err),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zapLogger.Info("models migration completed successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getModelName возвращает имя модели для логирования
|
|
||||||
func getModelName(model interface{}) string {
|
|
||||||
switch model.(type) {
|
|
||||||
case *models.User:
|
|
||||||
return "User"
|
|
||||||
case *models.News:
|
|
||||||
return "News"
|
|
||||||
case *models.Comment:
|
|
||||||
return "Comment"
|
|
||||||
case *models.Review:
|
|
||||||
return "Reviews"
|
|
||||||
case *models.UserStats:
|
|
||||||
return "Статистика Пользователя"
|
|
||||||
case *models.Workout:
|
|
||||||
return "Тренировки пользователя"
|
|
||||||
case *models.Achievement:
|
|
||||||
return "Достижения пользователя"
|
|
||||||
case *models.Event:
|
|
||||||
return "Событие"
|
|
||||||
case *models.EventRegistration:
|
|
||||||
return "Администрирование события"
|
|
||||||
case *models.PersonalBest:
|
|
||||||
return "Персональные достижения"
|
|
||||||
case *models.TrainingPlan:
|
|
||||||
return "Тренировочный план"
|
|
||||||
case *models.EmailVerification:
|
|
||||||
return "Верификация email"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
DROP TABLE IF EXISTS galleries CASCADE;
|
||||||
|
DROP TABLE IF EXISTS email_verifications CASCADE;
|
||||||
|
DROP TABLE IF EXISTS training_workouts CASCADE;
|
||||||
|
DROP TABLE IF EXISTS training_plans CASCADE;
|
||||||
|
DROP TABLE IF EXISTS personal_bests CASCADE;
|
||||||
|
DROP TABLE IF EXISTS event_registrations CASCADE;
|
||||||
|
DROP TABLE IF EXISTS events CASCADE;
|
||||||
|
DROP TABLE IF EXISTS achievements CASCADE;
|
||||||
|
DROP TABLE IF EXISTS workouts CASCADE;
|
||||||
|
DROP TABLE IF EXISTS user_stats CASCADE;
|
||||||
|
DROP TABLE IF EXISTS reviews CASCADE;
|
||||||
|
DROP TABLE IF EXISTS comments CASCADE;
|
||||||
|
DROP TABLE IF EXISTS news CASCADE;
|
||||||
|
DROP TABLE IF EXISTS users CASCADE;
|
||||||
@@ -0,0 +1,217 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
first_name VARCHAR(255) NOT NULL,
|
||||||
|
last_name VARCHAR(255) NOT NULL,
|
||||||
|
avatar VARCHAR(255),
|
||||||
|
phone VARCHAR(255),
|
||||||
|
experience VARCHAR(255),
|
||||||
|
goals VARCHAR(255),
|
||||||
|
newsletter BOOLEAN DEFAULT FALSE,
|
||||||
|
role VARCHAR(255) DEFAULT 'user',
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
email_verified BOOLEAN DEFAULT FALSE,
|
||||||
|
verified_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_users_deleted_at ON users(deleted_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS news (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
excerpt VARCHAR(500) NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
image VARCHAR(255),
|
||||||
|
category VARCHAR(20) NOT NULL,
|
||||||
|
views BIGINT DEFAULT 0,
|
||||||
|
author_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_news_deleted_at ON news(deleted_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS comments (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
news_id BIGINT NOT NULL,
|
||||||
|
author_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS reviews (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
|
rating BIGINT NOT NULL,
|
||||||
|
text TEXT NOT NULL,
|
||||||
|
achievement VARCHAR(255),
|
||||||
|
distance VARCHAR(50),
|
||||||
|
improvement VARCHAR(100),
|
||||||
|
trainings BIGINT DEFAULT 0,
|
||||||
|
verified BOOLEAN DEFAULT FALSE,
|
||||||
|
author_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_reviews_deleted_at ON reviews(deleted_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS user_stats (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
total_distance DECIMAL(10,2) DEFAULT 0,
|
||||||
|
total_time BIGINT DEFAULT 0,
|
||||||
|
avg_pace VARCHAR(20),
|
||||||
|
workouts_count BIGINT DEFAULT 0,
|
||||||
|
current_streak BIGINT DEFAULT 0,
|
||||||
|
longest_streak BIGINT DEFAULT 0,
|
||||||
|
weekly_distance DECIMAL(8,2) DEFAULT 0,
|
||||||
|
monthly_distance DECIMAL(8,2) DEFAULT 0,
|
||||||
|
best_5k VARCHAR(20),
|
||||||
|
best_10k VARCHAR(20),
|
||||||
|
best_half VARCHAR(20),
|
||||||
|
best_marathon VARCHAR(20),
|
||||||
|
last_workout TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_stats_user_id ON user_stats(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS workouts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL,
|
||||||
|
distance_km DECIMAL(5,2) NOT NULL,
|
||||||
|
duration_min BIGINT NOT NULL,
|
||||||
|
pace VARCHAR(20),
|
||||||
|
calories BIGINT DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
date TIMESTAMPTZ NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_workouts_user_id ON workouts(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_workouts_date ON workouts(date);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS achievements (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
result VARCHAR(100),
|
||||||
|
distance VARCHAR(50),
|
||||||
|
date TIMESTAMPTZ NOT NULL,
|
||||||
|
verified BOOLEAN DEFAULT FALSE,
|
||||||
|
badge_image VARCHAR(500),
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_achievements_user_id ON achievements(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
date TIMESTAMPTZ NOT NULL,
|
||||||
|
location VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(50) NOT NULL,
|
||||||
|
distance VARCHAR(50),
|
||||||
|
participants_count BIGINT DEFAULT 0,
|
||||||
|
max_participants BIGINT DEFAULT 0,
|
||||||
|
registration_open BOOLEAN DEFAULT TRUE,
|
||||||
|
image VARCHAR(500),
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS event_registrations (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
event_id BIGINT NOT NULL,
|
||||||
|
status VARCHAR(50) DEFAULT 'pending',
|
||||||
|
notes TEXT,
|
||||||
|
result_time VARCHAR(20),
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS personal_bests (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
distance_type VARCHAR(20) NOT NULL,
|
||||||
|
time VARCHAR(20) NOT NULL,
|
||||||
|
pace VARCHAR(20),
|
||||||
|
date TIMESTAMPTZ NOT NULL,
|
||||||
|
verified BOOLEAN DEFAULT FALSE,
|
||||||
|
event_name VARCHAR(255),
|
||||||
|
location VARCHAR(255),
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_personal_bests_user_id ON personal_bests(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS training_plans (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
weeks BIGINT NOT NULL DEFAULT 12,
|
||||||
|
workouts_per_week BIGINT NOT NULL DEFAULT 3,
|
||||||
|
target_distance VARCHAR(50),
|
||||||
|
target_date TIMESTAMPTZ,
|
||||||
|
current_week BIGINT DEFAULT 1,
|
||||||
|
completed BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_training_plans_user_id ON training_plans(user_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS training_workouts (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
plan_id BIGINT NOT NULL,
|
||||||
|
week BIGINT NOT NULL,
|
||||||
|
day BIGINT NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
distance_km DECIMAL(5,2),
|
||||||
|
duration_min BIGINT,
|
||||||
|
completed BOOLEAN DEFAULT FALSE,
|
||||||
|
completed_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_training_workouts_plan_id ON training_workouts(plan_id);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS email_verifications (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
token VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
type VARCHAR(20) NOT NULL,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
used BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ,
|
||||||
|
deleted_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_verifications_user_id ON email_verifications(user_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_email_verifications_token ON email_verifications(token);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_email_verifications_deleted_at ON email_verifications(deleted_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS galleries (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
image_path VARCHAR(500) NOT NULL,
|
||||||
|
category VARCHAR(20) NOT NULL,
|
||||||
|
author_id BIGINT NOT NULL,
|
||||||
|
event_date TIMESTAMPTZ,
|
||||||
|
views BIGINT DEFAULT 0,
|
||||||
|
likes BIGINT DEFAULT 0,
|
||||||
|
created_at TIMESTAMPTZ,
|
||||||
|
updated_at TIMESTAMPTZ
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_galleries_author_id ON galleries(author_id);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed *.sql
|
||||||
|
var FS embed.FS
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
│ Docker Compose Cluster │
|
│ Docker Compose Cluster │
|
||||||
├─────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────┤
|
||||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_ES │ │
|
│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_YAL │ │
|
||||||
│ │ (Proxy) │◄─┤(Yalarba) │ │(Бег.Баш)│ │(Easysite)│ │
|
│ │ (Proxy) │◄─┤(Yalarba) │ │(Бег.Баш)│ │(Easysite)│ │
|
||||||
│ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │
|
│ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
│ │ │ │ │ │
|
│ │ │ │ │ │
|
||||||
|
|||||||
@@ -139,6 +139,9 @@ easysite_build:
|
|||||||
easysite_start:
|
easysite_start:
|
||||||
docker compose up easysite -d && docker ps
|
docker compose up easysite -d && docker ps
|
||||||
|
|
||||||
|
# all
|
||||||
|
easysite: easysite_stop git easysite_build easysite_start easysite_logs
|
||||||
|
|
||||||
# Мониторинг системных ресурсов
|
# Мониторинг системных ресурсов
|
||||||
top:
|
top:
|
||||||
htop
|
htop
|
||||||
@@ -165,21 +168,6 @@ restart_analytics:
|
|||||||
# Полный цикл обновления analytics
|
# Полный цикл обновления analytics
|
||||||
analytics: stop_analitics git build_analititcs start_analytics wn
|
analytics: stop_analitics git build_analititcs start_analytics wn
|
||||||
|
|
||||||
# Остановка api_es
|
|
||||||
stop_api_es:
|
|
||||||
docker compose down api_es
|
|
||||||
|
|
||||||
# Пересборка api_es
|
|
||||||
build_api_es:
|
|
||||||
docker compose build api_es --no-cache
|
|
||||||
|
|
||||||
# Запуск api_es
|
|
||||||
start_api_es:
|
|
||||||
docker compose up api_es -d
|
|
||||||
|
|
||||||
# Полный цикл обновления api_es
|
|
||||||
api_es: stop_api_es git build_api_es start_api_es wn
|
|
||||||
|
|
||||||
# Остановка certbot
|
# Остановка certbot
|
||||||
stop_cerbot:
|
stop_cerbot:
|
||||||
docker compose down certbot
|
docker compose down certbot
|
||||||
@@ -195,26 +183,32 @@ start_certbot:
|
|||||||
# Полный цикл обновления certbot
|
# Полный цикл обновления certbot
|
||||||
certbot: stop_cerbot git build_certbot start_certbot wat
|
certbot: stop_cerbot git build_certbot start_certbot wat
|
||||||
|
|
||||||
|
# Сборка фронтенда valitovgaziz
|
||||||
|
valitovgaziz_build_spa: git
|
||||||
|
cd valitovgaziz && npm run build
|
||||||
|
|
||||||
|
# Остановка valitovgaziz
|
||||||
|
stop_valitovgaziz:
|
||||||
|
docker compose down valitovgaziz
|
||||||
|
|
||||||
|
# Пересборка valitovgaziz
|
||||||
|
build_valitovgaziz:
|
||||||
|
docker compose build valitovgaziz --no-cache
|
||||||
|
|
||||||
|
# Запуск valitovgaziz
|
||||||
|
start_valitovgaziz:
|
||||||
|
docker compose up valitovgaziz -d
|
||||||
|
|
||||||
|
# Полный цикл обновления valitovgaziz
|
||||||
|
valitovgaziz: stop_valitovgaziz git build_valitovgaziz start_valitovgaziz wn
|
||||||
|
|
||||||
|
# Сборка SPA + полный цикл обновления valitovgaziz
|
||||||
|
vue_site: valitovgaziz_build_spa stop_valitovgaziz build_valitovgaziz start_valitovgaziz wn
|
||||||
|
|
||||||
# Мониторинг состояния контейнеров каждые 2 секунды
|
# Мониторинг состояния контейнеров каждые 2 секунды
|
||||||
wn:
|
wn:
|
||||||
watch -n 2 'docker ps'
|
watch -n 2 'docker ps'
|
||||||
|
|
||||||
# Остановка api_tp
|
|
||||||
stop_api_tp:
|
|
||||||
docker compose down api_tp
|
|
||||||
|
|
||||||
# Пересборка api_tp
|
|
||||||
build_api_tp:
|
|
||||||
docker compose build api_tp --no-cache
|
|
||||||
|
|
||||||
# Запуск api_tp
|
|
||||||
start_api_tp:
|
|
||||||
docker compose up api_tp -d
|
|
||||||
|
|
||||||
# Полный цикл обновления api_tp
|
|
||||||
api_tp: stop_api_tp git build_api_tp start_api_tp wn
|
|
||||||
|
|
||||||
|
|
||||||
# Остановка api_yal
|
# Остановка api_yal
|
||||||
stop_api_yal:
|
stop_api_yal:
|
||||||
docker compose down api_yal
|
docker compose down api_yal
|
||||||
@@ -228,4 +222,136 @@ start_api_yal:
|
|||||||
docker compose up api_yal -d
|
docker compose up api_yal -d
|
||||||
|
|
||||||
# Полный цикл обновления api_yal
|
# Полный цикл обновления api_yal
|
||||||
api_yal: stop_api_yal git build_api_yal start_api_yal wn
|
api_yal: stop_api_yal git build_api_yal start_api_yal wn
|
||||||
|
|
||||||
|
# Остановка yalarba-nuxt
|
||||||
|
stop_yalarba:
|
||||||
|
docker compose down yalarba
|
||||||
|
|
||||||
|
# Пересборка yalarba-nuxt
|
||||||
|
build_yalarba:
|
||||||
|
docker compose build yalarba --no-cache
|
||||||
|
|
||||||
|
# Запуск yalarba-nuxt
|
||||||
|
start_yalarba:
|
||||||
|
docker compose up yalarba -d
|
||||||
|
|
||||||
|
# Полный цикл обновления yalarba-nuxt
|
||||||
|
yalarba: stop_yalarba git build_yalarba start_yalarba wn
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════
|
||||||
|
# НОВЫЕ ЦЕЛИ: generate-configs, deploy, backup
|
||||||
|
# ═══════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Генерация конфигов из sites.yml
|
||||||
|
generate-configs:
|
||||||
|
bash generate-configs.sh
|
||||||
|
|
||||||
|
# Генерация + рестарт nginx
|
||||||
|
reconfig: generate-configs
|
||||||
|
docker compose restart nginx
|
||||||
|
$(MAKE) wn
|
||||||
|
|
||||||
|
# Авто-детект изменённых сервисов и деплой только их
|
||||||
|
deploy: git
|
||||||
|
@echo "=== Detecting changes ==="
|
||||||
|
@CHANGED=$$(git diff --name-only HEAD~1 HEAD | grep -oP 'main_dc/\K[^/]+' | sort -u); \
|
||||||
|
for svc in $$CHANGED; do \
|
||||||
|
case "$$svc" in \
|
||||||
|
BB) name="api_bb" ;; \
|
||||||
|
certbot) name="certbot" ;; \
|
||||||
|
backup) name="backup" ;; \
|
||||||
|
gitea) name="gitea" ;; \
|
||||||
|
*) name="$$svc" ;; \
|
||||||
|
esac; \
|
||||||
|
if grep -q "^ $$name:" docker-compose.yml 2>/dev/null; then \
|
||||||
|
echo " → Rebuilding $$name..."; \
|
||||||
|
$(MAKE) stop_$$name build_$$name start_$$name 2>/dev/null || \
|
||||||
|
$(MAKE) stop_$$svc build_$$svc start_$$svc 2>/dev/null || true; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
if echo "$$CHANGED" | grep -q 'sites.yml\|nginx'; then \
|
||||||
|
echo " → Regenerating configs..."; \
|
||||||
|
bash generate-configs.sh; \
|
||||||
|
docker compose restart nginx; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ручной запуск бэкапа
|
||||||
|
backup:
|
||||||
|
docker compose exec backup /opt/backup.sh
|
||||||
|
|
||||||
|
# Ручной запуск бэкапа (разовый контейнер)
|
||||||
|
backup-run:
|
||||||
|
docker compose run --rm backup /opt/backup.sh
|
||||||
|
|
||||||
|
# Восстановление из бэкапа: make restore [DATE=2026-06-11]
|
||||||
|
restore:
|
||||||
|
docker compose run --rm backup /opt/restore.sh $(DATE)
|
||||||
|
|
||||||
|
# Gitea — полный цикл обновления
|
||||||
|
gitea: stop_gitea git build_gitea start_gitea wn
|
||||||
|
|
||||||
|
stop_gitea:
|
||||||
|
docker compose down gitea
|
||||||
|
|
||||||
|
build_gitea:
|
||||||
|
docker compose build gitea --no-cache
|
||||||
|
|
||||||
|
start_gitea:
|
||||||
|
docker compose up gitea -d
|
||||||
|
|
||||||
|
# Gitea Runner — полный цикл
|
||||||
|
gitea-runner: stop_gitea-runner git build_gitea-runner start_gitea-runner wn
|
||||||
|
|
||||||
|
stop_gitea-runner:
|
||||||
|
docker compose down gitea-runner
|
||||||
|
|
||||||
|
build_gitea-runner:
|
||||||
|
docker compose build gitea-runner --no-cache
|
||||||
|
|
||||||
|
start_gitea-runner:
|
||||||
|
docker compose up gitea-runner -d
|
||||||
|
|
||||||
|
# Gitea first-time setup helper
|
||||||
|
gitea-setup:
|
||||||
|
@echo "=== Gitea Setup ==="
|
||||||
|
@echo "1. Open http://94.41.23.97:3001 in browser"
|
||||||
|
@echo "2. Complete initial setup (DB: SQLite3 is fine)"
|
||||||
|
@echo "3. Create admin user"
|
||||||
|
@echo "4. Create new repository 'tp' and push:"
|
||||||
|
@echo " git remote add gitea http://94.41.23.97:3001/USER/tp.git"
|
||||||
|
@echo " git push -u gitea main"
|
||||||
|
@echo "5. Register runner:"
|
||||||
|
@echo " Settings → Actions → Runners → Create Token"
|
||||||
|
@echo " Update GITEA_RUNNER_REGISTRATION_TOKEN in docker-compose.yml"
|
||||||
|
@echo " Then: docker compose up -d gitea-runner"
|
||||||
|
@echo "6. Add secrets in repo Settings → Actions → Secrets:"
|
||||||
|
@echo " (none needed — runner runs locally)"
|
||||||
|
|
||||||
|
# Показать все доступные цели
|
||||||
|
help:
|
||||||
|
@echo "=== Make targets ==="
|
||||||
|
@echo ""
|
||||||
|
@echo "Site management:"
|
||||||
|
@echo " generate-configs — generate nginx configs from sites.yml"
|
||||||
|
@echo " reconfig — generate configs + restart nginx"
|
||||||
|
@echo ""
|
||||||
|
@echo "Deploy:"
|
||||||
|
@echo " all — full cycle all services"
|
||||||
|
@echo " deploy — auto-detect changes, rebuild only changed"
|
||||||
|
@echo " <service> — full cycle for one service"
|
||||||
|
@echo ""
|
||||||
|
@echo "Backup:"
|
||||||
|
@echo " backup — run backup via running container"
|
||||||
|
@echo " backup-run — run backup in one-shot container"
|
||||||
|
@echo " restore DATE=... — restore from backup"
|
||||||
|
@echo ""
|
||||||
|
@echo "Gitea:"
|
||||||
|
@echo " gitea — full cycle Gitea"
|
||||||
|
@echo " gitea-runner — full cycle Runner"
|
||||||
|
@echo " gitea-setup — first-time setup instructions"
|
||||||
|
@echo ""
|
||||||
|
@echo "Monitoring:"
|
||||||
|
@echo " wn — watch docker ps"
|
||||||
|
@echo " logs_<service> — logs for a service"
|
||||||
|
@echo " bb_db — psql into bb_db"
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
Текущие проблемы
|
||||||
|
1. Деплой в 5 шагов: локальный код → push → SSH → pull → make (ручной, медленный)
|
||||||
|
2. Добавление нового сайта требует правки 7+ файлов: .env, docker-compose.yml, nginx-http.conf, nginx-ssl.conf, switch-config.sh, init-certbot.sh, checkRenewCerts.sh, Makefile — легко забыть что-то
|
||||||
|
3. HTTPS all-or-nothing: если у одного домена нет сертификата — все сайты падают на HTTP
|
||||||
|
4. Certbot: дублирование кода на каждый домен в init-certbot.sh, checkRenewCerts.sh и 5 отдельных renew-скриптов
|
||||||
|
5. Makefile растёт — на каждый сервис 4-5 целей
|
||||||
|
Предложение
|
||||||
|
Фаза 1: CI/CD через GitHub Actions
|
||||||
|
Создать .github/workflows/deploy.yml:
|
||||||
|
name: Deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'main_dc/**'Текущие проблемы
|
||||||
|
1. Деплой в 5 шагов: локальный код → push → SSH → pull → make (ручной, медленный)
|
||||||
|
2. Добавление нового сайта требует правки 7+ файлов: .env, docker-compose.yml, nginx-http.conf, nginx-ssl.conf, switch-config.sh, init-certbot.sh, checkRenewCerts.sh, Makefile — легко забыть что-то
|
||||||
|
3. HTTPS all-or-nothing: если у одного домена нет сертификата — все сайты падают на HTTP
|
||||||
|
4. Certbot: дублирование кода на каждый домен в init-certbot.sh, checkRenewCerts.sh и 5 отдельных renew-скриптов
|
||||||
|
5. Makefile растёт — на каждый сервис 4-5 целей
|
||||||
|
Предложение
|
||||||
|
Фаза 1: CI/CD через GitHub Actions
|
||||||
|
Создать .github/workflows/deploy.yml:
|
||||||
|
name: Deploy
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'main_dc/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.HOST }}
|
||||||
|
username: ${{ secrets.USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd /home/valitovgaziz/tp
|
||||||
|
git pull origin main
|
||||||
|
# Определить какие сервисы изменились и пересобрать только их
|
||||||
|
# Или просто: make all
|
||||||
|
Преимущества: push → авто-деплой за ~1 мин, без SSH вручную.
|
||||||
|
Фаза 2: Единый источник истины — sites.yml
|
||||||
|
Ввести файл main_dc/sites.yml со списком всех сайтов:
|
||||||
|
sites:
|
||||||
|
yalarba:
|
||||||
|
domain: yalarba.ru
|
||||||
|
aliases: [www.yalarba.ru]
|
||||||
|
type: spa
|
||||||
|
root: /usr/share/nginx/yalarba/html
|
||||||
|
api: http://api_tp/
|
||||||
|
api_yal: http://api_yal/
|
||||||
|
|
||||||
|
valitovgaziz:
|
||||||
|
domain: valitovgaziz.ru
|
||||||
|
aliases: [www.valitovgaziz.ru]
|
||||||
|
type: container
|
||||||
|
upstream: http://valitovgaziz/
|
||||||
|
api: http://analytics:3000/
|
||||||
|
|
||||||
|
easysite102:
|
||||||
|
domain: easysite102.ru
|
||||||
|
aliases: [www.easysite102.ru]
|
||||||
|
type: container
|
||||||
|
upstream: http://easysite:3000
|
||||||
|
api: http://api_yal:8787/
|
||||||
|
|
||||||
|
begushiybashkir:
|
||||||
|
domain: begushiybashkir.ru
|
||||||
|
aliases: [www.begushiybashkir.ru]
|
||||||
|
type: spa
|
||||||
|
root: /usr/share/nginx/begushiybashkir/html
|
||||||
|
api: http://api_bb:8080/
|
||||||
|
Скрипт-генератор (generate-configs.sh) на основе sites.yml создаёт:
|
||||||
|
- nginx-http.conf — HTTP-блоки
|
||||||
|
- nginx-ssl.conf — HTTPS-блоки для каждого домена, каждый независимо проверяет свой сертификат
|
||||||
|
- nginx-partial-ssl.conf — комбинированный: HTTPS где есть серт, HTTP где нет
|
||||||
|
- certbot-domains.txt — список доменов для certbot
|
||||||
|
- .env — переменные окружения
|
||||||
|
Добавление нового сайта: 1 правка в sites.yml → ./generate-configs.sh → git commit.
|
||||||
|
Фаза 3: Умный nginx — per-domain HTTPS
|
||||||
|
Убрать all-or-nothing switch-config.sh. Вместо этого nginx грузит все конфиги через include:
|
||||||
|
/etc/nginx/conf.d/
|
||||||
|
├── 00-http-default.conf # HTTP на 80
|
||||||
|
├── 10-yalarba-ssl.conf # HTTPS, если есть серт
|
||||||
|
├── 20-valitovgaziz-ssl.conf # HTTPS, если есть серт
|
||||||
|
├── 30-easysite102-ssl.conf # HTTPS, если есть серт
|
||||||
|
└── common/ # Общие настройки
|
||||||
|
Каждый файл SSL генерируется с if-проверкой наличия сертификата:
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name valitovgaziz.ru www.valitovgaziz.ru;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
А HTTP-сервер делает return 301 https://$host$request_uri только для тех доменов, у кого есть сертификат. Остальные работают по HTTP.
|
||||||
|
Фаза 4: Certbot — один скрипт для всех
|
||||||
|
Вместо 5 renew-скриптов:
|
||||||
|
# /opt/renew-all.sh — единый скрипт
|
||||||
|
certbot renew --webroot -w /var/www/certbot
|
||||||
|
Certbot сам знает все домены, для которых получал сертификаты. --webroot с одним -w работает для всех. Убрать:
|
||||||
|
- renewBegushiyBAshkirLatin.sh, renewBegushiyBashkir.sh, renewEasysite102.sh, renewValitovGazizCert.sh, renewYalarbaCert.sh
|
||||||
|
- Заменить checkRenewCerts.sh на вызов certbot renew
|
||||||
|
Фаза 5: Makefile — авто-детект изменений
|
||||||
|
Оставить базовые цели, но добавить:
|
||||||
|
deploy: git
|
||||||
|
@echo "Detecting changes..."
|
||||||
|
@git diff --name-only HEAD~1 HEAD | grep -o 'main_dc/[^/]*/' | sort -u | while read dir; do \
|
||||||
|
service=$$(basename $$dir); \
|
||||||
|
if grep -q "^ $$service:" docker-compose.yml; then \
|
||||||
|
make stop_$$service build_$$service start_$$service; \
|
||||||
|
fi \
|
||||||
|
done
|
||||||
|
Или ещё проще: GitHub Actions сам определяет какие сервисы изменились и запускает только их make цели.
|
||||||
|
Дорожная карта
|
||||||
|
Шаг Что делаем Эффект
|
||||||
|
1 sites.yml + generate-configs.sh Добавление сайта = 1 файл
|
||||||
|
2 Переход на per-domain HTTPS в nginx Один сайт без серта не ломает другие
|
||||||
|
3 Упрощение certbot: единый certbot renew Удалить 5 скриптов
|
||||||
|
4 GitHub Actions deploy workflow push → авто-деплой
|
||||||
|
5 Makefile: deploy с авто-детектом Быстрый частичный деплой
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.HOST }}
|
||||||
|
username: ${{ secrets.USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd /home/valitovgaziz/tp
|
||||||
|
git pull origin main
|
||||||
|
# Определить какие сервисы изменились и пересобрать только их
|
||||||
|
# Или просто: make all
|
||||||
|
Преимущества: push → авто-деплой за ~1 мин, без SSH вручную.
|
||||||
|
Фаза 2: Единый источник истины — sites.yml
|
||||||
|
Ввести файл main_dc/sites.yml со списком всех сайтов:
|
||||||
|
sites:
|
||||||
|
yalarba:
|
||||||
|
domain: yalarba.ru
|
||||||
|
aliases: [www.yalarba.ru]
|
||||||
|
type: spa
|
||||||
|
root: /usr/share/nginx/yalarba/html
|
||||||
|
api: http://api_tp/
|
||||||
|
api_yal: http://api_yal/
|
||||||
|
|
||||||
|
valitovgaziz:
|
||||||
|
domain: valitovgaziz.ru
|
||||||
|
aliases: [www.valitovgaziz.ru]
|
||||||
|
type: container
|
||||||
|
upstream: http://valitovgaziz/
|
||||||
|
api: http://analytics:3000/
|
||||||
|
|
||||||
|
easysite102:
|
||||||
|
domain: easysite102.ru
|
||||||
|
aliases: [www.easysite102.ru]
|
||||||
|
type: container
|
||||||
|
upstream: http://easysite:3000
|
||||||
|
api: http://api_yal:8787/
|
||||||
|
|
||||||
|
begushiybashkir:
|
||||||
|
domain: begushiybashkir.ru
|
||||||
|
aliases: [www.begushiybashkir.ru]
|
||||||
|
type: spa
|
||||||
|
root: /usr/share/nginx/begushiybashkir/html
|
||||||
|
api: http://api_bb:8080/
|
||||||
|
Скрипт-генератор (generate-configs.sh) на основе sites.yml создаёт:
|
||||||
|
- nginx-http.conf — HTTP-блоки
|
||||||
|
- nginx-ssl.conf — HTTPS-блоки для каждого домена, каждый независимо проверяет свой сертификат
|
||||||
|
- nginx-partial-ssl.conf — комбинированный: HTTPS где есть серт, HTTP где нет
|
||||||
|
- certbot-domains.txt — список доменов для certbot
|
||||||
|
- .env — переменные окружения
|
||||||
|
Добавление нового сайта: 1 правка в sites.yml → ./generate-configs.sh → git commit.
|
||||||
|
Фаза 3: Умный nginx — per-domain HTTPS
|
||||||
|
Убрать all-or-nothing switch-config.sh. Вместо этого nginx грузит все конфиги через include:
|
||||||
|
/etc/nginx/conf.d/
|
||||||
|
├── 00-http-default.conf # HTTP на 80
|
||||||
|
├── 10-yalarba-ssl.conf # HTTPS, если есть серт
|
||||||
|
├── 20-valitovgaziz-ssl.conf # HTTPS, если есть серт
|
||||||
|
├── 30-easysite102-ssl.conf # HTTPS, если есть серт
|
||||||
|
└── common/ # Общие настройки
|
||||||
|
Каждый файл SSL генерируется с if-проверкой наличия сертификата:
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name valitovgaziz.ru www.valitovgaziz.ru;
|
||||||
|
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
|
||||||
|
...
|
||||||
|
}
|
||||||
|
А HTTP-сервер делает return 301 https://$host$request_uri только для тех доменов, у кого есть сертификат. Остальные работают по HTTP.
|
||||||
|
Фаза 4: Certbot — один скрипт для всех
|
||||||
|
Вместо 5 renew-скриптов:
|
||||||
|
# /opt/renew-all.sh — единый скрипт
|
||||||
|
certbot renew --webroot -w /var/www/certbot
|
||||||
|
Certbot сам знает все домены, для которых получал сертификаты. --webroot с одним -w работает для всех. Убрать:
|
||||||
|
- renewBegushiyBAshkirLatin.sh, renewBegushiyBashkir.sh, renewEasysite102.sh, renewValitovGazizCert.sh, renewYalarbaCert.sh
|
||||||
|
- Заменить checkRenewCerts.sh на вызов certbot renew
|
||||||
|
Фаза 5: Makefile — авто-детект изменений
|
||||||
|
Оставить базовые цели, но добавить:
|
||||||
|
deploy: git
|
||||||
|
@echo "Detecting changes..."
|
||||||
|
@git diff --name-only HEAD~1 HEAD | grep -o 'main_dc/[^/]*/' | sort -u | while read dir; do \
|
||||||
|
service=$$(basename $$dir); \
|
||||||
|
if grep -q "^ $$service:" docker-compose.yml; then \
|
||||||
|
make stop_$$service build_$$service start_$$service; \
|
||||||
|
fi \
|
||||||
|
done
|
||||||
|
Или ещё проще: GitHub Actions сам определяет какие сервисы изменились и запускает только их make цели.
|
||||||
|
Дорожная карта
|
||||||
|
Шаг Что делаем Эффект
|
||||||
|
1 sites.yml + generate-configs.sh Добавление сайта = 1 файл
|
||||||
|
2 Переход на per-domain HTTPS в nginx Один сайт без серта не ломает другие
|
||||||
|
3 Упрощение certbot: единый certbot renew Удалить 5 скриптов
|
||||||
|
4 GitHub Actions deploy workflow push → авто-деплой
|
||||||
|
5 Makefile: deploy с авто-детектом Быстрый частичный деплой
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
FROM alpine:3.19
|
||||||
|
|
||||||
|
RUN apk add --no-cache postgresql-client rclone bash curl
|
||||||
|
|
||||||
|
COPY scripts/ /opt/
|
||||||
|
RUN chmod +x /opt/*.sh
|
||||||
|
|
||||||
|
# crontab для расписания бэкапов
|
||||||
|
RUN echo "$BACKUP_TIME /opt/backup.sh > /proc/1/fd/1 2>&1" > /etc/crontabs/root
|
||||||
|
|
||||||
|
CMD ["crond", "-f", "-l", "2"]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[yadisk]
|
||||||
|
type = yandex
|
||||||
|
token = {"access_token":"y0__wgBEI6uquABGMKlCyC2ru7zFztUXB9VV10fCqLpn1iMh9-P7HDo","token_type":"bearer","refresh_token":"2:AAA:AAAAABwKlw4:1:XOD3WRFNbRzP_QWC:hVdNSjVSdfjzZNOXQy6eH7El9bRfWPxzGXvI99qACcdHl7qJrDbAug38IdTRnqglIcni00y1TA:Zbl9G33wrF55KgVeFtfgDQ","expiry":"2027-06-12T12:38:51.056926299+05:00"}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Пример конфига rclone для Яндекс.Диска
|
||||||
|
# Скопируй в backup/rclone.conf и заполни токен
|
||||||
|
# Инструкция: https://rclone.org/yandex/
|
||||||
|
[yadisk]
|
||||||
|
type = yandex
|
||||||
|
client_id =
|
||||||
|
client_secret =
|
||||||
|
token = {"access_token":"...","token_type":"...","expiry":"..."}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# backup.sh — ежедневный бэкап: pg_dump + файлы → локально + Яндекс.Диск
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BACKUP_DIR="/backups/$(date +%Y-%m-%d)"
|
||||||
|
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-7}"
|
||||||
|
DB_NAMES="${DB_NAMES:-mydb}"
|
||||||
|
TIMESTAMP=$(date +%H%M%S)
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR/db" "$BACKUP_DIR/files"
|
||||||
|
|
||||||
|
echo "=== Backup $TIMESTAMP ==="
|
||||||
|
|
||||||
|
# 1. Дампы всех БД
|
||||||
|
IFS=',' read -ra databases <<< "$DB_NAMES"
|
||||||
|
for db in "${databases[@]}"; do
|
||||||
|
db=$(echo "$db" | xargs) # trim
|
||||||
|
echo "→ Dumping database: $db"
|
||||||
|
PGPASSWORD="$DB_PASSWORD" pg_dump -h "$DB_HOST" -p "${DB_PORT:-5432}" \
|
||||||
|
-U "$DB_USER" -d "$db" --format=custom \
|
||||||
|
-f "$BACKUP_DIR/db/${db}-${TIMESTAMP}.dump"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 2. Архив файлов
|
||||||
|
echo "→ Archiving files..."
|
||||||
|
tar -czf "$BACKUP_DIR/files/certbot-${TIMESTAMP}.tar.gz" -C /data/certbot . 2>/dev/null || true
|
||||||
|
tar -czf "$BACKUP_DIR/files/uploads-${TIMESTAMP}.tar.gz" -C /data/uploads . 2>/dev/null || true
|
||||||
|
tar -czf "$BACKUP_DIR/files/analytics-${TIMESTAMP}.tar.gz" -C /data/analytics . 2>/dev/null || true
|
||||||
|
|
||||||
|
# 3. Создаём symlink latest
|
||||||
|
rm -f /backups/latest
|
||||||
|
ln -sf "$BACKUP_DIR" /backups/latest
|
||||||
|
|
||||||
|
# 4. Ротация — удаляем старше RETENTION_DAYS
|
||||||
|
find /backups -maxdepth 1 -type d -name '2*' -mtime "+$RETENTION_DAYS" -exec rm -rf {} \; 2>/dev/null || true
|
||||||
|
|
||||||
|
echo "✓ Local backup saved to $BACKUP_DIR"
|
||||||
|
|
||||||
|
# 5. Синхронизация с Яндекс.Диск
|
||||||
|
if command -v rclone > /dev/null 2>&1 && [ -n "${RCLONE_REMOTE:-}" ]; then
|
||||||
|
echo "→ Syncing to cloud: $RCLONE_REMOTE"
|
||||||
|
rclone sync /backups "$RCLONE_REMOTE" --progress 2>&1 || \
|
||||||
|
echo " ⚠ Cloud sync failed (check rclone config)"
|
||||||
|
echo "✓ Cloud sync complete"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Backup finished ==="
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# restore.sh — восстановление из бэкапа
|
||||||
|
# Использование: docker compose run --rm backup /opt/restore.sh [дата]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BACKUP_DATE="${1:-latest}"
|
||||||
|
BACKUP_DIR="/backups/$BACKUP_DATE"
|
||||||
|
|
||||||
|
if [ ! -d "$BACKUP_DIR" ]; then
|
||||||
|
echo "Ошибка: бэкап $BACKUP_DIR не найден"
|
||||||
|
echo "Доступные бэкапы:"
|
||||||
|
ls -d /backups/2* 2>/dev/null || echo " (нет бэкапов)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Restore from $BACKUP_DIR ==="
|
||||||
|
|
||||||
|
# Восстановить БД
|
||||||
|
if [ -d "$BACKUP_DIR/db" ]; then
|
||||||
|
for dump in "$BACKUP_DIR/db"/*.dump; do
|
||||||
|
[ -f "$dump" ] || continue
|
||||||
|
db=$(basename "$dump" | sed 's/-.*//')
|
||||||
|
echo "→ Restoring database: $db"
|
||||||
|
PGPASSWORD="$DB_PASSWORD" pg_restore -h "$DB_HOST" -p "${DB_PORT:-5432}" \
|
||||||
|
-U "$DB_USER" -d "$db" --clean --if-exists "$dump" || \
|
||||||
|
echo " ⚠ Restore of $db had warnings (non-fatal)"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Распаковать файлы
|
||||||
|
if [ -d "$BACKUP_DIR/files" ]; then
|
||||||
|
for archive in "$BACKUP_DIR/files"/*.tar.gz; do
|
||||||
|
[ -f "$archive" ] || continue
|
||||||
|
name=$(basename "$archive" | sed 's/-.*//')
|
||||||
|
target="/data/$name"
|
||||||
|
echo "→ Extracting $name to $target"
|
||||||
|
mkdir -p "$target"
|
||||||
|
tar -xzf "$archive" -C "$target" || true
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Restore completed ==="
|
||||||
|
echo "При необходимости перезапусти сервисы: docker compose restart"
|
||||||
@@ -1,20 +1,10 @@
|
|||||||
FROM certbot/certbot
|
FROM certbot/certbot
|
||||||
|
|
||||||
# Проверяем наличие crond (используем command -v вместо which)
|
RUN apk add --no-cache cronie docker-cli
|
||||||
RUN if ! command -v crond > /dev/null 2>&1; then \
|
|
||||||
echo "Cron not found. Installing cronie..."; \
|
|
||||||
apk add --no-cache cronie; \
|
|
||||||
else \
|
|
||||||
echo "Cron is already installed."; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Создаем директории для конфигов
|
|
||||||
RUN mkdir -p /etc/letsencrypt/config
|
RUN mkdir -p /etc/letsencrypt/config
|
||||||
|
|
||||||
# Копируем конфигурационные файлы
|
|
||||||
COPY scripts/ /opt/
|
COPY scripts/ /opt/
|
||||||
|
RUN chmod +x /opt/*.sh
|
||||||
|
|
||||||
# Устанавливаем права
|
ENTRYPOINT ["/opt/init-certbot.sh"]
|
||||||
RUN chmod +x /opt/*
|
|
||||||
|
|
||||||
ENTRYPOINT ["/opt/init-certbot.sh"]
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
yalarba.ru
|
||||||
|
www.yalarba.ru
|
||||||
|
valitovgaziz.ru
|
||||||
|
www.valitovgaziz.ru
|
||||||
|
easysite102.ru
|
||||||
|
www.easysite102.ru
|
||||||
|
begushiybashkir.ru
|
||||||
|
www.begushiybashkir.ru
|
||||||
|
xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||||
|
www.xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||||
@@ -19,7 +19,8 @@ check_local_cert() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Преобразуем дату истечения в UNIX-время
|
# Преобразуем дату истечения в UNIX-время
|
||||||
expiry_unix=$(date -d "$expiry_date" +%s)
|
# expiry_unix=$(date -d "$expiry_date" +%s)
|
||||||
|
expiry_unix=$(date -D "%b %d %H:%M:%S %Y %Z" -d "$expiry_date" +%s 2>/dev/null)
|
||||||
|
|
||||||
# Текущая дата в UNIX-времени
|
# Текущая дата в UNIX-времени
|
||||||
current_unix=$(date +%s)
|
current_unix=$(date +%s)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0 0 * * * root /opt/checkRenewCerts.sh > /proc/1/fd/1 2>&1
|
0 0 * * * /opt/renew-all.sh > /proc/1/fd/1 2>&1
|
||||||
|
|||||||
@@ -1,69 +1,32 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# init-certbot.sh — точка входа certbot контейнера
|
||||||
|
set -e
|
||||||
|
|
||||||
# Проверяем наличие сертификатов для yalarba.ru
|
echo "=== Certbot init ==="
|
||||||
if [ ! -d "/etc/letsencrypt/live/yalarba.ru" ]; then
|
|
||||||
echo "Получаем новые сертификаты yalarba.ru ..."
|
|
||||||
certbot certonly --webroot \
|
|
||||||
--config /etc/letsencrypt/config/certbot.ini \
|
|
||||||
-w /var/www/certbot \
|
|
||||||
-d ${DOMAINS_yalarba}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "сertificates for ${DOMAINS_yalarba} is ready"
|
# Получаем сертификаты для всех доменов из DOMAINS_* env
|
||||||
|
env | grep '^DOMAINS_' | grep -v '^ALL_DOMAINS' | sort | while IFS='=' read -r var_name domains; do
|
||||||
|
primary_domain=$(echo "$domains" | cut -d, -f1)
|
||||||
|
|
||||||
# Проверяем наличие сертификатов для valitovgaziz.ru
|
if [ ! -d "/etc/letsencrypt/live/$primary_domain" ]; then
|
||||||
if [ ! -d "/etc/letsencrypt/live/valitovgaziz.ru" ]; then
|
echo "→ Получаем сертификат для $primary_domain"
|
||||||
echo "Получаем новые сертификаты valitovgaziz ..."
|
certbot certonly --webroot \
|
||||||
certbot certonly --webroot \
|
--config /etc/letsencrypt/config/certbot.ini \
|
||||||
--config /etc/letsencrypt/config/certbot.ini \
|
-w /var/www/certbot \
|
||||||
-w /var/www/certbot \
|
-d "$domains"
|
||||||
-d ${DOMAINS_valitovgaziz}
|
echo "✓ Сертификат для $primary_domain получен"
|
||||||
fi
|
else
|
||||||
|
echo "✓ Сертификат для $primary_domain уже существует"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
echo "сertificates for ${DOMAINS_valitovgaziz} is ready"
|
# Настраиваем cron для ежедневного обновления
|
||||||
|
|
||||||
# Проверяем наличие сертификатов для easysite102.ru
|
|
||||||
if [ ! -d "/etc/letsencrypt/live/easysite102.ru" ]; then
|
|
||||||
echo "Получаем новые сертификаты easysite102.ru ..."
|
|
||||||
certbot certonly --webroot \
|
|
||||||
--config /etc/letsencrypt/config/certbot.ini \
|
|
||||||
-w /var/www/certbot \
|
|
||||||
-d ${DOMAINS_easysite102}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "сertificates for ${DOMAINS_easysite102} is ready"
|
|
||||||
|
|
||||||
# Проверяем наличие сертификатов для бегущийбашкир.рф
|
|
||||||
if [ ! -d "/etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai" ]; then
|
|
||||||
echo "Получаем новые сертификаты xn--80abahjtcfl5d0a8di.xn--p1ai(бегущийбашкир.рф) ..."
|
|
||||||
certbot certonly --webroot \
|
|
||||||
--config /etc/letsencrypt/config/certbot.ini \
|
|
||||||
-w /var/www/certbot \
|
|
||||||
-d ${DOMAINS_begushiybashkir}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "сertificates for ${DOMAINS_begushiybashkir} is ready"
|
|
||||||
|
|
||||||
# Проверяем наличие сертификатов для begushiybashkir.ru
|
|
||||||
if [ ! -d "/etc/letsencrypt/live/begushiybashkir.ru" ]; then
|
|
||||||
echo "Получаем новые сертификаты begushiybashkir.ru ..."
|
|
||||||
certbot certonly --webroot \
|
|
||||||
--config /etc/letsencrypt/config/certbot.ini \
|
|
||||||
-w /var/www/certbot \
|
|
||||||
-d ${DOMAINS_begushiybashkir_latin}
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "сertificates for ${DOMAINS_begushiybashkir_latin} is ready"
|
|
||||||
|
|
||||||
set -e # Завершаем работу, если любая команда вернёт ошибку
|
|
||||||
|
|
||||||
# Активируем сервис cron
|
|
||||||
/usr/sbin/crond -f &
|
|
||||||
crond -f &
|
|
||||||
|
|
||||||
# Копируем нашу собственную crontab таблицу
|
|
||||||
cp /opt/crontab.txt /etc/crontabs/root
|
cp /opt/crontab.txt /etc/crontabs/root
|
||||||
|
|
||||||
# Оставляем контейнер открытым
|
# Запускаем crond в фоне
|
||||||
tail -f /dev/null
|
crond -f &
|
||||||
|
|
||||||
|
echo "=== Init завершён, контейнер работает ==="
|
||||||
|
|
||||||
|
# Держим контейнер живым
|
||||||
|
tail -f /dev/null
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# renew-all.sh — единый скрипт обновления всех сертификатов
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "=== Certbot renewal ==="
|
||||||
|
|
||||||
|
# Обновляем все сертификаты
|
||||||
|
certbot renew --webroot -w /var/www/certbot
|
||||||
|
|
||||||
|
# Перезагружаем nginx чтобы он подхватил новые сертификаты
|
||||||
|
if command -v docker > /dev/null 2>&1; then
|
||||||
|
echo "→ Перезагружаем nginx..."
|
||||||
|
docker exec nginx nginx -s reload 2>/dev/null || \
|
||||||
|
echo " (nginx reload не удался, возможно контейнер не запущен)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Renewal завершён ==="
|
||||||
@@ -10,19 +10,15 @@ services:
|
|||||||
- ./certbot/config:/etc/letsencrypt/config
|
- ./certbot/config:/etc/letsencrypt/config
|
||||||
- certbot_data:/etc/letsencrypt
|
- certbot_data:/etc/letsencrypt
|
||||||
- certbot_www:/var/www/certbot
|
- certbot_www:/var/www/certbot
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
- EMAIL=${EMAIL}
|
- EMAIL=${EMAIL}
|
||||||
- DOMAINS=${ALL_DOMAINS}
|
|
||||||
- STAGING=0
|
- STAGING=0
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD-SHELL", "ls /etc/letsencrypt/live/*/fullchain.pem 2>/dev/null | head -1 | xargs test -f || exit 1"]
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"test -f /etc/letsencrypt/live/$$(echo $${DOMAINS} | cut -d',' -f1)/fullchain.pem || exit 1",
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -43,32 +39,28 @@ services:
|
|||||||
- certbot_data:/etc/letsencrypt
|
- certbot_data:/etc/letsencrypt
|
||||||
- certbot_www:/var/www/certbot
|
- certbot_www:/var/www/certbot
|
||||||
- ./stubSite:/usr/share/nginx/stub/html
|
- ./stubSite:/usr/share/nginx/stub/html
|
||||||
- ./yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html
|
|
||||||
- ./valitovgaziz/html:/usr/share/nginx/valitovgaziz/html
|
|
||||||
- ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html
|
- ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html
|
||||||
- analytics_logs:/var/log/analytics:ro
|
- analytics_logs:/var/log/analytics:ro
|
||||||
|
- ./nginx/conf.available:/etc/nginx/conf.available:ro
|
||||||
networks:
|
networks:
|
||||||
- web-network
|
- web-network
|
||||||
- internal
|
- internal
|
||||||
- app-network
|
- app-network
|
||||||
- bb-network
|
|
||||||
depends_on:
|
depends_on:
|
||||||
easysite:
|
easysite:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
api_es:
|
|
||||||
condition: service_healthy
|
|
||||||
certbot:
|
|
||||||
condition: service_healthy
|
|
||||||
api_tp:
|
|
||||||
condition: service_healthy
|
|
||||||
api_bb:
|
api_bb:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
analytics:
|
analytics:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
api_yal:
|
api_yal:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
yalarba:
|
||||||
|
condition: service_healthy
|
||||||
|
valitovgaziz:
|
||||||
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
|
test: ["CMD", "wget", "--spider", "http://localhost/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
@@ -101,41 +93,24 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
# REST API app on Golang (Gorm, Chi) бизнес логика приложения yalarba.ru. Работает с БД на PostgresQL db:db_tp
|
# Vue 3 SPA для valitovgaziz.ru
|
||||||
api_tp:
|
valitovgaziz:
|
||||||
build:
|
build:
|
||||||
context: ./yalarba/api_tp
|
context: ./valitovgaziz
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
container_name: valitovgaziz
|
||||||
- "8888:8080"
|
|
||||||
container_name: api_tp
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
# Database connection settings
|
|
||||||
DB_HOST: db
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_USER: postgres
|
|
||||||
DB_PASSWORD: postgres
|
|
||||||
DB_NAME: mydb
|
|
||||||
APP_PORT: 8080
|
|
||||||
networks:
|
networks:
|
||||||
- app-network
|
- web-network
|
||||||
|
depends_on:
|
||||||
|
analytics:
|
||||||
|
condition: service_healthy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test: ["CMD", "wget", "--spider", "http://localhost/"]
|
||||||
[
|
|
||||||
"CMD",
|
|
||||||
"wget",
|
|
||||||
"--no-verbose",
|
|
||||||
"--tries=1",
|
|
||||||
"--spider",
|
|
||||||
"http://localhost:8080/health",
|
|
||||||
]
|
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
# PostgresQL DB база данных для храниния информации приложений Yalarba.ru && Easysite102.ru
|
# PostgresQL DB база данных для храниния информации приложений Yalarba.ru && Easysite102.ru
|
||||||
db:
|
db:
|
||||||
@@ -159,7 +134,7 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир Работает с БД db_bb on PostgresQL
|
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир
|
||||||
api_bb:
|
api_bb:
|
||||||
build:
|
build:
|
||||||
context: ./BB/api_bb
|
context: ./BB/api_bb
|
||||||
@@ -169,22 +144,22 @@ services:
|
|||||||
container_name: api_bb
|
container_name: api_bb
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
db_bb:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- ./BB/api_bb/.env
|
- ./BB/api_bb/.env
|
||||||
volumes:
|
volumes:
|
||||||
- api_bb_uploads:/app/uploads
|
- api_bb_uploads:/app/uploads
|
||||||
environment:
|
environment:
|
||||||
# Database connection settings
|
DB_HOST: db
|
||||||
DB_HOST: db_bb
|
|
||||||
DB_PORT: 5432
|
DB_PORT: 5432
|
||||||
DB_USER: postgres
|
DB_USER: postgres
|
||||||
DB_PASSWORD: postgres
|
DB_PASSWORD: postgres
|
||||||
DB_NAME: bb_db
|
DB_NAME: bb_db
|
||||||
|
DB_SCHEMA: bb
|
||||||
APP_PORT: 8080
|
APP_PORT: 8080
|
||||||
networks:
|
networks:
|
||||||
- bb-network
|
- app-network
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
@@ -199,31 +174,10 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
# PostgresQL DB база данных для работы сайта Бегущий Башкир
|
# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_yal REST API app
|
||||||
db_bb:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "5433:5432"
|
|
||||||
container_name: db_bb
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: bb_db
|
|
||||||
volumes:
|
|
||||||
- db_bb_data:/var/lib/postgresql/data
|
|
||||||
networks:
|
|
||||||
- bb-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_es REST API app
|
|
||||||
easysite:
|
easysite:
|
||||||
build:
|
build:
|
||||||
context: ./yalarba/easySite/easySite
|
context: ./yalarba/easySite
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: easysite
|
container_name: easysite
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -233,6 +187,7 @@ services:
|
|||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
HOST: 0.0.0.0
|
HOST: 0.0.0.0
|
||||||
PORT: 3000
|
PORT: 3000
|
||||||
|
NUXT_PUBLIC_API_BASE: /api/v1
|
||||||
networks:
|
networks:
|
||||||
- web-network
|
- web-network
|
||||||
- app-network
|
- app-network
|
||||||
@@ -242,34 +197,6 @@ services:
|
|||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|
||||||
# REST API приложение для easysite102.ru тут бизнес логика и система для обращения к PostgresQL БД (тоже сервис db:db_tp)
|
|
||||||
api_es:
|
|
||||||
build:
|
|
||||||
context: ./yalarba/api_es
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: api_es
|
|
||||||
restart: unless-stopped
|
|
||||||
env_file:
|
|
||||||
- ./yalarba/api_es/.env
|
|
||||||
depends_on:
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
environment:
|
|
||||||
DB_HOST: db
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_USER: postgres
|
|
||||||
DB_PASSWORD: postgres
|
|
||||||
DB_NAME: mydb
|
|
||||||
APP_PORT: ${API_ES_APP_PORT}
|
|
||||||
networks:
|
|
||||||
- app-network
|
|
||||||
- web-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--spider", "http://localhost:8088/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
# REST API app on Golang для api_yal сервиса
|
# REST API app on Golang для api_yal сервиса
|
||||||
api_yal:
|
api_yal:
|
||||||
build:
|
build:
|
||||||
@@ -300,15 +227,119 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
|
# Nuxt 4 SPA для yalarba.ru
|
||||||
|
yalarba:
|
||||||
|
build:
|
||||||
|
context: ./yalarba/yalarba-nuxt
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: yalarba
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
HOST: 0.0.0.0
|
||||||
|
PORT: 3000
|
||||||
|
NUXT_PUBLIC_API_BASE: /api/v1
|
||||||
|
NUXT_PUBLIC_APP_URL: https://yalarba.ru
|
||||||
|
networks:
|
||||||
|
- web-network
|
||||||
|
- app-network
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Gitea — self-hosted Git сервер + CI/CD
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
gitea:
|
||||||
|
image: gitea/gitea:latest
|
||||||
|
container_name: gitea
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3001:3000"
|
||||||
|
- "2222:22"
|
||||||
|
volumes:
|
||||||
|
- gitea_data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
environment:
|
||||||
|
- USER_UID=1000
|
||||||
|
- USER_GID=1000
|
||||||
|
- GITEA__server__DOMAIN=git.yalarba.ru
|
||||||
|
- GITEA__server__SSH_DOMAIN=94.41.23.97
|
||||||
|
- GITEA__server__ROOT_URL=https://git.yalarba.ru
|
||||||
|
networks:
|
||||||
|
- web-network
|
||||||
|
- internal
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "wget", "--spider", "http://localhost:3000"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
gitea-runner:
|
||||||
|
image: gitea/act_runner:latest
|
||||||
|
container_name: gitea-runner
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /home/gaziz/artefacts/tp:/home/gaziz/artefacts/tp
|
||||||
|
- gitea_runner:/data
|
||||||
|
environment:
|
||||||
|
- GITEA_INSTANCE_URL=http://gitea:3000
|
||||||
|
- GITEA_RUNNER_REGISTRATION_TOKEN=
|
||||||
|
depends_on:
|
||||||
|
gitea:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# Backup — ежедневные бэкапы БД + файлов → локально + Яндекс.Диск
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
backup:
|
||||||
|
build:
|
||||||
|
context: ./backup
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: backup
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- /var/backups/tp:/backups
|
||||||
|
- certbot_data:/data/certbot:ro
|
||||||
|
- api_bb_uploads:/data/uploads:ro
|
||||||
|
- analytics_data:/data/analytics:ro
|
||||||
|
- ./backup/rclone.conf:/root/.config/rclone/rclone.conf:ro
|
||||||
|
environment:
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASSWORD: postgres
|
||||||
|
DB_NAMES: mydb,bb_db
|
||||||
|
RCLONE_REMOTE: "yadisk:tp-backups"
|
||||||
|
BACKUP_RETENTION_DAYS: 7
|
||||||
|
BACKUP_TIME: "0 3 * * *"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pidof crond > /dev/null && ls /backups/ > /dev/null || exit 1"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- app-network
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
certbot_data: # volume для данных Certbot
|
certbot_data: # volume для данных Certbot
|
||||||
certbot_www: # volume для данных Certbot
|
certbot_www: # volume для данных Certbot
|
||||||
db_tp_data: # Volume для данных БД yalarba.ru
|
db_tp_data: # Volume для данных БД yalarba.ru
|
||||||
db_bb_data: # Volume для данных БД Бегущий башкир
|
|
||||||
api_bb_uploads: # Volume для загружаемых файлов бегущий башкир
|
api_bb_uploads: # Volume для загружаемых файлов бегущий башкир
|
||||||
analytics_logs: # Volume для логов аналитики
|
analytics_logs: # Volume для логов аналитики
|
||||||
analytics_data: # Volume для данных аналитики
|
analytics_data: # Volume для данных аналитики
|
||||||
|
gitea_data: # Volume для Gitea
|
||||||
|
gitea_runner: # Volume для Gitea Runner
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
web-network:
|
web-network:
|
||||||
@@ -317,8 +348,6 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
app-network:
|
app-network:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
bb-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
# Эта опция автоматически удаляет orphans (Не используемые контейнеры)
|
# Эта опция автоматически удаляет orphans (Не используемые контейнеры)
|
||||||
x-remove-orphans: true
|
x-remove-orphans: true
|
||||||
@@ -0,0 +1,474 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# generate-configs.sh — генератор конфигов из sites.yml
|
||||||
|
# Генерирует: nginx-http.conf, nginx-ssl.conf, certbot/domains.txt, обновляет .env
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
cd "$DIR"
|
||||||
|
|
||||||
|
SITES_YML="$DIR/sites.yml"
|
||||||
|
NGINX_DIR="$DIR/nginx"
|
||||||
|
ENV_FILE="$DIR/.env"
|
||||||
|
|
||||||
|
if [ ! -f "$SITES_YML" ]; then
|
||||||
|
echo "Ошибка: $SITES_YML не найден"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== Генерация конфигов из sites.yml ==="
|
||||||
|
|
||||||
|
# Используем python3 с quoted heredoc — предотвращает интерпретацию $ переменных bash
|
||||||
|
python3 - "$DIR" "$NGINX_DIR" "$ENV_FILE" << 'PYEOF'
|
||||||
|
import yaml, os, sys
|
||||||
|
|
||||||
|
BASE_DIR = sys.argv[1]
|
||||||
|
NGINX_DIR = sys.argv[2]
|
||||||
|
ENV_FILE = sys.argv[3]
|
||||||
|
SITES_YML = os.path.join(BASE_DIR, "sites.yml")
|
||||||
|
|
||||||
|
with open(SITES_YML) as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
sites = data.get("sites", {})
|
||||||
|
if not sites:
|
||||||
|
print("Ошибка: в sites.yml нет сайтов")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# собираем данные
|
||||||
|
all_domains = []
|
||||||
|
env_domains = {}
|
||||||
|
site_list = []
|
||||||
|
|
||||||
|
for name, cfg in sites.items():
|
||||||
|
domain = cfg["domain"]
|
||||||
|
aliases = cfg.get("aliases", [])
|
||||||
|
|
||||||
|
all_domains.append(domain)
|
||||||
|
all_domains.extend(aliases)
|
||||||
|
|
||||||
|
env_key = f"DOMAINS_{name}"
|
||||||
|
env_val = ",".join([domain] + aliases)
|
||||||
|
env_domains[env_key] = env_val
|
||||||
|
|
||||||
|
site_list.append({
|
||||||
|
"name": name,
|
||||||
|
"domain": domain,
|
||||||
|
"aliases": aliases,
|
||||||
|
"type": cfg.get("type", "upstream"),
|
||||||
|
"upstream": cfg.get("upstream", ""),
|
||||||
|
"root": cfg.get("root", ""),
|
||||||
|
"api": cfg.get("api", {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
env_domains["ALL_DOMAINS"] = ",".join(all_domains)
|
||||||
|
|
||||||
|
def all_server_names():
|
||||||
|
"""Возвращает строку со всеми доменами и алиасами через пробел"""
|
||||||
|
parts = []
|
||||||
|
for s in site_list:
|
||||||
|
parts.append(s["domain"])
|
||||||
|
parts.extend(s["aliases"])
|
||||||
|
return " ".join(parts)
|
||||||
|
|
||||||
|
def all_server_names_multiline():
|
||||||
|
"""Возвращает строку с переносами для nginx server_name"""
|
||||||
|
lines = []
|
||||||
|
for s in site_list:
|
||||||
|
lines.append(s["domain"])
|
||||||
|
for a in s["aliases"]:
|
||||||
|
lines.append(a)
|
||||||
|
return " \\\n ".join(lines)
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# 2. Генерация nginx-http.conf
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
http_conf = f"""# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
|
||||||
|
# HTTP-only конфигурация (работает когда нет сертификатов)
|
||||||
|
|
||||||
|
server {{
|
||||||
|
listen 80;
|
||||||
|
server_name {all_server_names_multiline()};
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
root /usr/share/nginx/stub/html;
|
||||||
|
index index.html;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {{
|
||||||
|
root /var/www/certbot;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
# Блок для HTTPS → HTTP редиректа (порт 443)
|
||||||
|
server {{
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name {all_server_names_multiline()};
|
||||||
|
|
||||||
|
ssl_certificate /etc/nginx/ssl/dummy.crt;
|
||||||
|
ssl_certificate_key /etc/nginx/ssl/dummy.key;
|
||||||
|
|
||||||
|
return 301 http://$host$request_uri;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
http_conf_path = os.path.join(NGINX_DIR, "nginx-http.conf")
|
||||||
|
with open(http_conf_path, "w") as f:
|
||||||
|
f.write(http_conf.lstrip())
|
||||||
|
print(f" ✓ {http_conf_path}")
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# 3. Генерация nginx-ssl.conf
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
ssl_server_blocks = []
|
||||||
|
|
||||||
|
for s in site_list:
|
||||||
|
server_names = " ".join([s["domain"]] + s["aliases"])
|
||||||
|
|
||||||
|
block = f"""
|
||||||
|
server {{
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name {server_names};
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/{s["domain"]}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/{s["domain"]}/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
"""
|
||||||
|
if s["type"] == "upstream":
|
||||||
|
block += f"""
|
||||||
|
location / {{
|
||||||
|
proxy_pass {s["upstream"]};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
elif s["type"] == "static":
|
||||||
|
block += f"""
|
||||||
|
location / {{
|
||||||
|
root {s["root"]};
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
# API routes
|
||||||
|
for path, target in s["api"].items():
|
||||||
|
cors_block = ""
|
||||||
|
if "/api/" in path:
|
||||||
|
cors_block = """
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
block += f"""
|
||||||
|
location {path} {{
|
||||||
|
proxy_pass {target};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
{cors_block}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
if s["type"] == "static":
|
||||||
|
block += f"""
|
||||||
|
location /uploads/ {{
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
block += "}"
|
||||||
|
ssl_server_blocks.append(block)
|
||||||
|
|
||||||
|
ssl_conf = f"""# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
|
||||||
|
# Полная HTTPS конфигурация
|
||||||
|
|
||||||
|
# --- HTTP → HTTPS редирект ---
|
||||||
|
server {{
|
||||||
|
listen 80;
|
||||||
|
|
||||||
|
server_name {all_server_names()};
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {{
|
||||||
|
root /var/www/certbot;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location /uploads/ {{
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}}
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
# --- HTTPS серверные блоки ---
|
||||||
|
{''.join(ssl_server_blocks)}
|
||||||
|
"""
|
||||||
|
|
||||||
|
ssl_conf_path = os.path.join(NGINX_DIR, "nginx-ssl.conf")
|
||||||
|
with open(ssl_conf_path, "w") as f:
|
||||||
|
f.write(ssl_conf.lstrip())
|
||||||
|
print(f" ✓ {ssl_conf_path}")
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# 4. Генерация per-domain конфигов (conf.available/)
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
CONF_AVAILABLE = os.path.join(NGINX_DIR, "conf.available")
|
||||||
|
os.makedirs(CONF_AVAILABLE, exist_ok=True)
|
||||||
|
|
||||||
|
# 00-http.conf — базовый HTTP catch-all (всегда активен)
|
||||||
|
base_http = f"""# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {{
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {{
|
||||||
|
root /usr/share/nginx/stub/html;
|
||||||
|
index index.html;
|
||||||
|
}}
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {{
|
||||||
|
root /var/www/certbot;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
path = os.path.join(CONF_AVAILABLE, "00-http.conf")
|
||||||
|
with open(path, "w") as f:
|
||||||
|
f.write(base_http.lstrip())
|
||||||
|
print(f" ✓ conf.available/00-http.conf")
|
||||||
|
|
||||||
|
# per-domain: SSL + HTTP fallback
|
||||||
|
ORDER = ["10", "20", "30", "40", "50", "60", "70", "80", "90"]
|
||||||
|
for idx, s in enumerate(site_list):
|
||||||
|
prefix = ORDER[idx] if idx < len(ORDER) else f"{90 + idx}"
|
||||||
|
safe_name = s["name"]
|
||||||
|
server_names = " ".join([s["domain"]] + s["aliases"])
|
||||||
|
|
||||||
|
# --- SSL variant ---
|
||||||
|
ssl_block = f"""# CERT_DOMAIN={s["domain"]}
|
||||||
|
# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {{
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name {server_names};
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/{s["domain"]}/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/{s["domain"]}/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
"""
|
||||||
|
if s["type"] == "upstream":
|
||||||
|
ssl_block += f"""
|
||||||
|
location / {{
|
||||||
|
proxy_pass {s["upstream"]};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
elif s["type"] == "static":
|
||||||
|
ssl_block += f"""
|
||||||
|
location / {{
|
||||||
|
root {s["root"]};
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
for path, target in s["api"].items():
|
||||||
|
cors = ""
|
||||||
|
if "/api/" in path:
|
||||||
|
cors = """
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
ssl_block += f"""
|
||||||
|
location {path} {{
|
||||||
|
proxy_pass {target};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
{cors}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
if s["type"] == "static":
|
||||||
|
ssl_block += f"""
|
||||||
|
location /uploads/ {{
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
ssl_block += "}"
|
||||||
|
|
||||||
|
ssl_path = os.path.join(CONF_AVAILABLE, f"{prefix}-{safe_name}.ssl.conf")
|
||||||
|
with open(ssl_path, "w") as f:
|
||||||
|
f.write(ssl_block.lstrip())
|
||||||
|
|
||||||
|
# --- HTTP fallback variant ---
|
||||||
|
http_block = f"""# HTTP fallback for {s["domain"]} (no SSL cert)
|
||||||
|
server {{
|
||||||
|
listen 80;
|
||||||
|
server_name {server_names};
|
||||||
|
"""
|
||||||
|
if s["type"] == "upstream":
|
||||||
|
http_block += f"""
|
||||||
|
location / {{
|
||||||
|
proxy_pass {s["upstream"]};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
elif s["type"] == "static":
|
||||||
|
http_block += f"""
|
||||||
|
location / {{
|
||||||
|
root {s["root"]};
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
for path, target in s["api"].items():
|
||||||
|
cors = ""
|
||||||
|
if "/api/" in path:
|
||||||
|
cors = """
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
http_block += f"""
|
||||||
|
location {path} {{
|
||||||
|
proxy_pass {target};
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
{cors}
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
if s["type"] == "static":
|
||||||
|
http_block += f"""
|
||||||
|
location /uploads/ {{
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
http_block += "}"
|
||||||
|
|
||||||
|
http_path = os.path.join(CONF_AVAILABLE, f"{prefix}-{safe_name}.http.conf")
|
||||||
|
with open(http_path, "w") as f:
|
||||||
|
f.write(http_block.lstrip())
|
||||||
|
|
||||||
|
print(f" ✓ conf.available/{prefix}-{safe_name}.ssl.conf + .http.conf")
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# 5. Генерация certbot/domains.txt
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
domains_txt_path = os.path.join(BASE_DIR, "certbot", "domains.txt")
|
||||||
|
with open(domains_txt_path, "w") as f:
|
||||||
|
for d in all_domains:
|
||||||
|
f.write(d + "\n")
|
||||||
|
print(f" ✓ {domains_txt_path}")
|
||||||
|
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
# 6. Обновление .env
|
||||||
|
# ──────────────────────────────────────────────
|
||||||
|
if os.path.exists(ENV_FILE):
|
||||||
|
with open(ENV_FILE) as f:
|
||||||
|
env_lines = f.readlines()
|
||||||
|
else:
|
||||||
|
env_lines = []
|
||||||
|
|
||||||
|
new_env = []
|
||||||
|
for line in env_lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("DOMAINS_") or stripped.startswith("ALL_DOMAINS") or "CERTBOT NGINX VARIABLES" in stripped:
|
||||||
|
continue
|
||||||
|
new_env.append(line)
|
||||||
|
|
||||||
|
# удаляем пустые строки в начале
|
||||||
|
while new_env and not new_env[0].strip():
|
||||||
|
new_env.pop(0)
|
||||||
|
|
||||||
|
domain_keys = {k: v for k, v in env_domains.items()}
|
||||||
|
|
||||||
|
insert_idx = None
|
||||||
|
for i, line in enumerate(new_env):
|
||||||
|
if line.strip().startswith("EMAIL="):
|
||||||
|
insert_idx = i + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
env_header = "#CERTBOT NGINX VARIABLES — авто-сгенерировано, не редактировать вручную\n"
|
||||||
|
domain_lines = [f"{k}={v}\n" for k, v in sorted(domain_keys.items())]
|
||||||
|
|
||||||
|
if insert_idx is not None:
|
||||||
|
new_env.insert(insert_idx, env_header)
|
||||||
|
for dl in reversed(domain_lines):
|
||||||
|
new_env.insert(insert_idx + 1, dl)
|
||||||
|
else:
|
||||||
|
new_env = [env_header] + domain_lines + new_env
|
||||||
|
|
||||||
|
with open(ENV_FILE, "w") as f:
|
||||||
|
f.writelines(new_env)
|
||||||
|
print(f" ✓ {ENV_FILE} (обновлён)")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=== Генерация завершена ===")
|
||||||
|
print(f"Сгенерировано {len(site_list)} сайтов:")
|
||||||
|
for s in site_list:
|
||||||
|
print(f" • {s['domain']} ({s['type']})")
|
||||||
|
print()
|
||||||
|
print("Не забудь перезапустить nginx: docker compose restart nginx")
|
||||||
|
PYEOF
|
||||||
@@ -1,28 +1,17 @@
|
|||||||
FROM nginx:alpine
|
FROM nginx:alpine
|
||||||
|
|
||||||
# Установка зависимостей
|
|
||||||
RUN apk add --no-cache bash openssl
|
RUN apk add --no-cache bash openssl
|
||||||
|
|
||||||
# Создание директории для сертификатов
|
# dummy сертификаты для nginx (нужны чтобы nginx стартовал с любым конфигом)
|
||||||
RUN mkdir -p /etc/nginx/ssl
|
RUN mkdir -p /etc/nginx/ssl && \
|
||||||
|
openssl req -x509 -nodes -days 365 \
|
||||||
|
-newkey rsa:2048 \
|
||||||
|
-keyout /etc/nginx/ssl/dummy.key \
|
||||||
|
-out /etc/nginx/ssl/dummy.crt \
|
||||||
|
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
|
||||||
|
|
||||||
# Генерация самоподписанных сертификатов (действительны 365 дней)
|
RUN mkdir -p /var/www/certbot /etc/nginx/conf.d /etc/nginx/conf.available
|
||||||
RUN openssl req -x509 -nodes -days 365 \
|
|
||||||
-newkey rsa:2048 \
|
|
||||||
-keyout /etc/nginx/ssl/dummy.key \
|
|
||||||
-out /etc/nginx/ssl/dummy.crt \
|
|
||||||
-subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
|
|
||||||
|
|
||||||
# Копируем обе конфигурации
|
# per-domain entrypoint для проверки сертификатов
|
||||||
COPY nginx-http.conf /etc/nginx/nginx-http.conf
|
COPY entrypoint.sh /docker-entrypoint.d/switch-config.sh
|
||||||
COPY nginx-ssl.conf /etc/nginx/nginx-ssl.conf
|
|
||||||
|
|
||||||
# Создаем симлинк по умолчанию на HTTP конфиг
|
|
||||||
RUN ln -sf /etc/nginx/nginx-http.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# Скрипт для проверки сертификатов и переключения конфига
|
|
||||||
COPY switch-config.sh /docker-entrypoint.d/switch-config.sh
|
|
||||||
RUN chmod +x /docker-entrypoint.d/switch-config.sh
|
RUN chmod +x /docker-entrypoint.d/switch-config.sh
|
||||||
|
|
||||||
# Создаем необходимые директории
|
|
||||||
RUN mkdir -p /var/www/certbot
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {
|
||||||
|
listen 80 default_server;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/stub/html;
|
||||||
|
index index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /.well-known/acme-challenge/ {
|
||||||
|
root /var/www/certbot;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# HTTP fallback for yalarba.ru (no SSL cert)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name yalarba.ru www.yalarba.ru;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://yalarba:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/v1/ {
|
||||||
|
proxy_pass http://api_yal:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# CERT_DOMAIN=yalarba.ru
|
||||||
|
# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name yalarba.ru www.yalarba.ru;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/yalarba.ru/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/yalarba.ru/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://yalarba:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/v1/ {
|
||||||
|
proxy_pass http://api_yal:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# HTTP fallback for valitovgaziz.ru (no SSL cert)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name valitovgaziz.ru www.valitovgaziz.ru;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://valitovgaziz/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://analytics:3000/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# CERT_DOMAIN=valitovgaziz.ru
|
||||||
|
# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name valitovgaziz.ru www.valitovgaziz.ru;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/valitovgaziz.ru/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://valitovgaziz/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://analytics:3000/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# HTTP fallback for easysite102.ru (no SSL cert)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name easysite102.ru www.easysite102.ru;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://easysite:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/v1/ {
|
||||||
|
proxy_pass http://api_yal:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# CERT_DOMAIN=easysite102.ru
|
||||||
|
# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name easysite102.ru www.easysite102.ru;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/easysite102.ru/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/easysite102.ru/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://easysite:3000;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/v1/ {
|
||||||
|
proxy_pass http://api_yal:8787;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# HTTP fallback for begushiybashkir.ru (no SSL cert)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name begushiybashkir.ru www.begushiybashkir.ru;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/begushiybashkir/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api_bb:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /uploads/ {
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# CERT_DOMAIN=begushiybashkir.ru
|
||||||
|
# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name begushiybashkir.ru www.begushiybashkir.ru;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/begushiybashkir.ru/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/begushiybashkir.ru/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/begushiybashkir/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api_bb:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /uploads/ {
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# HTTP fallback for xn--80abahjtcfl5d0a8di.xn--p1ai (no SSL cert)
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/begushiybashkir/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api_bb:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /uploads/ {
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# CERT_DOMAIN=xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||||
|
# Автоматически сгенерировано generate-configs.sh
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/begushiybashkir/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api_bb:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /uploads/ {
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
│ • certbot - SSL сертификаты │
|
│ • certbot - SSL сертификаты │
|
||||||
│ • analytics - Статистика (Node.js) │
|
│ • analytics - Статистика (Node.js) │
|
||||||
│ • api_tp - API yalarba.ru (Go) │
|
│ • api_tp - API yalarba.ru (Go) │
|
||||||
│ • api_es - API easysite102.ru (Go) │
|
│ • api_yal - API easysite102.ru (Go) │
|
||||||
│ • api_bb - API Бегущий Башкир (Go) │
|
│ • api_bb - API Бегущий Башкир (Go) │
|
||||||
│ • easysite - SPA (Nuxt.js) │
|
│ • easysite - SPA (Nuxt.js) │
|
||||||
│ • db - PostgreSQL (yalarba/easy) │
|
│ • db - PostgreSQL (yalarba/easy) │
|
||||||
@@ -72,9 +72,9 @@
|
|||||||
|
|
||||||
| Домен | Тип | Backend сервис | Путь на диске |
|
| Домен | Тип | Backend сервис | Путь на диске |
|
||||||
|-------|-----|----------------|---------------|
|
|-------|-----|----------------|---------------|
|
||||||
| `yalarba.ru` | SPA (Vue) | `api_tp:8080` | `/usr/share/nginx/yalarba/html` |
|
| `yalarba.ru` | Nuxt 4 SSR | `yalarba:3000` + `api_yal:8787` | Прокси |
|
||||||
| `valitovgaziz.ru` | Статический сайт | - | `/usr/share/nginx/valitovgaziz/html` |
|
| `valitovgaziz.ru` | Статический сайт | - | `/usr/share/nginx/valitovgaziz/html` |
|
||||||
| `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_es:8088` | Прокси |
|
| `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_yal:8787` | Прокси |
|
||||||
| `begushiybashkir.ru` | SPA (Vue) | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
|
| `begushiybashkir.ru` | SPA (Vue) | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
|
||||||
| `xn--80abahjtcfl5d0a8di.xn--p1ai` | Альтернативный домен для Бегущий Башкир | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
|
| `xn--80abahjtcfl5d0a8di.xn--p1ai` | Альтернативный домен для Бегущий Башкир | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
|
||||||
|
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
```
|
```
|
||||||
EMAIL=admin@example.com # Для Let's Encrypt
|
EMAIL=admin@example.com # Для Let's Encrypt
|
||||||
ALL_DOMAINS=yalarba.ru,valitovgaziz.ru... # Все домены для SSL
|
ALL_DOMAINS=yalarba.ru,valitovgaziz.ru... # Все домены для SSL
|
||||||
API_ES_APP_PORT=8088 # Порт API easysite
|
# API_ES убран, используется api_yal:8787
|
||||||
```
|
```
|
||||||
|
|
||||||
### Сервисные
|
### Сервисные
|
||||||
@@ -141,14 +141,14 @@ STAGING=0 # 1 для тестового режима Let's Encrypt
|
|||||||
| certbot | Проверка файла сертификата | - | 30s |
|
| certbot | Проверка файла сертификата | - | 30s |
|
||||||
| analytics | `http://localhost:3000/health` | 3000 | 30s |
|
| analytics | `http://localhost:3000/health` | 3000 | 30s |
|
||||||
| api_tp | `http://localhost:8080/health` | 8080 | 30s |
|
| api_tp | `http://localhost:8080/health` | 8080 | 30s |
|
||||||
| api_es | `http://localhost:8088/health` | 8088 | 30s |
|
| api_yal | `http://localhost:8787/health` | 8787 | 30s |
|
||||||
| api_bb | `http://localhost:8080/api/health` | 8080 | 30s |
|
| api_bb | `http://localhost:8080/api/health` | 8080 | 30s |
|
||||||
| easysite | `http://localhost:3000/api/health` | 3000 | 30s |
|
| easysite | `http://localhost:3000/api/health` | 3000 | 30s |
|
||||||
| db, db_bb | `pg_isready -U postgres` | 5432 | 30s |
|
| db, db_bb | `pg_isready -U postgres` | 5432 | 30s |
|
||||||
|
|
||||||
### Зависимости запуска
|
### Зависимости запуска
|
||||||
Nginx запускается только после подтверждения здоровья:
|
Nginx запускается только после подтверждения здоровья:
|
||||||
- `easysite`, `api_es`, `certbot`, `api_tp`, `api_bb`, `analytics`
|
- `easysite`, `api_yal`, `certbot`, `api_tp`, `api_bb`, `analytics`
|
||||||
|
|
||||||
## Волумы
|
## Волумы
|
||||||
|
|
||||||
@@ -165,7 +165,7 @@ Nginx запускается только после подтверждения
|
|||||||
### Монтирование статических файлов
|
### Монтирование статических файлов
|
||||||
```
|
```
|
||||||
./stubSite → /usr/share/nginx/stub/html
|
./stubSite → /usr/share/nginx/stub/html
|
||||||
./yalarba/serv_spa/spa/vue/dist → /usr/share/nginx/yalarba/html
|
# удалено: serv_spa больше не используется, yalarba работает через Nuxt SSR (yalarba-nuxt)
|
||||||
./valitovgaziz/html → /usr/share/nginx/valitovgaziz/html
|
./valitovgaziz/html → /usr/share/nginx/valitovgaziz/html
|
||||||
./BB/bbvue/dist → /usr/share/nginx/begushiybashkir/html
|
./BB/bbvue/dist → /usr/share/nginx/begushiybashkir/html
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# entrypoint.sh — per-domain HTTPS переключение
|
||||||
|
# Для каждого домена проверяет сертификат и активирует SSL или HTTP конфиг
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
CONF_AVAILABLE="/etc/nginx/conf.available"
|
||||||
|
CONF_D="/etc/nginx/conf.d"
|
||||||
|
CERT_DIR="/etc/letsencrypt/live"
|
||||||
|
|
||||||
|
rm -f "$CONF_D"/*.conf
|
||||||
|
|
||||||
|
# базовый HTTP (ACME challenge, catch-all redirect)
|
||||||
|
if [ -f "$CONF_AVAILABLE/00-http.conf" ]; then
|
||||||
|
ln -sf "$CONF_AVAILABLE/00-http.conf" "$CONF_D/00-http.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# per-domain конфиги
|
||||||
|
shopt -s nullglob
|
||||||
|
for ssl_conf in "$CONF_AVAILABLE"/*.ssl.conf; do
|
||||||
|
base="$(basename "$ssl_conf" .ssl.conf)"
|
||||||
|
http_conf="$CONF_AVAILABLE/$base.http.conf"
|
||||||
|
|
||||||
|
# CERT_DOMAIN в первой строке: # CERT_DOMAIN=example.ru
|
||||||
|
cert_domain="$(head -1 "$ssl_conf" | sed -n 's/.*# CERT_DOMAIN=\(.*\)/\1/p')" || true
|
||||||
|
|
||||||
|
if [ -n "$cert_domain" ] && [ -f "$CERT_DIR/$cert_domain/fullchain.pem" ]; then
|
||||||
|
ln -sf "$ssl_conf" "$CONF_D/$base.ssl.conf"
|
||||||
|
echo " ✓ $base → HTTPS ($cert_domain)"
|
||||||
|
elif [ -f "$http_conf" ]; then
|
||||||
|
ln -sf "$http_conf" "$CONF_D/$base.http.conf"
|
||||||
|
echo " ✓ $base → HTTP (no cert for $cert_domain)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "---"
|
||||||
|
ls -la "$CONF_D/" | grep -v '^total'
|
||||||
|
nginx -t
|
||||||
@@ -1,16 +1,18 @@
|
|||||||
|
# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
|
||||||
|
# HTTP-only конфигурация (работает когда нет сертификатов)
|
||||||
|
|
||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
server_name yalarba.ru \
|
server_name yalarba.ru \
|
||||||
www.yalarba.ru \
|
www.yalarba.ru \
|
||||||
easysite102.ru \
|
|
||||||
www.easysite102.ru \
|
|
||||||
valitovgaziz.ru \
|
valitovgaziz.ru \
|
||||||
www.valitovgaziz.ru \
|
www.valitovgaziz.ru \
|
||||||
xn--80abahjtcfl5d0a8di.xn--p1ai \
|
easysite102.ru \
|
||||||
www.xn--80abahjtcfl5d0a8di.xn--p1ai \
|
www.easysite102.ru \
|
||||||
begushiybashkir.ru \
|
begushiybashkir.ru \
|
||||||
www.begushiybashkir.ru \
|
www.begushiybashkir.ru \
|
||||||
auth.yalarba.ru;
|
xn--80abahjtcfl5d0a8di.xn--p1ai \
|
||||||
|
www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/stub/html;
|
root /usr/share/nginx/stub/html;
|
||||||
@@ -25,12 +27,19 @@ server {
|
|||||||
# Блок для HTTPS → HTTP редиректа (порт 443)
|
# Блок для HTTPS → HTTP редиректа (порт 443)
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name yalarba.ru www.yalarba.ru easysite102.ru www.easysite102.ru valitovgaziz.ru www.valitovgaziz.ru xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai begushiybashkir.ru www.begushiybashkir.ru;
|
server_name yalarba.ru \
|
||||||
|
www.yalarba.ru \
|
||||||
|
valitovgaziz.ru \
|
||||||
|
www.valitovgaziz.ru \
|
||||||
|
easysite102.ru \
|
||||||
|
www.easysite102.ru \
|
||||||
|
begushiybashkir.ru \
|
||||||
|
www.begushiybashkir.ru \
|
||||||
|
xn--80abahjtcfl5d0a8di.xn--p1ai \
|
||||||
|
www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
||||||
|
|
||||||
# Указание пустых сертификатов (обязательно для запуска Nginx)
|
|
||||||
ssl_certificate /etc/nginx/ssl/dummy.crt;
|
ssl_certificate /etc/nginx/ssl/dummy.crt;
|
||||||
ssl_certificate_key /etc/nginx/ssl/dummy.key;
|
ssl_certificate_key /etc/nginx/ssl/dummy.key;
|
||||||
|
|
||||||
# Редирект всех HTTPS-запросов на HTTP
|
|
||||||
return 301 http://$host$request_uri;
|
return 301 http://$host$request_uri;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,341 +1,146 @@
|
|||||||
# ================================================
|
# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
|
||||||
# КОНФИГУРАЦИЯ NGINX С ПОДДЕРЖКОЙ SSL
|
# Полная HTTPS конфигурация
|
||||||
# Основные задачи:
|
|
||||||
# 1. Перенаправление HTTP → HTTPS
|
|
||||||
# 2. Обслуживание статических файлов
|
|
||||||
# 3. Проксирование к backend сервисам
|
|
||||||
# 4. Поддержка нескольких доменов
|
|
||||||
# ================================================
|
|
||||||
|
|
||||||
# ================================================
|
# --- HTTP → HTTPS редирект ---
|
||||||
# БЛОК 1: HTTP СЕРВЕР (ПОРТ 80)
|
|
||||||
# ================================================
|
|
||||||
server {
|
server {
|
||||||
# Прослушивание порта 80 для всех входящих HTTP соединений
|
|
||||||
listen 80;
|
listen 80;
|
||||||
|
|
||||||
# Список доменов, которые обслуживает этот сервер
|
|
||||||
# Все запросы к этим доменам по HTTP будут обработаны здесь
|
|
||||||
server_name yalarba.ru www.yalarba.ru
|
|
||||||
valitovgaziz.ru www.valitovgaziz.ru
|
|
||||||
easysite102.ru www.easysite102.ru
|
|
||||||
begushiybashkir.ru
|
|
||||||
xn--80abahjtcfl5d0a8di.xn--p1ai; # Punycode для IDN домена
|
|
||||||
|
|
||||||
# ============================================
|
server_name yalarba.ru www.yalarba.ru valitovgaziz.ru www.valitovgaziz.ru easysite102.ru www.easysite102.ru begushiybashkir.ru www.begushiybashkir.ru xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
||||||
# ЛОКАЦИЯ: Проверочные файлы для Certbot
|
|
||||||
# ============================================
|
|
||||||
# Этот блок КРИТИЧЕСКИ ВАЖЕН для получения SSL сертификатов
|
|
||||||
# Certbot (Let's Encrypt) размещает здесь временные файлы
|
|
||||||
# для подтверждения владения доменом
|
|
||||||
location /.well-known/acme-challenge/ {
|
location /.well-known/acme-challenge/ {
|
||||||
# Директория, где Certbot хранит проверочные файлы
|
|
||||||
root /var/www/certbot;
|
root /var/www/certbot;
|
||||||
|
|
||||||
# Дополнительные настройки не нужны - nginx просто отдает файлы
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: Основное перенаправление
|
|
||||||
# ============================================
|
|
||||||
# Все HTTP запросы перенаправляются на HTTPS
|
|
||||||
# Это обеспечивает безопасность и правильную SEO-практику
|
|
||||||
location / {
|
|
||||||
# 301 - постоянный редирект (лучше для SEO, кэшируется браузерами)
|
|
||||||
# https://$host$request_uri - сохраняет домен и полный путь запроса
|
|
||||||
return 301 https://$host$request_uri;
|
|
||||||
|
|
||||||
# Пример:
|
|
||||||
# HTTP: http://example.com/page?param=1
|
|
||||||
# ↓ перенаправление ↓
|
|
||||||
# HTTPS: https://example.com/page?param=1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: Загруженные файлы
|
|
||||||
# ============================================
|
|
||||||
# Обслуживание статических файлов (загрузок) по HTTP
|
|
||||||
# Может быть полезно для прямых ссылок или кэширования
|
|
||||||
location /uploads/ {
|
location /uploads/ {
|
||||||
# Псевдоним пути - запросы к /uploads/ обслуживаются из /uploads/ на диске
|
|
||||||
alias /uploads/;
|
alias /uploads/;
|
||||||
|
|
||||||
# Кэширование в браузере на 1 год
|
|
||||||
expires 1y;
|
expires 1y;
|
||||||
|
|
||||||
# Заголовки кэширования:
|
|
||||||
# "public" - может кэшироваться прокси-серверами
|
|
||||||
# "immutable" - файлы никогда не меняются, браузер не проверяет обновления
|
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
# Если файл не найден - вернуть 404 ошибку
|
|
||||||
try_files $uri =404;
|
location / {
|
||||||
|
return 301 https://$host$request_uri;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ================================================
|
# --- HTTPS серверные блоки ---
|
||||||
# БЛОК 2: HTTPS СЕРВЕР ДЛЯ YALARBA.RU
|
|
||||||
# ================================================
|
|
||||||
server {
|
server {
|
||||||
# Прослушивание порта 443 с SSL/TLS шифрованием
|
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
|
|
||||||
# Домены для этого сервера
|
|
||||||
server_name yalarba.ru www.yalarba.ru;
|
server_name yalarba.ru www.yalarba.ru;
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# НАСТРОЙКИ SSL СЕРТИФИКАТОВ
|
|
||||||
# ============================================
|
|
||||||
# Пути к SSL сертификатам, сгенерированным Certbot
|
|
||||||
ssl_certificate /etc/letsencrypt/live/yalarba.ru/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/yalarba.ru/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/yalarba.ru/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/yalarba.ru/privkey.pem;
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# НАСТРОЙКИ БЕЗОПАСНОСТИ SSL
|
|
||||||
# ============================================
|
|
||||||
# Разрешенные протоколы - только современные безопасные версии
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
|
||||||
# Сервер выбирает шифры (не клиент)
|
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
|
|
||||||
# Список безопасных шифров
|
|
||||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: Корневая (SPA приложение)
|
|
||||||
# ============================================
|
|
||||||
location / {
|
location / {
|
||||||
# Директория со скомпилированным Vue/React приложением
|
proxy_pass http://yalarba:3000;
|
||||||
root /usr/share/nginx/yalarba/html;
|
|
||||||
|
|
||||||
# Файл по умолчанию
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Логика SPA роутинга:
|
|
||||||
# 1. Пробуем найти точный файл ($uri)
|
|
||||||
# 2. Пробуем найти директорию ($uri/)
|
|
||||||
# 3. Если не нашли - отдаем index.html
|
|
||||||
# Это позволяет клиентскому роутингу работать корректно
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: REST API Backend
|
|
||||||
# ============================================
|
|
||||||
location /api/ {
|
|
||||||
# Проксирование всех запросов к API на Golang сервис
|
|
||||||
proxy_pass http://api_tp/; # Контейнер Docker
|
|
||||||
|
|
||||||
# Передача оригинальных заголовков от клиента
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
|
||||||
# Увеличенные таймауты для длительных операций (10 минут)
|
|
||||||
proxy_connect_timeout 600;
|
proxy_connect_timeout 600;
|
||||||
proxy_send_timeout 600;
|
proxy_send_timeout 600;
|
||||||
proxy_read_timeout 600;
|
proxy_read_timeout 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
location /auth/ {
|
location /api/v1/ {
|
||||||
# Проксирование всех запросов к API на Golang сервис
|
proxy_pass http://api_yal:8787;
|
||||||
proxy_pass http://api_yal/; # Контейнер Docker
|
|
||||||
|
|
||||||
# Передача оригинальных заголовков от клиента
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
|
||||||
# Увеличенные таймауты для длительных операций (10 минут)
|
|
||||||
proxy_connect_timeout 600;
|
proxy_connect_timeout 600;
|
||||||
proxy_send_timeout 600;
|
proxy_send_timeout 600;
|
||||||
proxy_read_timeout 600;
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ================================================
|
|
||||||
# БЛОК 3: HTTPS СЕРВЕР ДЛЯ VALITOVGAZIZ.RU
|
|
||||||
# ================================================
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name valitovgaziz.ru www.valitovgaziz.ru;
|
server_name valitovgaziz.ru www.valitovgaziz.ru;
|
||||||
|
|
||||||
# Свой SSL сертификат для этого домена
|
|
||||||
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/valitovgaziz.ru/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/valitovgaziz.ru/privkey.pem;
|
||||||
|
|
||||||
# Те же настройки безопасности SSL
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: Статический сайт
|
|
||||||
# ============================================
|
|
||||||
location / {
|
location / {
|
||||||
# Статические HTML файлы
|
proxy_pass http://valitovgaziz/;
|
||||||
root /usr/share/nginx/valitovgaziz/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Стандартная логика для статических сайтов
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: API для аналитики
|
|
||||||
# ============================================
|
|
||||||
location /api/ {
|
|
||||||
# Проксирование на Node.js сервис аналитики
|
|
||||||
proxy_pass http://analytics:3000/;
|
|
||||||
|
|
||||||
# Базовые заголовки прокси
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
}
|
||||||
|
|
||||||
# ========================================
|
location /api/ {
|
||||||
# НАСТРОЙКИ CORS (Cross-Origin Resource Sharing)
|
proxy_pass http://analytics:3000/;
|
||||||
# ========================================
|
proxy_set_header Host $host;
|
||||||
# Разрешаем запросы с ЛЮБОГО домена (*)
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
# Внимание: "*" может быть небезопасно в production
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
add_header Access-Control-Allow-Origin "*" always;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS" always;
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
proxy_connect_timeout 600;
|
||||||
add_header Access-Control-Allow-Credentials "true" always;
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
# Обработка предварительных OPTIONS запросов (preflight)
|
|
||||||
# Браузеры отправляют такие запросы перед основными
|
|
||||||
if ($request_method = OPTIONS) {
|
if ($request_method = OPTIONS) {
|
||||||
# 204 - No Content (успешный пустой ответ)
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Стандартные таймауты для API аналитики
|
|
||||||
proxy_connect_timeout 30s;
|
|
||||||
proxy_send_timeout 30s;
|
|
||||||
proxy_read_timeout 30s;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ================================================
|
|
||||||
# БЛОК 4: HTTPS СЕРВЕР ДЛЯ EASYSITE102.RU
|
|
||||||
# ================================================
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name easysite102.ru www.easysite102.ru;
|
server_name easysite102.ru www.easysite102.ru;
|
||||||
|
|
||||||
# Свой SSL сертификат
|
|
||||||
ssl_certificate /etc/letsencrypt/live/easysite102.ru/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/easysite102.ru/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/easysite102.ru/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/easysite102.ru/privkey.pem;
|
||||||
|
|
||||||
# Безопасные настройки SSL
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: Проксирование к Nuxt.js приложению
|
|
||||||
# ============================================
|
|
||||||
location / {
|
location / {
|
||||||
# ВСЕ запросы проксируются к Nuxt.js серверу
|
|
||||||
proxy_pass http://easysite:3000;
|
proxy_pass http://easysite:3000;
|
||||||
|
|
||||||
# Полный набор заголовков для корректной работы приложения
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
|
||||||
# Длинные таймауты для работы приложения
|
|
||||||
proxy_connect_timeout 600;
|
proxy_connect_timeout 600;
|
||||||
proxy_send_timeout 600;
|
proxy_send_timeout 600;
|
||||||
proxy_read_timeout 600;
|
proxy_read_timeout 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
location /api/v1/ {
|
||||||
# ЛОКАЦИЯ: API Backend для Easysite
|
proxy_pass http://api_yal:8787;
|
||||||
# ============================================
|
|
||||||
location /api/ {
|
|
||||||
# Отдельный API endpoint для backend
|
|
||||||
proxy_pass http://api_es:8088/;
|
|
||||||
|
|
||||||
# Заголовки прокси
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Port $server_port;
|
|
||||||
|
|
||||||
# Таймауты как у основного приложения
|
|
||||||
proxy_connect_timeout 600;
|
|
||||||
proxy_send_timeout 600;
|
|
||||||
proxy_read_timeout 600;
|
|
||||||
|
|
||||||
# ========================================
|
|
||||||
# ДЕТАЛЬНЫЕ НАСТРОЙКИ CORS ДЛЯ OPTIONS
|
|
||||||
# ========================================
|
|
||||||
if ($request_method = OPTIONS ) {
|
|
||||||
# Динамический заголовок Origin из запроса
|
|
||||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
|
||||||
|
|
||||||
# Подробный список разрешенных заголовков
|
|
||||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
|
||||||
|
|
||||||
# Время кэширования preflight ответа (20 дней)
|
|
||||||
add_header 'Access-Control-Max-Age' 1728000;
|
|
||||||
|
|
||||||
# Пустой ответ для OPTIONS
|
|
||||||
add_header 'Content-Length' 0;
|
|
||||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
|
||||||
|
|
||||||
# Возвращаем 204 без тела ответа
|
|
||||||
return 204;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# ================================================
|
|
||||||
# БЛОК 5: HTTPS СЕРВЕР ДЛЯ IDN ДОМЕНА
|
|
||||||
# (Punycode для "бегущийбашкир.рф")
|
|
||||||
# ================================================
|
|
||||||
server {
|
|
||||||
listen 443 ssl;
|
|
||||||
|
|
||||||
# Punycode представление кириллического домена
|
|
||||||
server_name xn--80abahjtcfl5d0a8di.xn--p1ai
|
|
||||||
www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
|
||||||
|
|
||||||
# Отдельный сертификат для IDN домена
|
|
||||||
ssl_certificate /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/fullchain.pem;
|
|
||||||
ssl_certificate_key /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/privkey.pem;
|
|
||||||
|
|
||||||
# Стандартные SSL настройки
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_prefer_server_ciphers on;
|
|
||||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: SPA приложение (такое же как begushiybashkir.ru)
|
|
||||||
# ============================================
|
|
||||||
location / {
|
|
||||||
root /usr/share/nginx/begushiybashkir/html;
|
|
||||||
index index.html;
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: API для "Бегущий Башкир"
|
|
||||||
# ============================================
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://api_bb:8080/;
|
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
@@ -345,8 +150,7 @@ server {
|
|||||||
proxy_send_timeout 600;
|
proxy_send_timeout 600;
|
||||||
proxy_read_timeout 600;
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
# Те же CORS настройки что и у Easysite
|
if ($request_method = OPTIONS) {
|
||||||
if ($request_method = OPTIONS ) {
|
|
||||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
@@ -355,55 +159,26 @@ server {
|
|||||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: Загруженные файлы (статическое обслуживание)
|
|
||||||
# ============================================
|
|
||||||
location /uploads/ {
|
|
||||||
# Обслуживание файлов загрузок напрямую из файловой системы
|
|
||||||
alias /uploads/;
|
|
||||||
|
|
||||||
# Долгое кэширование - файлы загрузок редко меняются
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
|
|
||||||
# try_files не нужен - nginx сам проверит существование файла
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# ================================================
|
|
||||||
# БЛОК 6: HTTPS СЕРВЕР ДЛЯ BEGUSHIYBASHKIR.RU
|
|
||||||
# (ДУБЛИРУЕТ БЛОК 5 С ДРУГИМ ДОМЕНОМ)
|
|
||||||
# ================================================
|
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
server_name begushiybashkir.ru www.begushiybashkir.ru;
|
server_name begushiybashkir.ru www.begushiybashkir.ru;
|
||||||
|
|
||||||
# Свой SSL сертификат для этого домена
|
|
||||||
ssl_certificate /etc/letsencrypt/live/begushiybashkir.ru/fullchain.pem;
|
ssl_certificate /etc/letsencrypt/live/begushiybashkir.ru/fullchain.pem;
|
||||||
ssl_certificate_key /etc/letsencrypt/live/begushiybashkir.ru/privkey.pem;
|
ssl_certificate_key /etc/letsencrypt/live/begushiybashkir.ru/privkey.pem;
|
||||||
|
|
||||||
# Стандартные SSL настройки
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
ssl_prefer_server_ciphers on;
|
ssl_prefer_server_ciphers on;
|
||||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
# ВНИМАНИЕ: Весь контент ниже ДОСЛОВНО ДУБЛИРУЕТ
|
|
||||||
# предыдущий серверный блок для IDN домена
|
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: SPA приложение
|
|
||||||
# ============================================
|
|
||||||
location / {
|
location / {
|
||||||
root /usr/share/nginx/begushiybashkir/html;
|
root /usr/share/nginx/begushiybashkir/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: API для "Бегущий Башкир"
|
|
||||||
# ============================================
|
|
||||||
location /api/ {
|
location /api/ {
|
||||||
proxy_pass http://api_bb:8080/;
|
proxy_pass http://api_bb:8080/;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
@@ -415,8 +190,7 @@ server {
|
|||||||
proxy_send_timeout 600;
|
proxy_send_timeout 600;
|
||||||
proxy_read_timeout 600;
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
# Копия CORS настроек
|
if ($request_method = OPTIONS) {
|
||||||
if ($request_method = OPTIONS ) {
|
|
||||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
@@ -425,17 +199,58 @@ server {
|
|||||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
return 204;
|
return 204;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /uploads/ {
|
||||||
|
alias /uploads/;
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
server_name xn--80abahjtcfl5d0a8di.xn--p1ai www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/fullchain.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/xn--80abahjtcfl5d0a8di.xn--p1ai/privkey.pem;
|
||||||
|
|
||||||
|
ssl_protocols TLSv1.2 TLSv1.3;
|
||||||
|
ssl_prefer_server_ciphers on;
|
||||||
|
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/begushiybashkir/html;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://api_bb:8080/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_connect_timeout 600;
|
||||||
|
proxy_send_timeout 600;
|
||||||
|
proxy_read_timeout 600;
|
||||||
|
|
||||||
|
if ($request_method = OPTIONS) {
|
||||||
|
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ============================================
|
|
||||||
# ЛОКАЦИЯ: Загруженные файлы
|
|
||||||
# ============================================
|
|
||||||
location /uploads/ {
|
location /uploads/ {
|
||||||
alias /uploads/;
|
alias /uploads/;
|
||||||
expires 1y;
|
expires 1y;
|
||||||
add_header Cache-Control "public, immutable";
|
add_header Cache-Control "public, immutable";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# ================================================
|
|
||||||
# КОНЕЦ КОНФИГУРАЦИИ
|
|
||||||
# ================================================
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Единый источник истины для всех сайтов проекта
|
||||||
|
# Добавление нового сайта = одна секция в этом файле
|
||||||
|
# После изменений запусти: bash generate-configs.sh
|
||||||
|
|
||||||
|
sites:
|
||||||
|
yalarba:
|
||||||
|
domain: yalarba.ru
|
||||||
|
aliases:
|
||||||
|
- www.yalarba.ru
|
||||||
|
type: upstream
|
||||||
|
upstream: http://yalarba:3000
|
||||||
|
api:
|
||||||
|
/api/v1/: http://api_yal:8787
|
||||||
|
|
||||||
|
valitovgaziz:
|
||||||
|
domain: valitovgaziz.ru
|
||||||
|
aliases:
|
||||||
|
- www.valitovgaziz.ru
|
||||||
|
type: upstream
|
||||||
|
upstream: http://valitovgaziz/
|
||||||
|
api:
|
||||||
|
/api/: http://analytics:3000/
|
||||||
|
|
||||||
|
easysite102:
|
||||||
|
domain: easysite102.ru
|
||||||
|
aliases:
|
||||||
|
- www.easysite102.ru
|
||||||
|
type: upstream
|
||||||
|
upstream: http://easysite:3000
|
||||||
|
api:
|
||||||
|
/api/v1/: http://api_yal:8787
|
||||||
|
|
||||||
|
begushiybashkir:
|
||||||
|
domain: begushiybashkir.ru
|
||||||
|
aliases:
|
||||||
|
- www.begushiybashkir.ru
|
||||||
|
type: static
|
||||||
|
root: /usr/share/nginx/begushiybashkir/html
|
||||||
|
api:
|
||||||
|
/api/: http://api_bb:8080/
|
||||||
|
|
||||||
|
begushiybashkir_idn:
|
||||||
|
domain: xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||||
|
aliases:
|
||||||
|
- www.xn--80abahjtcfl5d0a8di.xn--p1ai
|
||||||
|
type: static
|
||||||
|
root: /usr/share/nginx/begushiybashkir/html
|
||||||
|
api:
|
||||||
|
/api/: http://api_bb:8080/
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
analytics
|
||||||
|
src
|
||||||
|
package.json
|
||||||
|
package-lock.json
|
||||||
|
jsconfig.json
|
||||||
|
vite.config.js
|
||||||
|
index.html
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
COPY dist /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
# ValitovGaziz - Персональный сайт и портфолио
|
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
🌐 **Live Demo**: [valitovgaziz.ru](https://valitovgaziz.ru) |
|
|
||||||
💼 **Портфолио** |
|
|
||||||
🚀 **Проекты** |
|
|
||||||
👥 **Команда мечты**
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
## 📋 О проекте
|
|
||||||
|
|
||||||
Персональный сайт-портфолио Гализа Валитова - технологического предпринимателя и Fullstack-разработчика. Сайт представляет профессиональный профиль, проекты и возможности для сотрудничества.
|
|
||||||
|
|
||||||
### 🎯 Основные разделы:
|
|
||||||
- **Обо мне** - профессиональный профиль и подход к работе
|
|
||||||
- **Проекты** - текущие и завершенные разработки
|
|
||||||
- **Команда мечты** - приглашение к сотрудничеству
|
|
||||||
- **Yalarba.ru** - флагманский Travel Tech проект
|
|
||||||
- **Навыки** - технический стек и экспертиза
|
|
||||||
- **Опыт работы** - карьерный путь
|
|
||||||
|
|
||||||
## 🛠 Технологический стек
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **HTML5** - семантическая разметка
|
|
||||||
- **CSS3** - кастомные стили и анимации
|
|
||||||
- **JavaScript (ES6+)** - интерактивность и логика
|
|
||||||
- **Vue3.js** - современный фронтенд фреймворк
|
|
||||||
- **Nuxt.js 4** - SSR/SSG приложения
|
|
||||||
|
|
||||||
### Backend (Analytics Server)
|
|
||||||
- **Node.js** - серверная платформа
|
|
||||||
- **Express.js** - веб-фреймворк
|
|
||||||
- **Helmet** - безопасность HTTP заголовков
|
|
||||||
- **CORS** - кросс-доменные запросы
|
|
||||||
- **Compression** - сжатие ответов
|
|
||||||
- **Morgan** - логирование запросов
|
|
||||||
|
|
||||||
### Базы данных и инфраструктура
|
|
||||||
- **PostgreSQL** - реляционная БД
|
|
||||||
- **Docker** - контейнеризация
|
|
||||||
- **Docker Swarm** - оркестрация
|
|
||||||
|
|
||||||
## 🚀 Быстрый старт
|
|
||||||
|
|
||||||
### Предварительные требования
|
|
||||||
- Node.js 18+
|
|
||||||
- npm или yarn
|
|
||||||
- Современный браузер
|
|
||||||
|
|
||||||
### Установка и запуск
|
|
||||||
|
|
||||||
1. **Клонирование репозитория**
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/valitovgaziz/valitovgaziz.ru.git
|
|
||||||
cd valitovgaziz.ru
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
# Документация: ValitovGaziz.ru
|
|
||||||
|
|
||||||
## Обзор проекта
|
|
||||||
|
|
||||||
**ValitovGaziz.ru** — это персональный сайт-портфолио Гализа Валитова, технологического предпринимателя и Fullstack-разработчика. Сайт представляет собой профессиональную визитную карточку, демонстрирующую навыки, проекты и возможности для сотрудничества.
|
|
||||||
|
|
||||||
### Основные характеристики
|
|
||||||
- **Современный дизайн** с адаптивной версткой
|
|
||||||
- **Темная/светлая тема** с автоматическим определением системных предпочтений
|
|
||||||
- **Интерактивные элементы** для вовлечения пользователей
|
|
||||||
- **Цифровой фон** с анимациями в стиле "матрицы"
|
|
||||||
- **Полностью статический** (без серверного рендеринга)
|
|
||||||
- **Оптимизирован для SEO** и доступности
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
valitovgaziz.ru/
|
|
||||||
├── index.html # Главная страница
|
|
||||||
├── style.css # Основной файл стилей
|
|
||||||
├── scripts.js # Основные скрипты
|
|
||||||
├── darkThemeToggle.js # Переключение темной темы
|
|
||||||
├── digital_background.js # Создание цифрового фона
|
|
||||||
├── analytics.js # Пользовательская аналитика
|
|
||||||
├── README.md # Документация проекта
|
|
||||||
├── images/ # Изображения и иконки
|
|
||||||
│ ├── ValitovGaziz/ # Фотографии
|
|
||||||
│ └── favicon/ # Иконки и логотипы
|
|
||||||
└── style/ # Стилевые файлы
|
|
||||||
├── about.css
|
|
||||||
├── darkTheme.css
|
|
||||||
├── digital_background.css
|
|
||||||
├── footer.css
|
|
||||||
├── hero_section.css
|
|
||||||
├── links_style.css
|
|
||||||
├── repository_section.css
|
|
||||||
├── saveContactsButtonStyle.css
|
|
||||||
├── skill_section.css
|
|
||||||
├── social_link.css
|
|
||||||
└── yalarba_investmen.css
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Структура сайта
|
|
||||||
|
|
||||||
### 1. Hero Section (Заголовок)
|
|
||||||
**Файлы:** `hero_section.css`, `digital_background.css`
|
|
||||||
- Главный заголовок с приветствием
|
|
||||||
- Кнопки действий "Обсудить сотрудничество" и "Написать мне"
|
|
||||||
- Социальные ссылки (Telegram, VK)
|
|
||||||
- Кнопка переключения темы
|
|
||||||
- Анимированный цифровой фон
|
|
||||||
|
|
||||||
### 2. Обо мне (About Section)
|
|
||||||
**Файлы:** `about.css`
|
|
||||||
- Фотография профиля
|
|
||||||
- Описание профессионального подхода
|
|
||||||
- Ключевые компетенции:
|
|
||||||
- Техническое видение
|
|
||||||
- Бизнес-ориентация
|
|
||||||
- Практический подход
|
|
||||||
- Мотивация
|
|
||||||
|
|
||||||
### 3. О репозитории (Repository Section)
|
|
||||||
**Файлы:** `repository_section.css`
|
|
||||||
- Сетка проектов (3 карточки):
|
|
||||||
1. ValitovGaziz.ru
|
|
||||||
2. Yalarba.ru
|
|
||||||
3. BegushiyBashkir.ru
|
|
||||||
- Информация о текущей работе
|
|
||||||
- Ссылки на GitHub и проекты
|
|
||||||
|
|
||||||
### 4. Команда мечты (Team Section)
|
|
||||||
- Приглашение к сотрудничеству
|
|
||||||
- Роли для найма:
|
|
||||||
- Программисты
|
|
||||||
- Дизайнеры
|
|
||||||
- Аналитики
|
|
||||||
- Продавцы-стратеги
|
|
||||||
- Преимущества участия
|
|
||||||
- Кнопка "Присоединиться к команде"
|
|
||||||
|
|
||||||
### 5. Yalarba.ru (Travel Tech Project)
|
|
||||||
**Файлы:** `yalarba_investmen.css`
|
|
||||||
- Описание флагманского проекта
|
|
||||||
- Технологический стек
|
|
||||||
- Статистика и ценностное предложение
|
|
||||||
- Инвестиционные возможности
|
|
||||||
|
|
||||||
### 6. Навыки (Skills Section)
|
|
||||||
**Файлы:** `skill_section.css`
|
|
||||||
- Карточки навыков с уровнями:
|
|
||||||
- Golang (Продвинутый)
|
|
||||||
- JavaScript (Продвинутый)
|
|
||||||
- Vue3 (Средний)
|
|
||||||
- Nuxt (Средний)
|
|
||||||
- PostgreSQL (Средний)
|
|
||||||
- Docker (Средний)
|
|
||||||
- Java (Начинающий)
|
|
||||||
- Spring Framework (Начинающий)
|
|
||||||
|
|
||||||
### 7. Опыт работы и образование
|
|
||||||
- Таймлайн профессионального опыта
|
|
||||||
- Образование и курсы
|
|
||||||
- Языки
|
|
||||||
|
|
||||||
### 8. Контакты
|
|
||||||
**Файлы:** `saveContactsButtonStyle.css`
|
|
||||||
- Контактная информация
|
|
||||||
- Кнопка "Сохранить контакт" (vCard формат)
|
|
||||||
- Социальные сети и мессенджеры
|
|
||||||
|
|
||||||
### 9. Футер
|
|
||||||
**Файлы:** `footer.css`
|
|
||||||
- Технологии
|
|
||||||
- Контакты
|
|
||||||
- Сообщество
|
|
||||||
- Авторские права
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Технические особенности
|
|
||||||
|
|
||||||
### Темная тема
|
|
||||||
**Файлы:** `darkTheme.css`, `darkThemeToggle.js`
|
|
||||||
- Автоматическое определение системных предпочтений
|
|
||||||
- Сохранение выбора в localStorage
|
|
||||||
- Полная поддержка всех элементов интерфейса
|
|
||||||
|
|
||||||
### Цифровой фон
|
|
||||||
**Файлы:** `digital_background.css`, `digital_background.js`
|
|
||||||
- Анимированные двоичные потоки (бинарный дождь)
|
|
||||||
- Плавающие элементы кода
|
|
||||||
- Точки соединений и линии передачи данных
|
|
||||||
- Адаптация под текущую тему
|
|
||||||
|
|
||||||
### Аналитика
|
|
||||||
**Файл:** `analytics.js`
|
|
||||||
- Пользовательская система сбора данных
|
|
||||||
- Отслеживание событий и кликов
|
|
||||||
- Очередь с автосохранением в localStorage
|
|
||||||
- Отправка данных на сервер при возможности
|
|
||||||
- Отслеживание видимости секций
|
|
||||||
|
|
||||||
### Ссылки
|
|
||||||
**Файл:** `links_style.css`
|
|
||||||
- Анимированные внешние ссылки с иконками
|
|
||||||
- Индикация внутренних/внешних ссылок
|
|
||||||
- Адаптация под тему
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Технологический стек
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
- **HTML5** — семантическая разметка
|
|
||||||
- **CSS3** — Grid, Flexbox, CSS Variables, анимации
|
|
||||||
- **JavaScript (ES6+)** — нативный JS без фреймворков
|
|
||||||
- **CSS Grid Layout** — основная система верстки
|
|
||||||
|
|
||||||
### Особенности CSS
|
|
||||||
- CSS Custom Properties (переменные) для тем
|
|
||||||
- CSS Grid для сложных макетов
|
|
||||||
- CSS Flexbox для простых выравниваний
|
|
||||||
- CSS Animations для интерактивности
|
|
||||||
- Media Queries для адаптивности
|
|
||||||
|
|
||||||
### JavaScript функциональность
|
|
||||||
- Динамическое переключение тем
|
|
||||||
- Создание интерактивного фона
|
|
||||||
- Обработка форм и кнопок
|
|
||||||
- Сохранение контактов в vCard формате
|
|
||||||
- Интеграция с Telegram API
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## SEO и доступность
|
|
||||||
|
|
||||||
### Мета-теги
|
|
||||||
- Полный набор meta-тегов для SEO
|
|
||||||
- Ключевые слова для IT-специалистов и предпринимателей
|
|
||||||
- Атрибуты для доступности (alt, aria)
|
|
||||||
|
|
||||||
### Оптимизация
|
|
||||||
- Ленивая загрузка изображений
|
|
||||||
- Минификация CSS и JS
|
|
||||||
- Оптимизированные шрифты
|
|
||||||
- Быстрая загрузка страницы
|
|
||||||
|
|
||||||
### Адаптивность
|
|
||||||
- Mobile-first подход
|
|
||||||
- 4 точки останова:
|
|
||||||
- < 480px (мобильные)
|
|
||||||
- 480px - 768px (планшеты)
|
|
||||||
- 769px - 1024px (ноутбуки)
|
|
||||||
- > 1024px (десктопы)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Интеграции
|
|
||||||
|
|
||||||
### Telegram
|
|
||||||
- Отправка сообщений через Telegram Bot API
|
|
||||||
- Обработка кнопок "Написать мне"
|
|
||||||
- Форма для отправки предложений
|
|
||||||
|
|
||||||
### vCard
|
|
||||||
- Генерация контактов в формате vCard
|
|
||||||
- Автоматическое скачивание контакта
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Рекомендации по развитию
|
|
||||||
|
|
||||||
### Для добавления нового раздела:
|
|
||||||
1. Создайте HTML структуру в `index.html`
|
|
||||||
2. Добавьте стили в соответствующий CSS файл
|
|
||||||
3. Подключите через `@import` в `style.css`
|
|
||||||
4. Добавьте поддержку темной темы
|
|
||||||
5. Интегрируйте с аналитикой
|
|
||||||
|
|
||||||
### Для модификации существующего:
|
|
||||||
1. Найдите соответствующий CSS файл
|
|
||||||
2. Внесите изменения с учетом адаптивности
|
|
||||||
3. Проверьте поддержку темной темы
|
|
||||||
4. Протестируйте на разных устройствах
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Производительность
|
|
||||||
|
|
||||||
### Рекомендации по оптимизации:
|
|
||||||
1. **Изображения:** Используйте WebP формат с JPEG/PNG fallback
|
|
||||||
2. **Шрифты:** Локальное хранение системных шрифтов
|
|
||||||
3. **JavaScript:** Дефер загрузки скриптов
|
|
||||||
4. **CSS:** Критический CSS в head
|
|
||||||
|
|
||||||
### Мониторинг:
|
|
||||||
- Встроенная аналитика отслеживает загрузку страниц
|
|
||||||
- Google Analytics можно подключить через `analytics.js`
|
|
||||||
- Рекомендуется использовать Lighthouse для аудита
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Поддержка браузеров
|
|
||||||
|
|
||||||
- **Chrome** 60+
|
|
||||||
- **Firefox** 55+
|
|
||||||
- **Safari** 12+
|
|
||||||
- **Edge** 79+
|
|
||||||
- **iOS Safari** 12+
|
|
||||||
- **Android Chrome** 60+
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Лицензия
|
|
||||||
|
|
||||||
Проект распространяется под лицензией MIT. Все изображения и контент защищены авторскими правами Гализа Валитова.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Контакты для поддержки
|
|
||||||
|
|
||||||
- **Телеграм:** [@valitovgaziz](https://t.me/valitovgaziz)
|
|
||||||
- **Email:** valitovgaziz@yandex.ru
|
|
||||||
- **GitHub:** [valitovgaziz](https://github.com/valitovgaziz)
|
|
||||||
- **Сайт:** [valitovgaziz.ru](https://valitovgaziz.ru)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*Последнее обновление документации: 2025*
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
// analytics.js - собственный счетчик аналитики для браузера
|
|
||||||
class CustomAnalytics {
|
|
||||||
constructor() {
|
|
||||||
this.endpoint = 'https://valitovgaziz.ru/api/analytics'; // Ваш endpoint для сбора данных
|
|
||||||
this.queue = [];
|
|
||||||
this.isOnline = navigator.onLine;
|
|
||||||
this.sessionId = this.getSessionId();
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Загружаем сохраненные данные из localStorage
|
|
||||||
this.loadFromStorage();
|
|
||||||
|
|
||||||
// Отслеживание событий
|
|
||||||
this.trackPageView();
|
|
||||||
this.setupEventListeners();
|
|
||||||
|
|
||||||
// Периодическая отправка данных
|
|
||||||
setInterval(() => this.flushQueue(), 30000);
|
|
||||||
|
|
||||||
// Отслеживание онлайн/офлайн статуса
|
|
||||||
window.addEventListener('online', () => {
|
|
||||||
this.isOnline = true;
|
|
||||||
this.flushQueue();
|
|
||||||
});
|
|
||||||
window.addEventListener('offline', () => {
|
|
||||||
this.isOnline = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отправка данных перед закрытием страницы
|
|
||||||
window.addEventListener('beforeunload', () => {
|
|
||||||
this.trackEvent('page', 'unload');
|
|
||||||
this.flushQueueSync();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
trackPageView() {
|
|
||||||
const data = {
|
|
||||||
type: 'pageview',
|
|
||||||
url: window.location.href,
|
|
||||||
referrer: document.referrer,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
screen: `${screen.width}x${screen.height}`,
|
|
||||||
language: navigator.language,
|
|
||||||
sessionId: this.sessionId
|
|
||||||
};
|
|
||||||
this.addToQueue(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackEvent(category, action, label = null, value = null) {
|
|
||||||
const data = {
|
|
||||||
type: 'event',
|
|
||||||
category,
|
|
||||||
action,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
url: window.location.href,
|
|
||||||
sessionId: this.sessionId
|
|
||||||
};
|
|
||||||
this.addToQueue(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
trackClick(element, context = 'unknown') {
|
|
||||||
const data = {
|
|
||||||
type: 'click',
|
|
||||||
element: element.tagName,
|
|
||||||
text: element.textContent?.substring(0, 100),
|
|
||||||
context,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
url: window.location.href,
|
|
||||||
sessionId: this.sessionId
|
|
||||||
};
|
|
||||||
this.addToQueue(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
addToQueue(data) {
|
|
||||||
this.queue.push(data);
|
|
||||||
|
|
||||||
// Сохраняем в localStorage
|
|
||||||
this.saveToStorage();
|
|
||||||
|
|
||||||
// Отправляем сразу если онлайн и очередь большая
|
|
||||||
if (this.isOnline && this.queue.length >= 3) {
|
|
||||||
this.flushQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async flushQueue() {
|
|
||||||
if (!this.isOnline || this.queue.length === 0) return;
|
|
||||||
|
|
||||||
const batch = [...this.queue];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
events: batch,
|
|
||||||
sessionId: this.sessionId
|
|
||||||
}),
|
|
||||||
keepalive: true // Позволяет отправлять данные даже при закрытии страницы
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
// Удаляем отправленные данные из очереди
|
|
||||||
this.queue = this.queue.filter(item => !batch.includes(item));
|
|
||||||
this.saveToStorage();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Analytics offline, storing locally');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flushQueueSync() {
|
|
||||||
if (this.queue.length === 0) return;
|
|
||||||
|
|
||||||
// Используем sendBeacon для надежной отправки при закрытии страницы
|
|
||||||
const data = JSON.stringify({
|
|
||||||
events: this.queue,
|
|
||||||
sessionId: this.sessionId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (navigator.sendBeacon) {
|
|
||||||
navigator.sendBeacon(this.endpoint, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getSessionId() {
|
|
||||||
let sessionId = localStorage.getItem('ga_session_id');
|
|
||||||
const now = Date.now();
|
|
||||||
|
|
||||||
if (!sessionId) {
|
|
||||||
sessionId = 'sess_' + now + '_' + Math.random().toString(36).substr(2, 9);
|
|
||||||
localStorage.setItem('ga_session_id', sessionId);
|
|
||||||
localStorage.setItem('ga_session_start', now);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем время последней активности
|
|
||||||
localStorage.setItem('ga_last_activity', now);
|
|
||||||
|
|
||||||
return sessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
saveToStorage() {
|
|
||||||
try {
|
|
||||||
localStorage.setItem('ga_queue', JSON.stringify(this.queue));
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Cannot save analytics to localStorage');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadFromStorage() {
|
|
||||||
try {
|
|
||||||
const stored = localStorage.getItem('ga_queue');
|
|
||||||
if (stored) {
|
|
||||||
const parsed = JSON.parse(stored);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
this.queue = parsed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Cannot load analytics from localStorage');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setupEventListeners() {
|
|
||||||
// Отслеживание кликов по кнопкам
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (e.target.matches('button, .btn, a[href]')) {
|
|
||||||
const context = e.target.closest('.section') ?
|
|
||||||
e.target.closest('.section').querySelector('h2')?.textContent || 'unknown' :
|
|
||||||
'global';
|
|
||||||
this.trackClick(e.target, context);
|
|
||||||
|
|
||||||
// Специальные события для кнопок сотрудничества
|
|
||||||
if (e.target.textContent.includes('сотрудничество') || e.target.textContent.includes('Написать')) {
|
|
||||||
this.trackEvent('conversion', 'contact_click', e.target.textContent.trim());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отслеживание отправки форм
|
|
||||||
document.addEventListener('submit', (e) => {
|
|
||||||
this.trackEvent('form', 'submit', e.target.id || 'unknown');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отслеживание видимости секций
|
|
||||||
this.setupSectionTracking();
|
|
||||||
|
|
||||||
// Отслеживание внешних ссылок
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
const link = e.target.closest('a[href]');
|
|
||||||
if (link && link.hostname !== window.location.hostname) {
|
|
||||||
this.trackEvent('outbound', 'click', link.href);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSectionTracking() {
|
|
||||||
const sections = document.querySelectorAll('.section');
|
|
||||||
const observedSections = new Set();
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting && entry.intersectionRatio >= 0.5) {
|
|
||||||
const sectionId = entry.target.id ||
|
|
||||||
entry.target.querySelector('h2')?.textContent?.substring(0, 50) ||
|
|
||||||
'unknown_section';
|
|
||||||
|
|
||||||
if (!observedSections.has(sectionId)) {
|
|
||||||
observedSections.add(sectionId);
|
|
||||||
this.trackEvent('content', 'section_view', sectionId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, {
|
|
||||||
threshold: [0.5],
|
|
||||||
rootMargin: '0px 0px -10% 0px'
|
|
||||||
});
|
|
||||||
|
|
||||||
sections.forEach(section => {
|
|
||||||
observer.observe(section);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализация при полной загрузке DOM
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
window.analytics = new CustomAnalytics();
|
|
||||||
|
|
||||||
// Глобальные функции для ручного отслеживания
|
|
||||||
window.trackEvent = (category, action, label, value) => {
|
|
||||||
if (window.analytics) {
|
|
||||||
window.analytics.trackEvent(category, action, label, value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Отслеживание специальных событий для вашего сайта
|
|
||||||
const specialButtons = document.querySelectorAll('[onclick*="sendMessageTelegram"]');
|
|
||||||
specialButtons.forEach(btn => {
|
|
||||||
btn.addEventListener('click', () => {
|
|
||||||
trackEvent('business', 'telegram_click', btn.textContent.trim());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Отслеживание просмотра ключевых элементов
|
|
||||||
const keyElements = document.querySelectorAll('.hero, .team-section, .yalarba-section');
|
|
||||||
const elementObserver = new IntersectionObserver((entries) => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
const elementType = entry.target.className.split(' ')[0];
|
|
||||||
trackEvent('engagement', `${elementType}_viewed`);
|
|
||||||
elementObserver.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { threshold: 0.3 });
|
|
||||||
|
|
||||||
keyElements.forEach(el => elementObserver.observe(el));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fallback для старых браузеров
|
|
||||||
if (!window.Promise) {
|
|
||||||
console.warn('Custom analytics requires Promise support');
|
|
||||||
window.trackEvent = function () { };
|
|
||||||
}
|
|
||||||
@@ -1,590 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description"
|
|
||||||
content="Блог Валитова Газиза - мысли, проекты, обновления и размышления о разработке и предпринимательстве">
|
|
||||||
<title>Блог | ValitovGaziz - Мысли и обновления</title>
|
|
||||||
<link rel="icon" href="./images/favicon/code_orange.png">
|
|
||||||
<link rel="stylesheet" href="style/blog.css" />
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<!-- Кнопка переключения темы -->
|
|
||||||
<button class="theme-toggle" onclick="toggleTheme()">
|
|
||||||
🌙 Темная тема
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Навигация -->
|
|
||||||
<nav class="blog-nav">
|
|
||||||
<div class="blog-nav-container">
|
|
||||||
<a href="index.html" class="blog-nav-logo">ValitovGaziz</a>
|
|
||||||
<a href="index.html" class="blog-nav-link">← На главную</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Заголовок блога -->
|
|
||||||
<header class="blog-header">
|
|
||||||
<div class="blog-header-content">
|
|
||||||
<h1 class="blog-title">Блог</h1>
|
|
||||||
<p class="blog-subtitle">Мысли, проекты и обновления из мира разработки и предпринимательства</p>
|
|
||||||
<div class="blog-meta">
|
|
||||||
<span class="blog-meta-item">📝 Личный блог</span>
|
|
||||||
<span class="blog-meta-item">🔄 Регулярные обновления</span>
|
|
||||||
<span class="blog-meta-item">🎯 Фокус на содержании</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="blog-container">
|
|
||||||
<!-- Кнопка для мобильного меню (скрыта на десктопе) -->
|
|
||||||
<button class="blog-sidebar-toggle" onclick="toggleSidebar()">
|
|
||||||
📂 Меню блога
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Основное содержание блога - ЛЕВАЯ КОЛОНКА (70%) -->
|
|
||||||
<div class="blog-content">
|
|
||||||
<!-- Пример записи блога -->
|
|
||||||
<article class="blog-post" id="post1">
|
|
||||||
<header class="blog-post-header">
|
|
||||||
<span class="blog-post-category">Проекты</span>
|
|
||||||
<h2 class="blog-post-title">Новый этап развития Yalarba.ru</h2>
|
|
||||||
<div class="blog-post-meta">
|
|
||||||
<time datetime="2024-03-15">15 марта 2024</time>
|
|
||||||
<span>•</span>
|
|
||||||
<span>5 минут чтения</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="blog-post-content">
|
|
||||||
<p>Сегодня хочу поделиться важным обновлением по проекту Yalarba.ru. Мы завершили переход на новую
|
|
||||||
архитектуру и готовимся к запуску нескольких ключевых функций, которые существенно улучшат
|
|
||||||
пользовательский опыт.</p>
|
|
||||||
|
|
||||||
<h3>Что изменилось:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Полностью переработанный интерфейс поиска маршрутов</li>
|
|
||||||
<li>Интеграция с картографическими сервисами</li>
|
|
||||||
<li>Улучшенная система рекомендаций</li>
|
|
||||||
<li>Подготовка к мобильному приложению</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Этот этап занял больше времени, чем планировалось, но результат того стоит. Особенно горжусь тем,
|
|
||||||
как команда справилась с техническими вызовами.</p>
|
|
||||||
|
|
||||||
<blockquote class="blog-quote">
|
|
||||||
"Технологии должны решать реальные проблемы людей, а не создавать новые"
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<p>В ближайших планах — запуск бета-тестирования новых функций и привлечение первых партнеров из
|
|
||||||
туристической отрасли.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="blog-post-footer">
|
|
||||||
<div class="blog-post-tags">
|
|
||||||
<a href="#" class="blog-tag">#Yalarba</a>
|
|
||||||
<a href="#" class="blog-tag">#TravelTech</a>
|
|
||||||
<a href="#" class="blog-tag">#Разработка</a>
|
|
||||||
</div>
|
|
||||||
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
|
|
||||||
💬 Обсудить
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<!-- Вторая запись -->
|
|
||||||
<article class="blog-post" id="post2">
|
|
||||||
<header class="blog-post-header">
|
|
||||||
<span class="blog-post-category">Разработка</span>
|
|
||||||
<h2 class="blog-post-title">Переход с Vue 2 на Vue 3: опыт и выводы</h2>
|
|
||||||
<div class="blog-post-meta">
|
|
||||||
<time datetime="2024-03-10">10 марта 2024</time>
|
|
||||||
<span>•</span>
|
|
||||||
<span>7 минут чтения</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="blog-post-content">
|
|
||||||
<p>После нескольких месяцев работы с Vue 3 в продакшене хочу поделиться наблюдениями о переходе с
|
|
||||||
Vue 2.</p>
|
|
||||||
|
|
||||||
<h3>Основные преимущества:</h3>
|
|
||||||
<ol>
|
|
||||||
<li><strong>Composition API</strong> — действительно улучшает переиспользование кода</li>
|
|
||||||
<li><strong>Улучшенная производительность</strong> — заметный прирост в больших приложениях</li>
|
|
||||||
<li><strong>TypeScript поддержка</strong> — наконец-то полноценная интеграция</li>
|
|
||||||
<li><strong>Меньший размер бандла</strong> — tree-shaking работает лучше</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h3>Сложности перехода:</h3>
|
|
||||||
<p>Не всё прошло гладко. Некоторые библиотеки ещё не обновились, пришлось искать альтернативы или
|
|
||||||
писать собственные решения. Также Composition API требует изменения мышления, особенно для
|
|
||||||
разработчиков, долго работавших с Options API.</p>
|
|
||||||
|
|
||||||
<p>В целом, переход оправдан. Особенно для новых проектов — рекомендую сразу начинать с Vue 3.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="blog-post-footer">
|
|
||||||
<div class="blog-post-tags">
|
|
||||||
<a href="#" class="blog-tag">#Vue3</a>
|
|
||||||
<a href="#" class="blog-tag">#Frontend</a>
|
|
||||||
<a href="#" class="blog-tag">#JavaScript</a>
|
|
||||||
</div>
|
|
||||||
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
|
|
||||||
💬 Обсудить
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<!-- Третья запись -->
|
|
||||||
<article class="blog-post" id="post3">
|
|
||||||
<header class="blog-post-header">
|
|
||||||
<span class="blog-post-category">Мысли</span>
|
|
||||||
<h2 class="blog-post-title">О важности сообщества в разработке</h2>
|
|
||||||
<div class="blog-post-meta">
|
|
||||||
<time datetime="2024-03-05">5 марта 2024</time>
|
|
||||||
<span>•</span>
|
|
||||||
<span>4 минуты чтения</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="blog-post-content">
|
|
||||||
<p>В последнее время всё чаще задумываюсь о том, насколько важно окружение для профессионального
|
|
||||||
роста. Особенно в IT, где технологии меняются так быстро.</p>
|
|
||||||
|
|
||||||
<p>Когда работаешь один, легко застрять в своих паттернах, не замечать новые подходы или повторять
|
|
||||||
одни и те же ошибки. Сообщество — это не только нетворкинг, это:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><strong>Обратная связь</strong> — свежий взгляд на твои решения</li>
|
|
||||||
<li><strong>Совместное обучение</strong> — каждый знает что-то, чего не знаешь ты</li>
|
|
||||||
<li><strong>Поддержка</strong> — особенно важна в сложные периоды</li>
|
|
||||||
<li><strong>Вдохновение</strong> — видеть успехи других мотивирует</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Именно поэтому я решил больше инвестировать в развитие сообщества вокруг своих проектов. Если вы
|
|
||||||
читаете это — возможно, нам стоит пообщаться :)</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="blog-post-footer">
|
|
||||||
<div class="blog-post-tags">
|
|
||||||
<a href="#" class="blog-tag">#Сообщество</a>
|
|
||||||
<a href="#" class="blog-tag">#Развитие</a>
|
|
||||||
<a href="#" class="blog-tag">#IT</a>
|
|
||||||
</div>
|
|
||||||
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
|
|
||||||
💬 Присоединиться
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<!-- Четвёртая запись -->
|
|
||||||
<article class="blog-post" id="post4">
|
|
||||||
<header class="blog-post-header">
|
|
||||||
<span class="blog-post-category">Проекты</span>
|
|
||||||
<h2 class="blog-post-title">EasySite & YalArba: Текущее состояние и роадмап развития</h2>
|
|
||||||
<div class="blog-post-meta">
|
|
||||||
<time datetime="2024-03-20">20 марта 2024</time>
|
|
||||||
<span>•</span>
|
|
||||||
<span>6 минут чтения</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="blog-post-content">
|
|
||||||
<p>С момента запуска первых версий <strong>EasySite102.ru</strong> и <strong>YalArba.ru</strong>
|
|
||||||
прошло
|
|
||||||
несколько месяцев интенсивной разработки. Хочу поделиться текущим состоянием проекта,
|
|
||||||
достигнутыми
|
|
||||||
результатами и планами на ближайшее будущее.</p>
|
|
||||||
|
|
||||||
<h3>🎯 Суть проекта сегодня</h3>
|
|
||||||
<p>Мы строим полноценную экосистему для туристического рынка:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>EasySite (B2B)</strong> — конструктор сайтов для владельцев отелей, санаториев,
|
|
||||||
ресторанов и
|
|
||||||
достопримечательностей</li>
|
|
||||||
<li><strong>YalArba (B2C)</strong> — агрегатор для туристов с поиском, отзывами, маршрутами и
|
|
||||||
системой
|
|
||||||
бронирования</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>✅ Что уже работает (стабильно в продакшене)</h3>
|
|
||||||
<ul>
|
|
||||||
<li><strong>JWT-аутентификация</strong> — безопасный вход для всех типов пользователей</li>
|
|
||||||
<li><strong>Полностью контейнеризованная инфраструктура</strong> — Docker, Docker Compose</li>
|
|
||||||
<li><strong>SSL шифрование</strong> — HTTPS на всех доменах через Let's Encrypt</li>
|
|
||||||
<li><strong>Базовая аналитика</strong> — отслеживание посещений и пользовательского поведения
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>🛠️ Технологический стек (актуальный)</h3>
|
|
||||||
<div class="tech-stack">
|
|
||||||
<div class="tech-item">
|
|
||||||
<strong>Frontend:</strong> Nuxt.js 3 (EasySite), Vue 3 + Composition API (YalArba)
|
|
||||||
</div>
|
|
||||||
<div class="tech-item">
|
|
||||||
<strong>Backend:</strong> Go (Golang) с использованием GORM, Chi
|
|
||||||
</div>
|
|
||||||
<div class="tech-item">
|
|
||||||
<strong>База данных:</strong> PostgreSQL (раздельные инстансы для разных сервисов)
|
|
||||||
</div>
|
|
||||||
<div class="tech-item">
|
|
||||||
<strong>Инфраструктура:</strong> Docker, Nginx, система автоматического обновления SSL
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>📊 API-архитектура</h3>
|
|
||||||
<p>Проект построен по микросервисной архитектуре:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>EasySite API:</strong> <code>localhost:8088/docs</code> (управление сайтами)</li>
|
|
||||||
<li><strong>YalArba API:</strong> <code>localhost:8888/docs</code> (поиск и бронирование)</li>
|
|
||||||
<li><strong>Auth Service:</strong> централизованная аутентификация</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<blockquote class="blog-quote">
|
|
||||||
"Статус проекта на 20.03.2026: 🟢 Активная разработка. Основная функциональность работает, идёт
|
|
||||||
наполнение
|
|
||||||
контентом и привлечение первых пользователей."
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<h3>📅 Роадмап развития (2026 год)</h3>
|
|
||||||
<p>Приоритеты на ближайшие месяцы:</p>
|
|
||||||
|
|
||||||
<h4>Q3 2026 (Июль–Сентябрь)</h4>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Платежная система</strong> — интеграция с ЮKassa, Tinkoff</li>
|
|
||||||
<li><strong>Мультиязычность</strong> — поддержка английского и башкирского языков</li>
|
|
||||||
<li><strong>API для партнеров</strong> — возможность интеграции сторонних сервисов</li>
|
|
||||||
<li><strong>Система кэширования</strong> — Redis для повышения производительности</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h4>Q4 2026 (Октябрь–Декабрь)</h4>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Мобильные приложения</strong> — iOS и Android (React Native)</li>
|
|
||||||
<li><strong>Система рекомендаций</strong> — AI-based подборки на основе поведения</li>
|
|
||||||
<li><strong>Масштабирование инфраструктуры</strong> — переход на Kubernetes</li>
|
|
||||||
<li><strong>Реферальная программа</strong> — для владельцев и туристов</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>👥 Командная ситуация</h3>
|
|
||||||
<p>Сейчас проект развивается силами небольшой команды (2 человека). Мы активно ищем:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Frontend-разработчиков</strong> (Vue 3, Nuxt.js)</li>
|
|
||||||
<li><strong>Дизайнеров UI/UX</strong></li>
|
|
||||||
<li><strong>Маркетологов</strong> для продвижения в туристической нише</li>
|
|
||||||
<li><strong>Контент-менеджеров</strong> для наполнения платформы</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>🎯 Когда ждать полноценного запуска?</h3>
|
|
||||||
<p><strong>Бета-версия с основной функциональностью</strong> уже доступна по адресам:</p>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://easysite102.ru" target="_blank">easysite102.ru</a> (для владельцев)</li>
|
|
||||||
<li><a href="https://yalarba.ru" target="_blank">yalarba.ru</a> (для туристов)</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>Полноценный запуск</strong> с платежами и мобильным приложением планируется на
|
|
||||||
<strong>сентябрь
|
|
||||||
2026</strong>.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p><strong>Масштабирование на весь Урал и Поволжье</strong> — цель на <strong>2026 год</strong>.</p>
|
|
||||||
|
|
||||||
<h3>💬 Как можно поучаствовать?</h3>
|
|
||||||
<p>Проект открыт для сотрудничества в разных форматах:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Технические специалисты</strong> — присоединяйтесь к разработке (удаленно)</li>
|
|
||||||
<li><strong>Владельцы туристических объектов</strong> — создайте свой сайт на EasySite</li>
|
|
||||||
<li><strong>Инвесторы и партнеры</strong> — обсуждаем стратегическое сотрудничество</li>
|
|
||||||
<li><strong>Тестировщики</strong> — помогайте улучшать пользовательский опыт</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Если вас заинтересовал проект — давайте обсудим возможности сотрудничества!</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="blog-post-footer">
|
|
||||||
<div class="blog-post-tags">
|
|
||||||
<a href="#" class="blog-tag">#EasySite</a>
|
|
||||||
<a href="#" class="blog-tag">#YalArba</a>
|
|
||||||
<a href="#" class="blog-tag">#Туризм</a>
|
|
||||||
<a href="#" class="blog-tag">#Разработка</a>
|
|
||||||
<a href="#" class="blog-tag">#Стартап</a>
|
|
||||||
</div>
|
|
||||||
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
|
|
||||||
💬 Обсудить проект
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<!-- Пятая запись (новая) -->
|
|
||||||
<article class="blog-post" id="post5">
|
|
||||||
<header class="blog-post-header">
|
|
||||||
<span class="blog-post-category">Мысли</span>
|
|
||||||
<h2 class="blog-post-title">Зачем я создаю YalArba: история и миссия</h2>
|
|
||||||
<div class="blog-post-meta">
|
|
||||||
<time datetime="2024-03-25">25 марта 2024</time>
|
|
||||||
<span>•</span>
|
|
||||||
<span>8 минут чтения</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="blog-post-content">
|
|
||||||
<p>Эта история началась в 2017 году, когда я работал на заводе УМПО и параллельно учился в УКСиВТ.
|
|
||||||
Зимой
|
|
||||||
захотелось отдохнуть — съездить куда-нибудь на машине или просто развеяться в парке. Я, конечно,
|
|
||||||
полез в
|
|
||||||
интернет искать сайты и информацию. И ни на одном сайте не смог найти маршрут или место, куда
|
|
||||||
можно сходить
|
|
||||||
бесплатно.</p>
|
|
||||||
|
|
||||||
<p>Везде мне продавали туры, гостиницы, ещё много вариантов, которые для меня, простого рабочего,
|
|
||||||
совершенно не
|
|
||||||
имели никакой ценности. Пришлось искать через знакомых, через группы, куда можно поехать на
|
|
||||||
отдых с
|
|
||||||
корзинкой, бутербродами, на своей машине.</p>
|
|
||||||
|
|
||||||
<blockquote class="blog-quote">
|
|
||||||
«После этого случая мне сильно захотелось создать приложение, которое приводило бы людей к
|
|
||||||
простому и
|
|
||||||
быстрому решению по отдыху. Особенно это ценно для рабочих, у которых нет особой насмотренности,
|
|
||||||
много
|
|
||||||
возможностей и ресурсов для отдыха вдали от дома или за границей.»
|
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<h3>Социальность проекта</h3>
|
|
||||||
<p>Большая часть услуг будет бесплатной для всех, включая предпринимателей. Потому что я сам работал
|
|
||||||
на заводе и
|
|
||||||
был всегда (большую часть времени) за станком. Но остальная жизнь тогда больше походила на
|
|
||||||
несистематизированные пьянки и гулянки. В то время это было интересно, сейчас это совершенно не
|
|
||||||
вписывается
|
|
||||||
в моё мировоззрение.</p>
|
|
||||||
|
|
||||||
<p>Мне кажется, в те годы мне не хватало широты взгляда, в общем, некому было подсказать, что
|
|
||||||
отдыхать можно
|
|
||||||
по-другому. Что есть много исторических мест, памятников природы. Я просто не видел альтернативы
|
|
||||||
своему
|
|
||||||
образу отдыха.</p>
|
|
||||||
|
|
||||||
<h3>Миссия сегодня</h3>
|
|
||||||
<p>Сейчас я надеюсь, что смогу предоставить эту альтернативу. Зумеры, конечно, уже меньше подвержены
|
|
||||||
старым
|
|
||||||
способам отдыха (алкоголь употребляют меньше). Но я хочу добавить приложение (веб-портал),
|
|
||||||
которое сможет
|
|
||||||
подсказать, подкинуть идею, что отдых может быть более культурным, не таким дорогим. И главное —
|
|
||||||
недалеко от
|
|
||||||
дома. В рамках района, области или страны.</p>
|
|
||||||
|
|
||||||
<p>И я уверен, что это будет работать и в других странах. Ведь везде есть просто очень занятые люди,
|
|
||||||
всё
|
|
||||||
внимание которых направлено на работу и дом. В этом я вижу мейнстрим, большую цель для своего
|
|
||||||
приложения.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h3>Международный потенциал</h3>
|
|
||||||
<p>Через это же приложение можно будет привлекать самостоятельных туристов в нашу страну — через
|
|
||||||
рекламу,
|
|
||||||
распространение в другие страны. Открывать наши места отдыха не только для внутреннего туриста,
|
|
||||||
но и для
|
|
||||||
иностранного (выборочно, конечно).</p>
|
|
||||||
|
|
||||||
<h3>Бизнес-модель</h3>
|
|
||||||
<p>Основная прибыль в этом проекте спрятана в количестве пользователей, которые будут пользоваться
|
|
||||||
порталом
|
|
||||||
(приложением). Конечно, приложение должно буквально делать за пользователя часть работы по
|
|
||||||
поиску, подбору,
|
|
||||||
исследованию и выбору маршрутов отдыха — чтобы получить наилучший результат.</p>
|
|
||||||
|
|
||||||
<h3>Личный путь</h3>
|
|
||||||
<p>Поставленная высокая цель помогает мне добиваться высоких результатов в жизни. Для реализации
|
|
||||||
проекта я
|
|
||||||
выучил несколько языков программирования, английский язык, добился от себя внятных установок на
|
|
||||||
жизнь,
|
|
||||||
развил в себе планирование и смог познакомиться с невероятным количеством людей. Каждый новый
|
|
||||||
рубль,
|
|
||||||
потраченный на этом пути, будет воздан.</p>
|
|
||||||
|
|
||||||
<h3>Инвестиции или самостоятельная разработка?</h3>
|
|
||||||
<p>Часто ловлю себя на мысли: а нужны ли мне инвестиции? И да, я часто и с большой уверенностью
|
|
||||||
говорю: да!
|
|
||||||
Нужны. На сервер, на человеко-часы, на заказные части программы. С другой стороны, передо мной
|
|
||||||
часто
|
|
||||||
возникает дилемма — хочется сделать всё самому.</p>
|
|
||||||
|
|
||||||
<p>Это, конечно, ошибка, которая уже стоила мне пары лет в разработке и ещё аукнется большим
|
|
||||||
количеством
|
|
||||||
времени, потраченным на разработку приложения самостоятельно. Я всё ещё на что-то надеюсь, что
|
|
||||||
как-то смогу
|
|
||||||
завершить приложение (я смогу). Просто это будет не так пафосно и круто, как хотелось бы. И
|
|
||||||
дальше, конечно,
|
|
||||||
встанет вопрос о том, как же его продавать (продвигать). Здесь уже заложены некоторые
|
|
||||||
маркетинговые фишки и
|
|
||||||
ходы для создания нового рынка и выхода на существующие.</p>
|
|
||||||
|
|
||||||
<p>В данный момент больше стараюсь уделять время семье и дому. Но часть моих усилий всегда
|
|
||||||
направлена на работу
|
|
||||||
над проектом. Конкретно сейчас работаю над блогом для проекта, хотя, казалось бы, должен
|
|
||||||
вгрызаться в
|
|
||||||
реализацию серверного приложения на Golang (gorm, chi).</p>
|
|
||||||
|
|
||||||
<p>Но я верю, что этот блог — тоже часть пути. Часть истории, которую я хочу рассказать. Чтобы
|
|
||||||
другие, кто,
|
|
||||||
возможно, оказался в похожей ситуации, знали: альтернатива есть. И мы её создаём.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="blog-post-footer">
|
|
||||||
<div class="blog-post-tags">
|
|
||||||
<a href="#" class="blog-tag">#История</a>
|
|
||||||
<a href="#" class="blog-tag">#Миссия</a>
|
|
||||||
<a href="#" class="blog-tag">#СоциальныйПроект</a>
|
|
||||||
<a href="#" class="blog-tag">#Туризм</a>
|
|
||||||
<a href="#" class="blog-tag">#Развитие</a>
|
|
||||||
</div>
|
|
||||||
<button onclick="sendMessageTelegram()" class="blog-comment-btn">
|
|
||||||
💬 Обсудить идею
|
|
||||||
</button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Боковая панель - ПРАВАЯ КОЛОНКА (30%) -->
|
|
||||||
<aside class="blog-sidebar">
|
|
||||||
<div class="blog-sidebar-section">
|
|
||||||
<h3>О блоге</h3>
|
|
||||||
<p>Здесь я делюсь мыслями о разработке, обновлениями проектов и размышлениями о технологическом
|
|
||||||
предпринимательстве.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="blog-sidebar-section">
|
|
||||||
<h3>Категории</h3>
|
|
||||||
<ul class="blog-categories">
|
|
||||||
<li><a href="#" class="blog-category">Проекты</a></li>
|
|
||||||
<li><a href="#" class="blog-category">Разработка</a></li>
|
|
||||||
<li><a href="#" class="blog-category">Предпринимательство</a></li>
|
|
||||||
<li><a href="#" class="blog-category">Мысли</a></li>
|
|
||||||
<li><a href="#" class="blog-category">Обновления</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="blog-sidebar-section">
|
|
||||||
<h3>Последние записи</h3>
|
|
||||||
<ul class="blog-recent">
|
|
||||||
<li><a href="#post5">Зачем я создаю YalArba: история и миссия</a></li>
|
|
||||||
<li><a href="#post4">EasySite & YalArba: состояние и планы от Январь 2026</a></li>
|
|
||||||
<li><a href="#post1">Новые возможности Yalarba.ru</a></li>
|
|
||||||
<li><a href="#post2">Переход на Vue 3</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- Пагинация -->
|
|
||||||
<div class="blog-pagination">
|
|
||||||
<a href="#" class="blog-pagination-btn blog-pagination-prev">← Назад</a>
|
|
||||||
<span class="blog-pagination-current">Страница 1 из 4</span>
|
|
||||||
<a href="#" class="blog-pagination-btn blog-pagination-next">Вперед →</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Футер блога -->
|
|
||||||
<footer class="blog-footer">
|
|
||||||
<div class="blog-footer-content">
|
|
||||||
<p>© 2024 Блог Валитова Газиза. Все записи — личные размышления и опыт.</p>
|
|
||||||
<p>
|
|
||||||
<a href="index.html">На главную</a> •
|
|
||||||
<a href="https://t.me/valitovgaziz">Telegram</a> •
|
|
||||||
<a href="mailto:valitovgaziz@yandex.ru">Email</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Функция для переключения темы
|
|
||||||
function toggleTheme() {
|
|
||||||
document.body.classList.toggle('dark-mode');
|
|
||||||
const btn = document.querySelector('.theme-toggle');
|
|
||||||
|
|
||||||
if (document.body.classList.contains('dark-mode')) {
|
|
||||||
btn.textContent = '☀️ Светлая тема';
|
|
||||||
localStorage.setItem('blog-theme', 'dark');
|
|
||||||
} else {
|
|
||||||
btn.textContent = '🌙 Темная тема';
|
|
||||||
localStorage.setItem('blog-theme', 'light');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Загрузка сохраненной темы
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
const savedTheme = localStorage.getItem('blog-theme');
|
|
||||||
const btn = document.querySelector('.theme-toggle');
|
|
||||||
|
|
||||||
if (savedTheme === 'dark') {
|
|
||||||
document.body.classList.add('dark-mode');
|
|
||||||
btn.textContent = '☀️ Светлая тема';
|
|
||||||
} else {
|
|
||||||
btn.textContent = '🌙 Темная тема';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Адаптация для мобильных устройств
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
const sidebar = document.querySelector('.blog-sidebar');
|
|
||||||
const toggleBtn = document.querySelector('.blog-sidebar-toggle');
|
|
||||||
|
|
||||||
toggleBtn.style.display = 'block';
|
|
||||||
sidebar.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Функция для отправки сообщения в Telegram
|
|
||||||
function sendMessageTelegram() {
|
|
||||||
window.open('https://t.me/valitovgaziz', '_blank');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Функция для переключения сайдбара на мобильных
|
|
||||||
function toggleSidebar() {
|
|
||||||
const sidebar = document.querySelector('.blog-sidebar');
|
|
||||||
sidebar.style.display = sidebar.style.display === 'block' ? 'none' : 'block';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчик изменения размера окна
|
|
||||||
window.addEventListener('resize', function () {
|
|
||||||
const sidebar = document.querySelector('.blog-sidebar');
|
|
||||||
const toggleBtn = document.querySelector('.blog-sidebar-toggle');
|
|
||||||
|
|
||||||
if (window.innerWidth >= 768) {
|
|
||||||
sidebar.style.display = 'block';
|
|
||||||
toggleBtn.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
toggleBtn.style.display = 'block';
|
|
||||||
sidebar.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Плавная прокрутка для якорных ссылок
|
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
|
||||||
anchor.addEventListener('click', function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
const targetId = this.getAttribute('href');
|
|
||||||
if (targetId === '#') return;
|
|
||||||
|
|
||||||
const targetElement = document.querySelector(targetId);
|
|
||||||
if (targetElement) {
|
|
||||||
window.scrollTo({
|
|
||||||
top: targetElement.offsetTop - 100,
|
|
||||||
behavior: 'smooth'
|
|
||||||
});
|
|
||||||
|
|
||||||
// Закрываем сайдбар на мобильных
|
|
||||||
if (window.innerWidth < 768) {
|
|
||||||
const sidebar = document.querySelector('.blog-sidebar');
|
|
||||||
sidebar.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
function toggleTheme() {
|
|
||||||
document.body.classList.toggle('dark-mode');
|
|
||||||
const btn = document.querySelector('.theme-toggle');
|
|
||||||
|
|
||||||
if (document.body.classList.contains('dark-mode')) {
|
|
||||||
btn.textContent = '☀️ Светлая тема';
|
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
} else {
|
|
||||||
btn.textContent = '🌙 Темная тема';
|
|
||||||
localStorage.setItem('theme', 'light');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Загрузка темы при загрузке страницы
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const savedTheme = localStorage.getItem('theme');
|
|
||||||
const btn = document.querySelector('.theme-toggle');
|
|
||||||
|
|
||||||
if (savedTheme === 'dark') {
|
|
||||||
document.body.classList.add('dark-mode');
|
|
||||||
btn.textContent = '☀️ Светлая тема';
|
|
||||||
} else {
|
|
||||||
btn.textContent = '🌙 Темная тема';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
// Digital Background Initialization
|
|
||||||
// Обновляем функцию для интеграции с темной темой
|
|
||||||
function updateBackgroundForTheme() {
|
|
||||||
const isDarkMode = document.body.classList.contains('dark-mode');
|
|
||||||
const binaryDigits = document.querySelectorAll('.binary-digit');
|
|
||||||
const floatingCode = document.querySelectorAll('.floating-code');
|
|
||||||
const connectionNodes = document.querySelectorAll('.connection-node');
|
|
||||||
const dataFlows = document.querySelectorAll('.data-flow');
|
|
||||||
|
|
||||||
// Обновляем цвета элементов в реальном времени
|
|
||||||
const accentColor = isDarkMode ? 'rgba(41, 128, 185, 0.8)' : 'rgba(0, 123, 255, 0.8)';
|
|
||||||
|
|
||||||
binaryDigits.forEach(digit => {
|
|
||||||
digit.style.color = accentColor;
|
|
||||||
});
|
|
||||||
|
|
||||||
floatingCode.forEach(code => {
|
|
||||||
code.style.color = accentColor;
|
|
||||||
});
|
|
||||||
|
|
||||||
connectionNodes.forEach(node => {
|
|
||||||
node.style.background = accentColor;
|
|
||||||
});
|
|
||||||
|
|
||||||
dataFlows.forEach(flow => {
|
|
||||||
flow.style.background = `linear-gradient(90deg, transparent, ${accentColor}, transparent)`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create binary rain effect
|
|
||||||
function createBinaryRain() {
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.className = 'binary-rain';
|
|
||||||
document.body.appendChild(container);
|
|
||||||
|
|
||||||
// Создаем больше потоков для полного покрытия
|
|
||||||
for (let i = 0; i < 15; i++) { // Увеличиваем количество потоков
|
|
||||||
setTimeout(() => {
|
|
||||||
createBinaryStream(container);
|
|
||||||
}, i * 150);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function createBinaryStream(container) {
|
|
||||||
const stream = document.createElement('div');
|
|
||||||
stream.className = 'binary-stream';
|
|
||||||
// Распределяем потоки по всей ширине экрана
|
|
||||||
const left = Math.random() * 100;
|
|
||||||
stream.style.left = `${left}%`;
|
|
||||||
stream.style.position = 'absolute';
|
|
||||||
stream.style.width = 'auto';
|
|
||||||
|
|
||||||
// Создаем больше цифр в каждом потоке
|
|
||||||
for (let i = 0; i < 15; i++) {
|
|
||||||
const digit = document.createElement('div');
|
|
||||||
digit.className = 'binary-digit';
|
|
||||||
digit.textContent = Math.random() > 0.5 ? '1' : '0';
|
|
||||||
digit.style.position = 'absolute';
|
|
||||||
digit.style.left = '0';
|
|
||||||
digit.style.top = `${-i * 50}px`; // Увеличиваем расстояние между цифрами
|
|
||||||
digit.style.animationDuration = `${2 + Math.random() * 3}s`; // Быстрее анимация
|
|
||||||
digit.style.animationDelay = `${i * 0.15}s`;
|
|
||||||
digit.style.opacity = `${0.3 + Math.random() * 0.7}`; // Разная прозрачность
|
|
||||||
digit.style.fontSize = `${12 + Math.random() * 8}px`; // Разный размер шрифта
|
|
||||||
stream.appendChild(digit);
|
|
||||||
}
|
|
||||||
|
|
||||||
container.appendChild(stream);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create floating code elements
|
|
||||||
function createFloatingCode() {
|
|
||||||
const symbols = ['{', '}', '<>', '();', '[]', '</>', '=>', '&&', 'function', 'const', 'let', 'var', 'class', 'import', 'export', 'return'];
|
|
||||||
const classes = ['code-bracket', 'code-parenthesis', 'code-brace', 'code-tag'];
|
|
||||||
|
|
||||||
// Создаем больше плавающих элементов
|
|
||||||
for (let i = 0; i < 25; i++) {
|
|
||||||
const symbol = symbols[Math.floor(Math.random() * symbols.length)];
|
|
||||||
const element = document.createElement('div');
|
|
||||||
element.className = `floating-code ${classes[Math.floor(Math.random() * classes.length)]}`;
|
|
||||||
element.textContent = symbol;
|
|
||||||
element.style.left = `${Math.random() * 100}%`;
|
|
||||||
element.style.top = `${Math.random() * 100}%`;
|
|
||||||
element.style.animationDuration = `${20 + Math.random() * 20}s`;
|
|
||||||
element.style.fontSize = `${10 + Math.random() * 6}px`;
|
|
||||||
element.style.opacity = `${0.05 + Math.random() * 0.1}`;
|
|
||||||
document.body.appendChild(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create connection nodes
|
|
||||||
function createConnectionNodes() {
|
|
||||||
for (let i = 0; i < 20; i++) {
|
|
||||||
const node = document.createElement('div');
|
|
||||||
node.className = 'connection-node';
|
|
||||||
node.style.left = `${Math.random() * 100}%`;
|
|
||||||
node.style.top = `${Math.random() * 100}%`;
|
|
||||||
node.style.animationDelay = `${Math.random() * 4}s`;
|
|
||||||
node.style.width = `${4 + Math.random() * 6}px`;
|
|
||||||
node.style.height = node.style.width;
|
|
||||||
document.body.appendChild(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create data flow lines
|
|
||||||
function createDataFlows() {
|
|
||||||
for (let i = 0; i < 12; i++) {
|
|
||||||
const flow = document.createElement('div');
|
|
||||||
flow.className = 'data-flow';
|
|
||||||
flow.style.top = `${Math.random() * 100}%`;
|
|
||||||
flow.style.width = `${40 + Math.random() * 50}%`;
|
|
||||||
flow.style.left = `${-Math.random() * 30}%`;
|
|
||||||
flow.style.animationDuration = `${5 + Math.random() * 10}s`;
|
|
||||||
flow.style.animationDelay = `${Math.random() * 8}s`;
|
|
||||||
flow.style.height = `${1 + Math.random() * 2}px`;
|
|
||||||
document.body.appendChild(flow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize digital background with theme integration
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// Сначала создаем элементы
|
|
||||||
createBinaryRain();
|
|
||||||
createFloatingCode();
|
|
||||||
createConnectionNodes();
|
|
||||||
createDataFlows();
|
|
||||||
|
|
||||||
// Затем настраиваем наблюдение за темой
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
mutations.forEach(function(mutation) {
|
|
||||||
if (mutation.attributeName === 'class') {
|
|
||||||
setTimeout(updateBackgroundForTheme, 100); // Небольшая задержка для применения стилей
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class']
|
|
||||||
});
|
|
||||||
|
|
||||||
// Инициализируем цвета при загрузке
|
|
||||||
setTimeout(updateBackgroundForTheme, 500);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Также обновляем тему при переключении
|
|
||||||
function toggleTheme() {
|
|
||||||
document.body.classList.toggle('dark-mode');
|
|
||||||
const btn = document.querySelector('.theme-toggle');
|
|
||||||
|
|
||||||
if (document.body.classList.contains('dark-mode')) {
|
|
||||||
btn.textContent = '☀️ Светлая тема';
|
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
} else {
|
|
||||||
btn.textContent = '🌙 Темная тема';
|
|
||||||
localStorage.setItem('theme', 'light');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновляем фон после переключения темы
|
|
||||||
setTimeout(updateBackgroundForTheme, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Загрузка темы при загрузке страницы
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const savedTheme = localStorage.getItem('theme');
|
|
||||||
const btn = document.querySelector('.theme-toggle');
|
|
||||||
|
|
||||||
if (savedTheme === 'dark') {
|
|
||||||
document.body.classList.add('dark-mode');
|
|
||||||
btn.textContent = '☀️ Светлая тема';
|
|
||||||
} else {
|
|
||||||
btn.textContent = '🌙 Темная тема';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
Before Width: | Height: | Size: 2.1 MiB |
|
Before Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 4.7 MiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-telegram"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 10l-4 4l6 6l4 -16l-18 7l4 2l2 6l3 -4" /></svg>
|
|
||||||
|
Before Width: | Height: | Size: 364 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-brand-vk"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M14 19h-4a8 8 0 0 1 -8 -8v-5h4v5a4 4 0 0 0 4 4h0v-9h4v4.5l.03 0a4.531 4.531 0 0 0 3.97 -4.496h4l-.342 1.711a6.858 6.858 0 0 1 -3.658 4.789h0a5.34 5.34 0 0 1 3.566 4.111l.434 2.389h0h-4a4.531 4.531 0 0 0 -3.97 -4.496v4.5z" /></svg>
|
|
||||||
|
Before Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 329 B |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,681 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="keywords" content="
|
|
||||||
Fullstack-разработчик, Fullstack developer, Backend разработка, Frontend разработка, Веб-разработка, Программист Java, Программист Golang, Vue3.js разработка, JavaScript разработчик, Разработка веб-приложений, Создание сайтов, Микросервисная архитектура, REST API, PostgreSQL, Docker, Системное проектирование, Поиск тимейтов, Нетворкинг разработчиков, IT сообщество, Open-source проекты, Присоединиться к команде, Команда мечты, Рекрутинг разработчиков, Поиск программистов, Поиск дизайнеров, Поиск аналитиков, Удаленная команда, Профессиональный рост в IT, Совместная разработка, Технический предприниматель, Стартап партнерство, Инвестиции в IT, Соучредитель проекта, Бизнес-партнер, Tech Lead, Развитие проекта, Стратегическое партнерство, Венчурные инвестиции, Digital-продукты, Монетизация проектов, Travel Tech, Туристическая платформа, Планирование путешествий, Yalarba.ru, Туризм Башкортостан, Разработка платформы, Экосистема проектов, Маркетплейс туризма, Сайт-визитка разработчика, Портфолио программиста, Удаленная работа, Фриланс, Аутсорс разработка, Создание продукта с нуля, Agile разработка, Управление IT проектами, Цифровая трансформация
|
|
||||||
" />
|
|
||||||
|
|
||||||
<link rel="icon" href="./images/favicon/code_orange.png" />
|
|
||||||
<link rel="stylesheet" href="style.css" />
|
|
||||||
<script src="scripts.js"></script>
|
|
||||||
<script src="darkThemeToggle.js"></script>
|
|
||||||
<script src="digital_background.js"></script>
|
|
||||||
<script src="JavaScript/analytics.js"></script>
|
|
||||||
<title>ValitovGaziz - Предприниматель - Fullstack-разработчик</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<header class="hero">
|
|
||||||
<div class="hero-content">
|
|
||||||
<div class="hero-text">
|
|
||||||
<h1>ВАЛИТОВ ГАЗИЗ</h1>
|
|
||||||
<h3 class="hero-subtitle">
|
|
||||||
Технологический предриниматель & Fullstack-разработчик
|
|
||||||
</h3>
|
|
||||||
<p class="hero-description">
|
|
||||||
Создаю цифровое решение для отдыха. Развиваю проект
|
|
||||||
<strong>Yalarba.ru</strong> — платформу, которая меняет подход к
|
|
||||||
путешествиям по Башкортостану.
|
|
||||||
</p>
|
|
||||||
<div class="hero-buttons">
|
|
||||||
<button onclick="sendMessageTelegram()" class="btn btn-primary">
|
|
||||||
Обсудить сотрудничество
|
|
||||||
</button>
|
|
||||||
<button onclick="sendMessageTelegram()" class="btn btn-secondary">
|
|
||||||
Написать мне
|
|
||||||
</button>
|
|
||||||
<a href="blog.html" class="btn btn-secondary">
|
|
||||||
📝 Читать блог
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Кнопка переключения темы -->
|
|
||||||
<button class="theme-toggle" onclick="toggleTheme()">
|
|
||||||
🌙 Темная тема
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="social_links_block">
|
|
||||||
<div class="social_link_block">
|
|
||||||
<h4>Подписывайтесь в ВК</h4>
|
|
||||||
<a href="https://vk.com" target="_blank">
|
|
||||||
<div class="social_link">
|
|
||||||
<img src="./images/favicon/brand-vk.svg" alt="VK - вконтакте" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="social_link_block">
|
|
||||||
<h4>Пишите в телеграм</h4>
|
|
||||||
<a href="https://t.me/valitovgaziz" target="_blank">
|
|
||||||
<div class="social_link">
|
|
||||||
<img src="./images/favicon/brand-telegram.svg" alt="телеграмм" />
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="section about">
|
|
||||||
<div class="about-valitovgaziz-photo-box">
|
|
||||||
<img src="./images/ValitovGaziz/valitovgaziz3.jpg" alt="Valitov Gaziz" id="valitovgaziz-photo-img"
|
|
||||||
loading="lazy" />
|
|
||||||
</div>
|
|
||||||
<div class="about-text">
|
|
||||||
<h2>Технический предприниматель и Fullstack-разработчик</h2>
|
|
||||||
<ul>
|
|
||||||
<li>г. Кумерау, 1985 год родиля</li>
|
|
||||||
<li>1992 - 2002 г. Кумертау, БРГИ 3</li>
|
|
||||||
<li>2002 - 2005 г.Уфа, УГАТУ, специальность "Сварочное производство"</li>
|
|
||||||
<li>2005 - 2009 Росстовская область, СКВО, служба в армии по контракту</li>
|
|
||||||
<li>2009 - 2012 г. Кумертау, станочник "Токарь-расточник" КумАПП</li>
|
|
||||||
<li>2012 -2015 село Старосубхангулово, ремонт электроники. ООО "БААС - сервис" владелец</li>
|
|
||||||
<li>2015 - 2020 г. Уфа, учеба в УКСиВТ "Техник по Информационным Системам"</li>
|
|
||||||
<li>с 2021 самообучние и работа над проектом Ял Арба, владелец</li>
|
|
||||||
</ul>
|
|
||||||
<div class="resume-block">
|
|
||||||
<a href="resume/resume.html" id="resume-link" target="_blank">resume</a>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
Мой подход:
|
|
||||||
<strong>"Технологии как инструмент для решения реальных проблем"</strong>. Именно этот принцип лежит в основе
|
|
||||||
моего флагманского проекта
|
|
||||||
<a href="https://yalarba.ru" target="_blank">
|
|
||||||
Yalarba.ru
|
|
||||||
</a> <a href="https://easysite102.ru" target="_blank">
|
|
||||||
easysite102.ru
|
|
||||||
</a> —
|
|
||||||
платформы, которая упрощает планирование путешествий и открывает новые
|
|
||||||
возможности для туризма.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="entrepreneur-highlights">
|
|
||||||
<div class="highlight-item">
|
|
||||||
<h4>🎯 Техническое видение</h4>
|
|
||||||
<p>
|
|
||||||
Создаю архитектуру, которая масштабируется и адаптируется под
|
|
||||||
растущие потребности бизнеса
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="highlight-item">
|
|
||||||
<h4>💡 Бизнес-ориентация</h4>
|
|
||||||
<p>
|
|
||||||
Фокусируюсь на создании ценности для пользователей и устойчивых
|
|
||||||
бизнес-моделях
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="highlight-item">
|
|
||||||
<h4>🚀 Практический подход</h4>
|
|
||||||
<p>
|
|
||||||
От прототипа к продукту: быстрое тестирование гипотез и
|
|
||||||
итерационная разработка
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="highlight-item">
|
|
||||||
<h4>❤️🔥 Меня мотивирует</h4>
|
|
||||||
<p>
|
|
||||||
Процесс создания проекта с большой пользой многим людям - это то,
|
|
||||||
что по-настоящему подпитывает меня, давая энергию для ежедневного
|
|
||||||
стремления к лучшему будущему.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- НОВАЯ СЕКЦИЯ: О репозитории -->
|
|
||||||
<div class="section repository">
|
|
||||||
<h2>
|
|
||||||
👨💻 О репозитории
|
|
||||||
<a href="https://github.com/valitovgaziz" class="link-style-none" target="_blank">
|
|
||||||
ValitovGaziz-GitHub.com
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
<p>
|
|
||||||
Добро пожаловать! Этот репозиторий — моё цифровое портфолио и
|
|
||||||
пространство для экспериментов.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="projects-grid">
|
|
||||||
<div class="project-card">
|
|
||||||
<h3>
|
|
||||||
🌐
|
|
||||||
<a href="https://valitovgaziz.ru" class="link-style-none" target="_blank">ValitovGaziz.ru</a>
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
Сайт-визитка, который вы сейчас просматриваете. Здесь собрана
|
|
||||||
информация о моих навыках, проектах и способах связи.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="project-card">
|
|
||||||
<h3>
|
|
||||||
🏞️
|
|
||||||
<a href="https://yalarba.ru" class="link-style-none" target="_blank">Yalarba.ru</a>
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
Платформа для туризма по Башкортостану. Помогает путешественникам
|
|
||||||
открывать новые места и планировать маршруты.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="project-card">
|
|
||||||
<h3>
|
|
||||||
🏃♂️
|
|
||||||
<a href="https://BegushiyBashkir.ru" class="link-style-none" target="_blank">BegushiyBashkir.ru</a>
|
|
||||||
</h3>
|
|
||||||
<p>
|
|
||||||
Сайт бегового клуба "Бегущий Башкир", основанного моим другом
|
|
||||||
<a href="https://t.me/zagir_aminev">Аминевым Загиром.</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="current-info">
|
|
||||||
<h3>
|
|
||||||
Что сейчас в работе?
|
|
||||||
<a href="https://easysite102.ru" class="link-style-none" target="_blank"
|
|
||||||
title="Конструктор сайтов для туристических объектов">easysite102.ru</a>
|
|
||||||
</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<strong>Разрабатываю:</strong> easysite102.ru - как часть экосистемы
|
|
||||||
YalArba.ru.
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<strong>Открыт к сотрудничеству:</strong> Участвую в разработке
|
|
||||||
open-source проектов.
|
|
||||||
</li>
|
|
||||||
<li><strong>Нужна помощь:</strong> В развитии моих проектов.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<strong>Задавайте вопросы</strong> по моим проектам или всему, в чём
|
|
||||||
могу быть полезен.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- НОВАЯ СЕКЦИЯ: Команда мечты -->
|
|
||||||
<div class="section team-section">
|
|
||||||
<div class="team-header">
|
|
||||||
<h2>🚀 Ищем тимейтов для роста и прорыва</h2>
|
|
||||||
<p class="team-tagline">
|
|
||||||
Создаем digital-будущее вместе через разработку цифровых решений
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="team-content">
|
|
||||||
<div class="team-mission">
|
|
||||||
<h3>💫 Наша миссия</h3>
|
|
||||||
<p>
|
|
||||||
Мы строим сообщество профессионалов, которые через технологии
|
|
||||||
создают реальную пользу для людей. Это не коммерческая работа — это
|
|
||||||
возможность расти, решая сложные задачи и открывая новые горизонты.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="team-roles">
|
|
||||||
<h3>👥 Кого мы ищем:</h3>
|
|
||||||
<div class="roles-grid">
|
|
||||||
<div class="role-card">
|
|
||||||
<h4>💻 Программисты</h4>
|
|
||||||
<p>
|
|
||||||
Fullstack, Backend, Frontend, Mobile — все, кто готов строить
|
|
||||||
масштабируемые решения
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="role-card">
|
|
||||||
<h4>🎨 Дизайнеры</h4>
|
|
||||||
<p>UI/UX, продуктовые дизайнеры, креативные мыслители</p>
|
|
||||||
</div>
|
|
||||||
<div class="role-card">
|
|
||||||
<h4>📊 Аналитики</h4>
|
|
||||||
<p>
|
|
||||||
Ищем аналитика (системного и бизнес‑аналитика) для анализа
|
|
||||||
процессов, сбора требований и перевода бизнес‑потребностей в
|
|
||||||
технические решения.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="role-card">
|
|
||||||
<h4>🚀 Продавцы-стратеги</h4>
|
|
||||||
<p>Кто понимает, как digital-продукты меняют рынки</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="team-value">
|
|
||||||
<h3>🎯 Что получаете взамен:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
✅ <strong>Реальный опыт</strong> — задачи уровня коммерческих
|
|
||||||
проектов
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
✅ <strong>Профессиональный рост</strong> — следующий уровень
|
|
||||||
навыков гарантирован
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
✅ <strong>Нетворкинг</strong> — сообщество сильных специалистов
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
✅ <strong>Портфолио</strong> — проекты, которые впечатляют
|
|
||||||
работодателей
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
✅ <strong>Горизонтальное развитие</strong> — возможность
|
|
||||||
пробовать себя в смежных ролях
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="team-challenge">
|
|
||||||
<h3>⚡ Уровень сложности:</h3>
|
|
||||||
<p>
|
|
||||||
Спектр задач достаточно высок — решая их, вы гарантированно
|
|
||||||
подниметесь на следующую ступень развития. Мы работаем с
|
|
||||||
технологиями, которые определяют будущее: микросервисы, AI/ML,
|
|
||||||
масштабируемые архитектуры, современный UX и бизнес-модели.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="team-cta">
|
|
||||||
<h3>Готовы расти вместе?</h3>
|
|
||||||
<p>
|
|
||||||
Если вы ищете не просто проект, а сообщество для профессионального
|
|
||||||
прорыва — давайте знакомиться!
|
|
||||||
</p>
|
|
||||||
<button class="btn btn-primary" onclick="sendMessageTelegram()">
|
|
||||||
Присоединиться к команде
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ОБНОВЛЕННАЯ СЕКЦИЯ: Yalarba -->
|
|
||||||
<div id="yalarba-invest" class="section yalarba-section">
|
|
||||||
<div class="yalarba-header">
|
|
||||||
<h2>
|
|
||||||
🚀 <a href="https://yalarba.ru" target="_blank">Yalarba.ru</a> —
|
|
||||||
Travel Tech проект
|
|
||||||
</h2>
|
|
||||||
<p class="yalarba-tagline">
|
|
||||||
Платформа для планирования путешествий нового поколения
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="yalarba-content">
|
|
||||||
<div class="yalarba-stats">
|
|
||||||
<div class="stat">
|
|
||||||
<h3>❤️</h3>
|
|
||||||
<p>проект, рожденный от любви к краю</p>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<h3>🤝</h3>
|
|
||||||
<p>открыт для помощи и сотрудничества</p>
|
|
||||||
</div>
|
|
||||||
<div class="stat">
|
|
||||||
<h3>🚀</h3>
|
|
||||||
<p>готов к росту с правильной командой</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="yalarba-value">
|
|
||||||
<h3>Технологический стек проекта:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>✅ Микросервисная архитектура на Golang (Gorm, Chi)</li>
|
|
||||||
<li>✅ Современный фронтенд на Nuxt.js 4, Vue3.js</li>
|
|
||||||
<li>✅ Оптимизированная база данных PostgreSQL</li>
|
|
||||||
<li>
|
|
||||||
✅ Контейнеризация и легкое масштабирование через Docker, Docker
|
|
||||||
Swarm
|
|
||||||
</li>
|
|
||||||
<li>✅ Полный цикл разработки от идеи до продукта</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="investment-cta">
|
|
||||||
<h3>Инвестиционные возможности</h3>
|
|
||||||
<p>
|
|
||||||
Проект открыт для стратегических партнерств и инвестиций. Если вас
|
|
||||||
заинтересовала платформа, давайте обсудим перспективы
|
|
||||||
сотрудничества.
|
|
||||||
</p>
|
|
||||||
<button class="btn btn-primary" onclick="sendMessageTelegram()">
|
|
||||||
Обсудить детали
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Опыт работы</h2>
|
|
||||||
<div class="timeline">
|
|
||||||
<div class="timeline-item">
|
|
||||||
<h3>Основатель и Tech Lead - Yalarba.ru</h3>
|
|
||||||
<p><strong>2020 — настоящее время</strong> (5+ лет)</p>
|
|
||||||
<p>
|
|
||||||
Разработка и продвижение инновационной платформы для планирования
|
|
||||||
путешествий с полным циклом разработки:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Создание архитектуры микросервисов на Nuxt.js 4 и Golang</li>
|
|
||||||
<li>Разработка современного фронтенда на Nuxt.js 4 & Vue3.js</li>
|
|
||||||
<li>Проектирование и оптимизация баз данных PostgreSQL</li>
|
|
||||||
<li>Внедрение Docker и контейнеризации для масштабирования</li>
|
|
||||||
<li>Управление проектом, планирование развития продукта</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timeline-item">
|
|
||||||
<h3>Fullstack-разработчик (Проектная работа)</h3>
|
|
||||||
<p><strong>2017 — настоящее время</strong> (7+ лет)</p>
|
|
||||||
<p>Участие в различных IT-проектах:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Разработка лендинг-страниц и сайтов-визиток</li>
|
|
||||||
<li>Создание маркетплейсов и туристических агрегаторов</li>
|
|
||||||
<li>
|
|
||||||
Проектирование REST API на Golang (gorm, chi), PostgresQL, https
|
|
||||||
</li>
|
|
||||||
<li>Разработка фронтенда на Nuxt.js 4 (vue3.js)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Обновленная секция навыков в index.html -->
|
|
||||||
<div class="section">
|
|
||||||
<h2>Навыки</h2>
|
|
||||||
<div class="skills-container">
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">Golang</h3>
|
|
||||||
<span class="skill-level advanced">Продвинутый</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">
|
|
||||||
Высокопроизводительные backend сервисы
|
|
||||||
</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> 2+ лет коммерческой разработки, REST API,
|
|
||||||
best practices
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">
|
|
||||||
Concurrency patterns, advanced Go features
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">JavaScript</h3>
|
|
||||||
<span class="skill-level advanced">Продвинутый</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">
|
|
||||||
Fullstack разработка, современный ES6+
|
|
||||||
</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> 3+ лет коммерческой разработки, Vue.js,
|
|
||||||
Node.js
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">
|
|
||||||
TypeScript, advanced patterns, performance optimization
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">Vue3</h3>
|
|
||||||
<span class="skill-level intermediate">Средний</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">
|
|
||||||
Современный фронтенд с Composition API
|
|
||||||
</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> Разработка SPA приложений, Vue Router, Pinia
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">Vue 3 advanced patterns, testing, SSR</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">Nuxt</h3>
|
|
||||||
<span class="skill-level intermediate">Средний</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">SSR/SSG приложения на Vue.js</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> Nuxt 3, server-side rendering, static site
|
|
||||||
generation
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">Nuxt 4, advanced caching strategies</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">PostgreSQL</h3>
|
|
||||||
<span class="skill-level intermediate">Средний</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">
|
|
||||||
Реляционные базы данных, оптимизация запросов
|
|
||||||
</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> Проектирование схем, индексы, сложные запросы
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">
|
|
||||||
Advanced SQL, partitioning, replication
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">Docker</h3>
|
|
||||||
<span class="skill-level intermediate">Средний</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">Контейнеризация приложений</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> Docker Compose, multi-stage builds,
|
|
||||||
оптимизация образов
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">
|
|
||||||
Kubernetes, Docker Swarm, orchestration
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">Java</h3>
|
|
||||||
<span class="skill-level beginner">Начинающий</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">
|
|
||||||
Backend разработка микросервисов и enterprise приложений
|
|
||||||
</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> Коммерческая разработка 2+ лет, Spring
|
|
||||||
Framework, Hibernate
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">
|
|
||||||
Углубление в Spring Boot 3, reactive programming
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="skill-card">
|
|
||||||
<div class="skill-header">
|
|
||||||
<h3 class="skill-name">Spring Framework</h3>
|
|
||||||
<span class="skill-level beginner">Начинающий</span>
|
|
||||||
</div>
|
|
||||||
<p class="skill-description">
|
|
||||||
Создание масштабируемых enterprise приложений
|
|
||||||
</p>
|
|
||||||
<div class="skill-acquisition">
|
|
||||||
<strong>Опыт:</strong> Spring Boot, Spring Security, Spring Data,
|
|
||||||
Spring MVC
|
|
||||||
</div>
|
|
||||||
<div class="skill-growth">
|
|
||||||
Изучение Spring Cloud, микросервисная архитектура
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Образование</h2>
|
|
||||||
<div class="timeline">
|
|
||||||
<div class="timeline-item">
|
|
||||||
<h3>УКСИВТ</h3>
|
|
||||||
<p>Уфимский колледж статистики и информатики</p>
|
|
||||||
<p>Техник по информационным системам</p>
|
|
||||||
<p><strong>2016 - 2020</strong></p>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-item">
|
|
||||||
<h3>
|
|
||||||
Автономная некоммерческая организация высшего образования
|
|
||||||
«Университет Иннополис»
|
|
||||||
</h3>
|
|
||||||
<p>Java enterprise, Java enterprise developer</p>
|
|
||||||
<p><strong>2021 - 2021</strong></p>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-item">
|
|
||||||
<h3>МТИ - Московский технологический институт.</h3>
|
|
||||||
<p>Разработка программного обеспечения</p>
|
|
||||||
<p><strong>2025 - ></strong></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Курсы и сертификаты</h2>
|
|
||||||
<ul>
|
|
||||||
<li>2024: Управление проектами (Skillbox, Эффективный руководитель)</li>
|
|
||||||
<li>2022: Java Full Stack Developer (JetBrains Academy)</li>
|
|
||||||
<li>2021: Java Enterprise developer (Университет Иннополис)</li>
|
|
||||||
<li>
|
|
||||||
2020: Управление по Agile: Scrum, Kanban, Lean (Нетология-групп)
|
|
||||||
</li>
|
|
||||||
<li>2019: English intermediate (Frog-school)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Языки</h2>
|
|
||||||
<ul>
|
|
||||||
<li>Башкирский — Родной</li>
|
|
||||||
<li>Русский — C1 (Продвинутый)</li>
|
|
||||||
<li>Английский — B2 (Средне-продвинутый)</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
|
||||||
<h2>Контакты</h2>
|
|
||||||
<p>
|
|
||||||
Всегда рад новым знакомствам и интересным предложениям о сотрудничестве.
|
|
||||||
</p>
|
|
||||||
<div class="contact-info">
|
|
||||||
<p>
|
|
||||||
📱 Телеграм:
|
|
||||||
<a href="https://t.me/valitovgaziz" target="_blank">@valitovgaziz</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
📧 Email:
|
|
||||||
<a href="mailto:valitovgaziz@yandex.ru" target="_blank">valitovgaziz@yandex.ru</a>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
📞 Телефон:
|
|
||||||
<a href="tel:+79625439343" target="_blank">+7(962)543-93-43</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button id="saveContactBtn" onclick="saveContact()">
|
|
||||||
📇 Сохранить контакт
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="footer-links">
|
|
||||||
<div class="footer-section">
|
|
||||||
<h4>Технологии:</h4>
|
|
||||||
<div class="two-column-grid">
|
|
||||||
<div class="footer-box">
|
|
||||||
<ul>
|
|
||||||
<li>FrontEnd:</li>
|
|
||||||
<li>BackEnd:</li>
|
|
||||||
<li>DataBase:</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="footer-box">
|
|
||||||
<ul>
|
|
||||||
<li>Vue3.js Nuxt.js</li>
|
|
||||||
<li>Golang (Gorm, Chi)</li>
|
|
||||||
<li>PostgresQL</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-section">
|
|
||||||
<h4>Контакты:</h4>
|
|
||||||
<div class="two-column-grid">
|
|
||||||
<div class="footer-box">
|
|
||||||
<ul>
|
|
||||||
<li>Telegram:</li>
|
|
||||||
<li>Phone:</li>
|
|
||||||
<li>Email:</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="footer-box">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://t.me/valitovgaziz" target="_blank">@valitovgaziz</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="tel:+79625439343" target="_blank">8 (962) 543-93-43</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="mailto:valitovgaziz@yandex.ru" target="_blank">valitovgaziz@yandex.ru</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-section">
|
|
||||||
<h4>Сообщество:</h4>
|
|
||||||
<div class="two-column-grid">
|
|
||||||
<div class="footer-box">
|
|
||||||
<ul>
|
|
||||||
<li>Telegram channel:</li>
|
|
||||||
<li>Telegram channel:</li>
|
|
||||||
<li>VK group:</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="footer-box">
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a href="https://t.me/ValitovGaziz_Ufa" target="_blank">Мои новости</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://t.me/+oYymS0r6qG9lYWJi" target="_blank">YalArba.ru team</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="https://vk.com/club222248484?from=groups" target="_blank">ЯлАрба | Путевозитель</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-end-text">
|
|
||||||
<p>Уфа Ufa Өфө 2025 © Created by Valitov Gaziz</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<link rel="stylesheet" href="./style/main.css" />
|
|
||||||
<title>Валитов Газиз Камилевич · Резюме</title>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="resume-card">
|
|
||||||
<!-- header -->
|
|
||||||
<div class="header">
|
|
||||||
<h1 class="name">Валитов Газиз Камилевич</h1>
|
|
||||||
<div class="subhead">
|
|
||||||
<span class="subhead-item"><i>📅</i> 40 лет (27.10.1985)</span>
|
|
||||||
<span class="subhead-item"><i>📍</i> Уфа, готов к переезду/командировкам</span>
|
|
||||||
<span class="subhead-item"><i>📞</i> <a href="tel:+79625439343">+7 (962) 5439343</a></span>
|
|
||||||
<span class="subhead-item"><i>✉️</i> <a
|
|
||||||
href="mailto:valitovgaziz@gmail.com">valitovgaziz@yandex.ru</a></span>
|
|
||||||
<span class="subhead-item"><i>📱</i> telegram: @valitovgaziz</span>
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 0.5rem;">
|
|
||||||
<span class="badge">гражданство РФ</span>
|
|
||||||
<span class="badge">разрешение на работу РФ</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="job-title">Программист / Руководитель проектов</div>
|
|
||||||
|
|
||||||
<div class="specialization-block">
|
|
||||||
<span>Специализации:</span>
|
|
||||||
<div class="spec-list">
|
|
||||||
<div class="spec-item">Руководитель проектов</div>
|
|
||||||
<div class="spec-item">CIO (Директор по ИТ)</div>
|
|
||||||
<div class="spec-item">Программист-разработчик</div>
|
|
||||||
<div class="spec-item">Руководитель группы разработки</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 1rem; display: flex; flex-wrap: wrap; gap: 1rem 2rem;">
|
|
||||||
<span>✅ Полная занятость / частичная / проектная</span>
|
|
||||||
<span>✅ Полный день / сменный / гибкий / удалёнка</span>
|
|
||||||
<span>🚌 Время в пути до 1 часа</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- main two column -->
|
|
||||||
<div class="grid-2">
|
|
||||||
<!-- left column: skills, languages, образование, водительские -->
|
|
||||||
<div class="sidebar">
|
|
||||||
<!-- Ключевые навыки (из списка) -->
|
|
||||||
<div class="section-title">Ключевые навыки</div>
|
|
||||||
<div class="skill-tags">
|
|
||||||
<span class="skill-tag">Английский B2</span><span class="skill-tag">Linux</span><span
|
|
||||||
class="skill-tag">Adobe Photoshop</span>
|
|
||||||
<span class="skill-tag">CorelDRAW</span><span class="skill-tag">C</span><span
|
|
||||||
class="skill-tag">Figma</span>
|
|
||||||
<span class="skill-tag">Git</span><span class="skill-tag">SQL</span><span
|
|
||||||
class="skill-tag">Agile</span>
|
|
||||||
<span class="skill-tag">Java</span><span class="skill-tag">ООП</span><span
|
|
||||||
class="skill-tag">Управление персоналом</span>
|
|
||||||
<span class="skill-tag">Atlassian Jira</span><span class="skill-tag">Spring Framework</span><span
|
|
||||||
class="skill-tag">JUnit</span>
|
|
||||||
<span class="skill-tag">PostgreSQL</span><span class="skill-tag">XPath</span><span
|
|
||||||
class="skill-tag">Go</span>
|
|
||||||
<span class="skill-tag">Intellij IDEA</span><span class="skill-tag">Spring MVC</span><span
|
|
||||||
class="skill-tag">MySQL</span>
|
|
||||||
<span class="skill-tag">Internet Marketing</span><span class="skill-tag">Грамотная речь</span>
|
|
||||||
<span class="skill-tag">Организаторские навыки</span><span class="skill-tag">Обучение
|
|
||||||
персонала</span>
|
|
||||||
<span class="skill-tag">Разработка ПО</span><span class="skill-tag">Agile Project Management</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Знание языков -->
|
|
||||||
<div class="section-title">Языки</div>
|
|
||||||
<div class="lang-item">
|
|
||||||
<span class="lang-name">Башкирский</span>
|
|
||||||
<span class="lang-level">родной</span>
|
|
||||||
</div>
|
|
||||||
<div class="lang-item">
|
|
||||||
<span class="lang-name">Русский</span>
|
|
||||||
<span class="lang-level">C1 —
|
|
||||||
продвинутый</span>
|
|
||||||
</div>
|
|
||||||
<div class="lang-item">
|
|
||||||
<span class="lang-name">Английский</span>
|
|
||||||
<span class="lang-level">B1 - средний</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Образование -->
|
|
||||||
<div class="section-title">Образование</div>
|
|
||||||
<div style="margin-bottom: 1rem;">
|
|
||||||
<div><strong style="color: #0b3b5c;">2020 · УКСИВТ</strong> — техник по информационным системам
|
|
||||||
</div>
|
|
||||||
<div style="margin-top: 0.5rem;"><strong style="color: #0b3b5c;">2004 · УГАТУ</strong> —
|
|
||||||
Автоматизация технологических систем, сварочное производство (неоконч. высшее)</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- курсы, повышение квалификации -->
|
|
||||||
<div class="section-title">Повышение квалификации</div>
|
|
||||||
<ul style="list-style-type: none; padding-left: 0;">
|
|
||||||
<li style="margin-bottom: 0.5rem;">🔹 <strong>2024</strong> Skillbox — Эффективный руководитель
|
|
||||||
(управление проектами)</li>
|
|
||||||
<li style="margin-bottom: 0.5rem;">🔹 <strong>2022</strong> JetBrains Academy — Java FullStack
|
|
||||||
Developer</li>
|
|
||||||
<li style="margin-bottom: 0.5rem;">🔹 <strong>2021</strong> Университет Иннополис — java-программист
|
|
||||||
</li>
|
|
||||||
<li style="margin-bottom: 0.5rem;">🔹 <strong>2020</strong> Нетология — Управление по Agile: Scrum,
|
|
||||||
Kanban, Lean</li>
|
|
||||||
<li style="margin-bottom: 0.5rem;">🔹 <strong>2019</strong> Frog-school — English Intermediate</li>
|
|
||||||
<li style="margin-bottom: 0.5rem;">🔹 <strong>2019</strong> Школа студия телерадио (ГУП ТРК
|
|
||||||
"Башкортостан") — телерадиоведущий</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- правый столбец: опыт работы и обо мне -->
|
|
||||||
<div class="main-content">
|
|
||||||
<!-- Опыт работы 12 лет 1 месяц -->
|
|
||||||
<div class="section-title">Опыт работы — 12 лет 1 месяц</div>
|
|
||||||
<!-- Август 2023 — Март 2024 -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ООО "ИКЦ Ял Арба"</span>
|
|
||||||
<span class="job-period">Ноябрь 2022 — настоящее время (30 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Директор</div>
|
|
||||||
<div class="job-desc">Наем персонала, руководитель группы разработки, маркетинг, продажи,
|
|
||||||
разрбаотка, написание кода.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Август 2021 — Октябрь 2021 -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ИП Сафаров Я.Р., Уфа</span>
|
|
||||||
<span class="job-period">Авг 2021 — Окт 2021 (3 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Программист 1С</div>
|
|
||||||
<div class="job-desc">Разработка не типовых конфигураций для платформы 1С ERP.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Апрель 2019 — Октябрь 2019 -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ГУП ТРК "Башкортостан" БСТ (СМИ)</span>
|
|
||||||
<span class="job-period">Апр 2019 — Окт 2019 (7 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Инженер</div>
|
|
||||||
<div class="job-desc">Звукозапись, обслуживание кинокамер. Командировки в районы в качестве
|
|
||||||
звукового оператора. Сопровождение и ведение записи на реал тайм проекте "Республика лайв".
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Май 2017 — Июль 2017 -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ООО "ЭРУДИТ", Старосубхангулово</span>
|
|
||||||
<span class="job-period">Май 2017 — Июль 2017 (3 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Сетевой администратор</div>
|
|
||||||
<div class="job-desc"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Февраль 2017 — Май 2017 (ПАО УМПО) -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ПАО "УМПО", Уфа</span>
|
|
||||||
<span class="job-period">Фев 2017 — Май 2017 (4 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Токарь-расточник (5 разряд)</div>
|
|
||||||
<div class="job-desc">Цех по производству деталей редуктора Ка-32. Обработка на
|
|
||||||
координатно-расточном станке 1964г. Квалитеты до 6, точность менее ±0.01мм, шероховатость 5
|
|
||||||
класс. Чтение чертежей и техпроцесса.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Июнь 2016 — Октябрь 2016 ООО "ПФО Вертикаль" -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ООО "ПФО Вертикаль" (аутстафф), Уфа</span>
|
|
||||||
<span class="job-period">Июнь 2016 — Окт 2016 (5 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Токарь-расточник (5 разряд)</div>
|
|
||||||
<div class="job-desc">Инструментальный цех, ночные смены. Координатно-расточной станок, высокая
|
|
||||||
точность (до ±0.01мм), 6 квалитет.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Май 2013 — Окт 2015 ООО "БААС-сервис" -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ООО "БААС-сервис", Старосубхангулово</span>
|
|
||||||
<span class="job-period">Май 2013 — Окт 2015 (2 года 6 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Директор (услуги населению: фото/видео, ремонт)</div>
|
|
||||||
<div class="job-desc">Управление персоналом (8 чел), учет наличности, отчетность ИФНС, обучение.
|
|
||||||
Софт, диагностика железа, заправка картриджей, фото/видеосъемка, дизайн.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Март 2009 — Окт 2012 ОАО "КумаПП" -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ОАО "КумаПП", Кумертау</span>
|
|
||||||
<span class="job-period">Март 2009 — Окт 2012 (3 года 8 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Токарь-расточник (5 разряд)</div>
|
|
||||||
<div class="job-desc">Расточка деталей вертолета (втулки, качалки, рессоры). Координатно-расточные
|
|
||||||
работы, допуски до ±0.01мм, квалитет 6, шероховатость 5 класс.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Авг 2008 — Окт 2008 ООО НОП "Мега-Щит" -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">ООО НОП "Мега-Щит", Ханты-Мансийск</span>
|
|
||||||
<span class="job-period">Авг 2008 — Окт 2008 (3 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Охранник</div>
|
|
||||||
<div class="job-desc">КПП, контроль пропускного режима, досмотр.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Май 2005 — Июль 2008 Вооруженные силы РФ -->
|
|
||||||
<div class="job-entry">
|
|
||||||
<div class="job-header">
|
|
||||||
<span class="job-company">Вооруженные силы РФ</span>
|
|
||||||
<span class="job-period">Май 2005 — Июль 2008 (3 года 3 мес)</span>
|
|
||||||
</div>
|
|
||||||
<div class="job-position">Командир отделения</div>
|
|
||||||
<div class="job-desc">Командование отделением.</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Обо мне -->
|
|
||||||
<div class="section-title">Обо мне</div>
|
|
||||||
<div class="about-text">
|
|
||||||
Программист, коммуникатор, компанейский человек, всегда за положительный движ.
|
|
||||||
</div>
|
|
||||||
<!-- Дополнительные строки из резюме: Уфа, гражданство и пр уже вверху -->
|
|
||||||
<div
|
|
||||||
style="font-size: 0.9rem; color: #1f3f55; margin-top: 1rem; background: #f1f7fd; padding: 0.8rem; border-radius: 12px;">
|
|
||||||
<span style="font-weight:600;">Дополнительно:</span> linux, Figma, SQL, Agile, Jira, Java, Spring,
|
|
||||||
Go, интернет-маркетинг, командная работа, менторство.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- нижний колонтитул (страница 4) -->
|
|
||||||
<div style="background-color: #f4f9ff; padding: 1.5rem 2.5rem; border-top: 1px solid #bfd5e6;">
|
|
||||||
<div style="display: flex; flex-wrap: wrap; gap: 2rem; justify-content: space-between;">
|
|
||||||
<div>
|
|
||||||
<span style="font-weight:600;">🏠 Проживание:</span> Уфа · готов к переезду / командировкам
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span style="font-weight:600;">📄 обновлено:</span> февраль 2026 года
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span style="font-weight:600;">📞 +7 (962) 543 - 93 - 43</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: #eef2f5;
|
|
||||||
font-family: 'Segoe UI', Roboto, system-ui, -apple-system, sans-serif;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #1a2634;
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.resume-card {
|
|
||||||
max-width: 1100px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: #ffffff;
|
|
||||||
box-shadow: 0 10px 25px rgba(0, 35, 70, 0.1);
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-top: 6px solid #0b3b5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* header section */
|
|
||||||
.header {
|
|
||||||
background-color: #f9fcff;
|
|
||||||
padding: 2rem 2.5rem 1.5rem 2.5rem;
|
|
||||||
border-bottom: 1px solid #d9e4ed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 2.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
color: #0b3b5c;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subhead {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1.5rem 2.5rem;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
color: #2f4858;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subhead-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subhead-item i {
|
|
||||||
width: 20px;
|
|
||||||
color: #1e5a7a;
|
|
||||||
font-weight: 400;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
background-color: #e1edf7;
|
|
||||||
color: #0b3b5c;
|
|
||||||
padding: 0.3rem 1rem;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
font-weight: 600;
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
border: 1px solid #b9d1e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: #1d4e6b;
|
|
||||||
margin: 1rem 0 0.5rem 0;
|
|
||||||
border-bottom: 2px solid #b0c8da;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.specialization-block {
|
|
||||||
background: #e9f0f7;
|
|
||||||
padding: 1rem 2rem;
|
|
||||||
border-radius: 40px;
|
|
||||||
margin: 0.5rem 0 0 0;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.specialization-block span {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0b3b5c;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spec-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.6rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spec-item {
|
|
||||||
color: #1e3b4f;
|
|
||||||
border-left: 3px solid #1e5a7a;
|
|
||||||
padding-left: 10px;
|
|
||||||
font-size: 0.98rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-2 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 2.2fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sidebar */
|
|
||||||
.sidebar {
|
|
||||||
background-color: #f7fafd;
|
|
||||||
padding: 2rem 1.8rem;
|
|
||||||
border-right: 1px solid #cddeec;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
padding: 2rem 2rem 2rem 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 1.35rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0b3b5c;
|
|
||||||
border-bottom: 2px solid #b6d0e2;
|
|
||||||
padding-bottom: 6px;
|
|
||||||
margin: 1.8rem 0 1.2rem 0;
|
|
||||||
letter-spacing: -0.2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title:first-of-type {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-label {
|
|
||||||
width: 100px;
|
|
||||||
font-weight: 500;
|
|
||||||
color: #1e5a7a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-value {
|
|
||||||
color: #152b39;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-tags {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-tag {
|
|
||||||
background: white;
|
|
||||||
border: 1px solid #b0c5d6;
|
|
||||||
padding: 0.25rem 1rem;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #0f3a52;
|
|
||||||
font-weight: 500;
|
|
||||||
box-shadow: 0 1px 2px rgba(0, 20, 40, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-item {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
border-bottom: 1px dashed #b8cbd9;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-name {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #0f3f5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-level {
|
|
||||||
color: #1d5b81;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* опыт */
|
|
||||||
.job-entry {
|
|
||||||
margin-bottom: 1.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-header {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-company {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0b3b5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-period {
|
|
||||||
background: #dbe7f2;
|
|
||||||
padding: 0.2rem 1rem;
|
|
||||||
border-radius: 30px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #103a52;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-position {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1f5777;
|
|
||||||
margin: 0.15rem 0 0.4rem 0;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.job-desc {
|
|
||||||
color: #1e333f;
|
|
||||||
font-size: 0.93rem;
|
|
||||||
margin-left: 0.2rem;
|
|
||||||
padding-left: 0.8rem;
|
|
||||||
border-left: 3px solid #9bb7d0;
|
|
||||||
white-space: pre-line;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-mb {
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
hr {
|
|
||||||
border: 0;
|
|
||||||
border-top: 1px solid #c9dae7;
|
|
||||||
margin: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-text {
|
|
||||||
background: #f0f6fc;
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
border-radius: 50px 8px 50px 8px;
|
|
||||||
color: #103c58;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
border-left: 6px solid #1f6a92;
|
|
||||||
margin: 2rem 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-note {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #5e778a;
|
|
||||||
text-align: right;
|
|
||||||
border-top: 1px solid #b9cfdf;
|
|
||||||
padding-top: 0.8rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #1b5f89;
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px dotted #8eb1c7;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
border-bottom: 2px solid #0b3b5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 750px) {
|
|
||||||
.grid-2 {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
padding: 2rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
border-right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 2.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
function saveContact() {
|
|
||||||
// Создаем содержимое vCard (VCF)
|
|
||||||
const vCardData = `BEGIN:VCARD
|
|
||||||
VERSION:3.0
|
|
||||||
FN:Валитов Газиз Камилевич
|
|
||||||
N:Валитов;Газиз;Камилевич
|
|
||||||
ORG:FREELANCE
|
|
||||||
TITLE:FULLSTACK_DEVELOPER
|
|
||||||
TEL;TYPE=MOBILE:+79279238823
|
|
||||||
TEL;TYPE=MOBILE:+79044513441
|
|
||||||
TEL;TYPE=MOBILE:+79625439243
|
|
||||||
EMAIL;TYPE=HOME:valitovgaziz@gmail.com
|
|
||||||
EMAIL;TYPE=WORK:valitovgaziz@yandex.ru
|
|
||||||
URL:https://valitovgaziz.ru
|
|
||||||
URL:https://t.me/valitovgaziz
|
|
||||||
URL:https://vk.ru/id378105199
|
|
||||||
BDAY:1985-10-27
|
|
||||||
END:VCARD`;
|
|
||||||
|
|
||||||
// Создаем Blob (бинарный объект) с данными vCard
|
|
||||||
const blob = new Blob([vCardData], { type: 'text/vcard' });
|
|
||||||
|
|
||||||
// Создаем URL для скачивания
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// Создаем временную ссылку для скачивания
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = url;
|
|
||||||
link.download = 'valitovgaziz.vcf'; // Имя файла
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
// Освобождаем память
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTermSheet() {
|
|
||||||
// Create a temporary anchor element
|
|
||||||
const link = document.createElement('a');
|
|
||||||
|
|
||||||
// Set correct relative path to the PDF file
|
|
||||||
link.href = './assets/docs/TermSheet.pdf';
|
|
||||||
|
|
||||||
// Set download attribute with filename
|
|
||||||
link.download = 'TermSheet.pdf';
|
|
||||||
|
|
||||||
// Append to body to make it work in some browsers
|
|
||||||
document.body.appendChild(link);
|
|
||||||
|
|
||||||
// Trigger the download
|
|
||||||
link.click();
|
|
||||||
|
|
||||||
// Clean up
|
|
||||||
document.body.removeChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Обработчик для кнопки "Запросить презентацию"
|
|
||||||
function sendMessageTelegram() {
|
|
||||||
// Проверяем, поддерживает ли браузер диалоги
|
|
||||||
if (typeof window.orientation !== 'undefined' && !window.confirm) {
|
|
||||||
// Для мобильных браузеров без поддержки prompt - открываем Telegram напрямую
|
|
||||||
window.open('https://t.me/valitovgaziz', '_blank');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const message = prompt("Опишите, пожалуйста, ваше предложение или вопрос. Я свяжусь с вами в ближайшее время:");
|
|
||||||
if (message) {
|
|
||||||
const BOT_TOKEN = "8470085635:AAEPZcsN3n-3FkMdr7DzxbiQ3q8mXZTGwug";
|
|
||||||
const CHAT_ID = "559861569";
|
|
||||||
|
|
||||||
// Используем FormData вместо JSON (более надежно)
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('chat_id', CHAT_ID);
|
|
||||||
formData.append('text', `📥 Новое сообщение с сайта ValitovGaziz:\n\n${message}`);
|
|
||||||
formData.append('parse_mode', 'HTML');
|
|
||||||
|
|
||||||
// Альтернативный URL
|
|
||||||
fetch(`https://api.telegram.org/bot${BOT_TOKEN}/sendMessage`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
if (data.ok) {
|
|
||||||
alert("Сообщение успешно отправлено! Я свяжусь с вами в ближайшее время.");
|
|
||||||
} else {
|
|
||||||
console.error('Telegram API Error:', data);
|
|
||||||
alert("Ошибка: " + (data.description || 'Неизвестная ошибка'));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error("Ошибка:", error);
|
|
||||||
alert("Произошла ошибка сети. Попробуйте позже или свяжитесь со мной напрямую.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Универсальный обработчик для кнопок
|
|
||||||
function setupButtonHandlers() {
|
|
||||||
const buttons = document.querySelectorAll('button[onclick*="sendMessageTelegram"]');
|
|
||||||
|
|
||||||
buttons.forEach(button => {
|
|
||||||
// Удаляем старые обработчики
|
|
||||||
button.removeAttribute('onclick');
|
|
||||||
|
|
||||||
// Добавляем универсальные обработчики
|
|
||||||
button.addEventListener('click', handleTelegramButtonClick);
|
|
||||||
button.addEventListener('touchstart', handleTelegramButtonClick, { passive: true });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обработчик кликов для Telegram кнопок
|
|
||||||
function handleTelegramButtonClick(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
// Для touch-событий, предотвращаем повторное срабатывание
|
|
||||||
if (event.type === 'touchstart') {
|
|
||||||
const now = Date.now();
|
|
||||||
if (this.lastTouch && (now - this.lastTouch) < 500) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.lastTouch = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
sendMessageTelegram();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализация при загрузке страницы
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
setupButtonHandlers();
|
|
||||||
});
|
|
||||||
@@ -1,464 +0,0 @@
|
|||||||
@import url("./style/digital_background.css");
|
|
||||||
@import url("./style/saveContactsButtonStyle.css");
|
|
||||||
@import url("./style/darkTheme.css");
|
|
||||||
@import url("./style/about.css");
|
|
||||||
@import url("./style/social_link.css");
|
|
||||||
@import url("./style/hero_section.css");
|
|
||||||
@import url("./style/yalarba_investmen.css");
|
|
||||||
@import url("./style/footer.css");
|
|
||||||
@import url("./style/repository_section.css");
|
|
||||||
@import url("./style/links_style.css");
|
|
||||||
@import url("./style/skill_section.css");
|
|
||||||
|
|
||||||
/* style.css - обновленный */
|
|
||||||
:root {
|
|
||||||
--primary: #a9e299;
|
|
||||||
--secondary: #63c1ff;
|
|
||||||
--light: #ecf0f1;
|
|
||||||
--dark: #36304d;
|
|
||||||
--success: #2ecc71;
|
|
||||||
--border-radius: 12px;
|
|
||||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
--transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease,
|
|
||||||
border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
min-width: 300px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 10px auto 5px auto;
|
|
||||||
padding: 10px 20px;
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr auto;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшенная сетка для header */
|
|
||||||
header {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: var(--dark);
|
|
||||||
padding: 2rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
grid-template-rows: auto auto;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle {
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1;
|
|
||||||
justify-self: end;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: var(--transition);
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social_links_block {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
justify-items: center;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшенная сетка для секций */
|
|
||||||
.section {
|
|
||||||
background: rgb(226, 240, 241);
|
|
||||||
padding: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entrepreneur-highlights {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight-item {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сетка для команды */
|
|
||||||
.team-section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.team-header {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roles-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-card {
|
|
||||||
background: rgba(255, 255, 255, 0.7);
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
transition: var(--transition);
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.role-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сетка для Yalarba секции */
|
|
||||||
.yalarba-section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-stats {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-value ul {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сетка для контактов */
|
|
||||||
.contact-info {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сетка для футера */
|
|
||||||
footer {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
padding: 1em 0 0 0;
|
|
||||||
color: var(--dark);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.two-column-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
border-left: 1px solid black;
|
|
||||||
grid-template-rows: auto; /* Явно указываем одну строку */
|
|
||||||
grid-auto-flow: row; /* Запрещаем автоматическое создание новых строк */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопки */
|
|
||||||
.btn {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0.8rem 1.5rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: var(--transition);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background-color: white;
|
|
||||||
color: var(--primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Timeline улучшения */
|
|
||||||
.timeline {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 2px;
|
|
||||||
background: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: -30px;
|
|
||||||
top: 5px;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--primary);
|
|
||||||
border: 2px solid var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптация для мелких экранов */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
body {
|
|
||||||
padding: 5px 10px;
|
|
||||||
margin: 5px auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
grid-template-rows: auto auto auto;
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1;
|
|
||||||
justify-self: center;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-valitovgaziz-photo-box img {
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-text {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.roles-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-stats {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.two-column-grid {
|
|
||||||
border-left: none;
|
|
||||||
border-top: 1px solid black;
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline {
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline:before {
|
|
||||||
left: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timeline-item:before {
|
|
||||||
left: -20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптация для очень маленьких экранов */
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
body {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero {
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-grid,
|
|
||||||
.roles-grid,
|
|
||||||
.entrepreneur-highlights {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-stats {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skills-container {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшения для планшетов */
|
|
||||||
@media (min-width: 769px) and (max-width: 1024px) {
|
|
||||||
.about {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.roles-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.entrepreneur-highlights {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Анимации и улучшения UX */
|
|
||||||
@keyframes fadeIn {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.section {
|
|
||||||
animation: fadeIn 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card,
|
|
||||||
.role-card,
|
|
||||||
.highlight-item {
|
|
||||||
animation: fadeIn 0.5s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшения доступности */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
* {
|
|
||||||
animation-duration: 0.01ms !important;
|
|
||||||
animation-iteration-count: 1 !important;
|
|
||||||
transition-duration: 0.01ms !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшения фокуса для доступности */
|
|
||||||
button:focus,
|
|
||||||
a:focus {
|
|
||||||
outline: 2px solid var(--secondary);
|
|
||||||
outline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшения для темной темы */
|
|
||||||
body.dark-mode .highlight-item,
|
|
||||||
body.dark-mode .role-card {
|
|
||||||
background: var(--dark-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .project-card:hover,
|
|
||||||
body.dark-mode .role-card:hover,
|
|
||||||
body.dark-mode .highlight-item:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
.about {
|
|
||||||
display: flex;
|
|
||||||
width: inherit;
|
|
||||||
height: auto;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-valitovgaziz-photo-box {
|
|
||||||
width: fit-content;
|
|
||||||
height: fit-content;
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-valitovgaziz-photo-box img {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
object-fit: cover;
|
|
||||||
border-radius: 1em;
|
|
||||||
-webkit-box-shadow: 4px 4px 8px 9px rgba(34, 60, 80, 0.2);
|
|
||||||
-moz-box-shadow: 4px 4px 8px 9px rgba(34, 60, 80, 0.2);
|
|
||||||
box-shadow: 4px 4px 8px 9px rgba(34, 60, 80, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#about-valitovgaziz-photo-img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
/* Сетка для about секции */
|
|
||||||
.about {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-valitovgaziz-photo-box {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
justify-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.about-text {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,617 +0,0 @@
|
|||||||
/* Базовые стили для блога */
|
|
||||||
:root {
|
|
||||||
--primary: #9ab09492;
|
|
||||||
--secondary: #3498db;
|
|
||||||
--accent: #2ecc71;
|
|
||||||
--light: #f8f9fa;
|
|
||||||
--dark: #1a252f;
|
|
||||||
--gray: #6c757d;
|
|
||||||
--light-gray: #e9ecef;
|
|
||||||
--border-radius: 12px;
|
|
||||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
||||||
--shadow-hover: 0 8px 15px rgba(0, 0, 0, 0.15);
|
|
||||||
--transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: var(--dark);
|
|
||||||
background-color: var(--light);
|
|
||||||
transition: var(--transition);
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для темной темы */
|
|
||||||
body.dark-mode {
|
|
||||||
background-color: #121212;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-nav {
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
border-bottom: 1px solid #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-header {
|
|
||||||
background: linear-gradient(135deg, #2c3e50 0%, #1a252f 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-container,
|
|
||||||
body.dark-mode .blog-sidebar,
|
|
||||||
body.dark-mode .blog-post {
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-sidebar-section,
|
|
||||||
body.dark-mode .blog-post-content,
|
|
||||||
body.dark-mode .blog-post-footer {
|
|
||||||
border-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-quote {
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
border-left-color: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-tag {
|
|
||||||
background-color: #2c3e50;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-pagination-btn {
|
|
||||||
background-color: #2c3e50;
|
|
||||||
color: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-footer {
|
|
||||||
background-color: #1a252f;
|
|
||||||
color: #b0b0b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопка переключения темы */
|
|
||||||
.theme-toggle {
|
|
||||||
position: fixed;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 1000;
|
|
||||||
padding: 8px 16px;
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: var(--transition);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: var(--shadow-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Навигация */
|
|
||||||
.blog-nav {
|
|
||||||
background-color: white;
|
|
||||||
border-bottom: 1px solid var(--light-gray);
|
|
||||||
padding: 1rem 0;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-nav-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-nav-logo {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--dark);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-nav-link {
|
|
||||||
color: var(--secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-nav-link:hover {
|
|
||||||
color: var(--dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Заголовок блога */
|
|
||||||
.blog-header {
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
|
|
||||||
padding: 4rem 2rem;
|
|
||||||
text-align: center;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-header-content {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-title {
|
|
||||||
font-size: 3rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-subtitle {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-meta {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-meta-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Основной контейнер - ИСПРАВЛЕНЫ ПРОПОРЦИИ */
|
|
||||||
.blog-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 3rem auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
display: grid;
|
|
||||||
/* Основная колонка 70%, боковая 30% */
|
|
||||||
grid-template-columns: 1fr 280px;
|
|
||||||
gap: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Боковая панель - компактная */
|
|
||||||
.blog-sidebar {
|
|
||||||
position: sticky;
|
|
||||||
top: 100px;
|
|
||||||
height: fit-content;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-sidebar-section {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
transition: var(--transition);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-sidebar-section h3 {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
color: var(--dark);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-sidebar-section p {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-categories {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-categories li {
|
|
||||||
margin-bottom: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-category {
|
|
||||||
display: block;
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
color: var(--gray);
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-category:hover {
|
|
||||||
background-color: var(--light-gray);
|
|
||||||
color: var(--dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-recent {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-recent li {
|
|
||||||
margin-bottom: 0.8rem;
|
|
||||||
padding-bottom: 0.8rem;
|
|
||||||
border-bottom: 1px solid var(--light-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-recent li:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
padding-bottom: 0;
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-recent a {
|
|
||||||
color: var(--dark);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-recent a:hover {
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Основное содержание - шире */
|
|
||||||
.blog-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2.5rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
transition: var(--transition);
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: var(--shadow-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-header {
|
|
||||||
padding: 2rem 2rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-category {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
background-color: var(--light-gray);
|
|
||||||
color: var(--gray);
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: var(--dark);
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
color: var(--gray);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-content {
|
|
||||||
padding: 0 2rem 1rem;
|
|
||||||
border-bottom: 1px solid var(--light-gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-content p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-content h3 {
|
|
||||||
margin: 1.5rem 0 1rem;
|
|
||||||
color: var(--dark);
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-content ul,
|
|
||||||
.blog-post-content ol {
|
|
||||||
margin: 1rem 0 1.5rem 1.5rem;
|
|
||||||
padding-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-content li {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
font-size: 1.05rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-quote {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1.5rem;
|
|
||||||
background-color: var(--light-gray);
|
|
||||||
border-left: 4px solid var(--secondary);
|
|
||||||
font-style: italic;
|
|
||||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-footer {
|
|
||||||
padding: 1.5rem 2rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-tags {
|
|
||||||
display: flex;
|
|
||||||
gap: 0.5rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-tag {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
background-color: var(--light-gray);
|
|
||||||
color: var(--gray);
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-tag:hover {
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-comment-btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-comment-btn:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Пагинация */
|
|
||||||
.blog-pagination {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 3rem auto 4rem;
|
|
||||||
padding: 0 2rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-pagination-btn {
|
|
||||||
padding: 0.75rem 1.5rem;
|
|
||||||
background-color: white;
|
|
||||||
color: var(--dark);
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
transition: var(--transition);
|
|
||||||
border: 1px solid var(--light-gray);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-pagination-btn:hover {
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
border-color: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-pagination-current {
|
|
||||||
color: var(--gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Футер */
|
|
||||||
.blog-footer {
|
|
||||||
background-color: var(--dark);
|
|
||||||
color: white;
|
|
||||||
padding: 3rem 0;
|
|
||||||
margin-top: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-footer-content {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-footer-content p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-footer-content a {
|
|
||||||
color: var(--light-gray);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-footer-content a:hover {
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопка для мобильного меню */
|
|
||||||
.blog-sidebar-toggle {
|
|
||||||
display: none;
|
|
||||||
width: 100%;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 1100px) {
|
|
||||||
.blog-container {
|
|
||||||
grid-template-columns: 1fr 240px;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 992px) {
|
|
||||||
.blog-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-sidebar {
|
|
||||||
position: static;
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.blog-title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-container {
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-nav-container {
|
|
||||||
padding: 0 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-header {
|
|
||||||
padding: 3rem 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-meta {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-header,
|
|
||||||
.blog-post-content,
|
|
||||||
.blog-post-footer {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-title {
|
|
||||||
font-size: 1.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-pagination {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-sidebar-toggle {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-sidebar {
|
|
||||||
display: none;
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-sidebar.show {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.blog-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-title {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-container {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-header,
|
|
||||||
.blog-post-content,
|
|
||||||
.blog-post-footer {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-post-footer {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-quote {
|
|
||||||
padding: 1rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Улучшения для темной темы */
|
|
||||||
body.dark-mode .blog-sidebar-section {
|
|
||||||
background-color: #2d2d2d;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-category {
|
|
||||||
color: #b0b0b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-category:hover {
|
|
||||||
background-color: #3d3d3d;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-recent a {
|
|
||||||
color: #b0b0b0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .blog-recent a:hover {
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
/* Переменные для темной темы */
|
|
||||||
:root {
|
|
||||||
--dark-bg: #1a252f;
|
|
||||||
--dark-text: #ecf0f1;
|
|
||||||
--dark-card: #2c3e50;
|
|
||||||
--dark-border: #34495e;
|
|
||||||
--dark-secondary: #2980b9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Кнопка переключения темы */
|
|
||||||
header {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* В darkTheme.css - добавьте системные предпочтения */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--dark-bg: #0a0a0a;
|
|
||||||
--dark-text: #e0e0e0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: var(--secondary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 14px;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-toggle:hover {
|
|
||||||
background: var(--dark-secondary);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Стили для темной темы */
|
|
||||||
body.dark-mode {
|
|
||||||
background-color: var(--dark-bg);
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode header {
|
|
||||||
background-color: var(--dark-bg);
|
|
||||||
color: var(--dark-text);
|
|
||||||
background: linear-gradient(135deg, var(--dark-bg) 0%, #1a535c 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .section {
|
|
||||||
background: var(--dark-card);
|
|
||||||
color: var(--dark-text);
|
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
|
||||||
border: 1px solid var(--dark-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .contact-info a {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .skill-tag {
|
|
||||||
background-color: var(--dark-border);
|
|
||||||
color: var(--dark-text);
|
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode footer {
|
|
||||||
color: var(--dark-text);
|
|
||||||
background-color: var(--dark-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode h1,
|
|
||||||
body.dark-mode h2,
|
|
||||||
body.dark-mode h3 {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .project-link {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .project-link:hover {
|
|
||||||
color: #3498db;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .btn-primary {
|
|
||||||
background-color: var(--dark-secondary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .btn-primary:hover {
|
|
||||||
background-color: #3498db;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .btn-secondary {
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--dark-text);
|
|
||||||
border: 2px solid var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .btn-secondary:hover {
|
|
||||||
background-color: var(--dark-text);
|
|
||||||
color: var(--dark-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .yalarba-section {
|
|
||||||
background: linear-gradient(135deg, var(--dark-card) 0%, #34495e 100%);
|
|
||||||
border-left: 5px solid var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .investment-cta {
|
|
||||||
background-color: var(--dark-border);
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .timeline:before {
|
|
||||||
background: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .timeline-item:before {
|
|
||||||
background: var(--dark-card);
|
|
||||||
border: 2px solid var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .highlight {
|
|
||||||
color: #f39c12;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для социальных ссылок */
|
|
||||||
body.dark-mode .social_link {
|
|
||||||
background-color: var(--dark-card);
|
|
||||||
box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для фото */
|
|
||||||
body.dark-mode .about-valitovgaziz-photo-box img {
|
|
||||||
box-shadow: 4px 4px 8px 9px rgba(0, 0, 0, 0.3);
|
|
||||||
filter: brightness(0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ТЕМНАЯ ТЕМА ДЛЯ СЕКЦИИ "О РЕПОЗИТОРИИ" */
|
|
||||||
body.dark-mode .projects-grid {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .project-card {
|
|
||||||
background: var(--dark-card);
|
|
||||||
color: var(--dark-text);
|
|
||||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
|
|
||||||
border-left: 4px solid var(--dark-secondary);
|
|
||||||
border: 1px solid var(--dark-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .project-card h3 {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .current-info {
|
|
||||||
color: var(--dark-text);
|
|
||||||
border: 1px solid var(--dark-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .current-info h3 {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .current-info ul {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .current-info strong {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для контактной секции */
|
|
||||||
body.dark-mode .contact-info p {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .contact-info a {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode #saveContactBtn {
|
|
||||||
background: var(--dark-card);
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
border: 2px solid var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode #saveContactBtn:hover {
|
|
||||||
background: var(--dark-secondary);
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для футера */
|
|
||||||
body.dark-mode .footer-box {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .footer-box a {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .footer-box ul {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для hero section */
|
|
||||||
body.dark-mode .hero {
|
|
||||||
background: linear-gradient(135deg, var(--dark-bg) 0%, #1a535c 100%);
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .hero-description {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .hero-subtitle {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Темная тема для секции "Обо мне" */
|
|
||||||
body.dark-mode .about {
|
|
||||||
background: var(--dark-card);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .about-text {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .entrepreneur-highlights {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .highlight-item h4 {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .highlight-item p {
|
|
||||||
color: var(--dark-text);
|
|
||||||
}
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
/* Digital Background for Software Development Website */
|
|
||||||
/* Интеграция с существующей системой тем */
|
|
||||||
|
|
||||||
/* Используем переменные из darkTheme.css */
|
|
||||||
:root {
|
|
||||||
/* Light Theme Colors - интегрируем с существующими переменными */
|
|
||||||
--bg-primary-light: #f8f9fa;
|
|
||||||
--bg-secondary-light: #e9ecef;
|
|
||||||
--accent-primary-light: #007bff;
|
|
||||||
--accent-secondary-light: #6c757d;
|
|
||||||
--text-primary-light: #212529;
|
|
||||||
--particle-color-light: rgba(0, 123, 255, 0.1);
|
|
||||||
|
|
||||||
/* Dark Theme Colors - используем переменные из darkTheme.css */
|
|
||||||
--bg-primary-dark: var(--dark-bg, #1a252f);
|
|
||||||
--bg-secondary-dark: var(--dark-card, #2c3e50);
|
|
||||||
--accent-primary-dark: var(--dark-secondary, #2980b9);
|
|
||||||
--accent-secondary-dark: var(--dark-border, #34495e);
|
|
||||||
--text-primary-dark: var(--dark-text, #ecf0f1);
|
|
||||||
--particle-color-dark: rgba(41, 128, 185, 0.15);
|
|
||||||
|
|
||||||
/* Current Theme - defaults to light */
|
|
||||||
--bg-primary: var(--bg-primary-light);
|
|
||||||
--bg-secondary: var(--bg-secondary-light);
|
|
||||||
--accent-primary: var(--accent-primary-light);
|
|
||||||
--accent-secondary: var(--accent-secondary-light);
|
|
||||||
--text-primary: var(--text-primary-light);
|
|
||||||
--particle-color: var(--particle-color-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Интеграция с существующей темной темой */
|
|
||||||
body.dark-mode {
|
|
||||||
--bg-primary: var(--bg-primary-dark);
|
|
||||||
--bg-secondary: var(--bg-secondary-dark);
|
|
||||||
--accent-primary: var(--accent-primary-dark);
|
|
||||||
--accent-secondary: var(--accent-secondary-dark);
|
|
||||||
--text-primary: var(--text-primary-dark);
|
|
||||||
--particle-color: var(--particle-color-dark);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Base Body Styles */
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
|
|
||||||
color: var(--text-primary);
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
overflow-x: hidden;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animated Background Elements */
|
|
||||||
body::before {
|
|
||||||
content: '';
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 20% 80%, var(--particle-color) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 80% 20%, var(--particle-color) 0%, transparent 50%),
|
|
||||||
radial-gradient(circle at 40% 40%, var(--particle-color) 0%, transparent 50%);
|
|
||||||
animation: backgroundPulse 8s ease-in-out infinite;
|
|
||||||
z-index: -3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Binary Code Rain Effect - ИСПРАВЛЕННЫЙ СТИЛЬ */
|
|
||||||
.binary-rain {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: -1;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.binary-digit {
|
|
||||||
position: absolute;
|
|
||||||
color: var(--accent-primary);
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
font-weight: bold;
|
|
||||||
animation: fall linear infinite;
|
|
||||||
text-shadow: 0 0 5px currentColor;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Circuit Board Grid */
|
|
||||||
.circuit-grid {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(var(--accent-secondary) 1px, transparent 1px),
|
|
||||||
linear-gradient(90deg, var(--accent-secondary) 1px, transparent 1px);
|
|
||||||
background-size: 40px 40px;
|
|
||||||
opacity: 0.03;
|
|
||||||
z-index: -2;
|
|
||||||
animation: gridMove 20s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Floating Code Elements */
|
|
||||||
.floating-code {
|
|
||||||
position: fixed;
|
|
||||||
font-family: 'Courier New', monospace;
|
|
||||||
color: var(--accent-primary);
|
|
||||||
opacity: 0.1;
|
|
||||||
z-index: -1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code-bracket { animation: float 15s ease-in-out infinite; }
|
|
||||||
.code-parenthesis { animation: float 18s ease-in-out infinite reverse; }
|
|
||||||
.code-brace { animation: float 20s ease-in-out infinite; }
|
|
||||||
.code-tag { animation: float 16s ease-in-out infinite reverse; }
|
|
||||||
|
|
||||||
/* Connection Nodes */
|
|
||||||
.connection-node {
|
|
||||||
position: fixed;
|
|
||||||
background: var(--accent-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.2;
|
|
||||||
z-index: -1;
|
|
||||||
animation: nodePulse 4s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Data Flow Lines */
|
|
||||||
.data-flow {
|
|
||||||
position: fixed;
|
|
||||||
background: linear-gradient(90deg, transparent, var(--accent-primary), transparent);
|
|
||||||
opacity: 0.1;
|
|
||||||
z-index: -1;
|
|
||||||
animation: dataFlow 6s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ОБЯЗАТЕЛЬНО: Убедимся что основной контент поверх фона */
|
|
||||||
header, .section, footer {
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes backgroundPulse {
|
|
||||||
0%, 100% { opacity: 0.5; }
|
|
||||||
50% { opacity: 0.8; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes fall {
|
|
||||||
to {
|
|
||||||
transform: translateY(100vh);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float {
|
|
||||||
0%, 100% {
|
|
||||||
transform: translate(0, 0) rotate(0deg);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(20px, 20px) rotate(5deg);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(-15px, 30px) rotate(-5deg);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(10px, -10px) rotate(3deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes gridMove {
|
|
||||||
0% { transform: translate(0, 0); }
|
|
||||||
100% { transform: translate(20px, 20px); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes nodePulse {
|
|
||||||
0%, 100% { transform: scale(1); opacity: 0.2; }
|
|
||||||
50% { transform: scale(1.3); opacity: 0.4; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes dataFlow {
|
|
||||||
0% { transform: translateX(-100%); opacity: 0; }
|
|
||||||
50% { opacity: 0.3; }
|
|
||||||
100% { transform: translateX(100%); opacity: 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Interactive Elements */
|
|
||||||
.connection-node:hover {
|
|
||||||
opacity: 0.6;
|
|
||||||
transform: scale(1.5);
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Performance Optimizations */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.binary-digit,
|
|
||||||
.floating-code,
|
|
||||||
.connection-node,
|
|
||||||
.data-flow,
|
|
||||||
body::before {
|
|
||||||
animation: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive Adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.binary-digit {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circuit-grid {
|
|
||||||
background-size: 20px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.floating-code {
|
|
||||||
font-size: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Убедимся что кнопка переключения темы всегда поверх всего */
|
|
||||||
.theme-toggle {
|
|
||||||
z-index: 1000 !important;
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1em 0 0 0;
|
|
||||||
color: var(--dark);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-links {
|
|
||||||
padding: 1em;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section h4 {
|
|
||||||
margin: 0 0 0.5rem 0;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.two-column-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
border-left: 1px solid black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-box {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-box ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-box li {
|
|
||||||
margin-bottom: 0.3rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-box a {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-box a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-end-text {
|
|
||||||
margin: 2rem 0 3rem 0;
|
|
||||||
position: relative;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность для мобильных устройств */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.footer-links {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.two-column-grid {
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
/* Hero Section Styles */
|
|
||||||
.hero {
|
|
||||||
background: linear-gradient(135deg, var(--primary) 0%, #2fe892 100%);
|
|
||||||
padding: 4rem 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-text {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight {
|
|
||||||
color: #137c5c; /* Яркий акцентный цвет */
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-description {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-buttons {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background-color: var(--secondary);
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.8rem 1.5rem;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: bold;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
color: white;
|
|
||||||
border: 2px solid white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background-color: #2980b9;
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-image {
|
|
||||||
flex: 0 0 300px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-image img {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 300px;
|
|
||||||
border-radius: 10px;
|
|
||||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.resume-block {
|
|
||||||
justify-self: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
#resume-link {
|
|
||||||
color: #2980b9;
|
|
||||||
|
|
||||||
/* Адаптивность для героя */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero-content {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность для героя */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.hero-content {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-buttons {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/* Добавьте в style.css */
|
|
||||||
a {
|
|
||||||
color: var(--secondary);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
font-weight: 500;
|
|
||||||
position: relative;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:not(.btn):after {
|
|
||||||
content: "↗";
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
font-size: 0.8em;
|
|
||||||
opacity: 0;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:not(.btn):hover {
|
|
||||||
color: #2980b9;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:not(.btn):hover:after {
|
|
||||||
opacity: 1;
|
|
||||||
right: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Для внутренних ссылок (без внешней иконки) */
|
|
||||||
a[href*="valitovgaziz.ru"]:after,
|
|
||||||
a[href*="#"]:after {
|
|
||||||
content: "→";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Для темной темы */
|
|
||||||
body.dark-mode a:not(.btn) {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode a:not(.btn):hover {
|
|
||||||
color: #3498db;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Для ссылок в футере */
|
|
||||||
.footer-box a {
|
|
||||||
color: inherit;
|
|
||||||
transition: var(--transition);
|
|
||||||
border-bottom: 1px dotted transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-box a:hover {
|
|
||||||
border-bottom-color: currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Для ссылок в hero-секции */
|
|
||||||
.hero a {
|
|
||||||
color: #ffd166; /* Акцентный цвет из hero-секции */
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero a:hover {
|
|
||||||
color: #ffb347;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Для ссылок в карточках проектов */
|
|
||||||
.project-card a {
|
|
||||||
color: var(--dark-secondary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card a:hover {
|
|
||||||
color: var(--secondary);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
|
|
||||||
.current-info {
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.highlight-item:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Сетка для проектов */
|
|
||||||
.projects-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card {
|
|
||||||
background: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: var(--border-radius);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
border-left: 4px solid var(--secondary);
|
|
||||||
transition: var(--transition);
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
height: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#saveContactBtn {
|
|
||||||
padding: 10px 20px;
|
|
||||||
background: white;
|
|
||||||
color: #2541b2;
|
|
||||||
border: 2px solid #2541b2;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 15px;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
#saveContactBtn:hover {
|
|
||||||
background: #2541b2;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#saveContactBtn.dark-mode {
|
|
||||||
background: --dark-card;
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
/* [file name]: skill_section.css */
|
|
||||||
.skills-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card {
|
|
||||||
background: linear-gradient(135deg, var(--secondary) 0%, #2980b9 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
|
|
||||||
transition: var(--transition);
|
|
||||||
display: grid;
|
|
||||||
grid-template-rows: auto auto 1fr auto;
|
|
||||||
gap: 0.8rem;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 4px;
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-header {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto;
|
|
||||||
align-items: start;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-name {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
margin: 0;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-level {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
padding: 0.3rem 0.8rem;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
font-weight: bold;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-description {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
opacity: 0.9;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-acquisition {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
padding: 0.8rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-growth {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #e8f4fc;
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-growth::before {
|
|
||||||
content: '🚀';
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Уровни навыков */
|
|
||||||
.skill-level.beginner { background: rgba(231, 76, 60, 0.8); }
|
|
||||||
.skill-level.intermediate { background: rgba(241, 196, 15, 0.8); }
|
|
||||||
.skill-level.advanced { background: rgba(46, 204, 113, 0.8); }
|
|
||||||
.skill-level.expert { background: rgba(52, 152, 219, 0.8); }
|
|
||||||
|
|
||||||
/* Темная тема */
|
|
||||||
body.dark-mode .skill-card {
|
|
||||||
background: linear-gradient(135deg, var(--dark-card) 0%, #34495e 100%);
|
|
||||||
border: 1px solid var(--dark-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .skill-level {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.dark-mode .skill-acquisition {
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Адаптивность */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.skills-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card {
|
|
||||||
padding: 1.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-header {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 0.3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-level {
|
|
||||||
justify-self: start;
|
|
||||||
margin-top: 0.3rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
.skills-container {
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
.social_links_block {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
width: 100%;
|
|
||||||
height: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social_link_block {
|
|
||||||
width: fit-content;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social_links_block h4 {
|
|
||||||
width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social_link {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
-webkit-box-shadow: 0px 0px 14px 0px rgba(34, 60, 80, 0.2);
|
|
||||||
-moz-box-shadow: 0px 0px 14px 0px rgba(34, 60, 80, 0.2);
|
|
||||||
box-shadow: 0px 0px 14px 0px rgba(34, 60, 80, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.social_link a {
|
|
||||||
width: fit-content;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
/* Yalarba Investment Section */
|
|
||||||
.yalarba-section {
|
|
||||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
||||||
border-left: 5px solid var(--secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-tagline {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
color: var(--text);
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-stats {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
margin: 0;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat {
|
|
||||||
text-align: center;
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat h3 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: var(--secondary);
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-value ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yalarba-value li {
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.investment-cta {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding: 2rem;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Valitov Gaziz | Технологический предприниматель</title>
|
||||||
|
<meta name="description" content="Валитов Газиз — технологический предприниматель и fullstack-разработчик. Создатель Yalarba.ru, EasySite102.ru." />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "dist"],
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "my_site",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.22",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"vite": "^7.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app" :class="{ 'menu-open': mobileMenuOpen }">
|
||||||
|
<TheHeader @toggle-theme="toggleTheme" @toggle-menu="toggleMenu" :theme="theme" :menu-open="mobileMenuOpen" />
|
||||||
|
<main class="main-content">
|
||||||
|
<router-view />
|
||||||
|
</main>
|
||||||
|
<TheFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import TheHeader from './components/TheHeader.vue'
|
||||||
|
import TheFooter from './components/TheFooter.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
TheHeader,
|
||||||
|
TheFooter,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
theme: 'dark',
|
||||||
|
mobileMenuOpen: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
const saved = localStorage.getItem('theme')
|
||||||
|
if (saved) {
|
||||||
|
this.theme = saved
|
||||||
|
}
|
||||||
|
document.documentElement.setAttribute('data-theme', this.theme)
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleTheme() {
|
||||||
|
this.theme = this.theme === 'dark' ? 'light' : 'dark'
|
||||||
|
localStorage.setItem('theme', this.theme)
|
||||||
|
document.documentElement.setAttribute('data-theme', this.theme)
|
||||||
|
},
|
||||||
|
toggleMenu() {
|
||||||
|
this.mobileMenuOpen = !this.mobileMenuOpen
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.main-content {
|
||||||
|
min-height: calc(100vh - 160px);
|
||||||
|
padding-top: 70px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #ffffff;
|
||||||
|
--bg-secondary: #f5f7fa;
|
||||||
|
--text: #1a1a2e;
|
||||||
|
--text-secondary: #4a4a6a;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--accent-hover: #1d4ed8;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--header-bg: rgba(255, 255, 255, 0.9);
|
||||||
|
--skill-bg: #eef2ff;
|
||||||
|
--tag-bg: #e0e7ff;
|
||||||
|
--tag-text: #3730a3;
|
||||||
|
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
--gradient-hero: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
--max-width: 1100px;
|
||||||
|
--radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg: #0f172a;
|
||||||
|
--bg-secondary: #1e293b;
|
||||||
|
--text: #e2e8f0;
|
||||||
|
--text-secondary: #94a3b8;
|
||||||
|
--accent: #60a5fa;
|
||||||
|
--accent-hover: #3b82f6;
|
||||||
|
--card-bg: #1e293b;
|
||||||
|
--border: #334155;
|
||||||
|
--header-bg: rgba(15, 23, 42, 0.9);
|
||||||
|
--skill-bg: #1e293b;
|
||||||
|
--tag-bg: #1e293b;
|
||||||
|
--tag-text: #93c5fd;
|
||||||
|
--shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
--shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.4);
|
||||||
|
--gradient-hero: linear-gradient(135deg, #1e3a5f 0%, #2d1b69 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 80px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section:nth-child(even) {
|
||||||
|
background-color: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 48px;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 60px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--accent);
|
||||||
|
margin: 12px auto 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 12px 28px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition: opacity 0.6s ease, transform 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.section {
|
||||||
|
padding: 48px 0;
|
||||||
|
}
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 205 KiB |
@@ -0,0 +1,71 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="container footer-content">
|
||||||
|
<div class="footer-info">
|
||||||
|
<p>Уфа · Ufa · Өфө</p>
|
||||||
|
<p>© 2026 Valitov Gaziz</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="footer-link">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://vk.com/valitovgaziz" target="_blank" rel="noopener noreferrer" class="footer-link">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M15.684 0H8.316C2.879 0 0 2.879 0 8.316v7.368C0 21.121 2.879 24 8.316 24h7.368C21.121 24 24 21.121 24 15.684V8.316C24 2.879 21.121 0 15.684 0zm3.6 16.535h-1.651c-.849 0-1.08-.595-1.728-1.289-.566-.604-1.05-1.097-1.89-1.097-.97 0-1.4.513-1.4 1.316v1.07c0 .456-.178.72-1.076.72-1.583 0-3.308-1.088-4.33-2.539-1.443-1.875-1.83-3.36-1.83-3.636 0-.177.151-.343.448-.343h1.652c.468 0 .64.227.82.735.633 1.937 1.699 3.633 2.345 3.633.22 0 .306-.11.306-.576v-2.22c-.111-1.048-.672-1.124-.672-1.504 0-.242.168-.44.45-.44h2.578c.347 0 .46.196.46.573v2.176c0 .346.149.44.254.44.224 0 .38-.094.598-.317.604-.7 1.118-1.847 1.118-1.847.088-.215.224-.423.513-.423h1.651c.493 0 .597.252.493.619-.307.953-1.577 2.764-1.577 2.764-.168.257-.224.381 0 .638.168.224.739.672 1.126 1.09.392.423.672.747.784.985.196.44-.056.735-.54.735z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="mailto:valitovgaziz@yandex.ru" class="footer-link">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 4L12 13L2 4"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TheFooter',
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding: 32px 0;
|
||||||
|
margin-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-info p {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<div class="container header-container">
|
||||||
|
<router-link to="/" class="logo">Valitov<span>Gaziz</span></router-link>
|
||||||
|
<nav class="nav" :class="{ 'nav-open': menuOpen }">
|
||||||
|
<router-link to="/" class="nav-link" @click="$emit('toggle-menu')">Главная</router-link>
|
||||||
|
<a href="#about" class="nav-link" @click="closeMenu">Обо мне</a>
|
||||||
|
<a href="#projects" class="nav-link" @click="closeMenu">Проекты</a>
|
||||||
|
<a href="#experience" class="nav-link" @click="closeMenu">Опыт</a>
|
||||||
|
<a href="#skills" class="nav-link" @click="closeMenu">Навыки</a>
|
||||||
|
<a href="#contact" class="nav-link" @click="closeMenu">Контакты</a>
|
||||||
|
<router-link to="/blog" class="nav-link" @click="$emit('toggle-menu')">Блог</router-link>
|
||||||
|
</nav>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="theme-toggle" @click="$emit('toggle-theme')" :aria-label="theme === 'dark' ? 'Светлая тема' : 'Тёмная тема'">
|
||||||
|
<svg v-if="theme === 'dark'" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||||
|
<svg v-else width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="burger" @click="$emit('toggle-menu')" :aria-label="menuOpen ? 'Закрыть меню' : 'Открыть меню'">
|
||||||
|
<span></span><span></span><span></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'TheHeader',
|
||||||
|
props: {
|
||||||
|
theme: { type: String, required: true },
|
||||||
|
menuOpen: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
emits: ['toggle-theme', 'toggle-menu'],
|
||||||
|
methods: {
|
||||||
|
closeMenu() {
|
||||||
|
if (this.menuOpen) {
|
||||||
|
this.$emit('toggle-menu')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: var(--header-bg);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo span {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover,
|
||||||
|
.router-link-exact-active {
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--skill-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger span {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--text);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-open .burger span:nth-child(1) {
|
||||||
|
transform: rotate(45deg) translate(5px, 5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-open .burger span:nth-child(2) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-open .burger span:nth-child(3) {
|
||||||
|
transform: rotate(-45deg) translate(5px, -5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
gap: 4px;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-open {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.burger {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import './assets/main.css'
|
||||||
|
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import Home from '../views/Home.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Home',
|
||||||
|
component: Home,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/blog',
|
||||||
|
name: 'Blog',
|
||||||
|
component: () => import('../views/Blog.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div class="blog-page">
|
||||||
|
<section class="blog-hero">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="blog-title">Блог</h1>
|
||||||
|
<p class="blog-subtitle">Мысли, идеи и заметки о разработке, технологиях и предпринимательстве</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section">
|
||||||
|
<div class="container">
|
||||||
|
<div class="blog-list">
|
||||||
|
<article v-for="(post, index) in posts" :key="index" class="blog-post card" :class="{ 'fade-in': true, 'visible': true }">
|
||||||
|
<div class="post-meta">
|
||||||
|
<span class="post-date">{{ post.date }}</span>
|
||||||
|
<span class="post-read-time">{{ post.readTime }}</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="post-title">{{ post.title }}</h2>
|
||||||
|
<p class="post-excerpt">{{ post.excerpt }}</p>
|
||||||
|
<blockquote v-if="post.quote" class="post-quote">
|
||||||
|
{{ post.quote }}
|
||||||
|
</blockquote>
|
||||||
|
<p v-if="post.additional" class="post-excerpt">{{ post.additional }}</p>
|
||||||
|
<div class="post-tags">
|
||||||
|
<span v-for="(tag, tIndex) in post.tags" :key="tIndex" class="post-tag">{{ tag }}</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'BlogView',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
posts: [
|
||||||
|
{
|
||||||
|
date: '20 марта 2024',
|
||||||
|
readTime: '6 мин чтения',
|
||||||
|
title: 'EasySite & YalArba: текущее состояние и планы развития',
|
||||||
|
excerpt: 'EasySite (B2B) — конструктор сайтов для отелей, санаториев, ресторанов. YalArba (B2C) — агрегатор для туристов с поиском, отзывами и системой бронирования.',
|
||||||
|
additional: 'Уже работают: JWT-авторизация, Docker-инфраструктура, SSL (Let\'s Encrypt), базовая аналитика. В бете: easysite102.ru и yalarba.ru.',
|
||||||
|
tags: ['EasySite', 'YalArba', 'Туризм', 'Стартап'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '25 марта 2024',
|
||||||
|
readTime: '8 мин чтения',
|
||||||
|
title: 'Почему я создаю YalArba: история и миссия',
|
||||||
|
excerpt: 'История началась в 2017 году — работа на заводе и учёба не могли затмить желания создать что-то полезное. Не найдя бесплатных маршрутов для путешествий онлайн, решил сделать решение сам.',
|
||||||
|
additional: 'Большинство сервисов будут бесплатными — хочется предоставить доступные альтернативы для всех. Бизнес-модель строится на ценности от количества пользователей.',
|
||||||
|
quote: 'Технологии должны решать реальные проблемы людей, а не создавать новые.',
|
||||||
|
tags: ['История', 'Миссия', 'Социальный проект', 'Туризм'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '15 марта 2024',
|
||||||
|
readTime: '5 мин чтения',
|
||||||
|
title: 'Новый этап развития Yalarba.ru',
|
||||||
|
excerpt: 'Завершён переход на новую архитектуру. Реализованы: обновлённый интерфейс поиска маршрутов, интеграция картографических сервисов, улучшенная система рекомендаций, подготовка мобильного приложения.',
|
||||||
|
tags: ['Yalarba', 'TravelTech', 'Разработка'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '10 марта 2024',
|
||||||
|
readTime: '7 мин чтения',
|
||||||
|
title: 'Переход с Vue 2 на Vue 3: опыт и выводы',
|
||||||
|
excerpt: 'Ключевые преимущества: Composition API, улучшенная производительность, поддержка TypeScript, меньший размер бандла. Миграция прошла гладко, но потребовала внимания к деталям.',
|
||||||
|
tags: ['Vue3', 'Фронтенд', 'JavaScript'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: '5 марта 2024',
|
||||||
|
readTime: '4 мин чтения',
|
||||||
|
title: 'О важности сообщества в разработке',
|
||||||
|
excerpt: 'Профессиональный рост через сообщество: обратная связь, совместное обучение, поддержка и вдохновение. Нетворкинг и обмен опытом — ключ к развитию.',
|
||||||
|
tags: ['Сообщество', 'Разработка', 'IT'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.blog-hero {
|
||||||
|
padding: 80px 0 40px;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-subtitle {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-list {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blog-post {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-date {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-read-time {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
font-size: 1.4rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-excerpt {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-quote {
|
||||||
|
border-left: 3px solid var(--accent);
|
||||||
|
padding: 12px 20px;
|
||||||
|
margin: 16px 0;
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-tag {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--tag-bg);
|
||||||
|
color: var(--tag-text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.blog-title {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,771 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-bg"></div>
|
||||||
|
<div class="container hero-content">
|
||||||
|
<div class="hero-text">
|
||||||
|
<p class="hero-greeting">Привет, я</p>
|
||||||
|
<h1 class="hero-name">Валитов Газиз</h1>
|
||||||
|
<p class="hero-title">Технологический предприниматель & Fullstack-разработчик</p>
|
||||||
|
<p class="hero-desc">
|
||||||
|
Создаю цифровые продукты, которые меняют жизнь людей к лучшему.
|
||||||
|
Основатель проектов Yalarba.ru и EasySite102.ru.
|
||||||
|
</p>
|
||||||
|
<div class="hero-buttons">
|
||||||
|
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="btn btn-primary">
|
||||||
|
Написать в Telegram
|
||||||
|
</a>
|
||||||
|
<router-link to="/blog" class="btn btn-outline">
|
||||||
|
Читать блог
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="hero-social">
|
||||||
|
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="social-link" title="Telegram">@valitovgaziz</a>
|
||||||
|
<a href="https://vk.com/valitovgaziz" target="_blank" rel="noopener noreferrer" class="social-link" title="VK">vk.com/valitovgaziz</a>
|
||||||
|
<a href="mailto:valitovgaziz@yandex.ru" class="social-link" title="Email">valitovgaziz@yandex.ru</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-image">
|
||||||
|
<div class="hero-photo">
|
||||||
|
<img src="/src/assets/photo.jpg" alt="Valitov Gaziz" class="photo-img" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="about" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Обо мне</h2>
|
||||||
|
<div class="about-content">
|
||||||
|
<div class="about-text">
|
||||||
|
<p class="about-paragraph">
|
||||||
|
Родился в городе Кумертау в 1985 году. Окончил УГАТУ (Уфимский государственный авиационный технический университет), прошёл службу в армии, работал на производстве.
|
||||||
|
</p>
|
||||||
|
<p class="about-paragraph">
|
||||||
|
С 2015 года в IT — прошёл путь от техника до основателя собственного технологического проекта. За плечами опыт работы с Go, Vue 3, Nuxt.js, PostgreSQL, Docker и другими современными технологиями.
|
||||||
|
</p>
|
||||||
|
<p class="about-paragraph">
|
||||||
|
Моя миссия — создавать продукты, которые приносят реальную пользу людям и делают туризм в Башкортостане доступнее и удобнее.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="about-highlights">
|
||||||
|
<div class="highlight-card">
|
||||||
|
<div class="highlight-icon">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>
|
||||||
|
</div>
|
||||||
|
<h4>Техническое видение</h4>
|
||||||
|
<p>Создаю масштабируемую архитектуру, выбираю правильные инструменты под задачи</p>
|
||||||
|
</div>
|
||||||
|
<div class="highlight-card">
|
||||||
|
<div class="highlight-icon">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>
|
||||||
|
</div>
|
||||||
|
<h4>Бизнес-ориентация</h4>
|
||||||
|
<p>Фокус на пользовательской ценности и устойчивых бизнес-моделях</p>
|
||||||
|
</div>
|
||||||
|
<div class="highlight-card">
|
||||||
|
<div class="highlight-icon">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>
|
||||||
|
</div>
|
||||||
|
<h4>Практический подход</h4>
|
||||||
|
<p>От прототипа до продукта — быстрое тестирование гипотез и итеративное развитие</p>
|
||||||
|
</div>
|
||||||
|
<div class="highlight-card">
|
||||||
|
<div class="highlight-icon">
|
||||||
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h4>Мотивация</h4>
|
||||||
|
<p>Создание проекта, который приносит пользу многим людям</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="projects" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Проекты</h2>
|
||||||
|
<div class="projects-grid">
|
||||||
|
<div class="project-card card">
|
||||||
|
<div class="project-header">
|
||||||
|
<h3 class="project-name">Yalarba.ru</h3>
|
||||||
|
<span class="project-status">Активный</span>
|
||||||
|
</div>
|
||||||
|
<p class="project-desc">
|
||||||
|
Туристическая платформа для Республики Башкортостан. Помогает путешественникам открывать новые места, строить маршруты и планировать поездки.
|
||||||
|
</p>
|
||||||
|
<div class="project-tech">
|
||||||
|
<span class="tech-tag">Go</span>
|
||||||
|
<span class="tech-tag">Nuxt.js 4</span>
|
||||||
|
<span class="tech-tag">PostgreSQL</span>
|
||||||
|
<span class="tech-tag">Docker</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-card card">
|
||||||
|
<div class="project-header">
|
||||||
|
<h3 class="project-name">EasySite102.ru</h3>
|
||||||
|
<span class="project-status">Бета</span>
|
||||||
|
</div>
|
||||||
|
<p class="project-desc">
|
||||||
|
Конструктор сайтов для бизнеса в сфере туризма: отелей, санаториев, ресторанов. Часть экосистемы YalArba.
|
||||||
|
</p>
|
||||||
|
<div class="project-tech">
|
||||||
|
<span class="tech-tag">Vue 3</span>
|
||||||
|
<span class="tech-tag">Nuxt.js</span>
|
||||||
|
<span class="tech-tag">Go</span>
|
||||||
|
<span class="tech-tag">PostgreSQL</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="project-card card">
|
||||||
|
<div class="project-header">
|
||||||
|
<h3 class="project-name">BegushiyBashkir.ru</h3>
|
||||||
|
<span class="project-status">Активный</span>
|
||||||
|
</div>
|
||||||
|
<p class="project-desc">
|
||||||
|
Беговой клуб. Сайт для бегового сообщества, основанного другом и партнёром Аминевым Загиром.
|
||||||
|
</p>
|
||||||
|
<div class="project-tech">
|
||||||
|
<span class="tech-tag">Vue 3</span>
|
||||||
|
<span class="tech-tag">Go</span>
|
||||||
|
<span class="tech-tag">PostgreSQL</span>
|
||||||
|
<span class="tech-tag">Docker</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="experience" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Опыт работы</h2>
|
||||||
|
<div class="timeline">
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-dot"></div>
|
||||||
|
<div class="timeline-content card">
|
||||||
|
<div class="timeline-period">2020 — настоящее время</div>
|
||||||
|
<h3 class="timeline-title">Основатель и Tech Lead</h3>
|
||||||
|
<p class="timeline-company">Yalarba.ru</p>
|
||||||
|
<ul class="timeline-duties">
|
||||||
|
<li>Микросервисы на Go + Nuxt.js 4</li>
|
||||||
|
<li>Проектирование и оптимизация PostgreSQL</li>
|
||||||
|
<li>Docker-инфраструктура, управление продуктом</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-dot"></div>
|
||||||
|
<div class="timeline-content card">
|
||||||
|
<div class="timeline-period">2017 — настоящее время</div>
|
||||||
|
<h3 class="timeline-title">Fullstack-разработчик (Контракты)</h3>
|
||||||
|
<p class="timeline-company">Фриланс / Проектная работа</p>
|
||||||
|
<ul class="timeline-duties">
|
||||||
|
<li>REST API на Go (GORM, Chi)</li>
|
||||||
|
<li>Фронтенд на Nuxt.js 4 / Vue 3</li>
|
||||||
|
<li>Посадочные страницы, интеграции</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="education" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Образование</h2>
|
||||||
|
<div class="education-grid">
|
||||||
|
<div class="edu-card card">
|
||||||
|
<div class="edu-year">2025 — н.в.</div>
|
||||||
|
<h4>МТИ — Московский технологический институт</h4>
|
||||||
|
<p>Разработка программного обеспечения</p>
|
||||||
|
</div>
|
||||||
|
<div class="edu-card card">
|
||||||
|
<div class="edu-year">2021</div>
|
||||||
|
<h4>Университет Иннополис</h4>
|
||||||
|
<p>Java Enterprise Developer</p>
|
||||||
|
</div>
|
||||||
|
<div class="edu-card card">
|
||||||
|
<div class="edu-year">2016 — 2020</div>
|
||||||
|
<h4>Уфимский колледж статистики и информатики</h4>
|
||||||
|
<p>Техник по информационным системам</p>
|
||||||
|
</div>
|
||||||
|
<div class="edu-card card">
|
||||||
|
<div class="edu-year">2002 — 2005</div>
|
||||||
|
<h4>УГАТУ</h4>
|
||||||
|
<p>Технология сварочного производства</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="skills" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Навыки</h2>
|
||||||
|
<div class="skills-grid">
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">Golang</span>
|
||||||
|
<span class="skill-level">Продвинутый</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 90%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">JavaScript</span>
|
||||||
|
<span class="skill-level">Продвинутый</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 85%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">Vue 3</span>
|
||||||
|
<span class="skill-level">Средний</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 70%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">Nuxt.js</span>
|
||||||
|
<span class="skill-level">Средний</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 65%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">PostgreSQL</span>
|
||||||
|
<span class="skill-level">Средний</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 70%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">Docker</span>
|
||||||
|
<span class="skill-level">Средний</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 70%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">Java</span>
|
||||||
|
<span class="skill-level">Начальный</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 40%"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="skill-card card">
|
||||||
|
<div class="skill-header">
|
||||||
|
<span class="skill-name">Spring Framework</span>
|
||||||
|
<span class="skill-level">Начальный</span>
|
||||||
|
</div>
|
||||||
|
<div class="skill-bar"><div class="skill-fill" style="width: 35%"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="contact" class="section">
|
||||||
|
<div class="container">
|
||||||
|
<h2 class="section-title">Контакты</h2>
|
||||||
|
<div class="contact-content">
|
||||||
|
<p class="contact-text">Открыт к общению, сотрудничеству и новым проектам. Пишите!</p>
|
||||||
|
<div class="contact-links">
|
||||||
|
<a href="https://t.me/valitovgaziz" target="_blank" rel="noopener noreferrer" class="contact-item">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>
|
||||||
|
<span>@valitovgaziz</span>
|
||||||
|
</a>
|
||||||
|
<a href="mailto:valitovgaziz@yandex.ru" class="contact-item">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="M22 4L12 13L2 4"/></svg>
|
||||||
|
<span>valitovgaziz@yandex.ru</span>
|
||||||
|
</a>
|
||||||
|
<a href="tel:+79625439343" class="contact-item">
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
|
||||||
|
<span>+7 (962) 543-93-43</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HomeView',
|
||||||
|
mounted() {
|
||||||
|
this.setupScrollObserver()
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
if (this.observer) {
|
||||||
|
this.observer.disconnect()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setupScrollObserver() {
|
||||||
|
this.observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('visible')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 }
|
||||||
|
)
|
||||||
|
document.querySelectorAll('.fade-in').forEach((el) => {
|
||||||
|
this.observer.observe(el)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
min-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-bg {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--gradient-hero);
|
||||||
|
opacity: 0.05;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 60px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-greeting {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1.1;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-desc {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-social {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-link:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-image {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-photo {
|
||||||
|
width: 320px;
|
||||||
|
height: 320px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 4px solid var(--accent);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
opacity: 0.4;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-paragraph {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-highlights {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 20px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: var(--skill-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-card h4 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-card p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-name {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-status {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--tag-bg);
|
||||||
|
color: var(--tag-text);
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-desc {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-tech {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-tag {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--skill-bg);
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
position: relative;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 20px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-item {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 56px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 24px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--accent);
|
||||||
|
border: 4px solid var(--bg);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-period {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-title {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-company {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-duties {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-duties li {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 20px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-duties li::before {
|
||||||
|
content: '—';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.education-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 20px;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edu-card {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edu-year {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edu-card h4 {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edu-card p {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-level {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-bar {
|
||||||
|
height: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--gradient-hero);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-content {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-text {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-links {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-item svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.hero-content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
text-align: center;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-social {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-photo {
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.about-highlights {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.education-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skills-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.about-highlights {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-name {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,13 +2,10 @@ import { fileURLToPath, URL } from 'node:url'
|
|||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
vueDevTools(),
|
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
@@ -16,9 +13,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
fs: {
|
host: true,
|
||||||
strict: false,
|
port: 3002
|
||||||
},
|
}
|
||||||
},
|
|
||||||
clearScreen: true,
|
|
||||||
})
|
})
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# DB environment variabels
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASSWORD=postgres
|
|
||||||
DB_NAME=mydb
|
|
||||||
APP_PORT=8080
|
|
||||||
JWT_SECRET=secret
|
|
||||||
UPLOAD_PATH=./storage/uploads
|
|
||||||
ENVIRONMENT=development
|
|
||||||
LOG_LEVEL=debug
|
|
||||||
API_ES_APP_PORT=8088
|
|
||||||