Skip to content
Merged

Dev #39

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
83a56a8
Add slug functionality for collections items
YuriyPlotnikovv Apr 27, 2026
bada3bc
Fix main slider height
Apr 29, 2026
08a415f
Fix styles
Apr 29, 2026
b8a502c
Add scroll button
Apr 29, 2026
9afcc6b
Fix shadow at slider's cards
Apr 30, 2026
c838d91
Update components
Apr 30, 2026
a0e3743
Fix main slider
May 1, 2026
17359f2
Fix sliders
May 1, 2026
5c3a87b
Add gallery page with masonry layout and lightgallery
May 1, 2026
becf076
Add Yandex Maps
May 3, 2026
ece82c7
Add Yandex Maps
May 4, 2026
0c40337
Add Yandex Maps
May 4, 2026
6ec077d
Fix Gallery
May 4, 2026
1559ca4
Fix Menu
May 4, 2026
9dcc7fc
Add SEO optimization
May 4, 2026
df9589b
Fix types
May 4, 2026
a80b98c
Fix deploy
May 4, 2026
bbd08ef
Fix deploy
May 4, 2026
81e6e27
Create BFF architecture and add uploading form's data to CMS
May 7, 2026
428f67c
Fix deploy and api
May 7, 2026
95d1a14
Fix deploy and api
May 7, 2026
db62a2b
Fix deploy and api
May 7, 2026
0a90c7d
Fix deploy and api
May 7, 2026
ffd1500
Fix deploy and api
May 7, 2026
59919cb
Add revalidation Next.js cache
May 8, 2026
d673248
Add animations
May 8, 2026
1480719
Fix components to BFF
May 8, 2026
c3b7d86
Update forms, add pagination, fix code style
May 12, 2026
a16a5eb
Fix types
May 12, 2026
72008c5
Added the order form, updated the other forms, and added links to the…
May 12, 2026
fcd5280
Fix uploading assets
May 12, 2026
db2061f
Fix uploading assets
May 12, 2026
d67fd9f
Fix forms
May 13, 2026
e19c8a3
Added Lightgallery to the detailed page galleries
May 13, 2026
9da5a75
Fix images optimization
May 13, 2026
4ecafad
Fix images optimization
May 13, 2026
b04b505
Added cookie banner and scripts, fix code style
May 13, 2026
346241c
Fix components, data and code style
May 14, 2026
3a92809
Update project info
May 14, 2026
83a49ad
Fix metadata
May 15, 2026
f0a38f9
Fix components
May 15, 2026
d5014a2
Fix styles
May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 14 additions & 11 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
### Template .env for deployment
# Site Configuration
SITE_URL=https://your-domain.com
NEXT_PUBLIC_SITE_URL=https://your-domain.com
INTERNAL_URL=http://localhost:3000

IMAGE_NAME=
# Cockpit CMS
COCKPIT_API_URL=https://your-cockpit-url.com/
COCKPIT_API_KEY=your-cockpit-api-key

# Site
NEXT_PUBLIC_SITE_URL=https://icon-artel.ru
# On-demand revalidation
REVALIDATE_SECRET=your-random-secret-min-32-chars

# CAPTCHA
NEXT_PUBLIC_CAPTCHA_SITE_KEY=
CAPTCHA_SECRET=

# Cockpit API (если ваше приложение обращается к Cockpit через API)
COCKPIT_API_URL=
COCKPIT_API_KEY=
# Yandex Smart Captcha
NEXT_PUBLIC_CAPTCHA_SITE_KEY=your-yandex-captcha-site-key
CAPTCHA_SECRET=your-yandex-captcha-secret-key

# Yandex Maps
NEXT_PUBLIC_YANDEX_MAPS_API_KEY=your-yandex-maps-api-key
35 changes: 21 additions & 14 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NEXT_PUBLIC_SITE_URL=https://icon-artel.ru
NEXT_PUBLIC_CAPTCHA_SITE_KEY=${{ secrets.CAPTCHA_SITE_KEY }}
NEXT_PUBLIC_YANDEX_MAPS_API_KEY=${{ secrets.YANDEX_MAPS_API_KEY }}
COCKPIT_API_URL=${{ secrets.COCKPIT_API_URL }}
COCKPIT_API_KEY=${{ secrets.COCKPIT_API_KEY }}
cache-from: type=gha
cache-to: type=gha,mode=max

Expand All @@ -60,17 +62,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v5

- name: Copy docker-compose and nginx config to server
- name: Copy docker-compose to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT || 22 }}
source: "docker-compose.yml,deploy/nginx/icon-artel.conf"
source: "docker-compose.yml"
target: "/opt/icon-artel-deploy/"

- name: Deploy on server (write .env, pull image and restart + setup SSL)
- name: Deploy on server (write .env, pull image, restart container)
uses: appleboy/ssh-action@v1.0.0
env:
IMAGE_NAME: ${{ env.IMAGE_NAME }}
Expand All @@ -79,13 +81,14 @@ jobs:
CAPTCHA_SECRET: ${{ secrets.CAPTCHA_SECRET }}
COCKPIT_API_URL: ${{ secrets.COCKPIT_API_URL }}
COCKPIT_API_KEY: ${{ secrets.COCKPIT_API_KEY }}
SSL_EMAIL: ${{ secrets.SSL_EMAIL }}
YANDEX_MAPS_API_KEY: ${{ secrets.YANDEX_MAPS_API_KEY }}
REVALIDATE_SECRET: ${{ secrets.REVALIDATE_SECRET }}
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
port: ${{ secrets.VPS_PORT || 22 }}
envs: IMAGE_NAME,NEXT_PUBLIC_SITE_URL,NEXT_PUBLIC_CAPTCHA_SITE_KEY,CAPTCHA_SECRET,COCKPIT_API_URL,COCKPIT_API_KEY,SSL_EMAIL
envs: IMAGE_NAME,NEXT_PUBLIC_SITE_URL,NEXT_PUBLIC_CAPTCHA_SITE_KEY,CAPTCHA_SECRET,COCKPIT_API_URL,COCKPIT_API_KEY,YANDEX_MAPS_API_KEY,REVALIDATE_SECRET
script: |
set -euo pipefail
echo "🚀 Starting deployment process..."
Expand All @@ -94,15 +97,19 @@ jobs:
cd /opt/icon-artel-deploy

echo "📝 Writing .env file..."
cat > .env <<EOF
IMAGE_NAME=$IMAGE_NAME
NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL
NEXT_PUBLIC_CAPTCHA_SITE_KEY=$NEXT_PUBLIC_CAPTCHA_SITE_KEY
CAPTCHA_SECRET=$CAPTCHA_SECRET
COCKPIT_API_URL=$COCKPIT_API_URL
COCKPIT_API_KEY=$COCKPIT_API_KEY
EOF
printf '%s\n' \
"IMAGE_NAME=${IMAGE_NAME}" \
"NEXT_PUBLIC_SITE_URL=${NEXT_PUBLIC_SITE_URL}" \
"NEXT_PUBLIC_CAPTCHA_SITE_KEY=${NEXT_PUBLIC_CAPTCHA_SITE_KEY}" \
"NEXT_PUBLIC_YANDEX_MAPS_API_KEY=${YANDEX_MAPS_API_KEY}" \
"CAPTCHA_SECRET=${CAPTCHA_SECRET}" \
"COCKPIT_API_URL=${COCKPIT_API_URL}" \
"COCKPIT_API_KEY=${COCKPIT_API_KEY}" \
"REVALIDATE_SECRET=${REVALIDATE_SECRET}" \
> .env
echo "✅ .env file written"
echo "🔍 .env keys (values masked):"
cut -d= -f1 .env

echo "🔑 Logging into GitHub Container Registry..."
echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
Expand Down
Binary file modified .info/poster.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 58 additions & 13 deletions .info/project.json
Original file line number Diff line number Diff line change
@@ -1,31 +1,76 @@
{
"date": "04.2026",
"complexity": "4",
"date": "05.2026",
"complexity": "5",
"category": "commercial",
"deploy": "https:\/\/icon-artel.ru",
"title": {
"ru": "Иконописная Артель",
"en": "Icon Painting Artel"
},
"textFirst": {
"ru": "Частный проект на React + Next.js",
"en": "A private project on React + Next.js"
"ru": "Сложный проект на Next.js + TypeScript + Cockpit CMS",
"en": "A difficult project on Next.js + TypeScript + Cockpit CMS"
},
"textSecond": {
"ru": "",
"en": ""
"ru": "Иконописная Артель — сайт иконописной мастерской: галерея работ, полноэкранная галерея и контакты с картой.",
"en": "Icon Painting Artel — a site for icon painting artel: gallery of works, fullscreen gallery and contact info with map."
},
"functionality": {
"ru": ["Cockpit CMS", "Подключен Swiper"],
"en": ["Cockpit CMS", "Swiper is integrated"]
"ru": [
"Получение и отправка данных в Cockpit CMS",
"Слайдер Swiper для галерей",
"Lightgallery для полноэкранного просмотра фотографий",
"Интеграция с Яндекс.Картами",
"Yandex Smart Captcha на формах обратной связи",
"Генерация sitemap.xml через route",
"Публикация через GitHub Actions и Docker на сервер"
],
"en": [
"Data fetching and submission with Cockpit CMS",
"Swiper slider for galleries",
"Lightgallery for fullscreen photo viewing",
"Integration with Yandex.Maps",
"Yandex Smart Captcha on contact forms",
"sitemap.xml generation via route",
"Deployment via GitHub Actions and Docker on server"
]
},
"technologies": ["html", "scss", "javascript", "react", "next"],
"technologies": [
"html",
"scss",
"typescript",
"react",
"next",
"cockpit"
],
"pages": {
"ru": ["Главная"],
"en": ["Main"]
"ru": [
"Главная",
"О нас",
"Категории",
"Контакты",
"Галерея",
"В наличии",
"Доставка и заказ",
"Новости",
"Отзывы",
"Работы"
],
"en": [
"Main",
"About",
"Categories",
"Contacts",
"Gallery",
"In stock",
"Order & Delivery",
"News",
"Reviews",
"Works"
]
},
"notImplemented": {
"ru": ["Интеграция с Telegram и ВК"],
"en": ["Integration with Telegram and VK"]
"ru": ["Онлайн-оплаты"],
"en": ["Online payments"]
}
}
199 changes: 199 additions & 0 deletions COOKIE-CONSENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
# Cookie Consent — документация

## Обзор

Система управления согласием на использование cookie. Хранит выбор пользователя в одной куке `cookie-consent` (JSON), читает её на сервере через Next.js `cookies()`, не мерцает при загрузке.

| Файл | Роль |
| ---------------------------------------------------- | ---------------------------------- |
| `src/context/CookieConsentContext.tsx` | Провайдер и хук `useCookieConsent` |
| `src/components/CookieBanner/CookieBanner.tsx` | Server-обёртка баннера |
| `src/components/CookieBanner/CookieBannerClient.tsx` | UI баннера (клиент) |
| `src/components/ServicesInit/ServicesInit.tsx` | Регистрация колбеков сервисов |
| `src/app/layout.tsx` | Инициализация провайдера |

---

## Категории cookie

Категории `COOKIE_CATEGORIES` — в `src/const/const.ts`.
Типы `CookieCategoryId` и `CookieConsent` — в `src/types/types.ts`.

| id | Название | Обязательные |
| ------------- | --------------------- | :----------: |
| `necessary` | Обязательные cookie | ✅ да |
| `functional` | Функциональные cookie | ❌ нет |
| `statistical` | Статистические cookie | ❌ нет |
| `marketing` | Маркетинговые cookie | ❌ нет |

```typescript
// src/types/types.ts
export type CookieCategoryId = 'necessary' | 'functional' | 'statistical' | 'marketing'
export type CookieConsent = Record<CookieCategoryId, boolean>
```

---

## Как работает инициализация

```
SSR: layout.tsx читает куку cookie-consent из заголовков запроса
Передаёт initialConsent в <CookieConsentProvider initialConsent={...}>
Провайдер сразу знает состояние — баннер либо скрыт, либо показан с первого рендера
```

- Кука есть → `initialConsent = { necessary: true, statistical: true, ... }` → баннер не рендерится
- Куки нет → `initialConsent = null` → баннер показывается сразу

---

## API контекста

```typescript
const {
consent, // CookieConsent — текущее состояние согласия
saveConsent, // (c: CookieConsent) => void — сохранить выбор в куку
registerCallback, // (type: CookieCategoryId, fn: () => void) => void
isBannerVisible, // boolean — показывать ли баннер
openBanner, // () => void — открыть баннер повторно
} = useCookieConsent()
```

> Хук `useCookieConsent` можно использовать только внутри `CookieConsentProvider`.

---

## Сценарии использования

### 1. Проверить согласие в компоненте

Показывать блок только при наличии согласия:

```tsx
'use client'
import { useCookieConsent } from '@/context/CookieConsentContext'

export default function AdBlock() {
const { consent } = useCookieConsent()

if (!consent.marketing) return null

return <div>Реклама</div>
}
```

---

### 2. Зарегистрировать колбек на принятие куки

Основной способ подключения внешних сервисов. Логика `registerCallback`:

- согласие **уже есть** при регистрации → `fn()` вызывается немедленно
- согласия **нет** → `fn()` вызовется автоматически когда пользователь примет этот тип

```tsx
'use client'
import { useEffect } from 'react'
import { useCookieConsent } from '@/context/CookieConsentContext'

export default function MyAnalytics() {
const { registerCallback } = useCookieConsent()

useEffect(() => {
registerCallback('statistical', () => {
console.log('Загружаем метрику...')
})

registerCallback('marketing', () => {
console.log('Загружаем VK Pixel...')
})
}, [registerCallback])

return null
}
```

> `registerCallback` обёрнут в `useCallback([])` в провайдере — ссылка никогда не меняется, поэтому `[registerCallback]` в зависимостях безопасен и удовлетворяет правилу `exhaustive-deps`.

---

### 3. Открыть баннер повторно

Например, кнопка «Настройки cookie» в футере:

```tsx
'use client'
import { useCookieConsent } from '@/context/CookieConsentContext'

export default function CookieSettingsButton() {
const { openBanner } = useCookieConsent()

return <button onClick={openBanner}>Настройки cookie</button>
}
```

---

## Добавление нового сервиса

Всё делается в `src/components/ServicesInit/ServicesInit.tsx`:

```tsx
useEffect(() => {
// Яндекс.Метрика (statistical)
registerCallback('statistical', () => {
const counterId = process.env.NEXT_PUBLIC_METRIKA_ID
if (!counterId || (window as any).ym) return
const script = document.createElement('script')
// ... код метрики
document.head.appendChild(script)
})

// VK Pixel или Google Ads (marketing)
registerCallback('marketing', () => {
// вставить скрипт пикселя
})

// Чат-виджет или сохранение настроек темы (functional)
registerCallback('functional', () => {
// загрузить виджет
})
}, [registerCallback])
```

---

## Схема жизненного цикла

```
Пользователь открыл сайт
→ layout читает куку (SSR)
→ CookieConsentProvider(initialConsent)
→ ServicesInit монтируется: registerCallback('statistical', fn), ...
├─ consent.statistical = true → fn() вызывается сразу
└─ consent.statistical = false → fn() ждёт

Пользователь нажал «Принять все»
→ saveConsent({ necessary:true, functional:true, statistical:true, marketing:true })
→ document.cookie = 'cookie-consent=...' (max-age 1 год, SameSite=Lax)
→ setConsent(...) → useEffect в провайдере срабатывает
→ все зарегистрированные колбеки для принятых типов вызываются
→ баннер скрывается
```

---

## Кука

```
Имя: cookie-consent
Формат: URL-encoded JSON
Пример: %7B%22necessary%22%3Atrue%2C%22functional%22%3Afalse%2C%22statistical%22%3Atrue%2C%22marketing%22%3Afalse%7D
Декод.: {"necessary":true,"functional":false,"statistical":true,"marketing":false}
Срок: 1 год (max-age=31536000)
Флаги: path=/; SameSite=Lax
```

Поле `necessary` всегда `true` — принудительно выставляется в `saveConsent`.
Loading