получаю сервисы из yclients
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config'
|
||||
import { defineConfig, envField } from 'astro/config'
|
||||
import tailwind from '@astrojs/tailwind'
|
||||
|
||||
import react from '@astrojs/react'
|
||||
@@ -15,4 +15,12 @@ export default defineConfig({
|
||||
prefixDefaultLocale: false,
|
||||
},
|
||||
},
|
||||
env: {
|
||||
schema: {
|
||||
YCLIENTS_API_URL: envField.string({ context: 'server', access: 'public' }),
|
||||
YCLIENTS_TOKEN: envField.string({ context: 'server', access: 'secret' }),
|
||||
YCLIENTS_ABONEMENTS_API_URL: envField.string({ context: 'server', access: 'public' }),
|
||||
YCLIENTS_ABONEMENTS_TOKEN: envField.string({ context: 'server', access: 'secret' }),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Binary file not shown.
@@ -26,13 +26,6 @@ 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 && (
|
||||
|
||||
@@ -14,7 +14,7 @@ export const CharachteristicCard: React.FC<Props> = ({ title, description, price
|
||||
<div className="card flex h-full w-full flex-col justify-between">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h4>{title}</h4>
|
||||
{description && <span>{description}</span>}
|
||||
{description && <span dangerouslySetInnerHTML={{ __html: description }} />}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{price && (
|
||||
@@ -23,7 +23,9 @@ export const CharachteristicCard: React.FC<Props> = ({ title, description, price
|
||||
dangerouslySetInnerHTML={{ __html: price }}
|
||||
/>
|
||||
)}
|
||||
<YClientsWidgetButton className="brand-btn-outline">{link?.title}</YClientsWidgetButton>
|
||||
<YClientsWidgetButton className="brand-btn-outline" widgetLink={link?.href}>
|
||||
{link?.title}
|
||||
</YClientsWidgetButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React, { useState, useRef } from 'react'
|
||||
import Drawer from '../Drawer'
|
||||
import { isYClientsWidgetOpen, toggleYClientsWidget } from '@/stores/yclientsWidgetStore'
|
||||
import { YClientsWidgetState, 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)
|
||||
const $widgetState = useStore(YClientsWidgetState)
|
||||
|
||||
return (
|
||||
<Drawer onClose={toggleYClientsWidget} isOpen={$isYClientsWidgetOpen} loading={loading}>
|
||||
<Drawer onClose={toggleYClientsWidget} isOpen={$widgetState.isOpen} loading={loading}>
|
||||
<iframe
|
||||
ref={iframe}
|
||||
className="h-full w-full"
|
||||
src="https://w1381542.yclients.com/"
|
||||
// @ts-expect-error null тоже можно передавать в src чтобы обнулить iframe
|
||||
src={$widgetState.link}
|
||||
onLoad={() => setLoading(false)}
|
||||
/>
|
||||
</Drawer>
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { toggleYClientsWidget } from '@/stores/yclientsWidgetStore'
|
||||
import type { ButtonHTMLAttributes } from 'react'
|
||||
|
||||
export const YClientsWidgetButton: React.FC<ButtonHTMLAttributes<unknown>> = ({
|
||||
type YClientsWidgetButtonProps = ButtonHTMLAttributes<unknown> & {
|
||||
widgetLink?: string
|
||||
}
|
||||
|
||||
export const YClientsWidgetButton: React.FC<YClientsWidgetButtonProps> = ({
|
||||
children,
|
||||
widgetLink,
|
||||
...props
|
||||
}) => (
|
||||
<button {...props} onClick={toggleYClientsWidget}>
|
||||
<button {...props} onClick={() => toggleYClientsWidget({ link: widgetLink })}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -62,110 +62,6 @@ slider:
|
||||
imageAlt: 'twenty-fifth-slide'
|
||||
- image: './assets/slider/IMG_7075.jpg'
|
||||
imageAlt: 'twenty-sixth-slide'
|
||||
services:
|
||||
- title: 'Тренировка с тренером'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/message/JKEYZFZBLQBIF1'
|
||||
target: '_blank'
|
||||
charachteristics:
|
||||
- title: 'Разовая тренировка'
|
||||
description: 'Персональная тренировка 60 минут. Включает консультацию, обучение технике упражнений и рекомендации по питанию.'
|
||||
price: '5 000 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/9063789057065586/79804450446'
|
||||
target: '_blank'
|
||||
- title: 'Разовая СПЛИТ'
|
||||
description: 'Тренировка 75 минут для двух человек. Обучение технике, консультации по питанию и тренировкам.'
|
||||
price: '8 000 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/9525979067500181/79804450446'
|
||||
target: '_blank'
|
||||
- title: 'Блок 5 тренировок'
|
||||
description: 'Пять тренировок по 60 минут с планом питания и БЖУ. Срок - 40 дней.'
|
||||
price: '22 500 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/24101248972846560/79804450446'
|
||||
target: '_blank'
|
||||
- title: 'Блок 5 СПЛИТ'
|
||||
description: 'Пять парных тренировок по 75 минут. План тренировок и питания. Срок - 40 дней.'
|
||||
price: '38 500 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/79804450446?text=Здравствуйте!%20Хочу%20приобрести%20блок%205%20СПЛИТ%20тренировок'
|
||||
target: '_blank'
|
||||
- title: 'Блок 10 тренировок'
|
||||
description: 'Десять часовых тренировок. Программа тренировок и питания с регулярной корректировкой.'
|
||||
price: '42 500 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/79804450446?text=Здравствуйте!%20Хочу%20приобрести%20блок%2010%20персональных%20тренировок'
|
||||
target: '_blank'
|
||||
- title: 'Блок 10 СПЛИТ'
|
||||
description: 'Десять парных тренировок по 75 минут. Полное сопровождение с корректировкой программы.'
|
||||
price: '75 000 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/79804450446?text=Здравствуйте!%20Хочу%20приобрести%20блок%2010%20СПЛИТ%20тренировок'
|
||||
target: '_blank'
|
||||
- title: 'Блок 15 тренировок'
|
||||
description: 'Пятнадцать часовых тренировок. Расширенная программа тренировок и питания.'
|
||||
price: '60 000 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/79804450446?text=Здравствуйте!%20Хочу%20приобрести%20блок%2015%20персональных%20тренировок'
|
||||
target: '_blank'
|
||||
- title: 'Блок 15 СПЛИТ'
|
||||
description: 'Пятнадцать парных тренировок. Максимально эффективная программа с мониторингом.'
|
||||
price: '110 000 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/79804450446?text=Здравствуйте!%20Хочу%20приобрести%20блок%2015%20СПЛИТ%20тренировок'
|
||||
target: '_blank'
|
||||
- title: 'Безлимитные Абонементы'
|
||||
description: '30 календарных дней занятий с тренером'
|
||||
charachteristics:
|
||||
- title: 'Персональный безлимит'
|
||||
description: 'Неограниченное количество персональных тренировок на 30 дней.'
|
||||
price: '50 000 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/9194792393933300/79804450446'
|
||||
target: '_blank'
|
||||
- title: 'СПЛИТ безлимит'
|
||||
description: 'Безлимитные парные тренировки на месяц.'
|
||||
price: '80 000 ₽'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/9211908478906057/79804450446'
|
||||
target: '_blank'
|
||||
- title: 'Мини-группа безлимит'
|
||||
description: 'Безлимитные тренировки для 3-5 человек на месяц.'
|
||||
price: '30 000 ₽<br />c человека'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/9767701363241058/79804450446'
|
||||
target: '_blank'
|
||||
- title: 'Аренда зала / Разовые посещения'
|
||||
description: 'Присутствие тренера БЕСПЛАТНО'
|
||||
charachteristics:
|
||||
- title: 'Разовое посещение'
|
||||
description: 'Свободное посещение зала 75 минут с доступом к оборудованию.'
|
||||
price: '1500 ₽<br />c человека'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/9987000717998326/79804450446'
|
||||
target: '_blank'
|
||||
- title: 'Приходящий тренер'
|
||||
description: 'Тренировка 75 минут со своим тренером.'
|
||||
price: '1500 ₽<br />c человека'
|
||||
link:
|
||||
title: 'Записаться'
|
||||
href: 'https://wa.me/p/8974731312631431/79804450446'
|
||||
target: '_blank'
|
||||
reviews:
|
||||
- review: 'Место силы. Комфортная студия. Для тренировок есть всё необходимое . Раздевалки уютные , так же всё продумано до мелочей . Можно после тренировки насладиться кофе . По месторасположению очень комфортно , для парковки места есть всегда. Уютно. Спортивно . Красиво . Стильно . Профессионально! От души рекомендую'
|
||||
author:
|
||||
|
||||
113
src/libs/api/yclients.ts
Normal file
113
src/libs/api/yclients.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { servicesSchema } from '@/content.config'
|
||||
import type { z } from 'astro/zod'
|
||||
import {
|
||||
YCLIENTS_TOKEN,
|
||||
YCLIENTS_API_URL,
|
||||
YCLIENTS_ABONEMENTS_API_URL,
|
||||
YCLIENTS_ABONEMENTS_TOKEN,
|
||||
} from 'astro:env/server'
|
||||
|
||||
type HTML = string
|
||||
|
||||
export type YclientsBookinRawResponse = {
|
||||
category: {
|
||||
id: number
|
||||
title: string
|
||||
}[]
|
||||
services: {
|
||||
id: number
|
||||
title: string
|
||||
price_min: number
|
||||
price_max: number
|
||||
discount: number
|
||||
comment: string
|
||||
weight: number
|
||||
active: number
|
||||
sex: number
|
||||
image: string
|
||||
category_id: number
|
||||
}[]
|
||||
}
|
||||
|
||||
export type YClientsAbonementRawItem = {
|
||||
id: number
|
||||
image: string
|
||||
online_sale_title: string
|
||||
online_sale_description: HTML
|
||||
online_sale_price: number
|
||||
online_sale_image: string
|
||||
cost: number
|
||||
price: number
|
||||
}
|
||||
|
||||
export type YclientsAbonementsRawResponse = {
|
||||
data: {
|
||||
loyalty_abonement_types: YClientsAbonementRawItem[]
|
||||
}
|
||||
}
|
||||
|
||||
const fetcher = <R>(...[path, options = {}]: Parameters<typeof fetch>): Promise<R> =>
|
||||
fetch(path, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${YCLIENTS_TOKEN}`,
|
||||
...(options.headers || {}),
|
||||
},
|
||||
}).then((res) => res.json() as Promise<R>)
|
||||
|
||||
export const yclientsClient = {
|
||||
getAbonements: (): Promise<z.infer<typeof servicesSchema.shape.charachteristics>> =>
|
||||
fetcher<YclientsAbonementsRawResponse>(
|
||||
`${YCLIENTS_ABONEMENTS_API_URL}/chain/1259425/online_sale/loyalty`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${YCLIENTS_ABONEMENTS_TOKEN}`,
|
||||
},
|
||||
}
|
||||
).then((res) =>
|
||||
res.data.loyalty_abonement_types.map((rawItem) => ({
|
||||
title: rawItem.online_sale_title,
|
||||
description: rawItem.online_sale_description,
|
||||
price: `${rawItem.online_sale_price} ₽`,
|
||||
link: {
|
||||
title: 'Купить абонемент',
|
||||
href: `https://o9998.yclients.com/subscriptions/${rawItem.id}`,
|
||||
},
|
||||
}))
|
||||
),
|
||||
|
||||
async getServices(): Promise<z.infer<typeof servicesSchema>[]> {
|
||||
const [booking, abonements] = await Promise.all([
|
||||
fetcher<YclientsBookinRawResponse>(`${YCLIENTS_API_URL}/book_services/1254211`, {
|
||||
method: 'GET',
|
||||
}),
|
||||
this.getAbonements(),
|
||||
])
|
||||
|
||||
const personalServicesCatergory = booking.category.find((category) => category.id === 18760529)
|
||||
const abonementsCategory = booking.category.find((category) => category.id === 19562190)
|
||||
const personalServiceCharachteristics = booking.services
|
||||
.filter((service) => service.category_id === 18760529)
|
||||
.map((service) => ({
|
||||
title: service.title,
|
||||
price: `${service.price_min} ₽`,
|
||||
description: service.comment,
|
||||
link: {
|
||||
title: 'Записаться',
|
||||
href: `https://n1381542.yclients.com/company/1254211/personal/select-services?o=s${service.id}`,
|
||||
},
|
||||
}))
|
||||
|
||||
return [
|
||||
{
|
||||
title: personalServicesCatergory?.title || '',
|
||||
charachteristics: personalServiceCharachteristics,
|
||||
},
|
||||
{
|
||||
title: abonementsCategory?.title || '',
|
||||
charachteristics: abonements,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { Services } from '@/components/services/Services'
|
||||
import { GymGallery } from '@/components/GymGallery'
|
||||
import { Coaches } from '@/components/coaches/Coaches'
|
||||
import YclientsWidget from '@/components/yclients-widget/YclientsWidget'
|
||||
import { yclientsClient } from '@/libs/api/yclients'
|
||||
|
||||
type Props = CollectionEntry<'pages'>['data']
|
||||
|
||||
@@ -27,11 +28,13 @@ const [page, coaches] = await Promise.all([
|
||||
coaches.sort((a, b) => (a.data.order ?? -1) - (b.data.order ?? -1))
|
||||
),
|
||||
])
|
||||
const { slider, services, hero, marquee, reviews, footer } = page?.data || {}
|
||||
const { slider, hero, marquee, reviews, footer } = page?.data || {}
|
||||
const optimizedSlider = slider
|
||||
? await optimizeSliderImages(slider.map(({ image, imageAlt }) => ({ src: image, alt: imageAlt })))
|
||||
: undefined
|
||||
|
||||
const services = await yclientsClient.getServices()
|
||||
|
||||
const optimizedReviews = reviews
|
||||
? await Promise.all(
|
||||
reviews.map(async (review): Promise<OptimizedReview> => {
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { atom } from 'nanostores'
|
||||
|
||||
export const isYClientsWidgetOpen = atom(false)
|
||||
export const toggleYClientsWidget = () => {
|
||||
isYClientsWidgetOpen.set(!isYClientsWidgetOpen.get())
|
||||
type ToggleWidgetProps = {
|
||||
link?: string | null
|
||||
}
|
||||
|
||||
export const YClientsWidgetState = atom<{ isOpen: boolean; link: string | null }>({
|
||||
isOpen: false,
|
||||
link: null,
|
||||
})
|
||||
|
||||
export const toggleYClientsWidget = ({ link = null }: ToggleWidgetProps = {}) => {
|
||||
const { isOpen } = YClientsWidgetState.get()
|
||||
YClientsWidgetState.set({
|
||||
isOpen: !isOpen,
|
||||
link,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user