diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..b512c09
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+node_modules
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index c20b5f5..5029acf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,10 +10,12 @@
"dependencies": {
"@astrojs/react": "^4.2.7",
"@astrojs/tailwind": "^6.0.2",
+ "@nanostores/react": "^1.0.0",
"@tailwindcss/vite": "^4.1.6",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"astro": "^5.7.12",
+ "nanostores": "^1.0.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"swiper": "^11.2.6",
@@ -1616,6 +1618,25 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@nanostores/react": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@nanostores/react/-/react-1.0.0.tgz",
+ "integrity": "sha512-eDduyNy+lbQJMg6XxZ/YssQqF6b4OXMFEZMYKPJCCmBevp1lg0g+4ZRi94qGHirMtsNfAWKNwsjOhC+q1gvC+A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ },
+ "peerDependencies": {
+ "nanostores": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^1.0.0",
+ "react": ">=18.0.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -7375,6 +7396,21 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
+ "node_modules/nanostores": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/nanostores/-/nanostores-1.0.1.tgz",
+ "integrity": "sha512-kNZ9xnoJYKg/AfxjrVL4SS0fKX++4awQReGqWnwTRHxeHGZ1FJFVgTqr/eMrNQdp0Tz7M7tG/TDaX8QfHDwVCw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": "^20.0.0 || >=22.0.0"
+ }
+ },
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
diff --git a/package.json b/package.json
index c07ad25..2977235 100644
--- a/package.json
+++ b/package.json
@@ -15,10 +15,12 @@
"dependencies": {
"@astrojs/react": "^4.2.7",
"@astrojs/tailwind": "^6.0.2",
+ "@nanostores/react": "^1.0.0",
"@tailwindcss/vite": "^4.1.6",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"astro": "^5.7.12",
+ "nanostores": "^1.0.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"swiper": "^11.2.6",
diff --git a/public/docs/oferta.pdf b/public/docs/oferta.pdf
new file mode 100644
index 0000000..50f772b
Binary files /dev/null and b/public/docs/oferta.pdf differ
diff --git a/src/assets/icons/Spinner.tsx b/src/assets/icons/Spinner.tsx
new file mode 100644
index 0000000..dd42733
--- /dev/null
+++ b/src/assets/icons/Spinner.tsx
@@ -0,0 +1,15 @@
+import { cn } from '@/libs/cn'
+import React from 'react'
+
+export type SpinnerProps = {
+ className?: string
+}
+
+export const Spinner: React.FC = ({ className }: SpinnerProps) => (
+
+)
diff --git a/src/components/Drawer.tsx b/src/components/Drawer.tsx
new file mode 100644
index 0000000..7d6e12f
--- /dev/null
+++ b/src/components/Drawer.tsx
@@ -0,0 +1,81 @@
+import { Spinner } from '@/assets/icons/Spinner'
+import { cn } from '@/libs/cn'
+import React, { useCallback, useEffect } from 'react'
+
+export interface DrawerProps {
+ isOpen: boolean
+ onClose: () => void
+ loading?: boolean
+ children: React.ReactNode
+}
+
+const Drawer: React.FC = ({ isOpen, onClose, loading = false, children }) => {
+ const handleBackdropClick = useCallback(
+ (e: React.MouseEvent) => {
+ if (e.target === e.currentTarget) {
+ onClose()
+ }
+ },
+ [onClose]
+ )
+
+ const handleDrawerClick = useCallback((e: React.MouseEvent) => {
+ e.stopPropagation()
+ }, [])
+
+ useEffect(() => {
+ document.body.style.overflow = isOpen ? 'hidden' : 'auto'
+ }, [isOpen])
+
+ return (
+
+ {/* Backdrop */}
+
+
+
+
+
+
+ {loading ? (
+
+
+
+ ) : (
+ children
+ )}
+
+
+
+ )
+}
+
+export default Drawer
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index 89731b6..30e6b18 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -26,6 +26,13 @@ const t = useTranslations(lang)
© {new Date().getFullYear()}
{t('footer.copyright')}
+
+ {t('oferta')}
+
{
socialMediaLinks && (
diff --git a/src/components/services/CharachteristicCard.tsx b/src/components/services/CharachteristicCard.tsx
index 66c346b..594d2b8 100644
--- a/src/components/services/CharachteristicCard.tsx
+++ b/src/components/services/CharachteristicCard.tsx
@@ -1,5 +1,6 @@
import type { linkSchema } from '@/content.config'
import type { z } from 'astro:content'
+import { YClientsWidgetButton } from '../yclients-widget/YclientsWidgetButton'
type Props = {
title: string
@@ -22,11 +23,7 @@ export const CharachteristicCard: React.FC = ({ title, description, price
dangerouslySetInnerHTML={{ __html: price }}
/>
)}
- {link && (
-
- {link.title}
-
- )}
+ {link?.title}
)
diff --git a/src/components/services/Services.tsx b/src/components/services/Services.tsx
index 0008562..5aba367 100644
--- a/src/components/services/Services.tsx
+++ b/src/components/services/Services.tsx
@@ -5,6 +5,7 @@ import { type SwiperRef, SwiperSlide } from 'swiper/react'
import { CharachteristicCard } from './CharachteristicCard'
import { cn } from '@/libs/cn'
import { SimpleSwiper } from '../swiper/SimpleSwiper'
+import React from 'react'
export type ServicesProps = {
services: z.infer[]
diff --git a/src/components/yclients-widget/YclientsWidget.tsx b/src/components/yclients-widget/YclientsWidget.tsx
new file mode 100644
index 0000000..f069fc2
--- /dev/null
+++ b/src/components/yclients-widget/YclientsWidget.tsx
@@ -0,0 +1,23 @@
+import React, { useState, useRef } from 'react'
+import Drawer from '../Drawer'
+import { isYClientsWidgetOpen, toggleYClientsWidget } from '@/stores/yclientsWidgetStore'
+import { useStore } from '@nanostores/react'
+
+const YclientsWidget: React.FC = () => {
+ const [loading, setLoading] = useState(false)
+ const iframe = useRef(null)
+ const $isYClientsWidgetOpen = useStore(isYClientsWidgetOpen)
+
+ return (
+
+
+ )
+}
+
+export default YclientsWidget
diff --git a/src/components/yclients-widget/YclientsWidgetButton.tsx b/src/components/yclients-widget/YclientsWidgetButton.tsx
new file mode 100644
index 0000000..236f7c7
--- /dev/null
+++ b/src/components/yclients-widget/YclientsWidgetButton.tsx
@@ -0,0 +1,11 @@
+import { toggleYClientsWidget } from '@/stores/yclientsWidgetStore'
+import type { ButtonHTMLAttributes } from 'react'
+
+export const YClientsWidgetButton: React.FC> = ({
+ children,
+ ...props
+}) => (
+
+)
diff --git a/src/i18n/ui.ts b/src/i18n/ui.ts
index 29f6c47..8f55316 100644
--- a/src/i18n/ui.ts
+++ b/src/i18n/ui.ts
@@ -36,6 +36,8 @@ export const ui = {
// Services Section
'services.title': 'НАШИ УСЛУГИ',
'services.subtitle': 'Широкий спектр услуг для достижения ваших целей',
+ 'services.oferts': (url: string) =>
+ `Нажимая на кнопку "Записатсья", я соглашаюсь с условиями оферты`,
// Reviews Section
'reviews.title': 'ЧТО О НАС ГОВОРЯТ',
'reviews.viewMore.title': 'Посмотреть все отзывы',
@@ -43,9 +45,10 @@ export const ui = {
'Мы рады видеть, как наши клиенты достигли своих целей и превзошли ожидания.',
// Footer
'footer.logo': 'THEREALSKILL',
- 'footer.copyright': 'Все права защищены. Политика конфиденциальности.',
+ 'footer.copyright': 'Все права защищены.',
'coaches.socialMedias.title': (name: string) => `${name} в соцсетях`,
'404.title': 'Страница не найдена',
+ oferta: 'Публичная оферта',
},
en: {
'seo.meta.description':
@@ -77,6 +80,8 @@ export const ui = {
// Services Section
'services.title': 'OUR SERVICES',
'services.subtitle': 'Wide range of services to achieve your goals',
+ 'services.oferts': (url: string) =>
+ `Нажимая на кнопку ниже, я соглашаюсь с условиями оферты`,
// Reviews Section
'reviews.title': 'WHAT THEY SAY',
'reviews.viewMore.title': 'View all reviews',
@@ -87,5 +92,6 @@ export const ui = {
'footer.copyright': 'All rights reserved. Privacy Policy.',
'coaches.socialMedias.title': (name: string) => `${name} in social medias`,
'404.title': 'Страница не найдена',
+ oferta: 'Оферта',
},
} as const
diff --git a/src/libs/use-toggle.ts b/src/libs/use-toggle.ts
new file mode 100644
index 0000000..afc5264
--- /dev/null
+++ b/src/libs/use-toggle.ts
@@ -0,0 +1,7 @@
+import { useState } from 'react'
+
+export const useToggle = () => {
+ const [isOpen, setIsOpen] = useState(false)
+ const toggle = () => setIsOpen(!isOpen)
+ return [isOpen, toggle] as const
+}
diff --git a/src/pages/index.astro b/src/pages/index.astro
index e5fde46..99a94f9 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -16,6 +16,7 @@ import { getCollection } from 'astro:content'
import { Services } from '@/components/services/Services'
import { GymGallery } from '@/components/GymGallery'
import { Coaches } from '@/components/coaches/Coaches'
+import YclientsWidget from '@/components/yclients-widget/YclientsWidget'
type Props = CollectionEntry<'pages'>['data']
@@ -69,7 +70,7 @@ const t = useTranslations(lang)
{
coaches.length > 0 && (
)
}
@@ -104,4 +105,5 @@ const t = useTranslations(lang)
}
+
diff --git a/src/stores/yclientsWidgetStore.ts b/src/stores/yclientsWidgetStore.ts
new file mode 100644
index 0000000..5e90465
--- /dev/null
+++ b/src/stores/yclientsWidgetStore.ts
@@ -0,0 +1,6 @@
+import { atom } from 'nanostores'
+
+export const isYClientsWidgetOpen = atom(false)
+export const toggleYClientsWidget = () => {
+ isYClientsWidgetOpen.set(!isYClientsWidgetOpen.get())
+}