Compare commits

..

50 Commits

Author SHA1 Message Date
valitovgaziz eee067f0ca fix: track Dockerfile and .dockerignore in easySite
.gitignore was ignoring these files, causing them to be missing
on fresh checkout (e.g. after git pull on server), which broke
the Docker build. Removed the ignore entries so the files are
tracked by git.
2026-06-12 11:19:33 +05:00
valitovgaziz 2941b14b38 flatten easySite directory: remove extra easySite/easySite nesting
- Moved contents of main_dc/yalarba/easySite/easySite/ up to easySite/
- Updated docker-compose.yml build context path
- Deleted empty nested easySite/ directory
2026-06-12 11:16:15 +05:00
valitovgaziz 888bb2d87b Fix import alias conflict: gormpg + migratepg 2026-06-12 10:59:39 +05:00
valitovgaziz 029812c6a4 Restructure Dockerfiles: copy source before go mod tidy 2026-06-12 10:57:50 +05:00
valitovgaziz 6a60d67b29 Fix Dockerfile: use go mod tidy instead of go mod download 2026-06-12 10:56:45 +05:00
valitovgaziz b0350abfbe DB optimization: pool, golang-migrate, consolidate to single Postgres
- Fix DB_NAME=db_yal -> mydb in api_yal .env
- Add connection pool (MaxOpenConns 25, MaxIdleConns 10, ConnMaxLifetime 30m)
- Replace GORM AutoMigrate with golang-migrate in api_yal and api_bb
- Create embedded SQL migrations for both APIs
- Add DB_SCHEMA support to api_bb config
- Consolidate to single Postgres: db_bb -> schema 'bb' on db container
- Remove db_bb service, bb-network, db_bb volume from compose
- Remove api_tp targets from Makefile
- Clean up old migrate.go
2026-06-12 10:47:41 +05:00
valitovgaziz ec83b97c25 Remove api_tp from docker-compose.yml (service already deleted) 2026-06-12 10:20:13 +05:00
valitovgaziz 86b8968dce add all command for easysite 2026-06-12 10:18:45 +05:00
valitovgaziz 90a96b4125 Migrate easysite from api_es to api_yal
- Remove api_es service, Dockerfile, all Go source files
- Remove api_es from docker-compose.yml, nginx-ssl.conf, .env, Makefile
- Replace nginx /api/ proxy with /api/v1/ → api_yal:8787
- Add amenity/upload domains, AuthResponse, GET /auth/me, GET /objects/my to api_yal
- Rewrite easysite frontend: types, composables, and all 5 pages to use api_yal DTOs
- Wire nuxt.config public.apiBase, add useObjects CRUD composable
- Update docs references from api_es to api_yal
2026-06-12 10:14:38 +05:00
valitovgaziz 64295b689b docs: add Windows test run commands to README.md 2026-06-12 08:51:16 +05:00
valitovgaziz 75198ed00f docs: add integration test run instructions to README.md
- Added section 8 'Тестирование' with run instructions, structure, features, and diagnostics
- Also includes test file route path adjustments and import reordering
2026-06-12 08:46:20 +05:00
valitovgaziz 01e8226c2b Add integration test suite with in-memory SQLite, mock repos, and test server
- Add test_server.go with chi-based router, shared in-memory SQLite DB, mock repositories
- Add mock_object_repository.go and mock_appeal_repository.go for lightweight testing
- Add setup.go with TestConfig/TestUser helpers, HTTP request builder, and fixtures
- Add go-sqlite3 dependency for in-memory test database
- Rewrite all 7 integration test suites (account, appeal, auth, comment, feedback, object, rating)
  using the new test infrastructure
2026-06-12 08:42:04 +05:00
valitovgaziz 4d5090d76c moove font to fonts directory 2026-06-12 02:49:24 +05:00
valitovgaziz 02c6cb680b fix: align API response shapes (items instead of data), add fallbacks to prevent .length crash 2026-06-12 02:31:37 +05:00
valitovgaziz 86f37dde2d add font Bellaboo.woff and change fonts.ccs 2026-06-12 01:57:32 +05:00
valitovgaziz 9c793bad1b fix: align frontend types and forms with api_yal backend (name→first_name+last_name, token→access_token) 2026-06-12 01:41:23 +05:00
valitovgaziz ba7b757541 fix(nginx): preserve /api/v1 prefix when proxying to api_yal, add favicon 2026-06-12 01:23:06 +05:00
valitovgaziz edb7eabd18 On branch main
modified:   main_dc/yalarba/yalarba-nuxt/package-lock.json
	new file:   opencode.json
chagne temperature for opencode into this project
2026-06-12 01:08:14 +05:00
valitovgaziz d8349a0936 feat: add yalarba-nuxt to infra, wire fonts, switch nginx from static SPA to SSR proxy 2026-06-12 00:37:49 +05:00
valitovgaziz 60867af69c feat: create Nuxt 4 SPA for yalarba.ru (yalarba-nuxt) 2026-06-12 00:29:34 +05:00
valitovgaziz 35ba568d97 On branch main
new file:   main_dc/README.md
	modified:   main_dc/valitovgaziz/package-lock.json
	modified:   main_dc/valitovgaziz/src/components/TheFooter.vue
fix 2025 to 2026 into valitovgaziz site
2026-06-10 13:02:00 +05:00
valitovgaziz f06968eb46 fix: add healthcheck to valitovgaziz container 2026-06-10 11:32:22 +05:00
valitovgaziz 075f29cde1 feat: add personal photo to valitovgaziz hero section 2026-06-10 11:24:19 +05:00
valitovgaziz e8a655d54c feat: containerize valitovgaziz site, add Dockerfile, nginx proxy, Makefile targets 2026-06-10 11:03:11 +05:00
valitovgaziz 6ba49127aa feat: add Vue 3 personal site for valitovgaziz in main_dc/my_site 2026-06-10 10:47:59 +05:00
valitovgaziz 2084acb078 redesign valitovgaziz.ru personal site with clean & minimal UI
- Rewrite index.html with modern layout: sticky nav, hero, about, projects, timeline, skills badges, contact
- Consolidate 12 fragmented CSS files into one cohesive style.css with CSS custom properties and dark mode
- Consolidate JS into scripts.js (dark toggle + scroll animations), remove exposed telegram bot token
- Update blog.html to match new design
- Add AGENTS.md
2026-06-10 10:01:55 +05:00
valitovgaziz d1e45c7686 On branch main
modified:   main_dc/yalarba/api_yal/cmd/testrunner/main.go
	modified:   main_dc/yalarba/api_yal/cmd/testrunner/runner.go
	modified:   main_dc/yalarba/api_yal/tests/integration/account_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/appeal_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/auth_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/comment_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/feedback_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/object_test.go
	modified:   main_dc/yalarba/api_yal/tests/integration/rating_test.go
	deleted:    main_dc/yalarba/api_yal/tests/testutils/client.go
	modified:   main_dc/yalarba/api_yal/tests/testutils/fixtures.go
	modified:   main_dc/yalarba/api_yal/tests/testutils/setup.go
write comments for and into test's functions
2026-06-08 01:58:04 +05:00
valitovgaziz b4574f9df1 On branch main
modified:   main_dc/yalarba/api_yal/go.mod
	modified:   main_dc/yalarba/api_yal/go.sum
go mod tidy
2026-06-08 01:46:10 +05:00
valitovgaziz 8dfe7e8b4a On branch main
new file:   main_dc/yalarba/api_yal/cmd/testrunner/README.md
	new file:   main_dc/yalarba/api_yal/cmd/testrunner/main.go
	new file:   main_dc/yalarba/api_yal/cmd/testrunner/runner.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/setup_test.go
	deleted:    main_dc/yalarba/api_yal/test/setup_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/account_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/appeal_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/auth_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/comment_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/feedback_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/object_test.go
	new file:   main_dc/yalarba/api_yal/tests/integration/rating_test.go
	new file:   main_dc/yalarba/api_yal/tests/testutils/client.go
	new file:   main_dc/yalarba/api_yal/tests/testutils/fixtures.go
	new file:   main_dc/yalarba/api_yal/tests/testutils/setup.go
write tests
2026-06-08 01:44:23 +05:00
valitovgaziz bdf3ba2483 On branch main
modified:   main_dc/yalarba/api_yal/documentation/TEST_REST_API_REQUESTS.md
replace base url
2026-06-07 22:33:35 +05:00
valitovgaziz b98d1f65d3 On branch main
modified:   main_dc/yalarba/api_yal/documentation/README.md
date last commit that set base URL into easysite102.ru doc
2026-06-07 22:15:30 +05:00
valitovgaziz 787f90b5cf On branch main
modified:   documentation/README.md
SET new base URL for api_yal
2026-06-07 21:56:14 +05:00
valitovgaziz d2b77d4553 On branch main
modified:   main_dc/nginx/nginx-ssl.conf
	modified:   main_dc/yalarba/api_es/internal/config/config.go
add config into enginx for api_yal REST_API
2026-06-07 21:42:16 +05:00
valitovgaziz eb5b8fbf26 On branch main
new file:   .gitattributes
	modified:   main_dc/yalarba/api_yal/go.mod
	modified:   main_dc/yalarba/api_yal/go.sum
	deleted:    main_dc/yalarba/api_yal/test/e2e/api_test.go
	deleted:    main_dc/yalarba/api_yal/test/fixtures/test_data.go
	deleted:    main_dc/yalarba/api_yal/test/intergration/account_intergration_test.go
	modified:   main_dc/yalarba/api_yal/test/intergration/setup_test.go
	new file:   main_dc/yalarba/api_yal/test/setup_test.go
create gitattributes text=auto chate LF=CRLF=>auto
create test's file's
2026-06-07 21:10:44 +05:00
valitovgaziz 1bb91820d0 On branch main
new file:   main_dc/yalarba/api_yal/test/e2e/api_test.go
	new file:   main_dc/yalarba/api_yal/test/fixtures/test_data.go
	new file:   main_dc/yalarba/api_yal/test/intergration/account_intergration_test.go
	new file:   main_dc/yalarba/api_yal/test/intergration/auth_integration_test.go
	new file:   main_dc/yalarba/api_yal/test/intergration/objects_integration_test.go
	new file:   main_dc/yalarba/api_yal/test/intergration/setup_test.go
add test files
not implemented
2026-05-31 05:12:49 +05:00
valitovgaziz 9dd4b5f067 On branch main
new file:   main_dc/yalarba/api_yal/documentation/TEST_REST_API_REQUESTS.md
add tests docs
2026-05-27 14:09:18 +05:00
valitovgaziz 5c34816359 On branch main
modified:   main_dc/yalarba/api_yal/documentation/README.md
Add the patch numbers
2026-05-21 10:16:18 +05:00
valitovgaziz 5eb2f5220b On branch main
new file:   main_dc/yalarba/api_yal/documentation/README.md
Add documentation for endpoints for now
`1
2026-05-21 10:15:02 +05:00
valitovgaziz 318075d686 On branch main
modified:   internal/domain/appeal/dto.go
	new file:   internal/domain/appeal/handler.go
	modified:   internal/domain/appeal/router.go
	modified:   internal/domain/appeal/service.go
	modified:   internal/models/appeal.go
	modified:   internal/router/router.go
fix bag with no embeded the Base into appeal
2026-05-21 05:04:34 +05:00
valitovgaziz ba2e3b9545 On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/rating/dto.go
	new file:   main_dc/yalarba/api_yal/internal/domain/rating/handler.go
	new file:   main_dc/yalarba/api_yal/internal/domain/rating/router.go
	new file:   main_dc/yalarba/api_yal/internal/domain/rating/service.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
add raing domain without test
2026-05-20 13:23:38 +05:00
valitovgaziz 508eb8b981 On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/object/router.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
add register router into main router
2026-05-20 13:17:19 +05:00
valitovgaziz cc3d0a8b07 On branch main
modified:   yalarba/api_yal/internal/domain/account/service.go
	modified:   yalarba/api_yal/internal/domain/comment/dto.go
	new file:   yalarba/api_yal/internal/domain/comment/handler.go
	new file:   yalarba/api_yal/internal/domain/comment/router.go
	new file:   yalarba/api_yal/internal/domain/comment/service.go
	modified:   yalarba/api_yal/internal/repository/feedback_repository.go
	new file:   yalarba/api_yal/internal/util/JSON_resp.go
Realize comment domain hole
2026-05-19 18:11:20 +05:00
valitovgaziz 63d486f48d On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/appeal/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/handler.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
	modified:   main_dc/yalarba/api_yal/internal/models/feedback.go
	modified:   main_dc/yalarba/api_yal/internal/repository/comment_repository.go
	modified:   main_dc/yalarba/api_yal/internal/repository/feedback_repository.go
	modified:   main_dc/yalarba/api_yal/internal/repository/feedback_repository_impl.go
	modified:   main_dc/yalarba/api_yal/internal/router/router.go
craete routerRegister, service, hander, dto for feedback
2026-05-19 15:01:57 +05:00
valitovgaziz 42549eb116 last 2026-05-19 14:16:43 +05:00
valitovgaziz 894415e3ac On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/feetback/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
last
2026-05-19 14:07:06 +05:00
valitovgaziz e4a1fcfd25 On branch main
modified:   main_dc/yalarba/api_yal/internal/domain/feetback/dto.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/handler.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/router.go
	modified:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
feedback domain is almost ready
2026-05-19 13:19:47 +05:00
valitovgaziz 4e80d525db Merge branch 'main' of github.com:valitovgaziz/tp 2026-05-18 09:37:32 +05:00
valitovgaziz 8d30480bdc On branch main
new file:   main_dc/yalarba/api_yal/internal/domain/appeal/dto.go
	new file:   main_dc/yalarba/api_yal/internal/domain/appeal/router.go
	new file:   main_dc/yalarba/api_yal/internal/domain/appeal/service.go
	new file:   main_dc/yalarba/api_yal/internal/domain/feetback/handler.go
	new file:   main_dc/yalarba/api_yal/internal/domain/feetback/router.go
	new file:   main_dc/yalarba/api_yal/internal/domain/feetback/service.go
try add domains for appeal
2026-05-18 09:36:53 +05:00
Ildar 4cf8543c82 Fix typo in README documentation location 2026-05-04 15:28:58 +05:00
valitovgaziz bffdf0ec6c On branch main
new file:   documentation/LLM_Information.md
	modified:   main_dc/certbot/scripts/checkRenewCerts.sh
renew checkRenewCerts replace line with expirY_unix parse date formate
2026-05-02 09:15:43 +05:00
380 changed files with 39648 additions and 12685 deletions
+20
View File
@@ -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
-24
View File
@@ -1,24 +0,0 @@
name: Deploy api_yal
on:
push:
branches:
- main
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Go binary (api_yal)
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-w /workspace/main_dc/yalarba/api_yal \
golang:1.26.0-alpine \
sh -c 'CGO_ENABLED=0 GOOS=linux go build -o /workspace/main_dc/yalarba/api_yal/bin/api_yal ./cmd/main.go'
- name: Copy binary and restart service
run: |
cp main_dc/yalarba/api_yal/bin/api_yal /home/gaziz/artefacts/tp/main_dc/yalarba/api_yal/bin/api_yal
docker compose -f /home/gaziz/artefacts/tp/main_dc/docker-compose.yml restart api_yal
+15 -25
View File
@@ -1,7 +1,4 @@
/spa/node_modules /spa/node_modules
.env
# Logs
logs logs
*.log *.log
npm-debug.log* npm-debug.log*
@@ -9,39 +6,32 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
node_modules node_modules
*.node_modules
/node_modules
/.pnp
.pnp.js
.DS_Store .DS_Store
dist
dist-ssr
coverage coverage
/coverage
/build
*.local *.local
.env.local
.env.development.local
.env.test.local
.env.production.local
/cypress/videos/ /cypress/videos/
/cypress/screenshots/ /cypress/screenshots/
# Editor directories and files
.vscode/* .vscode/*
!.vscode/extensions.json .vscode/extensions.json
.idea .idea
*.suo *.suo
*.ntvs* *.ntvs*
*.njsproj *.njsproj
*.sln *.sln
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
*.node_modules
# Binaries /node_modules
api/bin/ /.pnp
.pnp.js
/coverage
/build
.env.local
.env.development.local
.env.test.local
.env.production.local
dist
dist-ssr
!.vscode/extensions.json
.vscode/extensions.json
+54 -88
View File
@@ -1,103 +1,69 @@
# YalArba (ЯлАрба) — Tourist Aggregator # AGENTS.md
**Updated:** 2026-06-13 ## Repo overview
**Branch:** main (merged develop)
## OVERVIEW Docker Compose hosting for 4 websites (yalarba.ru, begushiybashkir.ru, easysite102.ru, valitovgaziz.ru).
Multi-service platform: Go REST APIs + Vue3/Nuxt SPAs + Nginx reverse proxy + Certbot SSL. All infrastructure lives under `main_dc/`. Root `package.json` is vestigial — do not use it.
All deployable code lives under `main_dc/`. Docker Compose orchestrates ~10 services across 4 Docker networks.
PostgreSQL databases, JWT auth, Gitea Actions CI/CD. ## Directory structure
## STRUCTURE
``` ```
main_dc/ main_dc/
├── docker-compose.yml # All services (nginx, certbot, db, apis, spas) docker-compose.yml -- single compose file orchestrating everything
├── Makefile # Per-service build/restart targets Makefile -- the primary dev/ops interface; use `make` not raw docker
├── .env # Environment variables (DO NOT COMMIT) .env -- shared env: domains, email, api_es port
├── certbot/ # Let's Encrypt auto-renewal BB/api_bb/ -- Go REST API (GORM+Chi), port 7777, DB: db_bb (5433)
├── nginx/ # Reverse proxy + SSL termination BB/bbvue/ -- Vue 3 + Vite frontend for begushiybashkir.ru
├── stubSite/ # Maintenance/placeholder page yalarba/api_tp/ -- Go REST API (GORM+Chi), port 8888, DB: db (5432)
├── valitovgaziz/ # Personal site + analytics yalarba/api_es/ -- Go REST API (GORM+Chi), port 8088, DB: db (5432)
├── html/ # Static site (valitovgaziz.ru) yalarba/api_yal/ -- Go REST API (GORM+Chi), port 8787, DB: db (5432)
│ └── analytics/ # Node.js analytics server yalarba/easySite/easySite/ -- Nuxt 4 SPA for easysite102.ru
├── BB/ # «Бегущий Башкир» project yalarba/serv_spa/spa/vue/ -- Vue 3 + Vite SPA for yalarba.ru
│ ├── api_bb/ # Go REST API (chi + GORM) valitovgaziz/analytics/ -- Node.js (Express) analytics server, port 9999
│ └── bbvue/ # Vue3 SPA frontend valitovgaziz/html/ -- static HTML for valitovgaziz.ru
└── yalarba/ # YalArba platform nginx/ -- nginx with automatic HTTP↔HTTPS switching
├── api_tp/ # Go API — auth + users (chi + GORM) certbot/ -- Let's Encrypt cert management
├── api_yal/ # Go API — accounts, objects, ratings (DDD) stubSite/ -- placeholder site while building
├── api_es/ # Go API — easysite102 backend
├── easySite/ # Nuxt.js SPA (easysite102.ru)
├── serv_spa/ # Vue3 SPA (yalarba.ru frontend)
└── tourism_types/ # Tourism classification data
``` ```
## KEY SERVICES ## Developer commands (always run from `main_dc/`)
| Service | Port | Tech | Description | | Command | What it does |
|---------|------|------|-------------| |---|---|
| nginx | 80, 443 | Nginx | Reverse proxy, SSL, static files | | `make all` | Full cycle: down → git pull → build --no-cache → up -d → watch |
| certbot | — | Certbot | Auto-renew Let's Encrypt certs | | `make <svc>` | Full cycle for one service, e.g. `make api_bb`, `make nginx`, `make es`, `make analytics` |
| db | 5432 | PostgreSQL 15 | Main DB (yalarba + easysite) | | `make bbvue` | Rebuild Vue frontend (calls `npm run build` in `BB/bbvue/`) |
| db_bb | 5433 | PostgreSQL 15 | «Бегущий Башкир» DB | | `make vue_bb` | git pull + npm cache clean + bbvue build + watch |
| api_tp | 8888 | Go/Chi/GORM | Auth + user management | | `make wn` | `watch -n2 docker ps` — monitor containers |
| api_yal | 8787 (internal) | Go/Chi/GORM | Accounts, objects, ratings | | `make bb_db` | `psql -U postgres -d bb_db` inside db_bb container |
| api_es | 8088 (internal) | Go/Chi/GORM | easysite102 backend |
| api_bb | 7777 | Go/Chi/GORM | «Бегущий Башкир» backend |
| easysite | 3000 | Nuxt.js | easysite102.ru frontend |
| analytics | 9999 | Node.js | valitovgaziz.ru analytics |
## WHERE TO LOOK All `build_*` targets use `--no-cache`.
All full-cycle targets follow: `stop_<svc> → git → build_<svc> → start_<svc> → wn`.
| Task | Location | ## Frontend dev (outside compose)
|------|----------|
| YalArba auth API | `main_dc/yalarba/api_tp/` |
| YalArba business logic (DDD) | `main_dc/yalarba/api_yal/internal/domain/` |
| Main SPA (Vue3) | `main_dc/yalarba/serv_spa/spa/vue/` |
| EasySite (Nuxt) | `main_dc/yalarba/easySite/easySite/` |
| Nginx config | `main_dc/nginx/nginx-ssl.conf` |
| Docker compose | `main_dc/docker-compose.yml` |
| CI/CD workflow | `.gitea/workflows/deploy.yml` |
| Personal site | `main_dc/valitovgaziz/html/` |
## CODE MAP — api_yal (main business logic) ```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/
| Symbol | Location | Role | cd main_dc/yalarba/serv_spa/spa/vue && npm run dev # Vite dev (yalarba SPA)
|--------|----------|------|
| `main` | `main_dc/yalarba/api_yal/cmd/main.go` | Entry point |
| `router.SetupRoutes` | `main_dc/yalarba/api_yal/internal/router/router.go` | Chi routes + middleware |
| `domain/auth` | `main_dc/yalarba/api_yal/internal/domain/auth/` | Login, Register, JWT, refresh tokens |
| `domain/account` | `main_dc/yalarba/api_yal/internal/domain/account/` | User profile CRUD |
| `domain/object` | `main_dc/yalarba/api_yal/internal/domain/object/` | Tourist objects CRUD |
| `domain/rating` | `main_dc/yalarba/api_yal/internal/domain/rating/` | Ratings (stub) |
| `domain/comment` | `main_dc/yalarba/api_yal/internal/domain/comment/` | Comments (stub) |
| `domain/feetback` | `main_dc/yalarba/api_yal/internal/domain/feetback/` | Feedback (stub) |
| `models` | `main_dc/yalarba/api_yal/internal/models/` | GORM models |
| `repository` | `main_dc/yalarba/api_yal/internal/repository/` | DB access layer |
| `middleware` | `main_dc/yalarba/api_yal/internal/middleware/` | Auth, admin, logging, context |
## CONVENTIONS cd main_dc/yalarba/easySite/easySite && npm run dev # Nuxt dev
- **Go**: Chi router, GORM, PostgreSQL. DDD-layered (handler → service → repository). cd main_dc/yalarba/easySite/easySite && npm run build # Nuxt build
- **Auth**: JWT in HttpOnly/Secure/SameSite cookies. Context key: user claims. ```
- **Config**: Environment variables via `.env` files, loaded by docker-compose.
- **DB Migrations**: Manual SQL in `main_dc/yalarba/api_tp/migrations/`.
- **Naming**: Go — short package names. Vue — PascalCase components.
## CI/CD ## Service quirks
- **Trigger**: Push to `main` branch
- **Workflow**: `.gitea/workflows/deploy.yml`
- **What it does**: Builds `api_yal` Go binary → copies to server → restarts container
- **Manual deploy**: Use `make` targets from `main_dc/Makefile`:
```bash
# On server:
make api_yal # Full cycle: stop → pull → build → start api_yal
make all # Full cycle for ALL services
make restart # Quick restart all
make nginx # Rebuild nginx
```
## NOTES - **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.
- `.env` and `*.env` files contain secrets — **never commit**. - **`yalarba/serv_spa/spa/`**: Dockerfile is incomplete (build stage only, no runtime). The `spa/vue/` package.json includes express/pg deps despite being a Vite SPA — likely unused or legacy. The nginx compose mounts `yalarba/serv_spa/spa/vue/dist`.
- The old `api/`, `spa/`, `migrator/` at repo root are deleted; all code is in `main_dc/`. - **`api_yal`** is the only container that runs as non-root. Runs on port 8787.
- `develop` branch was merged into `main` on 2026-06-13. - **`api_es`** port is configurable via `API_ES_APP_PORT` in `.env` (default 8088). All other API ports are hardcoded.
- Google Goose migrations were removed; migrations are plain SQL in `api_tp/migrations/`. - **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.
+1 -1
View File
@@ -38,4 +38,4 @@ yalarba.ru on vue3.js (pinia) need to redevelop on nuxt.js
1. Написать документацию к api всех сайтов 1. Написать документацию к api всех сайтов
2. Доработать begushiybashkir.ru && easysite102.rr 2. Доработать begushiybashkir.ru && easysite102.rr
# документация находиться в директории documentation в корне проекта # документация находится в директории documentation в корне проекта
+53
View File
@@ -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
-2
View File
@@ -12,5 +12,3 @@ ALL_DOMAINS=yalarba.ru,www.yalarba.ru,valitovgaziz.ru,www.valitovgaziz.ru,easysi
KEYCLOAK_ADMIN_PASSWORD=your_secure_password KEYCLOAK_ADMIN_PASSWORD=your_secure_password
KEYCLOAK_DB_PASSWORD=your_secure_db_password KEYCLOAK_DB_PASSWORD=your_secure_db_password
# API_ES port
API_ES_APP_PORT=8088
+4 -5
View File
@@ -3,13 +3,12 @@ FROM golang:1.26.0-alpine
WORKDIR /app WORKDIR /app
# Копируем go.mod и go.sum # Копируем весь исходный код
COPY go.mod go.sum ./
RUN go mod download
# Копируем исходный код
COPY . . COPY . .
# Скачиваем зависимости
RUN go mod tidy && go mod download
# Компилируем БЕЗ CGO # Компилируем БЕЗ CGO
RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go RUN CGO_ENABLED=0 GOOS=linux go build -o bin/main ./cmd/main.go
+5
View File
@@ -6,6 +6,7 @@ require (
github.com/go-chi/chi/v5 v5.2.3 github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang-migrate/migrate/v4 v4.18.2
golang.org/x/crypto v0.43.0 golang.org/x/crypto v0.43.0
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.0
@@ -15,8 +16,12 @@ require (
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/stretchr/testify v1.11.1 // indirect github.com/stretchr/testify v1.11.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.37.0 // indirect golang.org/x/sys v0.37.0 // indirect
) )
+2 -6
View File
@@ -32,7 +32,8 @@ func (a *App) Initialize() error {
// Инициализация базы данных // Инициализация базы данных
dbConfig := &database.Config{ dbConfig := &database.Config{
URL: a.cfg.DatabaseURL, URL: a.cfg.DatabaseURL,
Schema: a.cfg.DBSchema,
} }
a.db = database.NewDatabase(dbConfig) a.db = database.NewDatabase(dbConfig)
@@ -46,11 +47,6 @@ func (a *App) Initialize() error {
return err return err
} }
// Выполнение миграций
if err := a.db.Migrate(); err != nil {
return err
}
// Настройка роутера // Настройка роутера
router := routes.SetupRouter(a.db.DB, a.cfg) router := routes.SetupRouter(a.db.DB, a.cfg)
@@ -11,6 +11,7 @@ import (
type Config struct { type Config struct {
Port string Port string
DatabaseURL string DatabaseURL string
DBSchema string
StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"` StaticURL string `env:"STATIC_URL" envDefault:"http://localhost:8080"`
JWTSecret string `env:"JWT_SECRET,required"` JWTSecret string `env:"JWT_SECRET,required"`
@@ -34,6 +35,7 @@ func Load() *Config {
return &Config{ return &Config{
Port: port, Port: port,
DatabaseURL: databaseURL, DatabaseURL: databaseURL,
DBSchema: getEnv("DB_SCHEMA", "public"),
JWTSecret: jwtSecret, JWTSecret: jwtSecret,
} }
} }
+60 -16
View File
@@ -1,11 +1,17 @@
package database package database
import ( import (
"api_bb/migrations"
"database/sql"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/golang-migrate/migrate/v4"
migratepg "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
"go.uber.org/zap" "go.uber.org/zap"
"gorm.io/driver/postgres" gormpg "gorm.io/driver/postgres"
"gorm.io/gorm" "gorm.io/gorm"
"api_bb/pkg/logger" "api_bb/pkg/logger"
@@ -17,26 +23,34 @@ type Database struct {
} }
type Config struct { type Config struct {
URL string URL string
Schema string
} }
func NewDatabase(cfg *Config) *Database { func NewDatabase(cfg *Config) *Database {
if cfg.Schema == "" {
cfg.Schema = "public"
}
return &Database{ return &Database{
cfg: cfg, cfg: cfg,
} }
} }
// Connect устанавливает соединение с базой данных
func (d *Database) Connect() error { func (d *Database) Connect() error {
zapLogger := logger.Get() zapLogger := logger.Get()
// Логирование попытки подключения к БД
zapLogger.Info("attempting to connect to database", zapLogger.Info("attempting to connect to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)), zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
zap.String("schema", d.cfg.Schema),
) )
db, err := gorm.Open(postgres.Open(d.cfg.URL), &gorm.Config{}) dsn := d.cfg.URL
if d.cfg.Schema != "public" {
dsn = dsn + fmt.Sprintf(" search_path=%s", d.cfg.Schema)
}
db, err := gorm.Open(gormpg.Open(dsn), &gorm.Config{})
if err != nil { if err != nil {
zapLogger.Error("failed to connect to database", zapLogger.Error("failed to connect to database",
zap.Error(err), zap.Error(err),
@@ -47,7 +61,21 @@ func (d *Database) Connect() error {
d.DB = db d.DB = db
// Логирование успешного подключения к БД zapLogger.Info("Configure connection pool")
sqlDB, err := d.DB.DB()
if err != nil {
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
sqlDB.SetMaxOpenConns(25)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(30 * time.Minute)
zapLogger.Info("Run database migrations")
if err := d.runMigrations(sqlDB); err != nil {
return fmt.Errorf("failed to run migrations: %w", err)
}
zapLogger.Info("Migrations completed successfully")
zapLogger.Info("successfully connected to database", zapLogger.Info("successfully connected to database",
zap.String("host", ExtractHostFromDSN(d.cfg.URL)), zap.String("host", ExtractHostFromDSN(d.cfg.URL)),
zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)), zap.String("database", ExtractDBNameFromDSN(d.cfg.URL)),
@@ -56,7 +84,32 @@ func (d *Database) Connect() error {
return nil return nil
} }
// Ping проверяет соединение с базой данных func (d *Database) runMigrations(sqlDB *sql.DB) error {
zapLogger := logger.Get()
source, err := iofs.New(migrations.FS, ".")
if err != nil {
return fmt.Errorf("failed to create migration source: %w", err)
}
driver, err := migratepg.WithInstance(sqlDB, &migratepg.Config{})
if err != nil {
return fmt.Errorf("failed to create postgres driver: %w", err)
}
m, err := migrate.NewWithInstance("iofs", source, "postgres", driver)
if err != nil {
return fmt.Errorf("failed to create migrate instance: %w", err)
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
zapLogger.Error("Migration error", zap.Error(err))
return fmt.Errorf("failed to apply migrations: %w", err)
}
return nil
}
func (d *Database) Ping() error { func (d *Database) Ping() error {
zapLogger := logger.Get() zapLogger := logger.Get()
@@ -75,7 +128,6 @@ func (d *Database) Ping() error {
return nil return nil
} }
// Close закрывает соединение с базой данных
func (d *Database) Close() error { func (d *Database) Close() error {
zapLogger := logger.Get() zapLogger := logger.Get()
@@ -99,11 +151,7 @@ func (d *Database) Close() error {
return nil return nil
} }
// Вспомогательные функции для работы с DSN
// ExtractHostFromDSN извлекает хост из DSN строки
func ExtractHostFromDSN(dsn string) string { func ExtractHostFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ") parts := strings.Split(dsn, " ")
for _, part := range parts { for _, part := range parts {
if strings.HasPrefix(part, "host=") { if strings.HasPrefix(part, "host=") {
@@ -113,9 +161,7 @@ func ExtractHostFromDSN(dsn string) string {
return "unknown" return "unknown"
} }
// ExtractDBNameFromDSN извлекает имя базы данных из DSN строки
func ExtractDBNameFromDSN(dsn string) string { func ExtractDBNameFromDSN(dsn string) string {
// Простая реализация для PostgreSQL DSN
parts := strings.Split(dsn, " ") parts := strings.Split(dsn, " ")
for _, part := range parts { for _, part := range parts {
if strings.HasPrefix(part, "dbname=") { if strings.HasPrefix(part, "dbname=") {
@@ -125,9 +171,7 @@ func ExtractDBNameFromDSN(dsn string) string {
return "unknown" return "unknown"
} }
// MaskPassword маскирует пароль в DSN строке для безопасного логирования
func MaskPassword(dsn string) string { func MaskPassword(dsn string) string {
// Простая реализация - заменяет пароль на ***
parts := strings.Split(dsn, " ") parts := strings.Split(dsn, " ")
for i, part := range parts { for i, part := range parts {
if strings.HasPrefix(part, "password=") { if strings.HasPrefix(part, "password=") {
@@ -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);
+6
View File
@@ -0,0 +1,6 @@
package migrations
import "embed"
//go:embed *.sql
var FS embed.FS
+1 -1
View File
@@ -30,7 +30,7 @@
│ Docker Compose Cluster │ │ Docker Compose Cluster │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Nginx │ │ API_TP │ │ API_BB │ │ API_ES │ │ │ │ Nginx │ │ API_TP │ │ API_BB │ │ API_YAL │ │
│ │ (Proxy) │◄─┤(Yalarba) │ │(Бег.Баш)│ │(Easysite)│ │ │ │ (Proxy) │◄─┤(Yalarba) │ │(Бег.Баш)│ │(Easysite)│ │
│ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │ │ └────┬─────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │ │ │ │ │ │ │
+40 -31
View File
@@ -139,6 +139,9 @@ easysite_build:
easysite_start: easysite_start:
docker compose up easysite -d && docker ps docker compose up easysite -d && docker ps
# all
easysite: easysite_stop git easysite_build easysite_start easysite_logs
# Мониторинг системных ресурсов # Мониторинг системных ресурсов
top: top:
htop htop
@@ -165,21 +168,6 @@ restart_analytics:
# Полный цикл обновления analytics # Полный цикл обновления analytics
analytics: stop_analitics git build_analititcs start_analytics wn analytics: stop_analitics git build_analititcs start_analytics wn
# Остановка api_es
stop_api_es:
docker compose down api_es
# Пересборка api_es
build_api_es:
docker compose build api_es --no-cache
# Запуск api_es
start_api_es:
docker compose up api_es -d
# Полный цикл обновления api_es
api_es: stop_api_es git build_api_es start_api_es wn
# Остановка certbot # Остановка certbot
stop_cerbot: stop_cerbot:
docker compose down certbot docker compose down certbot
@@ -195,26 +183,32 @@ start_certbot:
# Полный цикл обновления certbot # Полный цикл обновления certbot
certbot: stop_cerbot git build_certbot start_certbot wat certbot: stop_cerbot git build_certbot start_certbot wat
# Сборка фронтенда valitovgaziz
valitovgaziz_build_spa: git
cd valitovgaziz && npm run build
# Остановка valitovgaziz
stop_valitovgaziz:
docker compose down valitovgaziz
# Пересборка valitovgaziz
build_valitovgaziz:
docker compose build valitovgaziz --no-cache
# Запуск valitovgaziz
start_valitovgaziz:
docker compose up valitovgaziz -d
# Полный цикл обновления valitovgaziz
valitovgaziz: stop_valitovgaziz git build_valitovgaziz start_valitovgaziz wn
# Сборка SPA + полный цикл обновления valitovgaziz
vue_site: valitovgaziz_build_spa stop_valitovgaziz build_valitovgaziz start_valitovgaziz wn
# Мониторинг состояния контейнеров каждые 2 секунды # Мониторинг состояния контейнеров каждые 2 секунды
wn: wn:
watch -n 2 'docker ps' watch -n 2 'docker ps'
# Остановка api_tp
stop_api_tp:
docker compose down api_tp
# Пересборка api_tp
build_api_tp:
docker compose build api_tp --no-cache
# Запуск api_tp
start_api_tp:
docker compose up api_tp -d
# Полный цикл обновления api_tp
api_tp: stop_api_tp git build_api_tp start_api_tp wn
# Остановка api_yal # Остановка api_yal
stop_api_yal: stop_api_yal:
docker compose down api_yal docker compose down api_yal
@@ -229,3 +223,18 @@ start_api_yal:
# Полный цикл обновления api_yal # Полный цикл обновления api_yal
api_yal: stop_api_yal git build_api_yal start_api_yal wn api_yal: stop_api_yal git build_api_yal start_api_yal wn
# Остановка yalarba-nuxt
stop_yalarba:
docker compose down yalarba
# Пересборка yalarba-nuxt
build_yalarba:
docker compose build yalarba --no-cache
# Запуск yalarba-nuxt
start_yalarba:
docker compose up yalarba -d
# Полный цикл обновления yalarba-nuxt
yalarba: stop_yalarba git build_yalarba start_yalarba wn
+223
View File
@@ -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 с авто-детектом Быстрый частичный деплой
+2 -1
View File
@@ -19,7 +19,8 @@ check_local_cert() {
fi fi
# Преобразуем дату истечения в UNIX-время # Преобразуем дату истечения в UNIX-время
expiry_unix=$(date -d "$expiry_date" +%s) # expiry_unix=$(date -d "$expiry_date" +%s)
expiry_unix=$(date -D "%b %d %H:%M:%S %Y %Z" -d "$expiry_date" +%s 2>/dev/null)
# Текущая дата в UNIX-времени # Текущая дата в UNIX-времени
current_unix=$(date +%s) current_unix=$(date +%s)
+43 -93
View File
@@ -43,30 +43,27 @@ services:
- certbot_data:/etc/letsencrypt - certbot_data:/etc/letsencrypt
- certbot_www:/var/www/certbot - certbot_www:/var/www/certbot
- ./stubSite:/usr/share/nginx/stub/html - ./stubSite:/usr/share/nginx/stub/html
- ./yalarba/serv_spa/spa/vue/dist:/usr/share/nginx/yalarba/html
- ./valitovgaziz/html:/usr/share/nginx/valitovgaziz/html
- ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html - ./BB/bbvue/dist:/usr/share/nginx/begushiybashkir/html
- analytics_logs:/var/log/analytics:ro - analytics_logs:/var/log/analytics:ro
networks: networks:
- web-network - web-network
- internal - internal
- app-network - app-network
- bb-network
depends_on: depends_on:
easysite: easysite:
condition: service_healthy condition: service_healthy
api_es:
condition: service_healthy
certbot: certbot:
condition: service_healthy condition: service_healthy
api_tp:
condition: service_healthy
api_bb: api_bb:
condition: service_healthy condition: service_healthy
analytics: analytics:
condition: service_healthy condition: service_healthy
api_yal: api_yal:
condition: service_healthy condition: service_healthy
yalarba:
condition: service_healthy
valitovgaziz:
condition: service_healthy
healthcheck: healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"] test: ["CMD-SHELL", "curl -f http://localhost/health || exit 1"]
interval: 30s interval: 30s
@@ -101,41 +98,24 @@ services:
retries: 3 retries: 3
start_period: 40s start_period: 40s
# REST API app on Golang (Gorm, Chi) бизнес логика приложения yalarba.ru. Работает с БД на PostgresQL db:db_tp # Vue 3 SPA для valitovgaziz.ru
api_tp: valitovgaziz:
build: build:
context: ./yalarba/api_tp context: ./valitovgaziz
dockerfile: Dockerfile dockerfile: Dockerfile
ports: container_name: valitovgaziz
- "8888:8080"
container_name: api_tp
restart: unless-stopped restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
# Database connection settings
DB_HOST: db
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: mydb
APP_PORT: 8080
networks: networks:
- app-network - web-network
depends_on:
analytics:
condition: service_healthy
healthcheck: healthcheck:
test: test: ["CMD", "wget", "--spider", "http://localhost/"]
[
"CMD",
"wget",
"--no-verbose",
"--tries=1",
"--spider",
"http://localhost:8080/health",
]
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s
# PostgresQL DB база данных для храниния информации приложений Yalarba.ru && Easysite102.ru # PostgresQL DB база данных для храниния информации приложений Yalarba.ru && Easysite102.ru
db: db:
@@ -159,7 +139,7 @@ services:
timeout: 10s timeout: 10s
retries: 5 retries: 5
# REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир Работает с БД db_bb on PostgresQL # REST API on Golang (Gorm, Chi) логика обработки информации для сайта БегущийБашкир
api_bb: api_bb:
build: build:
context: ./BB/api_bb context: ./BB/api_bb
@@ -169,22 +149,22 @@ services:
container_name: api_bb container_name: api_bb
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
db_bb: db:
condition: service_healthy condition: service_healthy
env_file: env_file:
- ./BB/api_bb/.env - ./BB/api_bb/.env
volumes: volumes:
- api_bb_uploads:/app/uploads - api_bb_uploads:/app/uploads
environment: environment:
# Database connection settings DB_HOST: db
DB_HOST: db_bb
DB_PORT: 5432 DB_PORT: 5432
DB_USER: postgres DB_USER: postgres
DB_PASSWORD: postgres DB_PASSWORD: postgres
DB_NAME: bb_db DB_NAME: bb_db
DB_SCHEMA: bb
APP_PORT: 8080 APP_PORT: 8080
networks: networks:
- bb-network - app-network
healthcheck: healthcheck:
test: test:
[ [
@@ -199,31 +179,10 @@ services:
timeout: 10s timeout: 10s
retries: 3 retries: 3
# PostgresQL DB база данных для работы сайта Бегущий Башкир # SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_yal REST API app
db_bb:
image: postgres:15-alpine
restart: unless-stopped
ports:
- "5433:5432"
container_name: db_bb
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: bb_db
volumes:
- db_bb_data:/var/lib/postgresql/data
networks:
- bb-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 30s
timeout: 10s
retries: 5
# SPA app прилжение выполнено на nuxt.js интерфейс для туристического бизнеса. Хранение информации в api_es REST API app
easysite: easysite:
build: build:
context: ./yalarba/easySite/easySite context: ./yalarba/easySite
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: easysite container_name: easysite
restart: unless-stopped restart: unless-stopped
@@ -233,6 +192,7 @@ services:
NODE_ENV: production NODE_ENV: production
HOST: 0.0.0.0 HOST: 0.0.0.0
PORT: 3000 PORT: 3000
NUXT_PUBLIC_API_BASE: /api/v1
networks: networks:
- web-network - web-network
- app-network - app-network
@@ -242,34 +202,6 @@ services:
timeout: 10s timeout: 10s
retries: 3 retries: 3
# REST API приложение для easysite102.ru тут бизнес логика и система для обращения к PostgresQL БД (тоже сервис db:db_tp)
api_es:
build:
context: ./yalarba/api_es
dockerfile: Dockerfile
container_name: api_es
restart: unless-stopped
env_file:
- ./yalarba/api_es/.env
depends_on:
db:
condition: service_healthy
environment:
DB_HOST: db
DB_PORT: 5432
DB_USER: postgres
DB_PASSWORD: postgres
DB_NAME: mydb
APP_PORT: ${API_ES_APP_PORT}
networks:
- app-network
- web-network
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:8088/health"]
interval: 30s
timeout: 10s
retries: 3
# REST API app on Golang для api_yal сервиса # REST API app on Golang для api_yal сервиса
api_yal: api_yal:
build: build:
@@ -300,12 +232,32 @@ services:
retries: 3 retries: 3
start_period: 40s start_period: 40s
# Nuxt 4 SPA для yalarba.ru
yalarba:
build:
context: ./yalarba/yalarba-nuxt
dockerfile: Dockerfile
container_name: yalarba
restart: unless-stopped
environment:
NODE_ENV: production
HOST: 0.0.0.0
PORT: 3000
NUXT_PUBLIC_API_BASE: /api/v1
NUXT_PUBLIC_APP_URL: https://yalarba.ru
networks:
- web-network
- app-network
healthcheck:
test: ["CMD", "wget", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes: volumes:
certbot_data: # volume для данных Certbot certbot_data: # volume для данных Certbot
certbot_www: # volume для данных Certbot certbot_www: # volume для данных Certbot
db_tp_data: # Volume для данных БД yalarba.ru db_tp_data: # Volume для данных БД yalarba.ru
db_bb_data: # Volume для данных БД Бегущий башкир
api_bb_uploads: # Volume для загружаемых файлов бегущий башкир api_bb_uploads: # Volume для загружаемых файлов бегущий башкир
analytics_logs: # Volume для логов аналитики analytics_logs: # Volume для логов аналитики
analytics_data: # Volume для данных аналитики analytics_data: # Volume для данных аналитики
@@ -317,8 +269,6 @@ networks:
driver: bridge driver: bridge
app-network: app-network:
driver: bridge driver: bridge
bb-network:
driver: bridge
# Эта опция автоматически удаляет orphans (Не используемые контейнеры) # Эта опция автоматически удаляет orphans (Не используемые контейнеры)
x-remove-orphans: true x-remove-orphans: true
+5 -5
View File
@@ -41,7 +41,7 @@
│ • certbot - SSL сертификаты │ │ • certbot - SSL сертификаты │
│ • analytics - Статистика (Node.js) │ │ • analytics - Статистика (Node.js) │
│ • api_tp - API yalarba.ru (Go) │ │ • api_tp - API yalarba.ru (Go) │
│ • api_es - API easysite102.ru (Go) │ │ • api_yal - API easysite102.ru (Go) │
│ • api_bb - API Бегущий Башкир (Go) │ │ • api_bb - API Бегущий Башкир (Go) │
│ • easysite - SPA (Nuxt.js) │ │ • easysite - SPA (Nuxt.js) │
│ • db - PostgreSQL (yalarba/easy) │ │ • db - PostgreSQL (yalarba/easy) │
@@ -74,7 +74,7 @@
|-------|-----|----------------|---------------| |-------|-----|----------------|---------------|
| `yalarba.ru` | SPA (Vue) | `api_tp:8080` | `/usr/share/nginx/yalarba/html` | | `yalarba.ru` | SPA (Vue) | `api_tp:8080` | `/usr/share/nginx/yalarba/html` |
| `valitovgaziz.ru` | Статический сайт | - | `/usr/share/nginx/valitovgaziz/html` | | `valitovgaziz.ru` | Статический сайт | - | `/usr/share/nginx/valitovgaziz/html` |
| `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_es:8088` | Прокси | | `easysite102.ru` | SPA (Nuxt.js) | `easysite:3000` + `api_yal:8787` | Прокси |
| `begushiybashkir.ru` | SPA (Vue) | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` | | `begushiybashkir.ru` | SPA (Vue) | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
| `xn--80abahjtcfl5d0a8di.xn--p1ai` | Альтернативный домен для Бегущий Башкир | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` | | `xn--80abahjtcfl5d0a8di.xn--p1ai` | Альтернативный домен для Бегущий Башкир | `api_bb:8080` | `/usr/share/nginx/begushiybashkir/html` |
@@ -118,7 +118,7 @@
``` ```
EMAIL=admin@example.com # Для Let's Encrypt EMAIL=admin@example.com # Для Let's Encrypt
ALL_DOMAINS=yalarba.ru,valitovgaziz.ru... # Все домены для SSL ALL_DOMAINS=yalarba.ru,valitovgaziz.ru... # Все домены для SSL
API_ES_APP_PORT=8088 # Порт API easysite # API_ES убран, используется api_yal:8787
``` ```
### Сервисные ### Сервисные
@@ -141,14 +141,14 @@ STAGING=0 # 1 для тестового режима Let's Encrypt
| certbot | Проверка файла сертификата | - | 30s | | certbot | Проверка файла сертификата | - | 30s |
| analytics | `http://localhost:3000/health` | 3000 | 30s | | analytics | `http://localhost:3000/health` | 3000 | 30s |
| api_tp | `http://localhost:8080/health` | 8080 | 30s | | api_tp | `http://localhost:8080/health` | 8080 | 30s |
| api_es | `http://localhost:8088/health` | 8088 | 30s | | api_yal | `http://localhost:8787/health` | 8787 | 30s |
| api_bb | `http://localhost:8080/api/health` | 8080 | 30s | | api_bb | `http://localhost:8080/api/health` | 8080 | 30s |
| easysite | `http://localhost:3000/api/health` | 3000 | 30s | | easysite | `http://localhost:3000/api/health` | 3000 | 30s |
| db, db_bb | `pg_isready -U postgres` | 5432 | 30s | | db, db_bb | `pg_isready -U postgres` | 5432 | 30s |
### Зависимости запуска ### Зависимости запуска
Nginx запускается только после подтверждения здоровья: Nginx запускается только после подтверждения здоровья:
- `easysite`, `api_es`, `certbot`, `api_tp`, `api_bb`, `analytics` - `easysite`, `api_yal`, `certbot`, `api_tp`, `api_bb`, `analytics`
## Волумы ## Волумы
+24 -68
View File
@@ -103,55 +103,35 @@ server {
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
# ============================================ # ============================================
# ЛОКАЦИЯ: Корневая (SPA приложение) # ЛОКАЦИЯ: Nuxt 4 SSR приложение
# ============================================ # ============================================
location / { location / {
# Директория со скомпилированным Vue/React приложением # Проксирование к Nuxt.js SSR серверу
root /usr/share/nginx/yalarba/html; proxy_pass http://yalarba:3000;
# Файл по умолчанию # Полный набор заголовков для корректной работы приложения
index index.html;
# Логика SPA роутинга:
# 1. Пробуем найти точный файл ($uri)
# 2. Пробуем найти директорию ($uri/)
# 3. Если не нашли - отдаем index.html
# Это позволяет клиентскому роутингу работать корректно
try_files $uri $uri/ /index.html;
}
# ============================================
# ЛОКАЦИЯ: REST API Backend
# ============================================
location /api/ {
# Проксирование всех запросов к API на Golang сервис
proxy_pass http://api_tp/; # Контейнер Docker
# Передача оригинальных заголовков от клиента
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $server_port;
# Увеличенные таймауты для длительных операций (10 минут) # Длинные таймауты
proxy_connect_timeout 600; proxy_connect_timeout 600;
proxy_send_timeout 600; proxy_send_timeout 600;
proxy_read_timeout 600; proxy_read_timeout 600;
} }
location /auth/ { # ============================================
# Проксирование всех запросов к API на Golang сервис # ЛОКАЦИЯ: REST API (api_yal)
proxy_pass http://api_yal/; # Контейнер Docker # ============================================
location /api/v1/ {
# Передача оригинальных заголовков от клиента proxy_pass http://api_yal:8787;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $server_port;
# Увеличенные таймауты для длительных операций (10 минут)
proxy_connect_timeout 600; proxy_connect_timeout 600;
proxy_send_timeout 600; proxy_send_timeout 600;
proxy_read_timeout 600; proxy_read_timeout 600;
@@ -175,48 +155,39 @@ server {
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
# ============================================ # ============================================
# ЛОКАЦИЯ: Статический сайт # ЛОКАЦИЯ: Проксирование к Vue SPA контейнеру
# ============================================ # ============================================
location / { location / {
# Статические HTML файлы proxy_pass http://valitovgaziz/;
root /usr/share/nginx/valitovgaziz/html; proxy_set_header Host $host;
index index.html; 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;
try_files $uri $uri/ /index.html; proxy_set_header X-Forwarded-Port $server_port;
proxy_connect_timeout 600;
proxy_send_timeout 600;
proxy_read_timeout 600;
} }
# ============================================ # ============================================
# ЛОКАЦИЯ: API для аналитики # ЛОКАЦИЯ: API для аналитики
# ============================================ # ============================================
location /api/ { location /api/ {
# Проксирование на Node.js сервис аналитики
proxy_pass http://analytics:3000/; proxy_pass http://analytics:3000/;
# Базовые заголовки прокси
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# ========================================
# НАСТРОЙКИ CORS (Cross-Origin Resource Sharing)
# ========================================
# Разрешаем запросы с ЛЮБОГО домена (*)
# Внимание: "*" может быть небезопасно в production
add_header Access-Control-Allow-Origin "*" always; add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "POST, GET, OPTIONS" 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-Headers "Content-Type, Authorization" always;
add_header Access-Control-Allow-Credentials "true" always; add_header Access-Control-Allow-Credentials "true" always;
# Обработка предварительных OPTIONS запросов (preflight)
# Браузеры отправляют такие запросы перед основными
if ($request_method = OPTIONS) { if ($request_method = OPTIONS) {
# 204 - No Content (успешный пустой ответ)
return 204; return 204;
} }
# Стандартные таймауты для API аналитики
proxy_connect_timeout 30s; proxy_connect_timeout 30s;
proxy_send_timeout 30s; proxy_send_timeout 30s;
proxy_read_timeout 30s; proxy_read_timeout 30s;
@@ -260,43 +231,28 @@ server {
} }
# ============================================ # ============================================
# ЛОКАЦИЯ: API Backend для Easysite # ЛОКАЦИЯ: API Backend для Easysite (api_yal)
# ============================================ # ============================================
location /api/ { location /api/v1/ {
# Отдельный API endpoint для backend proxy_pass http://api_yal:8787;
proxy_pass http://api_es:8088/;
# Заголовки прокси
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port $server_port; proxy_set_header X-Forwarded-Port $server_port;
# Таймауты как у основного приложения
proxy_connect_timeout 600; proxy_connect_timeout 600;
proxy_send_timeout 600; proxy_send_timeout 600;
proxy_read_timeout 600; proxy_read_timeout 600;
# ======================================== if ($request_method = OPTIONS) {
# ДЕТАЛЬНЫЕ НАСТРОЙКИ CORS ДЛЯ OPTIONS
# ========================================
if ($request_method = OPTIONS ) {
# Динамический заголовок Origin из запроса
add_header 'Access-Control-Allow-Origin' "$http_origin"; add_header 'Access-Control-Allow-Origin' "$http_origin";
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE'; add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, PATCH, DELETE';
# Подробный список разрешенных заголовков
add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
# Время кэширования preflight ответа (20 дней)
add_header 'Access-Control-Max-Age' 1728000; add_header 'Access-Control-Max-Age' 1728000;
# Пустой ответ для OPTIONS
add_header 'Content-Length' 0; add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain charset=UTF-8'; add_header 'Content-Type' 'text/plain charset=UTF-8';
# Возвращаем 204 без тела ответа
return 204; return 204;
} }
} }
+8
View File
@@ -0,0 +1,8 @@
node_modules
analytics
src
package.json
package-lock.json
jsconfig.json
vite.config.js
index.html
+4
View File
@@ -0,0 +1,4 @@
FROM nginx:alpine
COPY dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
-62
View File
@@ -1,62 +0,0 @@
# ValitovGaziz - Персональный сайт и портфолио
![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)
![Node.js](https://img.shields.io/badge/Node.js-18%2B-green.svg)
![License](https://img.shields.io/badge/license-MIT-lightgrey.svg)
<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
-274
View File
@@ -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 () { };
}
Binary file not shown.
-590
View File
@@ -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 = '🌙 Темная тема';
}
});
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

-681
View File
@@ -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;
}
}
-132
View File
@@ -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();
});
-464
View File
@@ -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);
}
-52
View File
@@ -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;
}
-617
View File
@@ -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);
}
+14
View File
@@ -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>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
File diff suppressed because it is too large Load Diff
+19
View File
@@ -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"
}
}
+52
View File
@@ -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>
+174
View File
@@ -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;
}
}
Binary file not shown.

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>&copy; 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>
+11
View File
@@ -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')
+20
View File
@@ -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
+181
View File
@@ -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>
+771
View File
@@ -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">Технологический предприниматель &amp; 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>
+19
View File
@@ -0,0 +1,19 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
server: {
host: true,
port: 3002
}
})
-12
View File
@@ -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
-20
View File
@@ -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"]
-71
View File
@@ -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)
}
}
@@ -1,259 +0,0 @@
# Документация REST API сервиса "Travel Platform"
## Общая информация
API сервиса для управления туристическими объектами (отели, санатории, достопримечательности и др.) с системой аутентификации пользователей, отзывами и фильтрацией.
## Базовый URL
`http://localhost:8080` (или другой хост/порт в зависимости от конфигурации)
---
## Модели данных
### Пользователь (User)
**Поля:**
- `id` - уникальный идентификатор
- `email` - электронная почта (уникальный)
- `password_hash` - хеш пароля
- `full_name`, `first_name`, `last_name` - имя пользователя
- `phone`, `city` - контактная информация
- `organization_*` - бизнес-данные для владельцев
- `is_active`, `is_verified`, `role` - статус и права доступа
### Объект (Object)
**Типы объектов:**
- `hotel` - отель
- `sanatorium` - санаторий
- `guest_house` - гостевой дом
- `tour` - тур
- `restaurant` - ресторан
- `museum` - музей
- `landmark` - достопримечательность
- `event` - мероприятие
- `route` - маршрут
**Статусы объектов:**
- `draft` - черновик
- `moderation` - на модерации
- `active` - активен
- `inactive` - неактивен
- `rejected` - отклонен
### Отзыв (Review)
- Оценка от 1 до 5 звезд
- Текстовый отзыв
- Связь с объектом и автором
---
## Аутентификация и авторизация
### Система токенов:
- **Access Token** - для доступа к защищенным ресурсам
- **Refresh Token** - для обновления access token
- **Token Type**: Bearer
- Токены передаются в заголовке `Authorization: Bearer <token>`
### Роли пользователей:
1. **user** - обычный пользователь
2. **moderator** - модератор
3. **admin** - администратор
---
## Эндпоинты API
### 1. Проверка работоспособности
**GET /health**
**GET /check**
*Проверка доступности сервиса*
### 2. Аутентификация
#### Регистрация пользователя
**POST /auth/register**
*Создание нового аккаунта*
**Тело запроса (UserRegisterRequest):**
```json
{
"email": "user@example.com",
"password": "password123",
"full_name": "Иван Иванов",
"phone": "+79991234567",
"city": "Москва"
}
```
#### Вход в систему
**POST /auth/login**
*Получение токенов доступа*
**Тело запроса (AuthRequest):**
```json
{
"email": "user@example.com",
"password": "password123"
}
```
**Ответ (AuthResponse):**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"refresh_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"user": {
"id": 1,
"email": "user@example.com",
"full_name": "Иван Иванов"
// ... остальные поля UserResponse
}
}
```
#### Обновление токена
**POST /auth/refresh**
*Получение нового access token по refresh token*
**Тело запроса (RefreshTokenRequest):**
```json
{
"refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}
```
#### Выход из системы
**POST /auth/logout**
*Инвалидация токенов*
### 3. Профиль пользователя
#### Получение профиля
**GET /users/profile**
*Требуется аутентификация*
*Получение данных текущего пользователя*
#### Обновление профиля
**PUT /users/profile**
*Требуется аутентификация*
*Обновление данных пользователя*
### 4. Управление пользователями (Admin)
#### Список пользователей
**GET /users**
*Требуется роль admin*
*Получение списка всех пользователей*
#### Получение пользователя по ID
**GET /users/{id}**
*Требуется роль admin*
*Получение данных конкретного пользователя*
---
## Фильтрация и пагинация
Для эндпоинтов списков объектов поддерживается фильтрация через `ObjectFilter`:
**Параметры запроса:**
- `search` - текстовый поиск
- `type` - тип объекта (hotel, sanatorium и т.д.)
- `city` - город
- `min_price`, `max_price` - диапазон цен
- `min_rating` - минимальный рейтинг
- `status` - статус объекта
- `owner_id` - ID владельца
- `page` - номер страницы (начинается с 1)
- `page_size` - количество элементов на странице (1-100)
- `sort_by` - поле сортировки (title, price, rating, city, created_at)
- `sort_order` - порядок сортировки (asc, desc)
**Пример запроса:**
```
GET /objects?city=Москва&min_price=1000&max_price=5000&page=1&page_size=20&sort_by=price&sort_order=asc
```
---
## Формат ответа с пагинацией
Для списков возвращается `PaginatedResponse`:
```json
{
"data": [...], // массив объектов
"total": 150, // общее количество
"page": 1, // текущая страница
"page_size": 20, // элементов на странице
"total_pages": 8 // всего страниц
}
```
---
## Обработка ошибок
Сервис использует стандартные HTTP статусы:
- `200` - успешный запрос
- `201` - создан новый ресурс
- `400` - ошибка валидации
- `401` - неавторизован
- `403` - доступ запрещен
- `404` - ресурс не найден
- `500` - внутренняя ошибка сервера
---
## Следующие шаги (планируемые эндпоинты)
На основе моделей данных ожидаются следующие API:
### Управление объектами:
- `GET /objects` - список объектов с фильтрацией
- `GET /objects/{id}` - получение объекта
- `POST /objects` - создание объекта (требуется аутентификация)
- `PUT /objects/{id}` - обновление объекта
- `DELETE /objects/{id}` - удаление объекта
### Управление отзывами:
- `GET /objects/{id}/reviews` - отзывы объекта
- `POST /reviews` - создание отзыва
- `PUT /reviews/{id}` - обновление отзыва
- `DELETE /reviews/{id}` - удаление отзыва
### Модерация:
- `GET /moderation/objects` - объекты на модерации
- `POST /moderation/objects/{id}/approve` - утвердить объект
- `POST /moderation/objects/{id}/reject` - отклонить объект
### Отчеты и аналитика:
- `GET /reports/popular-objects` - популярные объекты
- `GET /reports/user-activity` - активность пользователей
- `GET /reviews/revenue` - аналитика доходов
---
## Технические детали
### База данных:
- Используется GORM (Go ORM)
- Поддерживаются миграции
- Soft delete для основных сущностей
### Логирование:
- Структурированное логирование через Zap
- Логирование маршрутов при запуске
- Middleware для логирования запросов
### Конфигурация:
- Централизованная конфигурация через `config.Config`
- Поддержка разных окружений
### Middleware:
- Аутентификация (`AuthMiddleware`)
- Авторизация по ролям (`AdminMiddleware`)
- Логирование
- Recovery от паник
-34
View File
@@ -1,34 +0,0 @@
module api_es
go 1.25.1
require (
github.com/go-chi/chi/v5 v5.2.3
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.25.10
)
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/leodido/go-urn v1.4.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)
require (
github.com/go-chi/cors v1.2.2
github.com/go-playground/validator/v10 v10.28.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
go.uber.org/zap v1.27.0
golang.org/x/crypto v0.42.0
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect
)
-62
View File
@@ -1,62 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
@@ -1,42 +0,0 @@
package config
import (
"os"
)
type Config struct {
DBHost string
DBPort string
DBUser string
DBPassword string
DBName string
JWTSecret string
ServerPort string
UploadPath string
LogLevel string
Environment string
AppPort string
}
func Load() *Config {
return &Config{
DBHost: getEnv("DB_HOST", "localhost"),
DBPort: getEnv("DB_PORT", "5432"),
DBUser: getEnv("DB_USER", "postgres"),
DBPassword: getEnv("DB_PASSWORD", "postgres"),
DBName: getEnv("DB_NAME", "mydb"),
JWTSecret: getEnv("JWT_SECRET", "secret"),
ServerPort: getEnv("SERVER_PORT", "8080"),
UploadPath: getEnv("UPLOAD_PATH", "./storage/uploads"),
LogLevel: getEnv("LOG_LEVEL", "debug"),
Environment: getEnv("ENVIRONMENT", "development"),
AppPort: getEnv("APP_PORT", "8088"),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
@@ -1,29 +0,0 @@
package database
import (
"api_es/internal/models"
"api_es/pkg/logger"
"gorm.io/gorm"
)
func SeedInitialData(db *gorm.DB) error {
zapLogger := logger.Get()
zapLogger.Debug("start fill init data")
// Создание базовых удобств
amenities := []models.Amenity{
{Name: "Wi-Fi", Category: "basic", Icon: "wifi"},
{Name: "Парковка", Category: "basic", Icon: "parking"},
{Name: "Бассейн", Category: "comfort", Icon: "pool"},
// ... другие удобства
}
for _, amenity := range amenities {
if err := db.FirstOrCreate(&amenity, models.Amenity{Name: amenity.Name}).Error; err != nil {
return err
}
}
zapLogger.Debug("end fill init data")
return nil
}
@@ -1,60 +0,0 @@
package database
import (
"api_es/internal/config"
"api_es/internal/models"
"api_es/pkg/logger"
"fmt"
"go.uber.org/zap"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func NewPostgresConnection(cfg *config.Config) (*gorm.DB, error) {
zapLogger := logger.Get()
zapLogger.Info("Start connect to Postgres DB")
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=UTC",
cfg.DBHost, cfg.DBUser, cfg.DBPassword, cfg.DBName, cfg.DBPort)
zapLogger.Info("dsn = %s", zap.String("dsn", dsn))
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
zapLogger.Info("AutoMigrate models")
// Автомиграция
if err := autoMigrate(db); err != nil {
zapLogger.Error("can't migrate models, error = %s", zap.Error(err))
return nil, fmt.Errorf("can't migrate models, error = %s", err)
}
zapLogger.Info("Migrate complite successfully")
zapLogger.Info("Fill init data")
SeedInitialData(db)
zapLogger.Info("Successfully connected to database")
return db, nil
}
func autoMigrate(db *gorm.DB) error {
zapLogger := logger.Get()
zapLogger.Debug("Start migration")
models := []interface{}{
&models.User{},
&models.Object{},
&models.ObjectImage{},
&models.Amenity{},
&models.ObjectAmenity{},
}
for _, model := range models {
if err := db.AutoMigrate(model); err != nil {
return fmt.Errorf("failed to migrate %T: %w", model, err)
}
}
zapLogger.Debug("End migration seccessfully")
return nil
}
@@ -1,90 +0,0 @@
package dto
import (
"api_es/internal/models"
"time"
)
// RegisterRequest - запрос на регистрацию
type RegisterRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=6"`
FullName string `json:"full_name" validate:"required"`
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
Phone string `json:"phone"`
City string `json:"city"`
}
// LoginRequest - запрос на вход
type LoginRequest struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}
// UpdateUserRequest - запрос на обновление пользователя
type UpdateUserRequest struct {
FullName string `json:"full_name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Phone string `json:"phone"`
City string `json:"city"`
OrganizationForm string `json:"organization_form"`
OrganizationName string `json:"organization_name"`
OrganizationShort string `json:"organization_short"`
INN string `json:"inn"`
PersonalINN string `json:"personal_inn"`
}
// UserResponse - ответ с данными пользователя
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
FullName string `json:"full_name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Phone string `json:"phone"`
City string `json:"city"`
OrganizationForm string `json:"organization_form"`
OrganizationName string `json:"organization_name"`
OrganizationShort string `json:"organization_short"`
INN string `json:"inn"`
PersonalINN string `json:"personal_inn"`
IsActive bool `json:"is_active"`
IsVerified bool `json:"is_verified"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
}
// AuthResponse - ответ с токеном
type AuthResponse struct {
Token string `json:"token"`
User UserResponse `json:"user"`
}
// ToUserResponse преобразует модель в DTO
func ToUserResponse(user *models.User) UserResponse {
return UserResponse{
ID: user.ID,
Email: user.Email,
FullName: user.FullName,
FirstName: user.FirstName,
LastName: user.LastName,
Phone: user.Phone,
City: user.City,
OrganizationForm: user.OrganizationForm,
OrganizationName: user.OrganizationName,
OrganizationShort: user.OrganizationShort,
INN: user.INN,
PersonalINN: user.PersonalINN,
IsActive: user.IsActive,
IsVerified: user.IsVerified,
Role: user.Role,
CreatedAt: user.CreatedAt,
}
}
// dto/auth.go (добавляем если нужно)
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" validate:"required"`
}
@@ -1,39 +0,0 @@
package handler
import (
"api_es/internal/config"
"api_es/internal/repository"
"api_es/internal/service"
"api_es/internal/utils"
"gorm.io/gorm"
)
type AllHandler struct {
userHandler *UserHandler
healthHandler *HealthHandler
}
func NewAllHandler(db *gorm.DB, cfg *config.Config) *AllHandler {
userRepo := repository.NewUserRepository(db)
userService := service.NewUserService(userRepo, utils.NewJWTUtil(cfg.JWTSecret))
userHandler := NewUserHandler(userService)
healthHandler := NewHealthHandler()
return &AllHandler{
userHandler: userHandler,
healthHandler: healthHandler,
}
}
func (h *AllHandler) UserHandler() *UserHandler {
return h.userHandler
}
func (h *AllHandler) HealthHandler() *HealthHandler {
return h.healthHandler
}
@@ -1,31 +0,0 @@
package handler
import (
"net/http"
"api_es/internal/utils"
)
type HealthHandler struct{}
func NewHealthHandler() *HealthHandler {
return &HealthHandler{}
}
func (h *HealthHandler) HealthCheck(w http.ResponseWriter, r *http.Request) {
response := map[string]string{
"status": "ok",
"message": "Service is healthy",
}
utils.RespondWithJSON(w, http.StatusOK, response)
}
func (h *HealthHandler) Check(w http.ResponseWriter, r *http.Request) {
response := map[string]string{
"status": "ok",
"message": "API is working",
}
utils.RespondWithJSON(w, http.StatusOK, response)
}
@@ -1,317 +0,0 @@
package handler
import (
"api_es/internal/dto"
appMiddleware "api_es/internal/middleware"
"api_es/internal/service"
"api_es/internal/utils"
"api_es/pkg/logger"
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/go-playground/validator/v10"
)
type UserHandler struct {
userService service.UserService
validator *validator.Validate
}
func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{
userService: userService,
validator: validator.New(),
}
}
// Register godoc
// @Summary Register new user
// @Description Create a new user account
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.RegisterRequest true "Register request"
// @Success 201 {object} dto.AuthResponse
// @Failure 400 {object} map[string]string
// @Failure 500 {object} map[string]string
// @Router /auth/register [post]
func (h *UserHandler) Register(w http.ResponseWriter, r *http.Request) {
zapLogger := logger.Get()
zapLogger.Debug("Start register")
var req dto.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := h.validator.Struct(req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
response, err := h.userService.Register(r.Context(), req)
if err != nil {
switch err {
case service.ErrUserAlreadyExists:
http.Error(w, "User already exists", http.StatusConflict)
default:
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
return
}
// Устанавливаем куку с токеном
appMiddleware.SetAuthCookie(w, response.Token)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
zapLogger.Debug("End register")
json.NewEncoder(w).Encode(response)
}
// Login godoc
// @Summary Login user
// @Description Authenticate user and get token
// @Tags auth
// @Accept json
// @Produce json
// @Param request body dto.LoginRequest true "Login request"
// @Success 200 {object} dto.AuthResponse
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Router /auth/login [post]
func (h *UserHandler) Login(w http.ResponseWriter, r *http.Request) {
zapLogger := logger.Get()
zapLogger.Debug("Start login")
var req dto.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
if err := h.validator.Struct(req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
response, err := h.userService.Login(r.Context(), req)
if err != nil {
switch err {
case service.ErrInvalidCredentials:
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
default:
http.Error(w, err.Error(), http.StatusInternalServerError)
}
return
}
// Устанавливаем куку с токеном
appMiddleware.SetAuthCookie(w, response.Token)
w.Header().Set("Content-Type", "application/json")
zapLogger.Debug("End login")
json.NewEncoder(w).Encode(response)
}
// Добавляем новый метод для logout
// Logout godoc
// @Summary Logout user
// @Description Clear authentication cookies and tokens
// @Tags auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} map[string]string
// @Router /auth/logout [post]
func (h *UserHandler) Logout(w http.ResponseWriter, r *http.Request) {
// Очищаем auth cookie
appMiddleware.ClearAuthCookie(w)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Successfully logged out",
})
}
// Добавляем метод для обновления токена
// RefreshToken godoc
// @Summary Refresh authentication token
// @Description Refresh JWT token using refresh token or existing auth
// @Tags auth
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} dto.AuthResponse
// @Failure 401 {object} map[string]string
// @Router /auth/refresh [post]
func (h *UserHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
user, err := h.userService.GetUserProfile(r.Context(), userID)
if err != nil {
http.Error(w, "User not found", http.StatusUnauthorized)
return
}
// Генерируем новый токен
// В реальном приложении здесь должна быть логика с refresh token
jwtUtil := utils.NewJWTUtil("secret")
newToken, err := jwtUtil.GenerateToken(userID, user.Email, user.Role)
if err != nil {
http.Error(w, "Failed to generate token", http.StatusInternalServerError)
return
}
// Обновляем куку
appMiddleware.SetAuthCookie(w, newToken)
response := &dto.AuthResponse{
Token: newToken,
User: *user,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// GetProfile godoc
// @Summary Get user profile
// @Description Get current user profile
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} dto.UserResponse
// @Failure 404 {object} map[string]string
// @Router /users/profile [get]
func (h *UserHandler) GetProfile(w http.ResponseWriter, r *http.Request) {
zapLogger := logger.Get()
zapLogger.Debug("GetProfile start debug level")
userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
user, err := h.userService.GetUserProfile(r.Context(), userID)
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
zapLogger.Debug("GetProfile end debug level")
json.NewEncoder(w).Encode(user)
}
// UpdateProfile godoc
// @Summary Update user profile
// @Description Update current user profile
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.UpdateUserRequest true "Update request"
// @Success 200 {object} dto.UserResponse
// @Failure 400 {object} map[string]string
// @Router /users/profile [put]
func (h *UserHandler) UpdateProfile(w http.ResponseWriter, r *http.Request) {
userID, ok := r.Context().Value(appMiddleware.UserIDKey).(uint)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
var req dto.UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
user, err := h.userService.UpdateUser(r.Context(), userID, req)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// GetUser godoc
// @Summary Get user by ID
// @Description Get user details by ID (admin only)
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "User ID"
// @Success 200 {object} dto.UserResponse
// @Failure 404 {object} map[string]string
// @Router /users/{id} [get]
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
idStr := chi.URLParam(r, "id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
user, err := h.userService.GetUser(r.Context(), uint(id))
if err != nil {
http.Error(w, "User not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// ListUsers godoc
// @Summary List users
// @Description Get paginated list of users (admin only)
// @Tags users
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param limit query int false "Limit" default(10)
// @Param offset query int false "Offset" default(0)
// @Success 200 {array} dto.UserResponse
// @Router /users [get]
func (h *UserHandler) ListUsers(w http.ResponseWriter, r *http.Request) {
zapLogger := logger.Get()
zapLogger.Debug("Debug start handler listUsers")
limitStr := r.URL.Query().Get("limit")
offsetStr := r.URL.Query().Get("offset")
limit := 10
offset := 0
if limitStr != "" {
if l, err := strconv.Atoi(limitStr); err == nil {
limit = l
}
}
if offsetStr != "" {
if o, err := strconv.Atoi(offsetStr); err == nil {
offset = o
}
}
users, err := h.userService.ListUsers(r.Context(), limit, offset)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
zapLogger.Debug("Debug end handler listUsers")
json.NewEncoder(w).Encode(users)
}
@@ -1,119 +0,0 @@
// auth.go
package middleware
import (
"api_es/internal/utils"
"api_es/pkg/logger"
"context"
"net/http"
"strings"
"go.uber.org/zap"
)
type contextKey string
const (
UserIDKey contextKey = "userID"
UserEmailKey contextKey = "userEmail"
UserRoleKey contextKey = "userRole"
)
// Cookie конфигурация
const (
AuthCookieName = "auth_token"
CookieMaxAge = 24 * 60 * 60 // 24 часа
)
func AuthMiddleware(next http.Handler) http.Handler {
zapLogger := logger.Get()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
zapLogger.Debug("Debug start AuthMiddleware")
var tokenString string
// Пробуем получить токен из заголовка Authorization
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
tokenString = strings.Replace(authHeader, "Bearer ", "", 1)
zapLogger.Debug("Token from Authorization header", zap.String("token", tokenString))
}
// Если токена нет в заголовке, пробуем получить из куки
if tokenString == "" {
cookie, err := r.Cookie(AuthCookieName)
if err == nil && cookie.Value != "" {
tokenString = cookie.Value
zapLogger.Debug("Token from cookie", zap.String("token", tokenString))
}
}
if tokenString == "" {
http.Error(w, "Authorization required", http.StatusUnauthorized)
return
}
// Валидируем токен
jwtUtil := utils.NewJWTUtil("secret")
claims, err := jwtUtil.ValidateToken(tokenString)
if err != nil {
// Если токен невалиден, удаляем куку
http.SetCookie(w, &http.Cookie{
Name: AuthCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserEmailKey, claims.Email)
ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
zapLogger.Debug("Debug end AuthMiddleware")
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Вспомогательная функция для установки auth cookie
func SetAuthCookie(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: AuthCookieName,
Value: token,
Path: "/",
MaxAge: CookieMaxAge,
HttpOnly: true,
Secure: true, // В production должно быть true
SameSite: http.SameSiteStrictMode,
})
}
// Вспомогательная функция для удаления auth cookie
func ClearAuthCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{
Name: AuthCookieName,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteStrictMode,
})
}
func AdminMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
role, ok := r.Context().Value(UserRoleKey).(string)
if !ok || role != "admin" {
http.Error(w, "Admin access required", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
@@ -1,45 +0,0 @@
package models
import (
)
// AuthRequest - запрос на аутентификацию
type AuthRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
}
// AuthResponse - ответ с токенами
type AuthResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"` // Bearer
ExpiresIn int64 `json:"expires_in"`
User UserResponse `json:"user"`
}
// RefreshTokenRequest - запрос на обновление токена
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required"`
}
// UserRegisterRequest - запрос на регистрацию
type UserRegisterRequest struct {
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
FullName string `json:"full_name" binding:"required"`
Phone string `json:"phone"`
City string `json:"city"`
}
// PasswordResetRequest - запрос на сброс пароля
type PasswordResetRequest struct {
Email string `json:"email" binding:"required,email"`
}
// PasswordResetConfirmRequest - подтверждение сброса пароля
type PasswordResetConfirmRequest struct {
Token string `json:"token" binding:"required"`
Password string `json:"password" binding:"required,min=6"`
}
@@ -1,29 +0,0 @@
package models
type ObjectFilter struct {
Search string `form:"search" json:"search"`
Type ObjectType `form:"type" json:"type"`
City string `form:"city" json:"city"`
MinPrice float64 `form:"min_price" json:"min_price"`
MaxPrice float64 `form:"max_price" json:"max_price"`
MinRating float64 `form:"min_rating" json:"min_rating"`
Status ObjectStatus `form:"status" json:"status"`
OwnerID uint `form:"owner_id" json:"owner_id"`
// Пагинация
Page int `form:"page" json:"page" binding:"min=1"`
PageSize int `form:"page_size" json:"page_size" binding:"min=1,max=100"`
// Сортировка
SortBy string `form:"sort_by" json:"sort_by"` // title, price, rating, city, created_at
SortOrder string `form:"sort_order" json:"sort_order"` // asc, desc
}
// PaginatedResponse - общий ответ с пагинацией
type PaginatedResponse struct {
Data interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
@@ -1 +0,0 @@
package models
@@ -1,121 +0,0 @@
package models
import (
"gorm.io/gorm"
"time"
)
type ObjectType string
const (
ObjectTypeHotel ObjectType = "hotel"
ObjectTypeSanatorium ObjectType = "sanatorium"
ObjectTypeGuestHouse ObjectType = "guest_house"
ObjectTypeTour ObjectType = "tour"
ObjectTypeRestaurant ObjectType = "restaurant"
ObjectTypeMuseum ObjectType = "museum"
ObjectTypeLandmark ObjectType = "landmark"
ObjectTypeEvent ObjectType = "event"
ObjectTypeRoute ObjectType = "route"
)
type ObjectStatus string
const (
ObjectStatusDraft ObjectStatus = "draft"
ObjectStatusModeration ObjectStatus = "moderation"
ObjectStatusActive ObjectStatus = "active"
ObjectStatusInactive ObjectStatus = "inactive"
ObjectStatusRejected ObjectStatus = "rejected"
)
type Object struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Основная информация
Title string `gorm:"not null" json:"title"`
Type ObjectType `gorm:"not null" json:"type"`
Description string `gorm:"type:text" json:"description"`
// Локация
City string `gorm:"not null" json:"city"`
Address string `json:"address"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
// Цена и условия
Price float64 `gorm:"default:0" json:"price"`
PricePeriod string `gorm:"default:'per_night'" json:"price_period"` // per_night, per_person, per_tour
// Статус и рейтинг
Status ObjectStatus `gorm:"default:draft" json:"status"`
Rating float64 `gorm:"default:0" json:"rating"`
ReviewCount int `gorm:"default:0" json:"review_count"`
ViewCount int `gorm:"default:0" json:"view_count"`
// Владелец
OwnerID uint `gorm:"not null;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"owner_id"`
Owner User `gorm:"foreignKey:OwnerID;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" json:"owner,omitempty"`
// Связи
Images []ObjectImage `gorm:"foreignKey:ObjectID" json:"images"`
Amenities []Amenity `gorm:"many2many:object_amenities;" json:"amenities"`
Reviews []Review `gorm:"foreignKey:ObjectID" json:"-"`
}
// ObjectImage представляет изображения объекта
type ObjectImage struct {
ID uint `gorm:"primaryKey" json:"id"`
ObjectID uint `gorm:"not null;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"object_id"`
URL string `gorm:"not null" json:"url"`
IsPrimary bool `gorm:"default:false" json:"is_primary"`
Order int `gorm:"default:0" json:"order"`
CreatedAt time.Time `json:"created_at"`
}
// Amenity представляет удобства объекта
type Amenity struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"uniqueIndex;not null" json:"name"`
Category string `json:"category"` // basic, comfort, safety, entertainment, etc.
Icon string `json:"icon"` // иконка для фронтенда
Description string `json:"description"`
}
// ObjectAmenity связь многие-ко-многим между Object и Amenity
type ObjectAmenity struct {
ObjectID uint `gorm:"primaryKey" json:"object_id"`
AmenityID uint `gorm:"primaryKey" json:"amenity_id"`
}
// ObjectCreateRequest - запрос на создание объекта
type ObjectCreateRequest struct {
Title string `json:"title" binding:"required"`
Type ObjectType `json:"type" binding:"required"`
Description string `json:"description" binding:"required"`
City string `json:"city" binding:"required"`
Address string `json:"address"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Price float64 `json:"price"`
PricePeriod string `json:"price_period"`
AmenityIDs []uint `json:"amenity_ids"`
}
// ObjectUpdateRequest - запрос на обновление объекта
type ObjectUpdateRequest struct {
Title string `json:"title"`
Type ObjectType `json:"type"`
Description string `json:"description"`
City string `json:"city"`
Address string `json:"address"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Price float64 `json:"price"`
PricePeriod string `json:"price_period"`
Status ObjectStatus `json:"status"`
AmenityIDs []uint `json:"amenity_ids"`
}
@@ -1 +0,0 @@
package models
@@ -1,28 +0,0 @@
package models
import (
"time"
)
type Review struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
// Связи
ObjectID uint `gorm:"not null" json:"object_id"`
Object Object `gorm:"foreignKey:ObjectID" json:"object,omitempty"`
AuthorID uint `gorm:"not null" json:"author_id"`
Author User `gorm:"foreignKey:AuthorID" json:"author"`
// Контент отзыва
Rating int `gorm:"not null;check:rating >= 1 AND rating <= 5" json:"rating"`
Text string `gorm:"type:text" json:"text"`
IsActive bool `gorm:"default:true" json:"is_active"`
}
// ReviewCreateRequest - запрос на создание отзыва
type ReviewCreateRequest struct {
ObjectID uint `json:"object_id" binding:"required"`
Rating int `json:"rating" binding:"required,min=1,max=5"`
Text string `json:"text" binding:"required,min=10"`
}
@@ -1,68 +0,0 @@
package models
import (
"gorm.io/gorm"
"time"
)
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Основная информация
Email string `gorm:"uniqueIndex;not null" json:"email"`
PasswordHash string `gorm:"not null" json:"-"`
FullName string `gorm:"not null;default:'Unknown'" json:"full_name"`
FirstName string `gorm:"not null;default:'FirstName'" json:"first_name"`
LastName string `gorm:"not null;default:'LastName'" json:"last_name"`
Phone string `json:"phone"`
City string `json:"city"`
// Бизнес информация (для владельцев объектов)
OrganizationForm string `json:"organization_form"` // ИП, ООО и т.д.
OrganizationName string `json:"organization_name"`
OrganizationShort string `json:"organization_short"`
INN string `json:"inn"` // ИНН организации
PersonalINN string `json:"personal_inn"` // Личный ИНН
// Статус
IsActive bool `gorm:"default:true" json:"is_active"`
IsVerified bool `gorm:"default:false" json:"is_verified"`
Role string `gorm:"default:user" json:"role"` // user, admin, moderator
// Связи
Objects []Object `gorm:"foreignKey:OwnerID" json:"-"`
Reviews []Review `gorm:"foreignKey:AuthorID" json:"-"`
}
// UserStats представляет статистику пользователя
type UserStats struct {
UserID uint `gorm:"primaryKey" json:"user_id"`
TotalObjects int `gorm:"default:0" json:"total_objects"`
ActiveObjects int `gorm:"default:0" json:"active_objects"`
ModerationObjects int `gorm:"default:0" json:"moderation_objects"`
TotalReviews int `gorm:"default:0" json:"total_reviews"`
}
// UserResponse - структура для ответа API (без чувствительных данных)
type UserResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
FullName string `json:"full_name"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Phone string `json:"phone"`
City string `json:"city"`
OrganizationForm string `json:"organization_form"`
OrganizationName string `json:"organization_name"`
OrganizationShort string `json:"organization_short"`
INN string `json:"inn"`
PersonalINN string `json:"personal_inn"`
IsActive bool `json:"is_active"`
IsVerified bool `json:"is_verified"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
Stats UserStats `json:"stats,omitempty"`
}
@@ -1,398 +0,0 @@
package repository
import (
"errors"
"gorm.io/gorm"
"api_es/internal/models"
)
var (
ErrObjectNotFound = errors.New("object not found")
)
type ObjectRepository interface {
// Основные операции
Create(object *models.Object) error
GetByID(id uint) (*models.Object, error)
Update(id uint, updates *models.ObjectUpdateRequest) error
Delete(id uint) error
List(filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error)
// Специфичные операции
GetByOwner(ownerID uint, filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error)
UpdateStatus(id uint, status models.ObjectStatus) error
IncrementViewCount(id uint) error
UpdateRating(id uint, rating float64, reviewCount int) error
// Работа с изображениями
AddImage(objectID uint, image *models.ObjectImage) error
RemoveImage(objectID uint, imageID uint) error
SetPrimaryImage(objectID uint, imageID uint) error
GetImages(objectID uint) ([]models.ObjectImage, error)
// Работа с удобствами
AddAmenities(objectID uint, amenityIDs []uint) error
RemoveAmenities(objectID uint, amenityIDs []uint) error
GetAmenities(objectID uint) ([]models.Amenity, error)
}
type ObjectFilter struct {
Type []models.ObjectType
City string
Status []models.ObjectStatus
OwnerID uint
MinPrice float64
MaxPrice float64
MinRating float64
AmenityIDs []uint
Search string
}
type Pagination struct {
Page int `form:"page" default:"1"`
PageSize int `form:"page_size" default:"20"`
}
type objectRepository struct {
db *gorm.DB
}
func NewObjectRepository(db *gorm.DB) ObjectRepository {
return &objectRepository{db: db}
}
// Create создает новый объект
func (r *objectRepository) Create(object *models.Object) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Создаем основной объект
if err := tx.Create(object).Error; err != nil {
return err
}
// Добавляем связи с удобствами, если они есть
if len(object.Amenities) > 0 {
if err := tx.Model(object).Association("Amenities").Append(object.Amenities); err != nil {
return err
}
}
return nil
})
}
// GetByID возвращает объект по ID с связанными данными
func (r *objectRepository) GetByID(id uint) (*models.Object, error) {
var object models.Object
err := r.db.
Preload("Owner", func(db *gorm.DB) *gorm.DB {
return db.Select("id, first_name, last_name, email, phone")
}).
Preload("Images", func(db *gorm.DB) *gorm.DB {
return db.Order("is_primary DESC, order ASC")
}).
Preload("Amenities").
First(&object, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrObjectNotFound
}
return nil, err
}
return &object, nil
}
// Update обновляет объект
func (r *objectRepository) Update(id uint, updates *models.ObjectUpdateRequest) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Обновляем основные поля
updateData := map[string]interface{}{}
if updates.Title != "" {
updateData["title"] = updates.Title
}
if updates.Type != "" {
updateData["type"] = updates.Type
}
if updates.Description != "" {
updateData["description"] = updates.Description
}
if updates.City != "" {
updateData["city"] = updates.City
}
if updates.Address != "" {
updateData["address"] = updates.Address
}
if updates.Latitude != 0 {
updateData["latitude"] = updates.Latitude
}
if updates.Longitude != 0 {
updateData["longitude"] = updates.Longitude
}
if updates.Price != 0 {
updateData["price"] = updates.Price
}
if updates.PricePeriod != "" {
updateData["price_period"] = updates.PricePeriod
}
if updates.Status != "" {
updateData["status"] = updates.Status
}
if len(updateData) > 0 {
if err := tx.Model(&models.Object{}).Where("id = ?", id).Updates(updateData).Error; err != nil {
return err
}
}
// Обновляем удобства, если переданы
if updates.AmenityIDs != nil {
var object models.Object
if err := tx.First(&object, id).Error; err != nil {
return err
}
var amenities []models.Amenity
if err := tx.Where("id IN ?", updates.AmenityIDs).Find(&amenities).Error; err != nil {
return err
}
if err := tx.Model(&object).Association("Amenities").Replace(amenities); err != nil {
return err
}
}
return nil
})
}
// Delete удаляет объект (мягкое удаление)
func (r *objectRepository) Delete(id uint) error {
result := r.db.Delete(&models.Object{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrObjectNotFound
}
return nil
}
// List возвращает список объектов с фильтрацией и пагинацией
func (r *objectRepository) List(filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) {
var objects []models.Object
var total int64
query := r.db.Model(&models.Object{})
// Применяем фильтры
if filter != nil {
query = r.applyFilters(query, filter)
}
// Считаем общее количество
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Применяем пагинацию
if pagination != nil {
offset := (pagination.Page - 1) * pagination.PageSize
query = query.Offset(offset).Limit(pagination.PageSize)
}
// Загружаем данные с прелоадами
err := query.
Preload("Images", func(db *gorm.DB) *gorm.DB {
return db.Where("is_primary = ?", true).Limit(1)
}).
Preload("Amenities").
Order("created_at DESC").
Find(&objects).Error
if err != nil {
return nil, 0, err
}
return objects, total, nil
}
// GetByOwner возвращает объекты владельца
func (r *objectRepository) GetByOwner(ownerID uint, filter *ObjectFilter, pagination *Pagination) ([]models.Object, int64, error) {
if filter == nil {
filter = &ObjectFilter{}
}
filter.OwnerID = ownerID
return r.List(filter, pagination)
}
// UpdateStatus обновляет статус объекта
func (r *objectRepository) UpdateStatus(id uint, status models.ObjectStatus) error {
result := r.db.Model(&models.Object{}).Where("id = ?", id).Update("status", status)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrObjectNotFound
}
return nil
}
// IncrementViewCount увеличивает счетчик просмотров
func (r *objectRepository) IncrementViewCount(id uint) error {
return r.db.Model(&models.Object{}).
Where("id = ?", id).
Update("view_count", gorm.Expr("view_count + ?", 1)).
Error
}
// UpdateRating обновляет рейтинг и количество отзывов
func (r *objectRepository) UpdateRating(id uint, rating float64, reviewCount int) error {
return r.db.Model(&models.Object{}).
Where("id = ?", id).
Updates(map[string]interface{}{
"rating": rating,
"review_count": reviewCount,
}).Error
}
// AddImage добавляет изображение к объекту
func (r *objectRepository) AddImage(objectID uint, image *models.ObjectImage) error {
image.ObjectID = objectID
return r.db.Create(image).Error
}
// RemoveImage удаляет изображение объекта
func (r *objectRepository) RemoveImage(objectID uint, imageID uint) error {
result := r.db.Where("object_id = ? AND id = ?", objectID, imageID).Delete(&models.ObjectImage{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrObjectNotFound
}
return nil
}
// SetPrimaryImage устанавливает основное изображение
func (r *objectRepository) SetPrimaryImage(objectID uint, imageID uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Сбрасываем все is_primary для объекта
if err := tx.Model(&models.ObjectImage{}).
Where("object_id = ?", objectID).
Update("is_primary", false).Error; err != nil {
return err
}
// Устанавливаем новое основное изображение
result := tx.Model(&models.ObjectImage{}).
Where("object_id = ? AND id = ?", objectID, imageID).
Update("is_primary", true)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrObjectNotFound
}
return nil
})
}
// GetImages возвращает изображения объекта
func (r *objectRepository) GetImages(objectID uint) ([]models.ObjectImage, error) {
var images []models.ObjectImage
err := r.db.Where("object_id = ?", objectID).
Order("is_primary DESC, order ASC").
Find(&images).Error
return images, err
}
// AddAmenities добавляет удобства к объекту
func (r *objectRepository) AddAmenities(objectID uint, amenityIDs []uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var object models.Object
if err := tx.First(&object, objectID).Error; err != nil {
return err
}
var amenities []models.Amenity
if err := tx.Where("id IN ?", amenityIDs).Find(&amenities).Error; err != nil {
return err
}
return tx.Model(&object).Association("Amenities").Append(amenities)
})
}
// RemoveAmenities удаляет удобства у объекта
func (r *objectRepository) RemoveAmenities(objectID uint, amenityIDs []uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
var object models.Object
if err := tx.First(&object, objectID).Error; err != nil {
return err
}
var amenities []models.Amenity
if err := tx.Where("id IN ?", amenityIDs).Find(&amenities).Error; err != nil {
return err
}
return tx.Model(&object).Association("Amenities").Delete(amenities)
})
}
// GetAmenities возвращает удобства объекта
func (r *objectRepository) GetAmenities(objectID uint) ([]models.Amenity, error) {
var amenities []models.Amenity
err := r.db.Joins("JOIN object_amenities ON amenities.id = object_amenities.amenity_id").
Where("object_amenities.object_id = ?", objectID).
Find(&amenities).Error
return amenities, err
}
// applyFilters применяет фильтры к запросу
func (r *objectRepository) applyFilters(query *gorm.DB, filter *ObjectFilter) *gorm.DB {
if len(filter.Type) > 0 {
query = query.Where("type IN ?", filter.Type)
}
if filter.City != "" {
query = query.Where("city = ?", filter.City)
}
if len(filter.Status) > 0 {
query = query.Where("status IN ?", filter.Status)
}
if filter.OwnerID != 0 {
query = query.Where("owner_id = ?", filter.OwnerID)
}
if filter.MinPrice > 0 {
query = query.Where("price >= ?", filter.MinPrice)
}
if filter.MaxPrice > 0 {
query = query.Where("price <= ?", filter.MaxPrice)
}
if filter.MinRating > 0 {
query = query.Where("rating >= ?", filter.MinRating)
}
if filter.Search != "" {
search := "%" + filter.Search + "%"
query = query.Where("title ILIKE ? OR description ILIKE ?", search, search)
}
// Фильтр по удобствам
if len(filter.AmenityIDs) > 0 {
query = query.Joins("JOIN object_amenities ON objects.id = object_amenities.object_id").
Where("object_amenities.amenity_id IN ?", filter.AmenityIDs)
}
return query
}
@@ -1,389 +0,0 @@
package repository
import (
"errors"
"gorm.io/gorm"
"api_es/internal/models"
)
var (
ErrReviewNotFound = errors.New("review not found")
ErrDuplicateReview = errors.New("user already has review for this object")
)
type ReviewRepository interface {
// Основные операции
Create(review *models.Review) error
GetByID(id uint) (*models.Review, error)
Update(id uint, updates map[string]interface{}) error
Delete(id uint) error
// Списки отзывов
GetByObject(objectID uint, pagination *Pagination) ([]models.Review, int64, error)
GetByAuthor(authorID uint, pagination *Pagination) ([]models.Review, int64, error)
GetByObjectAndAuthor(objectID, authorID uint) (*models.Review, error)
// Статистика
GetObjectRatingStats(objectID uint) (float64, int, error)
GetUserReviewStats(authorID uint) (int, float64, error)
// Административные методы
SetActive(id uint, isActive bool) error
GetAll(pagination *Pagination, filters *ReviewFilter) ([]models.Review, int64, error)
}
type ReviewFilter struct {
ObjectID uint
AuthorID uint
Rating int
IsActive *bool
MinRating int
MaxRating int
}
type reviewRepository struct {
db *gorm.DB
}
func NewReviewRepository(db *gorm.DB) ReviewRepository {
return &reviewRepository{db: db}
}
// Create создает новый отзыв
func (r *reviewRepository) Create(review *models.Review) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Проверяем, не оставлял ли пользователь уже отзыв на этот объект
var existingReview models.Review
err := tx.Where("object_id = ? AND author_id = ?", review.ObjectID, review.AuthorID).
First(&existingReview).Error
if err == nil {
return ErrDuplicateReview
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
// Создаем отзыв
if err := tx.Create(review).Error; err != nil {
return err
}
// Обновляем рейтинг объекта
return r.updateObjectRating(tx, review.ObjectID)
})
}
// GetByID возвращает отзыв по ID
func (r *reviewRepository) GetByID(id uint) (*models.Review, error) {
var review models.Review
err := r.db.
Preload("Author", func(db *gorm.DB) *gorm.DB {
return db.Select("id, first_name, last_name, avatar")
}).
Preload("Object", func(db *gorm.DB) *gorm.DB {
return db.Select("id, title, type")
}).
First(&review, id).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrReviewNotFound
}
return nil, err
}
return &review, nil
}
// Update обновляет отзыв
func (r *reviewRepository) Update(id uint, updates map[string]interface{}) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Получаем отзыв для получения object_id
var review models.Review
if err := tx.Select("object_id").First(&review, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrReviewNotFound
}
return err
}
// Обновляем отзыв
result := tx.Model(&models.Review{}).Where("id = ?", id).Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrReviewNotFound
}
// Обновляем рейтинг объекта, если изменился рейтинг
if _, hasRating := updates["rating"]; hasRating {
return r.updateObjectRating(tx, review.ObjectID)
}
return nil
})
}
// Delete удаляет отзыв
func (r *reviewRepository) Delete(id uint) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Получаем отзыв для получения object_id
var review models.Review
if err := tx.Select("object_id").First(&review, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrReviewNotFound
}
return err
}
// Удаляем отзыв
result := tx.Delete(&models.Review{}, id)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrReviewNotFound
}
// Обновляем рейтинг объекта
return r.updateObjectRating(tx, review.ObjectID)
})
}
// GetByObject возвращает отзывы для объекта
func (r *reviewRepository) GetByObject(objectID uint, pagination *Pagination) ([]models.Review, int64, error) {
var reviews []models.Review
var total int64
query := r.db.Model(&models.Review{}).Where("object_id = ? AND is_active = ?", objectID, true)
// Считаем общее количество
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Применяем пагинацию
if pagination != nil {
offset := (pagination.Page - 1) * pagination.PageSize
query = query.Offset(offset).Limit(pagination.PageSize)
}
// Загружаем данные
err := query.
Preload("Author", func(db *gorm.DB) *gorm.DB {
return db.Select("id, first_name, last_name, avatar")
}).
Order("created_at DESC").
Find(&reviews).Error
if err != nil {
return nil, 0, err
}
return reviews, total, nil
}
// GetByAuthor возвращает отзывы пользователя
func (r *reviewRepository) GetByAuthor(authorID uint, pagination *Pagination) ([]models.Review, int64, error) {
var reviews []models.Review
var total int64
query := r.db.Model(&models.Review{}).Where("author_id = ?", authorID)
// Считаем общее количество
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Применяем пагинацию
if pagination != nil {
offset := (pagination.Page - 1) * pagination.PageSize
query = query.Offset(offset).Limit(pagination.PageSize)
}
// Загружаем данные
err := query.
Preload("Object", func(db *gorm.DB) *gorm.DB {
return db.Select("id, title, type, city")
}).
Order("created_at DESC").
Find(&reviews).Error
if err != nil {
return nil, 0, err
}
return reviews, total, nil
}
// GetByObjectAndAuthor возвращает отзыв конкретного пользователя для объекта
func (r *reviewRepository) GetByObjectAndAuthor(objectID, authorID uint) (*models.Review, error) {
var review models.Review
err := r.db.
Where("object_id = ? AND author_id = ?", objectID, authorID).
First(&review).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrReviewNotFound
}
return nil, err
}
return &review, nil
}
// GetObjectRatingStats возвращает статистику рейтинга для объекта
func (r *reviewRepository) GetObjectRatingStats(objectID uint) (float64, int, error) {
var stats struct {
AverageRating float64
ReviewCount int
}
err := r.db.Model(&models.Review{}).
Select("AVG(rating) as average_rating, COUNT(*) as review_count").
Where("object_id = ? AND is_active = ?", objectID, true).
Scan(&stats).Error
if err != nil {
return 0, 0, err
}
return stats.AverageRating, stats.ReviewCount, nil
}
// GetUserReviewStats возвращает статистику отзывов пользователя
func (r *reviewRepository) GetUserReviewStats(authorID uint) (int, float64, error) {
var stats struct {
ReviewCount int
AverageRating float64
}
err := r.db.Model(&models.Review{}).
Select("COUNT(*) as review_count, AVG(rating) as average_rating").
Where("author_id = ? AND is_active = ?", authorID, true).
Scan(&stats).Error
if err != nil {
return 0, 0, err
}
return stats.ReviewCount, stats.AverageRating, nil
}
// SetActive активирует/деактивирует отзыв
func (r *reviewRepository) SetActive(id uint, isActive bool) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// Получаем отзыв для получения object_id
var review models.Review
if err := tx.Select("object_id").First(&review, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrReviewNotFound
}
return err
}
// Обновляем статус
result := tx.Model(&models.Review{}).Where("id = ?", id).Update("is_active", isActive)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrReviewNotFound
}
// Обновляем рейтинг объекта
return r.updateObjectRating(tx, review.ObjectID)
})
}
// GetAll возвращает все отзывы с фильтрацией (для админки)
func (r *reviewRepository) GetAll(pagination *Pagination, filters *ReviewFilter) ([]models.Review, int64, error) {
var reviews []models.Review
var total int64
query := r.db.Model(&models.Review{})
// Применяем фильтры
if filters != nil {
query = r.applyFilters(query, filters)
}
// Считаем общее количество
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
// Применяем пагинацию
if pagination != nil {
offset := (pagination.Page - 1) * pagination.PageSize
query = query.Offset(offset).Limit(pagination.PageSize)
}
// Загружаем данные
err := query.
Preload("Author", func(db *gorm.DB) *gorm.DB {
return db.Select("id, first_name, last_name, email")
}).
Preload("Object", func(db *gorm.DB) *gorm.DB {
return db.Select("id, title, type")
}).
Order("created_at DESC").
Find(&reviews).Error
if err != nil {
return nil, 0, err
}
return reviews, total, nil
}
// updateObjectRating обновляет рейтинг объекта
func (r *reviewRepository) updateObjectRating(tx *gorm.DB, objectID uint) error {
stats, _, err := r.GetObjectRatingStats(objectID)
if err != nil {
return err
}
count_ := int64(0)
// Обновляем рейтинг объекта
return tx.Model(&models.Object{}).
Where("id = ?", objectID).
Updates(map[string]interface{}{
"rating": stats,
"review_count": tx.Model(&models.Review{}).
Where("object_id = ? AND is_active = ?", objectID, true).
Count(&count_),
}).Error
}
// applyFilters применяет фильтры к запросу
func (r *reviewRepository) applyFilters(query *gorm.DB, filters *ReviewFilter) *gorm.DB {
if filters.ObjectID != 0 {
query = query.Where("object_id = ?", filters.ObjectID)
}
if filters.AuthorID != 0 {
query = query.Where("author_id = ?", filters.AuthorID)
}
if filters.Rating != 0 {
query = query.Where("rating = ?", filters.Rating)
}
if filters.IsActive != nil {
query = query.Where("is_active = ?", *filters.IsActive)
}
if filters.MinRating > 0 {
query = query.Where("rating >= ?", filters.MinRating)
}
if filters.MaxRating > 0 {
query = query.Where("rating <= ?", filters.MaxRating)
}
return query
}
@@ -1,74 +0,0 @@
package repository
import (
"api_es/internal/models"
"context"
"gorm.io/gorm"
)
type UserRepository interface {
Create(ctx context.Context, user *models.User) error
GetByID(ctx context.Context, id uint) (*models.User, error)
GetByEmail(ctx context.Context, email string) (*models.User, error)
Update(ctx context.Context, user *models.User) error
Delete(ctx context.Context, id uint) error
List(ctx context.Context, limit, offset int) ([]*models.User, error)
GetUserStats(ctx context.Context, userID uint) (*models.UserStats, error)
}
type userRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) Create(ctx context.Context, user *models.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *userRepository) GetByID(ctx context.Context, id uint) (*models.User, error) {
var user models.User
err := r.db.WithContext(ctx).First(&user, id).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) GetByEmail(ctx context.Context, email string) (*models.User, error) {
var user models.User
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return &user, nil
}
func (r *userRepository) Update(ctx context.Context, user *models.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *userRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&models.User{}, id).Error
}
func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*models.User, error) {
var users []*models.User
err := r.db.WithContext(ctx).Limit(limit).Offset(offset).Find(&users).Error
if err != nil {
return nil, err
}
return users, nil
}
func (r *userRepository) GetUserStats(ctx context.Context, userID uint) (*models.UserStats, error) {
var stats models.UserStats
err := r.db.WithContext(ctx).First(&stats, userID).Error
if err != nil {
return nil, err
}
return &stats, nil
}
@@ -1,66 +0,0 @@
package router
import (
"api_es/internal/config"
"api_es/pkg/logger"
"encoding/json"
"net/http"
"api_es/internal/handler"
appMiddleware "api_es/internal/middleware"
"github.com/go-chi/chi/v5"
"gorm.io/gorm"
)
func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
zapLogger := logger.Get()
zapLogger.Debug("Start setup rounting")
r := chi.NewRouter()
// Initialize logger
baseLogger := logger.NewWrapper(logger.Get())
setupMiddlewares(r)
// Health check
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
})
h := handler.NewAllHandler(db, config)
// Health routes
r.Route("/", func(r chi.Router) {
r.Get("/health", h.HealthHandler().HealthCheck)
r.Get("/check", h.HealthHandler().Check)
})
// router.go (обновляем секцию auth routes)
r.Route("/auth", func(r chi.Router) {
r.Post("/register", h.UserHandler().Register)
r.Post("/login", h.UserHandler().Login)
r.Post("/logout", h.UserHandler().Logout)
r.Post("/refresh", h.UserHandler().RefreshToken)
})
r.Route("/users", func(r chi.Router) {
r.Use(appMiddleware.AuthMiddleware)
r.Get("/profile", h.UserHandler().GetProfile)
r.Put("/profile", h.UserHandler().UpdateProfile)
// Admin routes
r.With(appMiddleware.AdminMiddleware).Get("/", h.UserHandler().ListUsers)
r.With(appMiddleware.AdminMiddleware).Get("/{id}", h.UserHandler().GetUser)
})
zapLogger.Debug("End setup routing")
// Логируем все зарегистрированные маршруты
routeLogger := logger.NewRouteLogger(baseLogger)
routeLogger.LogRoutes(r)
return r
}
@@ -1,41 +0,0 @@
package router
import (
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
// setupMiddlewares — устанавливает общие middleware для роутера.
func setupMiddlewares(r *chi.Mux) {
// Логирование всех запросов
r.Use(middleware.Logger)
// Восстановление после паник
r.Use(middleware.Recoverer)
// Удаление завершающих слешей
r.Use(middleware.StripSlashes)
// Установка реального IP из заголовков (X-Forwarded-For, X-Real-IP)
r.Use(middleware.RealIP)
// Таймаут обработки запроса
r.Use(middleware.Timeout(30 * time.Second))
// Поддержка CORS
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"https://easysite102.ru", "http://localhost:3000"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With"},
ExposedHeaders: []string{"Link"},
AllowCredentials: true,
MaxAge: 300, // 5 минут
}))
// Можно добавить и другие кастомные middleware при необходимости
}
@@ -1,201 +0,0 @@
package service
import (
"context"
"errors"
"api_es/internal/dto"
"api_es/internal/models"
"api_es/internal/repository"
"api_es/internal/utils"
"api_es/pkg/logger"
"golang.org/x/crypto/bcrypt"
)
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidCredentials = errors.New("invalid credentials")
ErrUserAlreadyExists = errors.New("user already exists")
ErrInvalidPassword = errors.New("invalid password")
)
type UserService interface {
Register(ctx context.Context, req dto.RegisterRequest) (*dto.AuthResponse, error)
Login(ctx context.Context, req dto.LoginRequest) (*dto.AuthResponse, error)
GetUser(ctx context.Context, id uint) (*dto.UserResponse, error)
UpdateUser(ctx context.Context, id uint, req dto.UpdateUserRequest) (*dto.UserResponse, error)
DeleteUser(ctx context.Context, id uint) error
ListUsers(ctx context.Context, limit, offset int) ([]*dto.UserResponse, error)
GetUserProfile(ctx context.Context, id uint) (*dto.UserResponse, error)
}
type userService struct {
userRepo repository.UserRepository
jwtUtil *utils.JWTUtil
}
func NewUserService(userRepo repository.UserRepository, jwtUtil *utils.JWTUtil) UserService {
return &userService{
userRepo: userRepo,
jwtUtil: jwtUtil,
}
}
func (s *userService) Register(ctx context.Context, req dto.RegisterRequest) (*dto.AuthResponse, error) {
zapLogger := logger.Get()
zapLogger.Debug("Start register")
// Проверяем существование пользователя
existingUser, _ := s.userRepo.GetByEmail(ctx, req.Email)
if existingUser != nil {
return nil, ErrUserAlreadyExists
}
// Хешируем пароль
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// Создаем пользователя
user := &models.User{
Email: req.Email,
PasswordHash: string(hashedPassword),
FullName: req.FullName,
FirstName: req.FirstName,
LastName: req.LastName,
Phone: req.Phone,
City: req.City,
IsActive: true,
IsVerified: false,
Role: "user",
}
if err := s.userRepo.Create(ctx, user); err != nil {
return nil, err
}
// Генерируем токен
token, err := s.jwtUtil.GenerateToken(user.ID, user.Email, user.Role)
if err != nil {
return nil, err
}
userResponse := dto.ToUserResponse(user)
zapLogger.Debug("End register")
return &dto.AuthResponse{
Token: token,
User: userResponse,
}, nil
}
func (s *userService) Login(ctx context.Context, req dto.LoginRequest) (*dto.AuthResponse, error) {
zapLogger := logger.Get()
zapLogger.Debug("Start login")
// Находим пользователя по email
user, err := s.userRepo.GetByEmail(ctx, req.Email)
if err != nil {
return nil, ErrInvalidCredentials
}
// Проверяем пароль
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
return nil, ErrInvalidCredentials
}
// Проверяем активность пользователя
if !user.IsActive {
return nil, errors.New("account is deactivated")
}
// Генерируем токен
token, err := s.jwtUtil.GenerateToken(user.ID, user.Email, user.Role)
if err != nil {
return nil, err
}
userResponse := dto.ToUserResponse(user)
zapLogger.Debug("End login")
return &dto.AuthResponse{
Token: token,
User: userResponse,
}, nil
}
func (s *userService) GetUser(ctx context.Context, id uint) (*dto.UserResponse, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrUserNotFound
}
response := dto.ToUserResponse(user)
return &response, nil
}
func (s *userService) UpdateUser(ctx context.Context, id uint, req dto.UpdateUserRequest) (*dto.UserResponse, error) {
user, err := s.userRepo.GetByID(ctx, id)
if err != nil {
return nil, ErrUserNotFound
}
// Обновляем поля
if req.FullName != "" {
user.FullName = req.FullName
}
if req.FirstName != "" {
user.FirstName = req.FirstName
}
if req.LastName != "" {
user.LastName = req.LastName
}
if req.Phone != "" {
user.Phone = req.Phone
}
if req.City != "" {
user.City = req.City
}
if req.OrganizationForm != "" {
user.OrganizationForm = req.OrganizationForm
}
if req.OrganizationName != "" {
user.OrganizationName = req.OrganizationName
}
if req.OrganizationShort != "" {
user.OrganizationShort = req.OrganizationShort
}
if req.INN != "" {
user.INN = req.INN
}
if req.PersonalINN != "" {
user.PersonalINN = req.PersonalINN
}
if err := s.userRepo.Update(ctx, user); err != nil {
return nil, err
}
response := dto.ToUserResponse(user)
return &response, nil
}
func (s *userService) DeleteUser(ctx context.Context, id uint) error {
return s.userRepo.Delete(ctx, id)
}
func (s *userService) ListUsers(ctx context.Context, limit, offset int) ([]*dto.UserResponse, error) {
users, err := s.userRepo.List(ctx, limit, offset)
if err != nil {
return nil, err
}
responses := make([]*dto.UserResponse, len(users))
for i, user := range users {
response := dto.ToUserResponse(user)
responses[i] = &response
}
return responses, nil
}
func (s *userService) GetUserProfile(ctx context.Context, id uint) (*dto.UserResponse, error) {
return s.GetUser(ctx, id)
}
@@ -1,27 +0,0 @@
package utils
// formatPace форматирует темп в строку "MM:SS"
func FormatPace(minutes, seconds int) string {
if seconds >= 60 {
minutes += seconds / 60
seconds = seconds % 60
}
return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds)
}
// formatTwoDigits форматирует число в двузначную строку
func FormatTwoDigits(num int) string {
if num < 10 {
return "0" + string(rune(num+'0'))
}
return string(rune(num/10+'0')) + string(rune(num%10+'0'))
}
// formatTime форматирует время в строку "MM:SS"
func FormatTime(minutes, seconds int) string {
if seconds >= 60 {
minutes += seconds / 60
seconds = seconds % 60
}
return FormatTwoDigits(minutes) + ":" + FormatTwoDigits(seconds)
}
@@ -1,53 +0,0 @@
package utils
import (
"time"
"github.com/golang-jwt/jwt/v4"
)
type JWTUtil struct {
secretKey string
}
type Claims struct {
UserID uint `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
jwt.RegisteredClaims
}
func NewJWTUtil(secretKey string) *JWTUtil {
return &JWTUtil{secretKey: secretKey}
}
func (j *JWTUtil) GenerateToken(userID uint, email, role string) (string, error) {
claims := Claims{
UserID: userID,
Email: email,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(j.secretKey))
}
func (j *JWTUtil) ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(j.secretKey), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, jwt.ErrInvalidKey
}

Some files were not shown because too many files have changed in this diff Show More