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:
@@ -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' }),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,58 +67,29 @@ 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`,
|
||||
{
|
||||
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`, {
|
||||
const booking = await 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: `${currencyFormatter.format(service.price_min)} ₽`,
|
||||
description: service.comment,
|
||||
link: {
|
||||
title: 'Записаться',
|
||||
href: `https://n1381542.yclients.com/company/1254211/personal/select-services?o=s${service.id}`,
|
||||
},
|
||||
}))
|
||||
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)} ₽`,
|
||||
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,
|
||||
},
|
||||
]
|
||||
return [services[0], RENT_GYM_SERVICE, ...services.slice(1)]
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user