Подключил yclients (пока без интеграции по апи) и добавил оферту
This commit is contained in:
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -10,10 +10,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^4.2.7",
|
"@astrojs/react": "^4.2.7",
|
||||||
"@astrojs/tailwind": "^6.0.2",
|
"@astrojs/tailwind": "^6.0.2",
|
||||||
|
"@nanostores/react": "^1.0.0",
|
||||||
"@tailwindcss/vite": "^4.1.6",
|
"@tailwindcss/vite": "^4.1.6",
|
||||||
"@types/react": "^19.1.3",
|
"@types/react": "^19.1.3",
|
||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.3",
|
||||||
"astro": "^5.7.12",
|
"astro": "^5.7.12",
|
||||||
|
"nanostores": "^1.0.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"swiper": "^11.2.6",
|
"swiper": "^11.2.6",
|
||||||
@@ -1616,6 +1618,25 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"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": "^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": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
|
|||||||
@@ -15,10 +15,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/react": "^4.2.7",
|
"@astrojs/react": "^4.2.7",
|
||||||
"@astrojs/tailwind": "^6.0.2",
|
"@astrojs/tailwind": "^6.0.2",
|
||||||
|
"@nanostores/react": "^1.0.0",
|
||||||
"@tailwindcss/vite": "^4.1.6",
|
"@tailwindcss/vite": "^4.1.6",
|
||||||
"@types/react": "^19.1.3",
|
"@types/react": "^19.1.3",
|
||||||
"@types/react-dom": "^19.1.3",
|
"@types/react-dom": "^19.1.3",
|
||||||
"astro": "^5.7.12",
|
"astro": "^5.7.12",
|
||||||
|
"nanostores": "^1.0.1",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"swiper": "^11.2.6",
|
"swiper": "^11.2.6",
|
||||||
|
|||||||
BIN
public/docs/oferta.pdf
Normal file
BIN
public/docs/oferta.pdf
Normal file
Binary file not shown.
15
src/assets/icons/Spinner.tsx
Normal file
15
src/assets/icons/Spinner.tsx
Normal file
@@ -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) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'h-8 w-8 animate-spin rounded-[50%] border-4 border-brand border-t-transparent',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
81
src/components/Drawer.tsx
Normal file
81
src/components/Drawer.tsx
Normal file
@@ -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<DrawerProps> = ({ 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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed inset-0 z-50 transition-opacity duration-300',
|
||||||
|
isOpen ? 'pointer-events-auto opacity-100' : 'pointer-events-none opacity-0'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 bg-black bg-opacity-50 transition-opacity duration-300"
|
||||||
|
onClick={handleBackdropClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'fixed bottom-0 left-0 right-0 w-full md:bottom-0 md:left-auto md:top-0 h-full md:w-[600px] md:rounded-t-none',
|
||||||
|
'transition-transform duration-300 ease-out md:translate-x-full md:translate-y-0',
|
||||||
|
'flex flex-col gap-4 md:flex-row',
|
||||||
|
isOpen ? 'translate-y-0 md:translate-x-0' : 'translate-y-full md:translate-x-full'
|
||||||
|
)}
|
||||||
|
onClick={handleDrawerClick}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="ml-2 self-start rounded-full bg-light p-3 focus:outline-none md:ml-0 mt-2"
|
||||||
|
aria-label="Close drawer"
|
||||||
|
>
|
||||||
|
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="h-full w-full overflow-hidden rounded-t-3xl bg-light shadow-2xl md:rounded-none">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Drawer
|
||||||
@@ -26,6 +26,13 @@ const t = useTranslations(lang)
|
|||||||
<p class="text-light text-sm">
|
<p class="text-light text-sm">
|
||||||
© {new Date().getFullYear()}
|
© {new Date().getFullYear()}
|
||||||
{t('footer.copyright')}
|
{t('footer.copyright')}
|
||||||
|
<a
|
||||||
|
href="/docs/oferta.pdf"
|
||||||
|
target="_blank"
|
||||||
|
class="text-light text-sm underline underline-offset-4 transition-colors duration-300 hover:text-white"
|
||||||
|
>
|
||||||
|
{t('oferta')}
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
{
|
{
|
||||||
socialMediaLinks && (
|
socialMediaLinks && (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { linkSchema } from '@/content.config'
|
import type { linkSchema } from '@/content.config'
|
||||||
import type { z } from 'astro:content'
|
import type { z } from 'astro:content'
|
||||||
|
import { YClientsWidgetButton } from '../yclients-widget/YclientsWidgetButton'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
@@ -22,11 +23,7 @@ export const CharachteristicCard: React.FC<Props> = ({ title, description, price
|
|||||||
dangerouslySetInnerHTML={{ __html: price }}
|
dangerouslySetInnerHTML={{ __html: price }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{link && (
|
<YClientsWidgetButton className="brand-btn-outline">{link?.title}</YClientsWidgetButton>
|
||||||
<a href={link.href} target={link.target} className="brand-btn-outline">
|
|
||||||
{link.title}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { type SwiperRef, SwiperSlide } from 'swiper/react'
|
|||||||
import { CharachteristicCard } from './CharachteristicCard'
|
import { CharachteristicCard } from './CharachteristicCard'
|
||||||
import { cn } from '@/libs/cn'
|
import { cn } from '@/libs/cn'
|
||||||
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
export type ServicesProps = {
|
export type ServicesProps = {
|
||||||
services: z.infer<typeof servicesSchema>[]
|
services: z.infer<typeof servicesSchema>[]
|
||||||
|
|||||||
23
src/components/yclients-widget/YclientsWidget.tsx
Normal file
23
src/components/yclients-widget/YclientsWidget.tsx
Normal file
@@ -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<HTMLIFrameElement>(null)
|
||||||
|
const $isYClientsWidgetOpen = useStore(isYClientsWidgetOpen)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer onClose={toggleYClientsWidget} isOpen={$isYClientsWidgetOpen} loading={loading}>
|
||||||
|
<iframe
|
||||||
|
ref={iframe}
|
||||||
|
className="h-full w-full"
|
||||||
|
src="https://w1381542.yclients.com/"
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
/>
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default YclientsWidget
|
||||||
11
src/components/yclients-widget/YclientsWidgetButton.tsx
Normal file
11
src/components/yclients-widget/YclientsWidgetButton.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { toggleYClientsWidget } from '@/stores/yclientsWidgetStore'
|
||||||
|
import type { ButtonHTMLAttributes } from 'react'
|
||||||
|
|
||||||
|
export const YClientsWidgetButton: React.FC<ButtonHTMLAttributes<unknown>> = ({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<button {...props} onClick={toggleYClientsWidget}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
@@ -36,6 +36,8 @@ export const ui = {
|
|||||||
// Services Section
|
// Services Section
|
||||||
'services.title': 'НАШИ <span class="text-brand">УСЛУГИ</span>',
|
'services.title': 'НАШИ <span class="text-brand">УСЛУГИ</span>',
|
||||||
'services.subtitle': 'Широкий спектр услуг для достижения ваших целей',
|
'services.subtitle': 'Широкий спектр услуг для достижения ваших целей',
|
||||||
|
'services.oferts': (url: string) =>
|
||||||
|
`Нажимая на кнопку "Записатсья", я соглашаюсь с <a href=${url} target="_blank" class="underline">условиями оферты</a>`,
|
||||||
// Reviews Section
|
// Reviews Section
|
||||||
'reviews.title': 'ЧТО О НАС <span class="text-brand">ГОВОРЯТ</span>',
|
'reviews.title': 'ЧТО О НАС <span class="text-brand">ГОВОРЯТ</span>',
|
||||||
'reviews.viewMore.title': 'Посмотреть все отзывы',
|
'reviews.viewMore.title': 'Посмотреть все отзывы',
|
||||||
@@ -43,9 +45,10 @@ export const ui = {
|
|||||||
'Мы рады видеть, как наши клиенты достигли своих целей и превзошли ожидания.',
|
'Мы рады видеть, как наши клиенты достигли своих целей и превзошли ожидания.',
|
||||||
// Footer
|
// Footer
|
||||||
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
||||||
'footer.copyright': 'Все права защищены. Политика конфиденциальности.',
|
'footer.copyright': 'Все права защищены.',
|
||||||
'coaches.socialMedias.title': (name: string) => `${name} в соцсетях`,
|
'coaches.socialMedias.title': (name: string) => `${name} в соцсетях`,
|
||||||
'404.title': 'Страница не найдена',
|
'404.title': 'Страница не найдена',
|
||||||
|
oferta: 'Публичная оферта',
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
'seo.meta.description':
|
'seo.meta.description':
|
||||||
@@ -77,6 +80,8 @@ export const ui = {
|
|||||||
// Services Section
|
// Services Section
|
||||||
'services.title': 'OUR <span class="text-brand">SERVICES</span>',
|
'services.title': 'OUR <span class="text-brand">SERVICES</span>',
|
||||||
'services.subtitle': 'Wide range of services to achieve your goals',
|
'services.subtitle': 'Wide range of services to achieve your goals',
|
||||||
|
'services.oferts': (url: string) =>
|
||||||
|
`Нажимая на кнопку ниже, я соглашаюсь с <a href=${url} target="_blank">условиями оферты</a>`,
|
||||||
// Reviews Section
|
// Reviews Section
|
||||||
'reviews.title': 'WHAT THEY <span class="text-brand">SAY</span>',
|
'reviews.title': 'WHAT THEY <span class="text-brand">SAY</span>',
|
||||||
'reviews.viewMore.title': 'View all reviews',
|
'reviews.viewMore.title': 'View all reviews',
|
||||||
@@ -87,5 +92,6 @@ export const ui = {
|
|||||||
'footer.copyright': 'All rights reserved. Privacy Policy.',
|
'footer.copyright': 'All rights reserved. Privacy Policy.',
|
||||||
'coaches.socialMedias.title': (name: string) => `${name} in social medias`,
|
'coaches.socialMedias.title': (name: string) => `${name} in social medias`,
|
||||||
'404.title': 'Страница не найдена',
|
'404.title': 'Страница не найдена',
|
||||||
|
oferta: 'Оферта',
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
7
src/libs/use-toggle.ts
Normal file
7
src/libs/use-toggle.ts
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import { getCollection } from 'astro:content'
|
|||||||
import { Services } from '@/components/services/Services'
|
import { Services } from '@/components/services/Services'
|
||||||
import { GymGallery } from '@/components/GymGallery'
|
import { GymGallery } from '@/components/GymGallery'
|
||||||
import { Coaches } from '@/components/coaches/Coaches'
|
import { Coaches } from '@/components/coaches/Coaches'
|
||||||
|
import YclientsWidget from '@/components/yclients-widget/YclientsWidget'
|
||||||
|
|
||||||
type Props = CollectionEntry<'pages'>['data']
|
type Props = CollectionEntry<'pages'>['data']
|
||||||
|
|
||||||
@@ -69,7 +70,7 @@ const t = useTranslations(lang)
|
|||||||
{
|
{
|
||||||
coaches.length > 0 && (
|
coaches.length > 0 && (
|
||||||
<Section id="coaches" title={t('coaches.title')} description={t('coaches.subtitle')}>
|
<Section id="coaches" title={t('coaches.title')} description={t('coaches.subtitle')}>
|
||||||
<Coaches client:load coaches={coaches} lang={lang} />
|
<Coaches client:only="react" coaches={coaches} lang={lang} />
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -104,4 +105,5 @@ const t = useTranslations(lang)
|
|||||||
}
|
}
|
||||||
|
|
||||||
<Footer lang={lang} {...footer} />
|
<Footer lang={lang} {...footer} />
|
||||||
|
<YclientsWidget client:only="react" />
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|||||||
6
src/stores/yclientsWidgetStore.ts
Normal file
6
src/stores/yclientsWidgetStore.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'nanostores'
|
||||||
|
|
||||||
|
export const isYClientsWidgetOpen = atom(false)
|
||||||
|
export const toggleYClientsWidget = () => {
|
||||||
|
isYClientsWidgetOpen.set(!isYClientsWidgetOpen.get())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user