Skip to content
Merged

Dev #41

Show file tree
Hide file tree
Changes from all commits
Commits
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
40 changes: 20 additions & 20 deletions COOKIE-CONSENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@
Категории `COOKIE_CATEGORIES` — в `src/const/const.ts`.
Типы `CookieCategoryId` и `CookieConsent` — в `src/types/types.ts`.

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

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

Expand All @@ -44,7 +44,7 @@ SSR: layout.tsx читает куку cookie-consent из заголовков
Провайдер сразу знает состояние — баннер либо скрыт, либо показан с первого рендера
```

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

---
Expand Down Expand Up @@ -78,7 +78,7 @@ import { useCookieConsent } from '@/context/CookieConsentContext'
export default function AdBlock() {
const { consent } = useCookieConsent()

if (!consent.marketing) return null
if (!consent.targeting) return null

return <div>Реклама</div>
}
Expand All @@ -102,11 +102,11 @@ export default function MyAnalytics() {
const { registerCallback } = useCookieConsent()

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

registerCallback('marketing', () => {
registerCallback('targeting', () => {
console.log('Загружаем VK Pixel...')
})
}, [registerCallback])
Expand Down Expand Up @@ -142,17 +142,17 @@ export default function CookieSettingsButton() {

```tsx
useEffect(() => {
// Яндекс.Метрика (statistical)
registerCallback('statistical', () => {
// Яндекс.Метрика (performance)
registerCallback('performance', () => {
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', () => {
// VK Pixel или Google Ads (targeting)
registerCallback('targeting', () => {
// вставить скрипт пикселя
})

Expand All @@ -171,12 +171,12 @@ useEffect(() => {
Пользователь открыл сайт
→ layout читает куку (SSR)
→ CookieConsentProvider(initialConsent)
→ ServicesInit монтируется: registerCallback('statistical', fn), ...
├─ consent.statistical = true → fn() вызывается сразу
└─ consent.statistical = false → fn() ждёт
→ ServicesInit монтируется: registerCallback('performance', fn), ...
├─ consent.performance = true → fn() вызывается сразу
└─ consent.performance = false → fn() ждёт

Пользователь нажал «Принять все»
→ saveConsent({ necessary:true, functional:true, statistical:true, marketing:true })
→ saveConsent({ necessary:true, performance:true, functional:true, targeting:true })
→ document.cookie = 'cookie-consent=...' (max-age 1 год, SameSite=Lax)
→ setConsent(...) → useEffect в провайдере срабатывает
→ все зарегистрированные колбеки для принятых типов вызываются
Expand All @@ -191,7 +191,7 @@ 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}
Декод.: {"necessary":true,"performance":false,"functional":true,"targeting":false}
Срок: 1 год (max-age=31536000)
Флаги: path=/; SameSite=Lax
```
Expand Down
8 changes: 4 additions & 4 deletions src/components/CookieBanner/CookieBannerClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ export default function CookieBannerClient({ cookiePolicyUrl }: Props): JSX.Elem
const [showDetails, setShowDetails] = useState(false)
const [selected, setSelected] = useState<CookieConsent>({
necessary: true,
performance: consent.performance,
functional: consent.functional,
statistical: consent.statistical,
marketing: consent.marketing,
targeting: consent.targeting,
})

if (!isBannerVisible) return null

const acceptAll = () => {
saveConsent({ necessary: true, functional: true, statistical: true, marketing: true })
saveConsent({ necessary: true, performance: true, functional: true, targeting: true })
}

const acceptNecessary = () => {
saveConsent({ necessary: true, functional: false, statistical: false, marketing: false })
saveConsent({ necessary: true, performance: false, functional: false, targeting: false })
}

const saveSelected = () => {
Expand Down
4 changes: 4 additions & 0 deletions src/components/Footer/Footer.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@
}
}

@media (max-width: $tablet-mid-width) {
text-align: center;
}

&:focus-visible {
border-radius: 2px;
outline: 2px solid $accent-add-bg;
Expand Down
10 changes: 5 additions & 5 deletions src/components/Forms/FormCalculation/FormCalculationClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export default function FormCalculationClient({ prices: initialPrices }: Props):
<div id="calculation-sort" ref={selectRef} className={formStyles['form__select']}>
<button
type="button"
className={clsx(formStyles['form__select__current'], formStyles['form__input'])}
className={clsx(formStyles['form__select-current'], formStyles['form__input'])}
aria-haspopup="listbox"
aria-expanded={selectOpen}
id="calculationSortingButton"
Expand All @@ -179,7 +179,7 @@ export default function FormCalculationClient({ prices: initialPrices }: Props):
: 'Размер/категория'}

<svg
className={formStyles['form__select__icon']}
className={formStyles['form__select-icon']}
width="10"
height="10"
viewBox="0 0 10 10"
Expand All @@ -198,7 +198,7 @@ export default function FormCalculationClient({ prices: initialPrices }: Props):
</button>

<ul
className={formStyles['form__select__list']}
className={formStyles['form__select-list']}
role="listbox"
tabIndex={-1}
aria-labelledby="calculationSortingButton"
Expand All @@ -212,8 +212,8 @@ export default function FormCalculationClient({ prices: initialPrices }: Props):
ref={(el: HTMLLIElement | null) => {
optionsRef.current[index] = el
}}
className={clsx(formStyles['form__select__option'], {
[formStyles['form__select__option--selected']]: item._id === selectedId,
className={clsx(formStyles['form__select-option'], {
[formStyles['form__select-option--selected']]: item._id === selectedId,
})}
data-value={item._id}
aria-selected={item._id === selectedId}
Expand Down
18 changes: 9 additions & 9 deletions src/components/Forms/FormOrder/FormOrderClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export default function FormOrderClient({ categories, prices: initialPrices }: P

const SelectArrow = () => (
<svg
className={formStyles['form__select__icon']}
className={formStyles['form__select-icon']}
width="10"
height="10"
viewBox="0 0 10 10"
Expand Down Expand Up @@ -417,7 +417,7 @@ export default function FormOrderClient({ categories, prices: initialPrices }: P
<button
type="button"
className={clsx(
formStyles['form__select__current'],
formStyles['form__select-current'],
formStyles['form__input'],
)}
aria-haspopup="listbox"
Expand All @@ -434,7 +434,7 @@ export default function FormOrderClient({ categories, prices: initialPrices }: P
</button>

<ul
className={formStyles['form__select__list']}
className={formStyles['form__select-list']}
role="listbox"
tabIndex={-1}
aria-labelledby="orderCategoryButton"
Expand All @@ -448,8 +448,8 @@ export default function FormOrderClient({ categories, prices: initialPrices }: P
ref={(el: HTMLLIElement | null) => {
categoryOptionsRef.current[index] = el
}}
className={clsx(formStyles['form__select__option'], {
[formStyles['form__select__option--selected']]:
className={clsx(formStyles['form__select-option'], {
[formStyles['form__select-option--selected']]:
cat.title === selectedCategory,
})}
aria-selected={cat.title === selectedCategory}
Expand All @@ -475,7 +475,7 @@ export default function FormOrderClient({ categories, prices: initialPrices }: P
<button
type="button"
className={clsx(
formStyles['form__select__current'],
formStyles['form__select-current'],
formStyles['form__input'],
)}
aria-haspopup="listbox"
Expand All @@ -492,7 +492,7 @@ export default function FormOrderClient({ categories, prices: initialPrices }: P
</button>

<ul
className={formStyles['form__select__list']}
className={formStyles['form__select-list']}
role="listbox"
tabIndex={-1}
aria-labelledby="orderSizeButton"
Expand All @@ -506,8 +506,8 @@ export default function FormOrderClient({ categories, prices: initialPrices }: P
ref={(el: HTMLLIElement | null) => {
sizeOptionsRef.current[index] = el
}}
className={clsx(formStyles['form__select__option'], {
[formStyles['form__select__option--selected']]:
className={clsx(formStyles['form__select-option'], {
[formStyles['form__select-option--selected']]:
item._id === selectedSizeId,
})}
aria-selected={item._id === selectedSizeId}
Expand Down
12 changes: 1 addition & 11 deletions src/components/Header/Header.module.scss
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
@use '../../styles/variables' as *;

@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@use '../../styles/animations';

.header {
position: fixed;
Expand Down
12 changes: 6 additions & 6 deletions src/components/ServicesInit/ServicesInit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ export default function ServicesInit(): JSX.Element | null {
const { registerCallback } = useCookieConsent()

useEffect(() => {
registerCallback('statistical', () => {
// Placeholder for statistical
console.log('[CookieConsent] Statistical callbacks initialized')
registerCallback('performance', () => {
// Placeholder for performance
console.log('[CookieConsent] Performance callbacks initialized')
})

registerCallback('marketing', () => {
// Placeholder for marketing
console.log('[CookieConsent] Marketing callbacks initialized')
registerCallback('targeting', () => {
// Placeholder for targeting
console.log('[CookieConsent] Targeting callbacks initialized')
})

registerCallback('functional', () => {
Expand Down
25 changes: 11 additions & 14 deletions src/const/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,29 +94,26 @@ export const COOKIE_CATEGORIES: {
}[] = [
{
id: 'necessary',
label: 'Обязательные cookie',
description: 'Обеспечивают базовую функциональность сайта',
label: 'Строго необходимые',
description: 'Эти cookie необходимы для правильного функционирования сайта и не могут быть отключены.',
required: true,
},
{
id: 'functional',
label: 'Функциональные cookie',
description:
'Адаптируют сайт к предпочтениям пользователя: запоминают выбранный язык, размер шрифта, тему оформления',
id: 'performance',
label: 'Производительности',
description: 'Эти cookie собирают анонимную информацию о том, как вы взаимодействуете с сайтом.',
required: false,
},
{
id: 'statistical',
label: 'Статистические cookie',
description:
'Собирают данные о том, как пользователи используют сайт: какие страницы посещают, сколько времени проводят на них',
id: 'functional',
label: 'Функциональные',
description: 'Эти cookie позволяют сайту запоминать ваш выбор и предоставлять улучшенные функции.',
required: false,
},
{
id: 'marketing',
label: 'Маркетинговые cookie',
description:
'Отслеживают поведение пользователей на сайте для предоставления персонализированной рекламы',
id: 'targeting',
label: 'Целевые и аналитические',
description: 'Эти cookie используются для предоставления релевантной рекламы.',
required: false,
},
]
4 changes: 2 additions & 2 deletions src/context/CookieConsentContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ type Props = {
export function CookieConsentProvider({ initialConsent, children }: Props): JSX.Element {
const defaultConsent: CookieConsent = {
necessary: true,
performance: false,
functional: false,
statistical: false,
marketing: false,
targeting: false,
}

const [consent, setConsent] = useState<CookieConsent>(initialConsent ?? defaultConsent)
Expand Down
11 changes: 11 additions & 0 deletions src/styles/animations.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,14 @@
opacity: 1;
}
}

@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-100%);
}
to {
opacity: 1;
transform: translateY(0);
}
}
3 changes: 2 additions & 1 deletion src/styles/globals.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@use './variables.scss' as *;
@use "./variables.scss" as *;

*,
*::before,
Expand All @@ -9,6 +9,7 @@
html,
body {
display: flex;
overflow-x: hidden;
flex-direction: column;
min-width: $mobile-min-width;
height: 100%;
Expand Down
Loading