Подключил yclients (пока без интеграции по апи) и добавил оферту

This commit is contained in:
Yuu Ottosoka
2025-06-10 00:28:11 +03:00
parent 93ffeae65a
commit 8359ab14ea
15 changed files with 202 additions and 7 deletions

View 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
View 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

View File

@@ -26,6 +26,13 @@ const t = useTranslations(lang)
<p class="text-light text-sm">
© {new Date().getFullYear()}
{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>
{
socialMediaLinks && (

View File

@@ -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<Props> = ({ title, description, price
dangerouslySetInnerHTML={{ __html: price }}
/>
)}
{link && (
<a href={link.href} target={link.target} className="brand-btn-outline">
{link.title}
</a>
)}
<YClientsWidgetButton className="brand-btn-outline">{link?.title}</YClientsWidgetButton>
</div>
</div>
)

View File

@@ -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<typeof servicesSchema>[]

View 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

View 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>
)

View File

@@ -36,6 +36,8 @@ export const ui = {
// Services Section
'services.title': 'НАШИ <span class="text-brand">УСЛУГИ</span>',
'services.subtitle': 'Широкий спектр услуг для достижения ваших целей',
'services.oferts': (url: string) =>
`Нажимая на кнопку "Записатсья", я соглашаюсь с <a href=${url} target="_blank" class="underline">условиями оферты</a>`,
// Reviews Section
'reviews.title': 'ЧТО О НАС <span class="text-brand">ГОВОРЯТ</span>',
'reviews.viewMore.title': 'Посмотреть все отзывы',
@@ -43,9 +45,10 @@ export const ui = {
'Мы рады видеть, как наши клиенты достигли своих целей и превзошли ожидания.',
// Footer
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
'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 <span class="text-brand">SERVICES</span>',
'services.subtitle': 'Wide range of services to achieve your goals',
'services.oferts': (url: string) =>
`Нажимая на кнопку ниже, я соглашаюсь с <a href=${url} target="_blank">условиями оферты</a>`,
// Reviews Section
'reviews.title': 'WHAT THEY <span class="text-brand">SAY</span>',
'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

7
src/libs/use-toggle.ts Normal file
View 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
}

View File

@@ -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 && (
<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>
)
}
@@ -104,4 +105,5 @@ const t = useTranslations(lang)
}
<Footer lang={lang} {...footer} />
<YclientsWidget client:only="react" />
</BaseLayout>

View File

@@ -0,0 +1,6 @@
import { atom } from 'nanostores'
export const isYClientsWidgetOpen = atom(false)
export const toggleYClientsWidget = () => {
isYClientsWidgetOpen.set(!isYClientsWidgetOpen.get())
}