refactor: simplify yclients API integration and improve UI components

remove unused abonements API configuration and endpoints
add escape key handler for drawer component
implement text truncation for service descriptions
sync swiper navigation between service tabs and content
add static gym rental service to services list
This commit is contained in:
Yuu Ottosoka
2025-09-13 20:40:20 +03:00
parent 5eb7407520
commit 2f4e0d59aa
6 changed files with 65 additions and 62 deletions

View File

@@ -24,8 +24,6 @@ export default defineConfig({
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' }),
},
},
})

View File

@@ -18,9 +18,6 @@ http {
gzip_proxied expired no-cache no-store private auth;
gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript;
# Fix Yandex.Metrika Topics API attestation error
add_header Permissions-Policy "browsing-topics=()" always;
error_page 404 /404.html;
location = /404.html {
root /usr/share/nginx/html;

View File

@@ -27,6 +27,22 @@ const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, loading = false, child
document.body.style.overflow = isOpen ? 'hidden' : 'auto'
}, [isOpen])
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape' && isOpen) {
onClose()
}
}
if (isOpen) {
document.addEventListener('keydown', handleKeyDown)
}
return () => {
document.removeEventListener('keydown', handleKeyDown)
}
}, [isOpen])
return (
<div
className={cn(

View File

@@ -9,12 +9,21 @@ type Props = {
link?: z.infer<typeof linkSchema>
}
const textEllipsis = (text: string, maxLength: number) => {
if (text.length <= maxLength) {
return text
}
return text.substring(0, maxLength) + '...'
}
export const CharachteristicCard: React.FC<Props> = ({ title, description, price, link }) => {
return (
<div className="card flex h-full w-full flex-col justify-between">
<div className="flex flex-col gap-2">
<h4>{title}</h4>
{description && <span dangerouslySetInnerHTML={{ __html: description }} />}
{description && (
<span dangerouslySetInnerHTML={{ __html: textEllipsis(description, 120)}} />
)}
</div>
<div className="flex flex-col gap-4">
{price && (

View File

@@ -13,17 +13,19 @@ export type ServicesProps = {
export const Services: React.FC<ServicesProps> = ({ services }) => {
const swiperRef = useRef<SwiperRef | null>(null)
const tagSwiper = useRef<SwiperRef | null>(null)
const [serviceId, setServiceId] = useState(0)
const selectedService = services[serviceId]
const isSelected = (index: number) => index === serviceId
const updateServiceHandler = useCallback((index: number) => {
setServiceId(index)
swiperRef.current?.swiper.slideTo(0)
tagSwiper.current?.swiper.slideTo(index)
}, [])
return (
<div className="flex flex-col gap-5">
<SimpleSwiper spaceBetween={8}>
<SimpleSwiper spaceBetween={8} ref={tagSwiper}>
{services.map((service, index) => (
<SwiperSlide style={{ width: 'auto' }} key={index}>
<button

View File

@@ -1,15 +1,25 @@
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'
import { YCLIENTS_TOKEN, YCLIENTS_API_URL } from 'astro:env/server'
type HTML = string
const currencyFormatter = Intl.NumberFormat('ru', { currency: 'rub' })
const RENT_GYM_SERVICE: z.infer<typeof servicesSchema> = {
title: 'Аренда зала',
charachteristics: [
{
title: 'Разовое посещение',
description: 'Разовое посещение студии для самостоятельной тренировки',
price: `${currencyFormatter.format(1500)} ₽`,
link: {
title: 'Записаться',
href: 'https://n1381542.yclients.com/company/1254211/activity/select?o=act',
},
},
],
}
export type YclientsBookinRawResponse = {
category: {
id: number
@@ -57,39 +67,18 @@ const fetcher = <R>(...[path, options = {}]: Parameters<typeof fetch>): Promise<
}).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`,
async getServices(): Promise<z.infer<typeof servicesSchema>[]> {
const booking = await fetcher<YclientsBookinRawResponse>(
`${YCLIENTS_API_URL}/book_services/1254211`,
{
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: `${currencyFormatter.format(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)
const services = booking.category.map((categoryItem) => ({
title: categoryItem.title,
charachteristics: booking.services
.filter((service) => service.category_id === categoryItem.id)
.map((service) => ({
title: service.title,
price: `${currencyFormatter.format(service.price_min)} ₽`,
@@ -98,17 +87,9 @@ export const yclientsClient = {
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,
},
]
return [services[0], RENT_GYM_SERVICE, ...services.slice(1)]
},
}