reworked slider + added coaches page

This commit is contained in:
Salam-vsem
2025-05-15 00:59:22 +03:00
parent 1b8cb5dae9
commit 2c1ebe5dae
32 changed files with 389 additions and 201 deletions

View File

@@ -7,6 +7,7 @@ import react from '@astrojs/react'
// https://astro.build/config
export default defineConfig({
integrations: [tailwind(), react()],
prefetch: true,
i18n: {
locales: ['ru', 'en'],
defaultLocale: 'ru',

View File

@@ -1,21 +1,22 @@
---
import type { coachesSchema } from '@/content.config'
import { useTranslations } from '@/i18n/utils'
import { Image } from 'astro:assets'
import type { z } from 'astro:content'
import type { CollectionEntry } from 'astro:content'
type Props = z.infer<ReturnType<typeof coachesSchema>> & { lang?: string }
type Props = CollectionEntry<'coaches'> & { lang?: string }
const { lang, name, description, image, imageAlt, link } = Astro.props
const { slug, lang, data } = Astro.props
const { name, description, cover, coverAlt } = data
const t = useTranslations(lang)
const coachPageLink = `/coaches/${slug}`
---
<div class="flex h-full w-full flex-col items-center justify-start gap-5">
<a class="relative h-[390px] w-full max-w-[300px]" href={link} target="_blank">
<a class="relative h-[390px] w-full max-w-[300px]" href={coachPageLink}>
<Image
src={image}
src={cover}
class="absolute h-full w-full object-cover"
alt={imageAlt}
alt={coverAlt}
width={300}
height={390}
format="webp"
@@ -26,12 +27,8 @@ const t = useTranslations(lang)
<div class="inline-flex w-full max-w-[300px] flex-col gap-1">
<h3 class="text-xl font-bold uppercase text-text-light">{name}</h3>
{description && <p>{description}</p>}
{
link && (
<a href={link} target="_blank" class="mt-2 text-brand hover:text-brand/80">
<a href={coachPageLink} class="mt-2 text-brand hover:text-brand/80" data-astro-prefetch>
{t('coaches.link')}
</a>
)
}
</div>
</div>

View File

@@ -4,20 +4,12 @@ import { Image } from 'astro:assets'
import logoSrc from '@/assets/logo.png'
import type { z } from 'astro:content'
import type { footerSchema } from '@/content.config'
import type React from 'react'
import { TelegramIcon } from '@/assets/icons/TelegramIcon'
import { InstagramIcon } from '@/assets/icons/InstagramIcon'
import type { IconProps } from '@/assets/icons/types'
import { SocialMediaIcon } from './SocialMediaIcon'
type Props = z.infer<typeof footerSchema> & {
lang?: string
}
const SOCIAL_MEDIA_ICONS_MAP: Record<string, React.ComponentType<IconProps>> = {
telegram: TelegramIcon,
instagram: InstagramIcon,
}
const { lang, socialMediaLinks } = Astro.props
const t = useTranslations(lang)
---
@@ -40,11 +32,12 @@ const t = useTranslations(lang)
socialMediaLinks && (
<div class="flex gap-4 text-text-light">
{socialMediaLinks.map(({ href, target, icon }) => {
const Icon = SOCIAL_MEDIA_ICONS_MAP[icon]
return (
<a href={href} target={target} rel="noopener noreferrer">
<Icon className="fill-text hover:fill-text-light transition-all duration-300" />
<SocialMediaIcon
type={icon}
className="fill-text hover:fill-text-light transition-all duration-300"
/>
</a>
)
})}

View File

@@ -4,7 +4,7 @@ import type { z } from 'astro:content'
type Props = z.infer<typeof servicesSchema> & { highlighted?: boolean }
const { title, charachteristics, highlighted = false, link } = Astro.props
const { title, description, charachteristics, highlighted = false, link } = Astro.props
---
<div
@@ -14,8 +14,9 @@ const { title, charachteristics, highlighted = false, link } = Astro.props
{ 'border-brand': highlighted },
]}
>
<div class="mb-8">
<h3 class="mb-4 text-2xl font-bold">{title}</h3>
<div class="mb-8 flex flex-col gap-2">
<h3 class="text-2xl font-bold">{title}</h3>
{description && <span>{description}</span>}
</div>
<ul class="mb-16 flex-grow space-y-4">

View File

@@ -0,0 +1,22 @@
import { InstagramIcon } from '@/assets/icons/InstagramIcon'
import { TelegramIcon } from '@/assets/icons/TelegramIcon'
import type { IconProps } from '@/assets/icons/types'
export const socialMedias = ['telegram', 'instagram']
export type SocialMedia = (typeof socialMedias)[number]
type SocialMediaIconProps = {
type: SocialMedia
className?: string
}
const SOCIAL_MEDIA_ICONS_MAP: Record<string, React.ComponentType<IconProps>> = {
telegram: TelegramIcon,
instagram: InstagramIcon,
}
export const SocialMediaIcon: React.FC<SocialMediaIconProps> = ({ type, className }) => {
const Icon = SOCIAL_MEDIA_ICONS_MAP[type]
return Icon ? <Icon className={className} /> : null
}

View File

@@ -1,41 +1,42 @@
---
import type { TranslationKey } from '@/i18n/ui'
import MobileNavigation from './MobileNavigation'
import { useTranslations } from '@/i18n/utils'
type Props = {
export type HeaderProps = {
navItems?: Array<{ key: TranslationKey; href: string }>
lang?: string
}
const { lang } = Astro.props
type Props = HeaderProps
const navItems = [
{ key: 'nav.coaches', href: '#coaches' },
{ key: 'nav.slider', href: '#image-slider' },
{ key: 'nav.services', href: '#services' },
{ key: 'nav.reviews', href: '#reviews' },
{ key: 'nav.contact', href: '#contacts' },
] as const
const { lang, navItems } = Astro.props
const t = useTranslations(lang)
---
<header
transition:persist
class="fixed left-0 right-0 top-0 z-50 flex h-[var(--header-height)] items-center justify-between bg-bg-main px-6 transition-transform duration-300 md:px-10"
id="main-header"
>
<a class="text-2xl font-bold text-text-light" set:html={t('header.logo')} href="#hero" />
<a class="text-2xl font-bold text-text-light" set:html={t('header.logo')} href="/" />
{
navItems && (
<>
<nav class="hidden md:block">
<ul class="grid grid-flow-col gap-6">
{
navItems.map((item) => (
{navItems.map((item) => (
<li>
<a href={item.href}>{t(item.key)}</a>
</li>
))
}
))}
</ul>
</nav>
<MobileNavigation navItems={navItems} client:visible />
</>
)
}
</header>
<script>

View File

@@ -1,10 +1,8 @@
import type { ImageMetadata } from 'astro'
import React, { type ButtonHTMLAttributes } from 'react'
import { useSwiper } from 'swiper/react'
import ChevronLeft from '@/assets/icons/LeftArrowIcon.svg'
import ChevronRight from '@/assets/icons/RightArrowIcon.svg'
import { cn } from '@/libs/cn'
import { useSlideChange } from './hooks'
type Props = ButtonHTMLAttributes<HTMLButtonElement> & { iconSrc: ImageMetadata }
@@ -13,7 +11,7 @@ export const NavButton: React.FC<Props> = ({ iconSrc, ...buttonProps }) => {
<button
{...buttonProps}
className={cn(
'hidden rounded-full border border-solid border-border-light md:block',
'rounded-full border border-solid border-border-light',
buttonProps.disabled && 'border-none',
buttonProps.className
)}
@@ -24,35 +22,9 @@ export const NavButton: React.FC<Props> = ({ iconSrc, ...buttonProps }) => {
}
export const PrevButton: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
const swiper = useSwiper()
const [canSlidePrev, setCanSlidePrev] = React.useState(false)
useSlideChange(() => setCanSlidePrev(!swiper.isBeginning))
return (
<NavButton
{...props}
className={cn('absolute left-0 top-1/2 z-10 -translate-y-1/2', props.className)}
iconSrc={ChevronLeft}
onClick={() => swiper.slidePrev()}
disabled={!canSlidePrev}
/>
)
return <NavButton {...props} iconSrc={ChevronLeft} />
}
export const NextButton: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
const swiper = useSwiper()
const [canSlideNext, setCanSlideNext] = React.useState(true)
useSlideChange(() => setCanSlideNext(!swiper.isEnd))
return (
<NavButton
{...props}
className={cn('absolute right-0 top-1/2 z-10 -translate-y-1/2', props.className)}
iconSrc={ChevronRight}
onClick={() => swiper.slideNext()}
disabled={!canSlideNext}
/>
)
return <NavButton {...props} iconSrc={ChevronRight} />
}

View File

@@ -0,0 +1,43 @@
import React from 'react'
import { Swiper, type SwiperProps, type SwiperRef, SwiperSlide } from 'swiper/react'
import { A11y, Navigation, Pagination } from 'swiper/modules'
import { cn } from '@/libs/cn'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
export type SimpleSwiperProps = SwiperProps & {
images: ImportAttributes[]
}
export const SimpleSwiper = React.forwardRef<SwiperRef, SimpleSwiperProps>(
({ images, className, modules = [], slideClass, ...otherProps }, ref) => {
return (
<Swiper
ref={ref}
modules={[Navigation, Pagination, A11y, ...modules]}
slidesPerView={1}
pagination={{
clickable: true,
}}
loop={true}
watchSlidesProgress={true}
className={cn('h-full w-full', className)}
{...otherProps}
>
{images.map((image, index) => (
<SwiperSlide key={index}>
<img
{...image}
loading={index === 0 ? 'eager' : 'lazy'}
decoding="async"
fetchPriority={index === 0 ? 'high' : 'auto'}
className={cn('absolute h-full w-full object-contain', slideClass)}
/>
</SwiperSlide>
))}
</Swiper>
)
}
)
SimpleSwiper.displayName = 'SimpleSwiper'

View File

@@ -1,42 +0,0 @@
import React from 'react'
import { Swiper as SwiperBase, SwiperSlide } from 'swiper/react'
import { A11y, Navigation, Pagination } from 'swiper/modules' // Added Lazy module
import { cn } from '@/libs/cn'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
import './styles.css'
import { NextButton, PrevButton } from './NavButtons'
type SwiperProps = {
images: ImportAttributes[]
className?: string
}
export const Swiper: React.FC<SwiperProps> = ({ images, className }) => {
return (
<SwiperBase
modules={[Navigation, Pagination, A11y]}
slidesPerView={1}
pagination={{
clickable: true,
}}
watchSlidesProgress={true}
className={cn('h-full w-full', className)}
>
{images.map((image, index) => (
<SwiperSlide key={index}>
<img
{...image}
loading={index === 0 ? 'eager' : 'lazy'}
decoding="async"
fetchPriority={index === 0 ? 'high' : 'auto'}
className="absolute h-full w-full object-cover md:px-[calc(50px+2rem)]"
/>
</SwiperSlide>
))}
<PrevButton />
<NextButton />
</SwiperBase>
)
}

View File

@@ -0,0 +1,21 @@
import React, { useRef } from 'react'
import { type SwiperRef } from 'swiper/react'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
import { NextButton, PrevButton } from './NavButtons'
import { useNavHandlers } from './hooks'
import { SimpleSwiper, type SimpleSwiperProps } from './SimpleSwiper'
export const SwiperWithNav: React.FC<SimpleSwiperProps> = (props) => {
const swiperRef = useRef<SwiperRef | null>(null)
const { handleNext, handlePrev } = useNavHandlers(swiperRef)
return (
<div className="flex h-full w-full items-center gap-10">
<PrevButton className="hidden md:block" onClick={handlePrev} />
<SimpleSwiper {...props} ref={swiperRef} />
<NextButton className="hidden md:block" onClick={handleNext} />
</div>
)
}

View File

@@ -0,0 +1,64 @@
import React, { useRef, useState } from 'react'
import { Swiper, SwiperSlide, type SwiperClass, type SwiperRef } from 'swiper/react'
import { FreeMode, Navigation, Thumbs } from 'swiper/modules'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
import 'swiper/css/thumbs'
import 'swiper/css/free-mode'
import { cn } from '@/libs/cn'
import { useNavHandlers } from './hooks'
import { PrevButton, NextButton } from './NavButtons'
import { SimpleSwiper } from './SimpleSwiper'
type SwiperProps = {
images: ImportAttributes[]
thumbs: ImportAttributes[]
swiperClassName?: string
}
export const SwiperWithThumbs: React.FC<SwiperProps> = ({ images, thumbs, swiperClassName }) => {
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperClass | null>(null)
const swiperRef = useRef<SwiperRef | null>(null)
const { handleNext, handlePrev } = useNavHandlers(swiperRef)
return (
<div className="grid w-full grid-cols-1 items-center gap-x-10 gap-y-3 md:grid-cols-[auto_1fr_auto]">
<PrevButton className="hidden md:block" onClick={handlePrev} />
<SimpleSwiper
modules={[Thumbs]}
thumbs={{ swiper: thumbsSwiper }}
watchSlidesProgress={true}
className={cn(swiperClassName)}
images={images}
ref={swiperRef}
pagination={false}
/>
<NextButton className="hidden md:block" onClick={handleNext} />
<Swiper
onSwiper={setThumbsSwiper}
spaceBetween={12}
slidesPerView={4}
freeMode={true}
watchSlidesProgress={true}
modules={[Navigation, Thumbs, FreeMode]}
// !!! TODO FIXME тут хардкод высоты так как если ставить в процентах, то появляется много пустого места снизу
className="relative h-24 w-full justify-center place-self-start md:col-start-2"
>
{thumbs.map((image, index) => (
<SwiperSlide
key={index}
className="w-1/4 opacity-40 [&.swiper-slide-thumb-active]:opacity-100"
>
<img
{...image}
loading="lazy"
decoding="async"
className="h-full w-full object-cover"
/>
</SwiperSlide>
))}
</Swiper>
</div>
)
}

View File

@@ -1,14 +1,16 @@
import { useEffect } from 'react'
import { useSwiper } from 'swiper/react'
import { type RefObject, useCallback } from 'react'
import type { SwiperRef } from 'swiper/react'
export const useSlideChange = (onUpdate: VoidFunction) => {
const swiper = useSwiper()
export const useNavHandlers = (swiperRef: RefObject<SwiperRef | null>) => {
const handlePrev = useCallback(() => {
if (!swiperRef.current) return
swiperRef.current.swiper.slidePrev()
}, [])
useEffect(() => {
if (!swiper) return
const handleNext = useCallback(() => {
if (!swiperRef.current) return
swiperRef.current.swiper.slideNext()
}, [])
swiper.on('slideChange', onUpdate)
return () => swiper.off('slideChange', onUpdate)
}, [swiper])
return { handlePrev, handleNext } as const
}

View File

@@ -1 +1 @@
export * from './Swiper'
export * from './SwiperWithNav'

View File

@@ -1,3 +0,0 @@
.swiper-pagination-bullet-active {
@apply bg-white;
}

View File

@@ -1,4 +1,10 @@
import { defineCollection, z, type ImageFunction } from 'astro:content'
import { defineCollection, reference, z, type ImageFunction } from 'astro:content'
export const socialMediaLinkSchema = z.object({
icon: z.string(),
href: z.string(),
target: z.string().optional(),
})
export const heroSchema = (image: ImageFunction) =>
z.object({
@@ -15,13 +21,16 @@ export const heroSchema = (image: ImageFunction) =>
.optional(),
})
export const coachesSchema = (image: ImageFunction) =>
export const coaches = defineCollection({
schema: ({ image }) =>
z.object({
name: z.string(),
description: z.string().optional(),
image: image(),
imageAlt: z.string(),
link: z.string().optional(),
cover: image(),
coverAlt: z.string(),
gallery: z.array(image()).optional(),
socialMediaLinks: z.array(socialMediaLinkSchema).optional(),
}),
})
export const servicesSchema = z.object({
@@ -57,15 +66,7 @@ export const reviewsSchema = (image: ImageFunction) =>
export const footerSchema = z.object({
copyright: z.string().optional(),
socialMediaLinks: z
.array(
z.object({
icon: z.string(),
href: z.string(),
target: z.string().optional(),
})
)
.optional(),
socialMediaLinks: z.array(socialMediaLinkSchema).optional(),
})
const pages = defineCollection({
@@ -73,7 +74,6 @@ const pages = defineCollection({
z.object({
hero: heroSchema(image).optional(),
marquee: z.array(z.string()).optional(),
coaches: z.array(coachesSchema(image)).optional(),
reviews: z.array(reviewsSchema(image)).optional(),
slider: z
.array(
@@ -90,4 +90,5 @@ const pages = defineCollection({
export const collections = {
pages,
coaches,
}

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,39 @@
---
cover: './assets/batyr-cover.jpeg'
coverAlt: 'batyr-suleimanov'
name: 'Батыр Сулейманов'
description: 'Профессиональный выступающий спортсмен и тренер. Опыт работы в фитнес индустрии более 12 лет'
gallery:
- './assets/batyr-slide-1.jpeg'
- './assets/batyr-slide-2.jpeg'
- './assets/batyr-slide-3.jpeg'
- './assets/batyr-slide-4.jpg'
- './assets/batyr-slide-5.jpg'
- './assets/batyr-slide-6.jpg'
---
🏆ЧЕМПИОН DUDUSHKIN FF 2022
🏆ЧЕМПИОН NPC REGIONAL CUP MOSCOW 2024
🏆ЧЕМПИОН OLYMPIA AMATEUR 2024 SOUTH KOREA
🏆ВИЦЕ-ЧЕМПИОН SPS 2022
🏆ВИЦЕ-ЧЕМПИОН OKSANA GRISHINA CLASSIC 2023
🏆ВИЦЕ-ЧЕМПИОН
MUSCLE MULISHA GRANDPRIX 2023
🏆TOP-5 DUBAI PRO 2024
Профессиональный выступающий спортсмен и тренер.
Опыт работы в фитнес индустрии более 12 лет.
Образование:
1. Школа бодибилдинга и фитнеса им. Б. Вейдера 2014г.
2. Ассоциация профессионалов фитнеса (FPA) 2015г.
Специализация:
Строительство тела (набор мышечной массы, сжигание подкожно-жировой клетчатки без вреда для организма), составление плана и методики тренировочного процесса, разработка и контроль рациона питания (БЖУ индивидуально под каждого подопечного).
Спортивное питание. Контроль анализов. Рекомендации по приему БАДов и витаминов.
Подготовка к соревнованиям по бодибилдингу. Опыт в подготовке бойцов ММА (весогонка, «витаминная» поддержка).

View File

@@ -8,11 +8,7 @@ hero:
title: 'Записаться'
href: '#services'
marquee:
- ДОСТИГАЙТЕ ЦЕЛЕЙ
- БУДЬТЕ СИЛЬНЕЕ
- ТРЕНИРУЙТЕСЬ С НАМИ
- МЕНЯЙТЕСЬ К ЛУЧШЕМУ
- СТАНОВИТЕСЬ ЗДОРОВЕЕ
- Ваш идеальный результат — в шаге от дома!
slider:
- image: './assets/slider/IMG_6491.jpg'
imageAlt: 'first-slide'
@@ -48,17 +44,6 @@ slider:
imageAlt: 'sixteenth-slide'
- image: './assets/slider/IMG_6520.jpg'
imageAlt: 'seventeenth-slide'
coaches:
- image: './assets/coaches/1.jpeg'
imageAlt: 'first-coach'
name: 'БАТЫР СУЛЕЙМАНОВ'
description: 'Профессиональный выступающий спортсмен и тренер. Опыт работы в фитнес индустрии более 12 лет'
link: 'https://t.me/therealskill_studio/4'
- image: './assets/coaches/2.jpeg'
imageAlt: 'second-coach'
name: 'СУЛЕЙМАНОВА АЛЕКСАНДРА'
description: 'Спортсмен и тренер. Опыт работы в фитнес индустрии 9 лет'
link: 'https://t.me/therealskill_studio/7'
services:
- title: 'Разовые Персональные тренировки с тренером'
link:
@@ -82,7 +67,7 @@ services:
price: '60 000 ₽'
- text: 'Блок из 15 тренировок СПЛИТ'
price: '110 000 ₽'
- title: 'Безлимитные Абонементы на месяц'
- title: 'Безлимитные Абонементы'
description: '30 календарных дней занятий с тренером'
link:
title: 'Записаться'

View File

@@ -3,6 +3,8 @@ export const languages = {
ru: 'Русский',
}
export type TranslationKey = keyof (typeof ui)[keyof typeof ui]
export const ui = {
ru: {
'seo.meta.description':

View File

@@ -1,4 +1,4 @@
import { ui } from './ui'
import { ui, type TranslationKey } from './ui'
import { i18n } from 'astro:config/client'
@@ -11,7 +11,7 @@ export function getLangFromUrl(url: URL) {
}
export function useTranslations(lang?: string) {
return function t(key: keyof (typeof ui)[keyof typeof ui]) {
return function t(key: TranslationKey) {
return ui[lang as keyof typeof ui]?.[key] || ui[i18n!.defaultLocale as keyof typeof ui][key]
}
}

View File

@@ -1,12 +1,15 @@
---
import { useTranslations } from '@/i18n/utils'
import { cn } from '@/libs/cn'
import '@/styles/global.css'
import { ClientRouter } from 'astro:transitions'
type Props = {
title: string
bodyClass?: string
}
const { title } = Astro.props
const { title, bodyClass } = Astro.props
const t = useTranslations(Astro.currentLocale)
---
@@ -26,8 +29,9 @@ const t = useTranslations(Astro.currentLocale)
<meta name="robots" content="index, follow" />
<link rel="canonical" href={Astro.url} />
<title>{`${title} | ${t('seo.og.title')}`}</title>
<ClientRouter />
</head>
<body class="flex flex-col">
<body class={cn('flex flex-col', bodyClass)}>
<slot />
</body>
</html>

View File

@@ -1,7 +1,39 @@
import type { GetImageResult } from 'astro'
import { TW_SCREENS, TW_WIDTHS } from '@/constants/images'
import type { GetImageResult, UnresolvedImageTransform } from 'astro'
import { getImage } from 'astro:assets'
export const imageResultToImageAttributes = (imageResult: GetImageResult): ImportAttributes => ({
src: imageResult.src,
srcSet: imageResult.srcSet?.attribute,
...imageResult.attributes,
})
export const optimizeSliderImages = (images: Pick<UnresolvedImageTransform, 'src' | 'alt'>[]) =>
Promise.all(
images.map(async ({ src, alt }) =>
getImage({
src,
alt,
widths: [TW_SCREENS.SM, TW_SCREENS.LG],
sizes: `(max-width: ${TW_WIDTHS.LG}px) ${TW_SCREENS.SM}px, ${TW_SCREENS.LG}px`,
quality: 80,
format: 'webp',
loading: 'lazy',
}).then(imageResultToImageAttributes)
)
)
export const optimizeSliderThumbs = (thumbs: Pick<UnresolvedImageTransform, 'src' | 'alt'>[]) =>
Promise.all(
thumbs.map(async ({ src, alt }) =>
getImage({
src,
alt,
widths: [TW_SCREENS.XXS, TW_SCREENS.XS],
sizes: `(max-width: ${TW_WIDTHS.LG}px) ${TW_SCREENS.XXS}px, ${TW_SCREENS.XS}px`,
quality: 60,
format: 'webp',
loading: 'lazy',
}).then(imageResultToImageAttributes)
)
)

View File

@@ -0,0 +1,48 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro'
import { getCollection } from 'astro:content'
import { optimizeSliderImages, optimizeSliderThumbs } from '@/libs/image'
import Section from '@/components/Section.astro'
import { SwiperWithThumbs } from '@/components/swiper/SwiperWithThumbs'
import Header from '@/components/header/Header.astro'
export const getStaticPaths = async () => {
const coaches = await getCollection('coaches')
return coaches.map(({ slug, ...props }) => ({ params: { slug }, props }))
}
const { data, render } = Astro.props
const { gallery, cover } = data
const { Content } = await render()
const galleryWithCover = gallery ? [cover, ...gallery] : undefined
const [thumbs, optimizedGallery] = galleryWithCover
? await Promise.all([
optimizeSliderThumbs(
galleryWithCover.map((src, index) => ({ src, alt: `gallery-thumb-${index}` }))
),
optimizeSliderImages(
galleryWithCover.map((src, index) => ({ src, alt: `gallery-slide-${index}` }))
),
])
: [undefined, undefined]
---
<BaseLayout title={data.name}>
<Header />
{
optimizedGallery && (
<Section>
<SwiperWithThumbs
swiperClassName="h-[540px]"
client:load
images={optimizedGallery}
thumbs={thumbs}
/>
</Section>
)
}
<Section>
<Content />
</Section>
</BaseLayout>

View File

@@ -2,8 +2,8 @@
import { useTranslations } from '@/i18n/utils'
import BaseLayout from '../layouts/BaseLayout.astro'
import Section from '@/components/Section.astro'
import { Swiper } from '@/components/swiper'
import Header from '@/components/header/Header.astro'
import { SwiperWithNav } from '@/components/swiper'
import Header, { type HeaderProps } from '@/components/header/Header.astro'
import Footer from '@/components/Footer.astro'
import type { CollectionEntry } from 'astro:content'
@@ -14,28 +14,24 @@ import Marquee from '@/components/Marquee.astro'
import { Reviews, type OptimizedReview } from '@/components/Reviews.tsx'
import Hero from '@/components/Hero.astro'
import { getImage } from 'astro:assets'
import { TW_SCREENS, TW_WIDTHS } from '@/constants/images'
import { imageResultToImageAttributes } from '@/libs/image'
import { imageResultToImageAttributes, optimizeSliderImages } from '@/libs/image'
import { getCollection } from 'astro:content'
type Props = CollectionEntry<'pages'>['data']
const navItems: HeaderProps['navItems'] = [
{ key: 'nav.coaches', href: '#coaches' },
{ key: 'nav.slider', href: '#image-slider' },
{ key: 'nav.services', href: '#services' },
{ key: 'nav.reviews', href: '#reviews' },
{ key: 'nav.contact', href: '#contacts' },
]
const { lang } = Astro.params
const page = await getEntry('pages', 'main')
const { slider, coaches, services, hero, marquee, reviews, footer } = page?.data || {}
const [page, coaches] = await Promise.all([getEntry('pages', 'main'), getCollection('coaches')])
const { slider, services, hero, marquee, reviews, footer } = page?.data || {}
const optimizedSlider = slider
? await Promise.all(
slider.map((slide) =>
getImage({
src: slide.image,
alt: slide.imageAlt,
widths: [TW_SCREENS.SM, TW_SCREENS.LG],
sizes: `(max-width: ${TW_WIDTHS.LG}px) ${TW_SCREENS.SM}px, ${TW_SCREENS.LG}px`,
quality: 80,
format: 'webp',
loading: 'eager',
}).then(imageResultToImageAttributes)
)
)
? await optimizeSliderImages(slider.map(({ image, imageAlt }) => ({ src: image, alt: imageAlt })))
: undefined
const optimizedReviews = reviews
@@ -61,7 +57,7 @@ const t = useTranslations(lang)
---
<BaseLayout title={t('home.title')}>
<Header lang={lang} />
<Header lang={lang} navItems={navItems} />
{hero && <Hero {...hero} />}
@@ -74,7 +70,7 @@ const t = useTranslations(lang)
}
{
coaches && (
coaches.length > 0 && (
<Section id="coaches" title={t('coaches.title')} description={t('coaches.subtitle')}>
<div class="grid place-items-center gap-4 sm:grid-cols-2 md:gap-8 lg:grid-cols-3">
{coaches.map((coach) => (
@@ -94,7 +90,12 @@ const t = useTranslations(lang)
className="bg-bg-light"
>
<div class="-ml-4 flex w-[calc(100%+(1rem*2))] md:ml-0 md:w-full">
<Swiper className="h-[540px]" client:visible images={optimizedSlider} />
<SwiperWithNav
className="h-[540px]"
slideClass="object-cover sm:object-contain"
client:visible
images={optimizedSlider}
/>
</div>
</Section>
)

View File

@@ -21,6 +21,10 @@
@layer base {
:root {
--header-height: 4rem;
.swiper-pagination-bullet-active {
@apply bg-brand;
}
}
html {