reworked slider + added coaches page
@@ -7,6 +7,7 @@ import react from '@astrojs/react'
|
|||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [tailwind(), react()],
|
integrations: [tailwind(), react()],
|
||||||
|
prefetch: true,
|
||||||
i18n: {
|
i18n: {
|
||||||
locales: ['ru', 'en'],
|
locales: ['ru', 'en'],
|
||||||
defaultLocale: 'ru',
|
defaultLocale: 'ru',
|
||||||
|
|||||||
@@ -1,21 +1,22 @@
|
|||||||
---
|
---
|
||||||
import type { coachesSchema } from '@/content.config'
|
|
||||||
import { useTranslations } from '@/i18n/utils'
|
import { useTranslations } from '@/i18n/utils'
|
||||||
import { Image } from 'astro:assets'
|
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 t = useTranslations(lang)
|
||||||
|
const coachPageLink = `/coaches/${slug}`
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-col items-center justify-start gap-5">
|
<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
|
<Image
|
||||||
src={image}
|
src={cover}
|
||||||
class="absolute h-full w-full object-cover"
|
class="absolute h-full w-full object-cover"
|
||||||
alt={imageAlt}
|
alt={coverAlt}
|
||||||
width={300}
|
width={300}
|
||||||
height={390}
|
height={390}
|
||||||
format="webp"
|
format="webp"
|
||||||
@@ -26,12 +27,8 @@ const t = useTranslations(lang)
|
|||||||
<div class="inline-flex w-full max-w-[300px] flex-col gap-1">
|
<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>
|
<h3 class="text-xl font-bold uppercase text-text-light">{name}</h3>
|
||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
{
|
<a href={coachPageLink} class="mt-2 text-brand hover:text-brand/80" data-astro-prefetch>
|
||||||
link && (
|
|
||||||
<a href={link} target="_blank" class="mt-2 text-brand hover:text-brand/80">
|
|
||||||
{t('coaches.link')}
|
{t('coaches.link')}
|
||||||
</a>
|
</a>
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,20 +4,12 @@ import { Image } from 'astro:assets'
|
|||||||
import logoSrc from '@/assets/logo.png'
|
import logoSrc from '@/assets/logo.png'
|
||||||
import type { z } from 'astro:content'
|
import type { z } from 'astro:content'
|
||||||
import type { footerSchema } from '@/content.config'
|
import type { footerSchema } from '@/content.config'
|
||||||
import type React from 'react'
|
import { SocialMediaIcon } from './SocialMediaIcon'
|
||||||
import { TelegramIcon } from '@/assets/icons/TelegramIcon'
|
|
||||||
import { InstagramIcon } from '@/assets/icons/InstagramIcon'
|
|
||||||
import type { IconProps } from '@/assets/icons/types'
|
|
||||||
|
|
||||||
type Props = z.infer<typeof footerSchema> & {
|
type Props = z.infer<typeof footerSchema> & {
|
||||||
lang?: string
|
lang?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const SOCIAL_MEDIA_ICONS_MAP: Record<string, React.ComponentType<IconProps>> = {
|
|
||||||
telegram: TelegramIcon,
|
|
||||||
instagram: InstagramIcon,
|
|
||||||
}
|
|
||||||
|
|
||||||
const { lang, socialMediaLinks } = Astro.props
|
const { lang, socialMediaLinks } = Astro.props
|
||||||
const t = useTranslations(lang)
|
const t = useTranslations(lang)
|
||||||
---
|
---
|
||||||
@@ -40,11 +32,12 @@ const t = useTranslations(lang)
|
|||||||
socialMediaLinks && (
|
socialMediaLinks && (
|
||||||
<div class="flex gap-4 text-text-light">
|
<div class="flex gap-4 text-text-light">
|
||||||
{socialMediaLinks.map(({ href, target, icon }) => {
|
{socialMediaLinks.map(({ href, target, icon }) => {
|
||||||
const Icon = SOCIAL_MEDIA_ICONS_MAP[icon]
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={href} target={target} rel="noopener noreferrer">
|
<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>
|
</a>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { z } from 'astro:content'
|
|||||||
|
|
||||||
type Props = z.infer<typeof servicesSchema> & { highlighted?: boolean }
|
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
|
<div
|
||||||
@@ -14,8 +14,9 @@ const { title, charachteristics, highlighted = false, link } = Astro.props
|
|||||||
{ 'border-brand': highlighted },
|
{ 'border-brand': highlighted },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<div class="mb-8">
|
<div class="mb-8 flex flex-col gap-2">
|
||||||
<h3 class="mb-4 text-2xl font-bold">{title}</h3>
|
<h3 class="text-2xl font-bold">{title}</h3>
|
||||||
|
{description && <span>{description}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="mb-16 flex-grow space-y-4">
|
<ul class="mb-16 flex-grow space-y-4">
|
||||||
|
|||||||
22
src/components/SocialMediaIcon.tsx
Normal 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
|
||||||
|
}
|
||||||
@@ -1,41 +1,42 @@
|
|||||||
---
|
---
|
||||||
|
import type { TranslationKey } from '@/i18n/ui'
|
||||||
import MobileNavigation from './MobileNavigation'
|
import MobileNavigation from './MobileNavigation'
|
||||||
import { useTranslations } from '@/i18n/utils'
|
import { useTranslations } from '@/i18n/utils'
|
||||||
|
|
||||||
type Props = {
|
export type HeaderProps = {
|
||||||
|
navItems?: Array<{ key: TranslationKey; href: string }>
|
||||||
lang?: string
|
lang?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { lang } = Astro.props
|
type Props = HeaderProps
|
||||||
|
|
||||||
const navItems = [
|
const { lang, navItems } = Astro.props
|
||||||
{ 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 t = useTranslations(lang)
|
const t = useTranslations(lang)
|
||||||
---
|
---
|
||||||
|
|
||||||
<header
|
<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"
|
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"
|
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">
|
<nav class="hidden md:block">
|
||||||
<ul class="grid grid-flow-col gap-6">
|
<ul class="grid grid-flow-col gap-6">
|
||||||
{
|
{navItems.map((item) => (
|
||||||
navItems.map((item) => (
|
|
||||||
<li>
|
<li>
|
||||||
<a href={item.href}>{t(item.key)}</a>
|
<a href={item.href}>{t(item.key)}</a>
|
||||||
</li>
|
</li>
|
||||||
))
|
))}
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
<MobileNavigation navItems={navItems} client:visible />
|
<MobileNavigation navItems={navItems} client:visible />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import type { ImageMetadata } from 'astro'
|
import type { ImageMetadata } from 'astro'
|
||||||
import React, { type ButtonHTMLAttributes } from 'react'
|
import React, { type ButtonHTMLAttributes } from 'react'
|
||||||
import { useSwiper } from 'swiper/react'
|
|
||||||
import ChevronLeft from '@/assets/icons/LeftArrowIcon.svg'
|
import ChevronLeft from '@/assets/icons/LeftArrowIcon.svg'
|
||||||
import ChevronRight from '@/assets/icons/RightArrowIcon.svg'
|
import ChevronRight from '@/assets/icons/RightArrowIcon.svg'
|
||||||
import { cn } from '@/libs/cn'
|
import { cn } from '@/libs/cn'
|
||||||
import { useSlideChange } from './hooks'
|
|
||||||
|
|
||||||
type Props = ButtonHTMLAttributes<HTMLButtonElement> & { iconSrc: ImageMetadata }
|
type Props = ButtonHTMLAttributes<HTMLButtonElement> & { iconSrc: ImageMetadata }
|
||||||
|
|
||||||
@@ -13,7 +11,7 @@ export const NavButton: React.FC<Props> = ({ iconSrc, ...buttonProps }) => {
|
|||||||
<button
|
<button
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
className={cn(
|
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.disabled && 'border-none',
|
||||||
buttonProps.className
|
buttonProps.className
|
||||||
)}
|
)}
|
||||||
@@ -24,35 +22,9 @@ export const NavButton: React.FC<Props> = ({ iconSrc, ...buttonProps }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const PrevButton: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
|
export const PrevButton: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
|
||||||
const swiper = useSwiper()
|
return <NavButton {...props} iconSrc={ChevronLeft} />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NextButton: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
|
export const NextButton: React.FC<ButtonHTMLAttributes<HTMLButtonElement>> = (props) => {
|
||||||
const swiper = useSwiper()
|
return <NavButton {...props} iconSrc={ChevronRight} />
|
||||||
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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/components/swiper/SimpleSwiper.tsx
Normal 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'
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
21
src/components/swiper/SwiperWithNav.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
64
src/components/swiper/SwiperWithThumbs.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
import { useEffect } from 'react'
|
import { type RefObject, useCallback } from 'react'
|
||||||
import { useSwiper } from 'swiper/react'
|
import type { SwiperRef } from 'swiper/react'
|
||||||
|
|
||||||
export const useSlideChange = (onUpdate: VoidFunction) => {
|
export const useNavHandlers = (swiperRef: RefObject<SwiperRef | null>) => {
|
||||||
const swiper = useSwiper()
|
const handlePrev = useCallback(() => {
|
||||||
|
if (!swiperRef.current) return
|
||||||
|
swiperRef.current.swiper.slidePrev()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
const handleNext = useCallback(() => {
|
||||||
if (!swiper) return
|
if (!swiperRef.current) return
|
||||||
|
swiperRef.current.swiper.slideNext()
|
||||||
|
}, [])
|
||||||
|
|
||||||
swiper.on('slideChange', onUpdate)
|
return { handlePrev, handleNext } as const
|
||||||
|
|
||||||
return () => swiper.off('slideChange', onUpdate)
|
|
||||||
}, [swiper])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './Swiper'
|
export * from './SwiperWithNav'
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.swiper-pagination-bullet-active {
|
|
||||||
@apply bg-white;
|
|
||||||
}
|
|
||||||
@@ -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) =>
|
export const heroSchema = (image: ImageFunction) =>
|
||||||
z.object({
|
z.object({
|
||||||
@@ -15,14 +21,17 @@ export const heroSchema = (image: ImageFunction) =>
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const coachesSchema = (image: ImageFunction) =>
|
export const coaches = defineCollection({
|
||||||
|
schema: ({ image }) =>
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
image: image(),
|
cover: image(),
|
||||||
imageAlt: z.string(),
|
coverAlt: z.string(),
|
||||||
link: z.string().optional(),
|
gallery: z.array(image()).optional(),
|
||||||
})
|
socialMediaLinks: z.array(socialMediaLinkSchema).optional(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
export const servicesSchema = z.object({
|
export const servicesSchema = z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
@@ -57,15 +66,7 @@ export const reviewsSchema = (image: ImageFunction) =>
|
|||||||
|
|
||||||
export const footerSchema = z.object({
|
export const footerSchema = z.object({
|
||||||
copyright: z.string().optional(),
|
copyright: z.string().optional(),
|
||||||
socialMediaLinks: z
|
socialMediaLinks: z.array(socialMediaLinkSchema).optional(),
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
icon: z.string(),
|
|
||||||
href: z.string(),
|
|
||||||
target: z.string().optional(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const pages = defineCollection({
|
const pages = defineCollection({
|
||||||
@@ -73,7 +74,6 @@ const pages = defineCollection({
|
|||||||
z.object({
|
z.object({
|
||||||
hero: heroSchema(image).optional(),
|
hero: heroSchema(image).optional(),
|
||||||
marquee: z.array(z.string()).optional(),
|
marquee: z.array(z.string()).optional(),
|
||||||
coaches: z.array(coachesSchema(image)).optional(),
|
|
||||||
reviews: z.array(reviewsSchema(image)).optional(),
|
reviews: z.array(reviewsSchema(image)).optional(),
|
||||||
slider: z
|
slider: z
|
||||||
.array(
|
.array(
|
||||||
@@ -90,4 +90,5 @@ const pages = defineCollection({
|
|||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
pages,
|
pages,
|
||||||
|
coaches,
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 82 KiB |
BIN
src/content/coaches/assets/batyr-slide-1.jpeg
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
src/content/coaches/assets/batyr-slide-2.jpeg
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
src/content/coaches/assets/batyr-slide-3.jpeg
Normal file
|
After Width: | Height: | Size: 63 KiB |
BIN
src/content/coaches/assets/batyr-slide-4.jpg
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
src/content/coaches/assets/batyr-slide-5.jpg
Normal file
|
After Width: | Height: | Size: 94 KiB |
BIN
src/content/coaches/assets/batyr-slide-6.jpg
Normal file
|
After Width: | Height: | Size: 86 KiB |
39
src/content/coaches/batyr-suleimanov.md
Normal 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г.
|
||||||
|
|
||||||
|
Специализация:
|
||||||
|
Строительство тела (набор мышечной массы, сжигание подкожно-жировой клетчатки без вреда для организма), составление плана и методики тренировочного процесса, разработка и контроль рациона питания (БЖУ индивидуально под каждого подопечного).
|
||||||
|
Спортивное питание. Контроль анализов. Рекомендации по приему БАДов и витаминов.
|
||||||
|
Подготовка к соревнованиям по бодибилдингу. Опыт в подготовке бойцов ММА (весогонка, «витаминная» поддержка).
|
||||||
@@ -8,11 +8,7 @@ hero:
|
|||||||
title: 'Записаться'
|
title: 'Записаться'
|
||||||
href: '#services'
|
href: '#services'
|
||||||
marquee:
|
marquee:
|
||||||
- ДОСТИГАЙТЕ ЦЕЛЕЙ
|
- Ваш идеальный результат — в шаге от дома!
|
||||||
- БУДЬТЕ СИЛЬНЕЕ
|
|
||||||
- ТРЕНИРУЙТЕСЬ С НАМИ
|
|
||||||
- МЕНЯЙТЕСЬ К ЛУЧШЕМУ
|
|
||||||
- СТАНОВИТЕСЬ ЗДОРОВЕЕ
|
|
||||||
slider:
|
slider:
|
||||||
- image: './assets/slider/IMG_6491.jpg'
|
- image: './assets/slider/IMG_6491.jpg'
|
||||||
imageAlt: 'first-slide'
|
imageAlt: 'first-slide'
|
||||||
@@ -48,17 +44,6 @@ slider:
|
|||||||
imageAlt: 'sixteenth-slide'
|
imageAlt: 'sixteenth-slide'
|
||||||
- image: './assets/slider/IMG_6520.jpg'
|
- image: './assets/slider/IMG_6520.jpg'
|
||||||
imageAlt: 'seventeenth-slide'
|
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:
|
services:
|
||||||
- title: 'Разовые Персональные тренировки с тренером'
|
- title: 'Разовые Персональные тренировки с тренером'
|
||||||
link:
|
link:
|
||||||
@@ -82,7 +67,7 @@ services:
|
|||||||
price: '60 000 ₽'
|
price: '60 000 ₽'
|
||||||
- text: 'Блок из 15 тренировок СПЛИТ'
|
- text: 'Блок из 15 тренировок СПЛИТ'
|
||||||
price: '110 000 ₽'
|
price: '110 000 ₽'
|
||||||
- title: 'Безлимитные Абонементы на месяц'
|
- title: 'Безлимитные Абонементы'
|
||||||
description: '30 календарных дней занятий с тренером'
|
description: '30 календарных дней занятий с тренером'
|
||||||
link:
|
link:
|
||||||
title: 'Записаться'
|
title: 'Записаться'
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export const languages = {
|
|||||||
ru: 'Русский',
|
ru: 'Русский',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TranslationKey = keyof (typeof ui)[keyof typeof ui]
|
||||||
|
|
||||||
export const ui = {
|
export const ui = {
|
||||||
ru: {
|
ru: {
|
||||||
'seo.meta.description':
|
'seo.meta.description':
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ui } from './ui'
|
import { ui, type TranslationKey } from './ui'
|
||||||
|
|
||||||
import { i18n } from 'astro:config/client'
|
import { i18n } from 'astro:config/client'
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ export function getLangFromUrl(url: URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTranslations(lang?: string) {
|
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]
|
return ui[lang as keyof typeof ui]?.[key] || ui[i18n!.defaultLocale as keyof typeof ui][key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
---
|
---
|
||||||
import { useTranslations } from '@/i18n/utils'
|
import { useTranslations } from '@/i18n/utils'
|
||||||
|
import { cn } from '@/libs/cn'
|
||||||
import '@/styles/global.css'
|
import '@/styles/global.css'
|
||||||
|
import { ClientRouter } from 'astro:transitions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
|
bodyClass?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title } = Astro.props
|
const { title, bodyClass } = Astro.props
|
||||||
const t = useTranslations(Astro.currentLocale)
|
const t = useTranslations(Astro.currentLocale)
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -26,8 +29,9 @@ const t = useTranslations(Astro.currentLocale)
|
|||||||
<meta name="robots" content="index, follow" />
|
<meta name="robots" content="index, follow" />
|
||||||
<link rel="canonical" href={Astro.url} />
|
<link rel="canonical" href={Astro.url} />
|
||||||
<title>{`${title} | ${t('seo.og.title')}`}</title>
|
<title>{`${title} | ${t('seo.og.title')}`}</title>
|
||||||
|
<ClientRouter />
|
||||||
</head>
|
</head>
|
||||||
<body class="flex flex-col">
|
<body class={cn('flex flex-col', bodyClass)}>
|
||||||
<slot />
|
<slot />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -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 => ({
|
export const imageResultToImageAttributes = (imageResult: GetImageResult): ImportAttributes => ({
|
||||||
src: imageResult.src,
|
src: imageResult.src,
|
||||||
srcSet: imageResult.srcSet?.attribute,
|
srcSet: imageResult.srcSet?.attribute,
|
||||||
...imageResult.attributes,
|
...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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
48
src/pages/coaches/[...slug].astro
Normal 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>
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
import { useTranslations } from '@/i18n/utils'
|
import { useTranslations } from '@/i18n/utils'
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||||
import Section from '@/components/Section.astro'
|
import Section from '@/components/Section.astro'
|
||||||
import { Swiper } from '@/components/swiper'
|
import { SwiperWithNav } from '@/components/swiper'
|
||||||
import Header from '@/components/header/Header.astro'
|
import Header, { type HeaderProps } from '@/components/header/Header.astro'
|
||||||
import Footer from '@/components/Footer.astro'
|
import Footer from '@/components/Footer.astro'
|
||||||
|
|
||||||
import type { CollectionEntry } from 'astro:content'
|
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 { Reviews, type OptimizedReview } from '@/components/Reviews.tsx'
|
||||||
import Hero from '@/components/Hero.astro'
|
import Hero from '@/components/Hero.astro'
|
||||||
import { getImage } from 'astro:assets'
|
import { getImage } from 'astro:assets'
|
||||||
import { TW_SCREENS, TW_WIDTHS } from '@/constants/images'
|
import { imageResultToImageAttributes, optimizeSliderImages } from '@/libs/image'
|
||||||
import { imageResultToImageAttributes } from '@/libs/image'
|
import { getCollection } from 'astro:content'
|
||||||
|
|
||||||
type Props = CollectionEntry<'pages'>['data']
|
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 { lang } = Astro.params
|
||||||
const page = await getEntry('pages', 'main')
|
const [page, coaches] = await Promise.all([getEntry('pages', 'main'), getCollection('coaches')])
|
||||||
const { slider, coaches, services, hero, marquee, reviews, footer } = page?.data || {}
|
const { slider, services, hero, marquee, reviews, footer } = page?.data || {}
|
||||||
const optimizedSlider = slider
|
const optimizedSlider = slider
|
||||||
? await Promise.all(
|
? await optimizeSliderImages(slider.map(({ image, imageAlt }) => ({ src: image, alt: imageAlt })))
|
||||||
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)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const optimizedReviews = reviews
|
const optimizedReviews = reviews
|
||||||
@@ -61,7 +57,7 @@ const t = useTranslations(lang)
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={t('home.title')}>
|
<BaseLayout title={t('home.title')}>
|
||||||
<Header lang={lang} />
|
<Header lang={lang} navItems={navItems} />
|
||||||
|
|
||||||
{hero && <Hero {...hero} />}
|
{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')}>
|
<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">
|
<div class="grid place-items-center gap-4 sm:grid-cols-2 md:gap-8 lg:grid-cols-3">
|
||||||
{coaches.map((coach) => (
|
{coaches.map((coach) => (
|
||||||
@@ -94,7 +90,12 @@ const t = useTranslations(lang)
|
|||||||
className="bg-bg-light"
|
className="bg-bg-light"
|
||||||
>
|
>
|
||||||
<div class="-ml-4 flex w-[calc(100%+(1rem*2))] md:ml-0 md:w-full">
|
<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>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--header-height: 4rem;
|
--header-height: 4rem;
|
||||||
|
|
||||||
|
.swiper-pagination-bullet-active {
|
||||||
|
@apply bg-brand;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||