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
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Deploy
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'main_dc/**'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
cd /home/gaziz/artefacts/tp/main_dc
|
||||
git pull origin main
|
||||
|
||||
# Если изменился sites.yml — генерируем конфиги
|
||||
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
|
||||
@@ -0,0 +1,81 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Repo overview
|
||||
|
||||
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.
|
||||
|
||||
## Directory structure
|
||||
|
||||
```
|
||||
main_dc/
|
||||
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` |
|
||||
@@ -38,4 +38,4 @@ yalarba.ru on vue3.js (pinia) need to redevelop on nuxt.js
|
||||
1. Написать документацию к api всех сайтов
|
||||
2. Доработать begushiybashkir.ru && easysite102.rr
|
||||
|
||||
# документация находиться в директории documentation в корне проекта
|
||||
# документация находится в директории 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
|
||||
DOMAINS_yalarba=yalarba.ru,www.yalarba.ru
|
||||
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
|
||||
#CERTBOT NGINX VARIABLES — авто-сгенерировано, не редактировать вручную
|
||||
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_ADMIN_PASSWORD=your_secure_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
|
||||
|
||||
# Копируем go.mod и go.sum
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Копируем исходный код
|
||||
# Копируем весь исходный код
|
||||
COPY . .
|
||||
|
||||
# Скачиваем зависимости
|
||||
RUN go mod tidy && go mod download
|
||||
|
||||
# Компилируем БЕЗ CGO
|
||||
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/cors v1.2.2
|
||||
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
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/gorm v1.31.0
|
||||
@@ -15,8 +16,12 @@ require (
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/go-playground/locales v0.14.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/lib/pq v1.10.9 // 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
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
)
|
||||
|
||||
@@ -32,7 +32,8 @@ func (a *App) Initialize() error {
|
||||
|
||||
// Инициализация базы данных
|
||||
dbConfig := &database.Config{
|
||||
URL: a.cfg.DatabaseURL,
|
||||
URL: a.cfg.DatabaseURL,
|
||||
Schema: a.cfg.DBSchema,
|
||||
}
|
||||
a.db = database.NewDatabase(dbConfig)
|
||||
|
||||
@@ -46,11 +47,6 @@ func (a *App) Initialize() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Выполнение миграций
|
||||
if err := a.db.Migrate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Настройка роутера
|
||||
router := routes.SetupRouter(a.db.DB, a.cfg)
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
type Config struct {
|
||||
Port string
|
||||
DatabaseURL string
|
||||
DBSchema string
|
||||
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
|
||||
JWTSecret string `env:"JWT_SECRET,required"`
|
||||
|
||||
@@ -34,6 +35,7 @@ func Load() *Config {
|
||||
return &Config{
|
||||
Port: port,
|
||||
DatabaseURL: databaseURL,
|
||||
DBSchema: getEnv("DB_SCHEMA", "public"),
|
||||
JWTSecret: jwtSecret,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"api_bb/migrations"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"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"
|
||||
"gorm.io/driver/postgres"
|
||||
gormpg "gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"api_bb/pkg/logger"
|
||||
@@ -17,26 +23,34 @@ type Database struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
URL string
|
||||
URL string
|
||||
Schema string
|
||||
}
|
||||
|
||||
func NewDatabase(cfg *Config) *Database {
|
||||
if cfg.Schema == "" {
|
||||
cfg.Schema = "public"
|
||||
}
|
||||
return &Database{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// Connect устанавливает соединение с базой данных
|
||||
func (d *Database) Connect() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
// Логирование попытки подключения к БД
|
||||
zapLogger.Info("attempting to connect to database",
|
||||
zap.String("host", ExtractHostFromDSN(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 {
|
||||
zapLogger.Error("failed to connect to database",
|
||||
zap.Error(err),
|
||||
@@ -47,7 +61,21 @@ func (d *Database) Connect() error {
|
||||
|
||||
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",
|
||||
zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
|
||||
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
|
||||
@@ -56,7 +84,32 @@ func (d *Database) Connect() error {
|
||||
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 {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
@@ -75,7 +128,6 @@ func (d *Database) Ping() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close закрывает соединение с базой данных
|
||||
func (d *Database) Close() error {
|
||||
zapLogger := logger.Get()
|
||||
|
||||
@@ -99,11 +151,7 @@ func (d *Database) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Вспомогательные функции для работы с DSN
|
||||
|
||||
// ExtractHostFromDSN извлекает хост из DSN строки
|
||||
func ExtractHostFromDSN(dsn string) string {
|
||||
// Простая реализация для PostgreSQL DSN
|
||||
parts := strings.Split(dsn, " ")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "host=") {
|
||||
@@ -113,9 +161,7 @@ func ExtractHostFromDSN(dsn string) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
|
||||
func ExtractDBNameFromDSN(dsn string) string {
|
||||
// Простая реализация для PostgreSQL DSN
|
||||
parts := strings.Split(dsn, " ")
|
||||
for _, part := range parts {
|
||||
if strings.HasPrefix(part, "dbname=") {
|
||||
@@ -125,9 +171,7 @@ func ExtractDBNameFromDSN(dsn string) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
|
||||
func MaskPassword(dsn string) string {
|
||||
// Простая реализация - заменяет пароль на ***
|
||||
parts := strings.Split(dsn, " ")
|
||||
for i, part := range parts {
|
||||
if strings.HasPrefix(part, "password=") {
|
||||
@@ -136,4 +180,4 @@ func MaskPassword(dsn string) string {
|
||||
}
|
||||
}
|
||||
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 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_ES │ │
|
||||
│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_YAL │ │
|
||||
│ │ (Proxy) │◄─┤(Yalarba) │ │(Бег.Баш)│ │(Easysite)│ │
|
||||
│ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │ │ │ │ │
|
||||
|
||||
@@ -139,6 +139,9 @@ easysite_build:
|
||||
easysite_start:
|
||||
docker compose up easysite -d && docker ps
|
||||
|
||||
# all
|
||||
easysite: easysite_stop git easysite_build easysite_start easysite_logs
|
||||
|
||||
# Мониторинг системных ресурсов
|
||||
top:
|
||||
htop
|
||||
@@ -165,21 +168,6 @@ restart_analytics:
|
||||
# Полный цикл обновления analytics
|
||||
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
|
||||
stop_cerbot:
|
||||
docker compose down certbot
|
||||
@@ -195,26 +183,32 @@ start_certbot:
|
||||
# Полный цикл обновления certbot
|
||||
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 секунды
|
||||
wn:
|
||||
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
|
||||
stop_api_yal:
|
||||
docker compose down api_yal
|
||||
@@ -228,4 +222,136 @@ start_api_yal:
|
||||
docker compose up api_yal -d
|
||||
|
||||
# Полный цикл обновления 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
|
||||
|
||||
# Проверяем наличие crond (используем command -v вместо which)
|
||||
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 apk add --no-cache cronie docker-cli
|
||||
|
||||
# Создаем директории для конфигов
|
||||
RUN mkdir -p /etc/letsencrypt/config
|
||||
|
||||
# Копируем конфигурационные файлы
|
||||
COPY scripts/ /opt/
|
||||
RUN chmod +x /opt/*.sh
|
||||
|
||||
# Устанавливаем права
|
||||
RUN chmod +x /opt/*
|
||||
|
||||
ENTRYPOINT ["/opt/init-certbot.sh"]
|
||||
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
|
||||
|
||||
# Преобразуем дату истечения в 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-времени
|
||||
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
|
||||
# init-certbot.sh — точка входа certbot контейнера
|
||||
set -e
|
||||
|
||||
# Проверяем наличие сертификатов для yalarba.ru
|
||||
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 "=== Certbot init ==="
|
||||
|
||||
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/valitovgaziz.ru" ]; then
|
||||
echo "Получаем новые сертификаты valitovgaziz ..."
|
||||
certbot certonly --webroot \
|
||||
--config /etc/letsencrypt/config/certbot.ini \
|
||||
-w /var/www/certbot \
|
||||
-d ${DOMAINS_valitovgaziz}
|
||||
fi
|
||||
if [ ! -d "/etc/letsencrypt/live/$primary_domain" ]; then
|
||||
echo "→ Получаем сертификат для $primary_domain"
|
||||
certbot certonly --webroot \
|
||||
--config /etc/letsencrypt/config/certbot.ini \
|
||||
-w /var/www/certbot \
|
||||
-d "$domains"
|
||||
echo "✓ Сертификат для $primary_domain получен"
|
||||
else
|
||||
echo "✓ Сертификат для $primary_domain уже существует"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "сertificates for ${DOMAINS_valitovgaziz} is ready"
|
||||
|
||||
# Проверяем наличие сертификатов для 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 таблицу
|
||||
# Настраиваем cron для ежедневного обновления
|
||||
cp /opt/crontab.txt /etc/crontabs/root
|
||||
|
||||
# Оставляем контейнер открытым
|
||||
tail -f /dev/null
|
||||
# Запускаем crond в фоне
|
||||
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_data:/etc/letsencrypt
|
||||
- certbot_www:/var/www/certbot
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- EMAIL=${EMAIL}
|
||||
- DOMAINS=${ALL_DOMAINS}
|
||||
- STAGING=0
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD-SHELL",
|
||||
"test -f /etc/letsencrypt/live/$$(echo $${DOMAINS} | cut -d',' -f1)/fullchain.pem || exit 1",
|
||||
]
|
||||
test: ["CMD-SHELL", "ls /etc/letsencrypt/live/*/fullchain.pem 2>/dev/null | head -1 | xargs test -f || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -43,32 +39,28 @@ services:
|
||||
- certbot_data:/etc/letsencrypt
|
||||
- certbot_www:/var/www/certbot
|
||||
- ./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
|
||||
- analytics_logs:/var/log/analytics:ro
|
||||
- ./nginx/conf.available:/etc/nginx/conf.available:ro
|
||||
networks:
|
||||
- web-network
|
||||
- internal
|
||||
- app-network
|
||||
- bb-network
|
||||
depends_on:
|
||||
easysite:
|
||||
condition: service_healthy
|
||||
api_es:
|
||||
condition: service_healthy
|
||||
certbot:
|
||||
condition: service_healthy
|
||||
api_tp:
|
||||
condition: service_healthy
|
||||
api_bb:
|
||||
condition: service_healthy
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
api_yal:
|
||||
condition: service_healthy
|
||||
yalarba:
|
||||
condition: service_healthy
|
||||
valitovgaziz:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
|
||||
test: ["CMD", "wget", "--spider", "http://localhost/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
@@ -101,41 +93,24 @@ services:
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# REST API app on Golang (Gorm, Chi) бизнес логика приложения yalarba.ru. Работает с БД на PostgresQL db:db_tp
|
||||
api_tp:
|
||||
# Vue 3 SPA для valitovgaziz.ru
|
||||
valitovgaziz:
|
||||
build:
|
||||
context: ./yalarba/api_tp
|
||||
context: ./valitovgaziz
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8888:8080"
|
||||
container_name: api_tp
|
||||
container_name: valitovgaziz
|
||||
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:
|
||||
- app-network
|
||||
- web-network
|
||||
depends_on:
|
||||
analytics:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"wget",
|
||||
"--no-verbose",
|
||||
"--tries=1",
|
||||
"--spider",
|
||||
"http://localhost:8080/health",
|
||||
]
|
||||
test: ["CMD", "wget", "--spider", "http://localhost/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
# PostgresQL DB база данных для храниния информации приложений Yalarba.ru && Easysite102.ru
|
||||
db:
|
||||
@@ -159,7 +134,7 @@ services:
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир Работает с БД db_bb on PostgresQL
|
||||
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир
|
||||
api_bb:
|
||||
build:
|
||||
context: ./BB/api_bb
|
||||
@@ -169,22 +144,22 @@ services:
|
||||
container_name: api_bb
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
db_bb:
|
||||
db:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- ./BB/api_bb/.env
|
||||
volumes:
|
||||
- api_bb_uploads:/app/uploads
|
||||
environment:
|
||||
# Database connection settings
|
||||
DB_HOST: db_bb
|
||||
DB_HOST: db
|
||||
DB_PORT: 5432
|
||||
DB_USER: postgres
|
||||
DB_PASSWORD: postgres
|
||||
DB_NAME: bb_db
|
||||
DB_SCHEMA: bb
|
||||
APP_PORT: 8080
|
||||
networks:
|
||||
- bb-network
|
||||
- app-network
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
@@ -199,31 +174,10 @@ services:
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# PostgresQL DB база данных для работы сайта Бегущий Башкир
|
||||
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
|
||||
# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_yal REST API app
|
||||
easysite:
|
||||
build:
|
||||
context: ./yalarba/easySite/easySite
|
||||
context: ./yalarba/easySite
|
||||
dockerfile: Dockerfile
|
||||
container_name: easysite
|
||||
restart: unless-stopped
|
||||
@@ -233,6 +187,7 @@ services:
|
||||
NODE_ENV: production
|
||||
HOST: 0.0.0.0
|
||||
PORT: 3000
|
||||
NUXT_PUBLIC_API_BASE: /api/v1
|
||||
networks:
|
||||
- web-network
|
||||
- app-network
|
||||
@@ -242,34 +197,6 @@ services:
|
||||
timeout: 10s
|
||||
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 сервиса
|
||||
api_yal:
|
||||
build:
|
||||
@@ -300,15 +227,119 @@ services:
|
||||
retries: 3
|
||||
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:
|
||||
certbot_data: # volume для данных Certbot
|
||||
certbot_www: # volume для данных Certbot
|
||||
db_tp_data: # Volume для данных БД yalarba.ru
|
||||
db_bb_data: # Volume для данных БД Бегущий башкир
|
||||
api_bb_uploads: # Volume для загружаемых файлов бегущий башкир
|
||||
analytics_logs: # Volume для логов аналитики
|
||||
analytics_data: # Volume для данных аналитики
|
||||
gitea_data: # Volume для Gitea
|
||||
gitea_runner: # Volume для Gitea Runner
|
||||
|
||||
networks:
|
||||
web-network:
|
||||
@@ -317,8 +348,6 @@ networks:
|
||||
driver: bridge
|
||||
app-network:
|
||||
driver: bridge
|
||||
bb-network:
|
||||
driver: bridge
|
||||
|
||||
# Эта опция автоматически удаляет orphans (Не используемые контейнеры)
|
||||
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
|
||||
|
||||
# Установка зависимостей
|
||||
RUN apk add --no-cache bash openssl
|
||||
|
||||
# Создание директории для сертификатов
|
||||
RUN mkdir -p /etc/nginx/ssl
|
||||
# dummy сертификаты для nginx (нужны чтобы nginx стартовал с любым конфигом)
|
||||
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 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"
|
||||
RUN mkdir -p /var/www/certbot /etc/nginx/conf.d /etc/nginx/conf.available
|
||||
|
||||
# Копируем обе конфигурации
|
||||
COPY nginx-http.conf /etc/nginx/nginx-http.conf
|
||||
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
|
||||
# per-domain entrypoint для проверки сертификатов
|
||||
COPY entrypoint.sh /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 сертификаты │
|
||||
│ • analytics - Статистика (Node.js) │
|
||||
│ • api_tp - API yalarba.ru (Go) │
|
||||
│ • api_es - API easysite102.ru (Go) │
|
||||
│ • api_yal - API easysite102.ru (Go) │
|
||||
│ • api_bb - API Бегущий Башкир (Go) │
|
||||
│ • easysite - SPA (Nuxt.js) │
|
||||
│ • db - PostgreSQL (yalarba/easy) │
|
||||
@@ -72,9 +72,9 @@
|
||||
|
||||
| Домен | Тип | 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` |
|
||||
| `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` |
|
||||
| `xn--80abahjtcfl5d0a8di.xn--p1ai` | Альтернативный домен для Бегущий Башкир | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
```
|
||||
EMAIL=admin@example.com # Для Let's Encrypt
|
||||
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 |
|
||||
| analytics | `http://localhost:3000/health` | 3000 | 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 |
|
||||
| easysite | `http://localhost:3000/api/health` | 3000 | 30s |
|
||||
| db, db_bb | `pg_isready -U postgres` | 5432 | 30s |
|
||||
|
||||
### Зависимости запуска
|
||||
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
|
||||
./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
|
||||
./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 {
|
||||
listen 80;
|
||||
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 \
|
||||
easysite102.ru \
|
||||
www.easysite102.ru \
|
||||
begushiybashkir.ru \
|
||||
www.begushiybashkir.ru \
|
||||
auth.yalarba.ru;
|
||||
xn--80abahjtcfl5d0a8di.xn--p1ai \
|
||||
www.xn--80abahjtcfl5d0a8di.xn--p1ai;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/stub/html;
|
||||
@@ -25,12 +27,19 @@ server {
|
||||
# Блок для HTTPS → HTTP редиректа (порт 443)
|
||||
server {
|
||||
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_key /etc/nginx/ssl/dummy.key;
|
||||
|
||||
# Редирект всех HTTPS-запросов на HTTP
|
||||
return 301 http://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,341 +1,146 @@
|
||||
# ================================================
|
||||
# КОНФИГУРАЦИЯ NGINX С ПОДДЕРЖКОЙ SSL
|
||||
# Основные задачи:
|
||||
# 1. Перенаправление HTTP → HTTPS
|
||||
# 2. Обслуживание статических файлов
|
||||
# 3. Проксирование к backend сервисам
|
||||
# 4. Поддержка нескольких доменов
|
||||
# ================================================
|
||||
# Автоматически сгенерировано generate-configs.sh — не редактировать вручную
|
||||
# Полная HTTPS конфигурация
|
||||
|
||||
# ================================================
|
||||
# БЛОК 1: HTTP СЕРВЕР (ПОРТ 80)
|
||||
# ================================================
|
||||
# --- HTTP → HTTPS редирект ---
|
||||
server {
|
||||
# Прослушивание порта 80 для всех входящих HTTP соединений
|
||||
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 домена
|
||||
|
||||
# ============================================
|
||||
# ЛОКАЦИЯ: Проверочные файлы для Certbot
|
||||
# ============================================
|
||||
# Этот блок КРИТИЧЕСКИ ВАЖЕН для получения SSL сертификатов
|
||||
# Certbot (Let's Encrypt) размещает здесь временные файлы
|
||||
# для подтверждения владения доменом
|
||||
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;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
# Директория, где 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/ {
|
||||
# Псевдоним пути - запросы к /uploads/ обслуживаются из /uploads/ на диске
|
||||
alias /uploads/;
|
||||
|
||||
# Кэширование в браузере на 1 год
|
||||
expires 1y;
|
||||
|
||||
# Заголовки кэширования:
|
||||
# "public" - может кэшироваться прокси-серверами
|
||||
# "immutable" - файлы никогда не меняются, браузер не проверяет обновления
|
||||
add_header Cache-Control "public, immutable";
|
||||
|
||||
# Если файл не найден - вернуть 404 ошибку
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
# ================================================
|
||||
# БЛОК 2: HTTPS СЕРВЕР ДЛЯ YALARBA.RU
|
||||
# ================================================
|
||||
# --- HTTPS серверные блоки ---
|
||||
|
||||
server {
|
||||
# Прослушивание порта 443 с SSL/TLS шифрованием
|
||||
listen 443 ssl;
|
||||
|
||||
# Домены для этого сервера
|
||||
server_name yalarba.ru www.yalarba.ru;
|
||||
|
||||
# ============================================
|
||||
# НАСТРОЙКИ SSL СЕРТИФИКАТОВ
|
||||
# ============================================
|
||||
# Пути к SSL сертификатам, сгенерированным Certbot
|
||||
ssl_certificate /etc/letsencrypt/live/yalarba.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/yalarba.ru/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 приложение)
|
||||
# ============================================
|
||||
location / {
|
||||
# Директория со скомпилированным Vue/React приложением
|
||||
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_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;
|
||||
|
||||
# Увеличенные таймауты для длительных операций (10 минут)
|
||||
proxy_connect_timeout 600;
|
||||
proxy_send_timeout 600;
|
||||
proxy_read_timeout 600;
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
# Проксирование всех запросов к API на Golang сервис
|
||||
proxy_pass http://api_yal/; # Контейнер Docker
|
||||
|
||||
# Передача оригинальных заголовков от клиента
|
||||
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;
|
||||
|
||||
# Увеличенные таймауты для длительных операций (10 минут)
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
# ================================================
|
||||
# БЛОК 3: HTTPS СЕРВЕР ДЛЯ VALITOVGAZIZ.RU
|
||||
# ================================================
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name valitovgaziz.ru www.valitovgaziz.ru;
|
||||
|
||||
# Свой SSL сертификат для этого домена
|
||||
ssl_certificate /etc/letsencrypt/live/valitovgaziz.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/valitovgaziz.ru/privkey.pem;
|
||||
|
||||
# Те же настройки безопасности SSL
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
|
||||
# ============================================
|
||||
# ЛОКАЦИЯ: Статический сайт
|
||||
# ============================================
|
||||
location / {
|
||||
# Статические HTML файлы
|
||||
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_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;
|
||||
}
|
||||
|
||||
# ========================================
|
||||
# НАСТРОЙКИ CORS (Cross-Origin Resource Sharing)
|
||||
# ========================================
|
||||
# Разрешаем запросы с ЛЮБОГО домена (*)
|
||||
# Внимание: "*" может быть небезопасно в production
|
||||
add_header Access-Control-Allow-Origin "*" always;
|
||||
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
|
||||
add_header Access-Control-Allow-Credentials "true" always;
|
||||
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;
|
||||
|
||||
# Обработка предварительных OPTIONS запросов (preflight)
|
||||
# Браузеры отправляют такие запросы перед основными
|
||||
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;
|
||||
}
|
||||
|
||||
# Стандартные таймауты для API аналитики
|
||||
proxy_connect_timeout 30s;
|
||||
proxy_send_timeout 30s;
|
||||
proxy_read_timeout 30s;
|
||||
}
|
||||
}
|
||||
|
||||
# ================================================
|
||||
# БЛОК 4: HTTPS СЕРВЕР ДЛЯ EASYSITE102.RU
|
||||
# ================================================
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name easysite102.ru www.easysite102.ru;
|
||||
|
||||
# Свой SSL сертификат
|
||||
ssl_certificate /etc/letsencrypt/live/easysite102.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/easysite102.ru/privkey.pem;
|
||||
|
||||
# Безопасные настройки SSL
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
|
||||
# ============================================
|
||||
# ЛОКАЦИЯ: Проксирование к Nuxt.js приложению
|
||||
# ============================================
|
||||
location / {
|
||||
# ВСЕ запросы проксируются к Nuxt.js серверу
|
||||
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;
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# ЛОКАЦИЯ: API Backend для Easysite
|
||||
# ============================================
|
||||
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/;
|
||||
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;
|
||||
@@ -345,8 +150,7 @@ server {
|
||||
proxy_send_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-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';
|
||||
@@ -355,55 +159,26 @@ server {
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# ЛОКАЦИЯ: Загруженные файлы (статическое обслуживание)
|
||||
# ============================================
|
||||
location /uploads/ {
|
||||
# Обслуживание файлов загрузок напрямую из файловой системы
|
||||
alias /uploads/;
|
||||
|
||||
# Долгое кэширование - файлы загрузок редко меняются
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
|
||||
# try_files не нужен - nginx сам проверит существование файла
|
||||
}
|
||||
}
|
||||
|
||||
# ================================================
|
||||
# БЛОК 6: HTTPS СЕРВЕР ДЛЯ BEGUSHIYBASHKIR.RU
|
||||
# (ДУБЛИРУЕТ БЛОК 5 С ДРУГИМ ДОМЕНОМ)
|
||||
# ================================================
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name begushiybashkir.ru www.begushiybashkir.ru;
|
||||
|
||||
# Свой SSL сертификат для этого домена
|
||||
ssl_certificate /etc/letsencrypt/live/begushiybashkir.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/begushiybashkir.ru/privkey.pem;
|
||||
|
||||
# Стандартные SSL настройки
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
|
||||
|
||||
# ВНИМАНИЕ: Весь контент ниже ДОСЛОВНО ДУБЛИРУЕТ
|
||||
# предыдущий серверный блок для IDN домена
|
||||
|
||||
# ============================================
|
||||
# ЛОКАЦИЯ: SPA приложение
|
||||
# ============================================
|
||||
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;
|
||||
@@ -415,8 +190,7 @@ server {
|
||||
proxy_send_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-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';
|
||||
@@ -425,17 +199,58 @@ server {
|
||||
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||
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/ {
|
||||
alias /uploads/;
|
||||
expires 1y;
|
||||
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": {
|
||||
"paths": {
|
||||
"@/*": ["./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 vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -16,9 +13,7 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
server: {
|
||||
fs: {
|
||||
strict: false,
|
||||
},
|
||||
},
|
||||
clearScreen: true,
|
||||
host: true,
|
||||
port: 3002
|
||||
}
|
||||
})
|
||||
@@ -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
|
||||
@@ -1,20 +0,0 @@
|
||||
FROM golang:1.25.1-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Устанавливаем зависимости для компиляции
|
||||
RUN apk add --no-cache gcc musl-dev
|
||||
|
||||
# Копируем go.mod и go.sum
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Копируем исходный код
|
||||
COPY . .
|
||||
|
||||
# Компилируем БЕЗ CGO (указываем путь к main.go)
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
|
||||
|
||||
EXPOSE 8081
|
||||
|
||||
CMD ["./bin/main"]
|
||||
@@ -1,71 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"api_es/internal/config"
|
||||
"api_es/internal/database"
|
||||
"api_es/internal/router"
|
||||
"api_es/pkg/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Загрузка конфигурации приложения из файлов окружения или конфигурационных файлов
|
||||
// Конфигурация включает параметры БД, уровень логирования, порт приложения и т.д.
|
||||
cfg := config.Load()
|
||||
|
||||
// Инициализация логгера с указанным уровнем логирования и окружением (dev/prod)
|
||||
// Логгер будет настроен соответствующим образом для заданного окружения
|
||||
logger.Init(cfg.LogLevel, cfg.Environment)
|
||||
|
||||
// Получение инстанса логгера для использования во всем приложении
|
||||
zapLogger := logger.Get()
|
||||
|
||||
// Логирование старта приложения с указанием используемого стека технологий
|
||||
zapLogger.Info("Start api_es REST API on stack Golang (gorm, chi) and PostgresDB connect")
|
||||
|
||||
// Инициализация подключения к базе данных PostgreSQL с использованием параметров из конфигурации
|
||||
// Возвращается объект gorm.DB для работы с ORM
|
||||
db, err := database.NewPostgresConnection(cfg)
|
||||
if err != nil {
|
||||
// Критическая ошибка подключения к БД - приложение не может работать без БД
|
||||
zapLogger.Panic("Failed to connect to database:", zap.Error(err))
|
||||
}
|
||||
|
||||
// Получение низкоуровневого объекта *sql.DB из gorm.DB для выполнения операций,
|
||||
// не поддерживаемых напрямую gorm (например, Ping)
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
// Ошибка получения инстанса БД, но приложение может продолжить работу
|
||||
zapLogger.Error("failed to get database instance", zap.Error(err))
|
||||
}
|
||||
|
||||
// Проверка доступности базы данных через ping-запрос
|
||||
// Убеждаемся, что соединение активно и БД отвечает
|
||||
if err := sqlDB.Ping(); err != nil {
|
||||
zapLogger.Error("database ping failed", zap.Error(err))
|
||||
}
|
||||
|
||||
// Успешная проверка соединения с БД
|
||||
zapLogger.Info("database ping successful")
|
||||
|
||||
// Настройка маршрутизатора (роутера) для обработки HTTP-запросов
|
||||
// Передаем подключение к БД и конфигурацию для инициализации обработчиков
|
||||
zapLogger.Info("setup router")
|
||||
r := router.SetupRouter(db, cfg)
|
||||
|
||||
// Запуск HTTP-сервера на порту, указанном в конфигурации
|
||||
// Сервер начинает прослушивать входящие соединения
|
||||
zapLogger.Info("Server starting on port %s", zap.String("AppPort", cfg.AppPort))
|
||||
log.Printf("Server starting on port %s", cfg.AppPort)
|
||||
|
||||
// Запуск HTTP-сервера с указанным роутером
|
||||
// ListenAndServe блокирует выполнение и обрабатывает входящие запросы
|
||||
// В случае ошибки запуска сервера, логируем ошибку и завершаем приложение
|
||||
if err := http.ListenAndServe(":"+cfg.AppPort, r); err != nil {
|
||||
log.Fatal("Failed to start server:", err)
|
||||
}
|
||||
}
|
||||