- one more time rework slider
- rework design system - fill trainer page
@@ -1,34 +0,0 @@
|
|||||||
---
|
|
||||||
import { useTranslations } from '@/i18n/utils'
|
|
||||||
import { Image } from 'astro:assets'
|
|
||||||
import type { CollectionEntry } from 'astro:content'
|
|
||||||
|
|
||||||
type Props = CollectionEntry<'coaches'> & { lang?: string }
|
|
||||||
|
|
||||||
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={coachPageLink}>
|
|
||||||
<Image
|
|
||||||
src={cover}
|
|
||||||
class="absolute h-full w-full object-cover"
|
|
||||||
alt={coverAlt}
|
|
||||||
width={300}
|
|
||||||
height={390}
|
|
||||||
format="webp"
|
|
||||||
fit="cover"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<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>}
|
|
||||||
<a href={coachPageLink} class="mt-2 text-brand hover:text-brand/80" data-astro-prefetch>
|
|
||||||
{t('coaches.link')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
9
src/components/GymGallery.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { MediaSwiperWithNav } from './swiper/MediaSwiperWithNav'
|
||||||
|
|
||||||
|
export type GymGalleryProps = {
|
||||||
|
images: ImportAttributes[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GymGallery: React.FC<GymGalleryProps> = ({ images }) => {
|
||||||
|
return <MediaSwiperWithNav images={images} />
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ const { title, description, cta, image, imageAlt } = Astro.props
|
|||||||
className="mt-[var(--header-height)] p-0 md:mt-0 md:flex-col-reverse md:py-24 lg:flex-row lg:items-center"
|
className="mt-[var(--header-height)] p-0 md:mt-0 md:flex-col-reverse md:py-24 lg:flex-row lg:items-center"
|
||||||
>
|
>
|
||||||
<div class="px-4 py-8 md:p-0 md:text-center lg:w-1/2 lg:text-start">
|
<div class="px-4 py-8 md:p-0 md:text-center lg:w-1/2 lg:text-start">
|
||||||
<h1 class="mb-4 text-4xl font-bold leading-tight md:text-6xl" set:html={title} />
|
<h1 class="mb-4 text-4xl font-bold leading-tight lg:text-6xl" set:html={title} />
|
||||||
<p class="text-brand-text mb-6">{description}</p>
|
<p class="text-brand-text mb-6">{description}</p>
|
||||||
{
|
{
|
||||||
cta && (
|
cta && (
|
||||||
@@ -26,7 +26,7 @@ const { title, description, cta, image, imageAlt } = Astro.props
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="relative hidden md:block md:h-[450px] lg:w-1/2">
|
<div class="relative hidden md:block md:h-[400px] lg:w-1/2">
|
||||||
<Image
|
<Image
|
||||||
src={image}
|
src={image}
|
||||||
alt={imageAlt}
|
alt={imageAlt}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const repeatedTexts =
|
|||||||
repeatedTexts.map((text) => (
|
repeatedTexts.map((text) => (
|
||||||
<>
|
<>
|
||||||
<h2>{text}</h2>
|
<h2>{text}</h2>
|
||||||
<Image src={wideCrossIconSrc} alt="cross-icon" />
|
<Image src={wideCrossIconSrc} alt="cross-icon" loading="eager" />
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import type { reviewsSchema } from '@/content.config'
|
import type { reviewsSchema } from '@/content.config'
|
||||||
import type { z } from 'astro:content'
|
import type { z } from 'astro:content'
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react'
|
import { SwiperSlide } from 'swiper/react'
|
||||||
import 'swiper/css'
|
|
||||||
import 'swiper/css/scrollbar'
|
|
||||||
import { Mousewheel, Scrollbar } from 'swiper/modules'
|
|
||||||
import StarIconSrc from '@/assets/icons/StarIcon.svg'
|
import StarIconSrc from '@/assets/icons/StarIcon.svg'
|
||||||
import { useTranslations } from '@/i18n/utils'
|
import { useTranslations } from '@/i18n/utils'
|
||||||
|
import { SimpleSwiper } from './swiper/SimpleSwiper'
|
||||||
|
|
||||||
type Review = z.infer<ReturnType<typeof reviewsSchema>>
|
type Review = z.infer<ReturnType<typeof reviewsSchema>>
|
||||||
type Author = Review['author']
|
type Author = Review['author']
|
||||||
@@ -19,30 +17,25 @@ type ReviewsProps = {
|
|||||||
|
|
||||||
const REVIEWS_LINK =
|
const REVIEWS_LINK =
|
||||||
'https://yandex.ru/maps/213/moscow/?ll=37.469982%2C55.781185&mode=poi&poi%5Bpoint%5D=37.469074%2C55.781319&poi%5Buri%5D=ymapsbm1%3A%2F%2Forg%3Foid%3D19715751392%26yclid%3D10576664134676381695%26banner_id%3D1873406736351323707&tab=reviews&z=17.75'
|
'https://yandex.ru/maps/213/moscow/?ll=37.469982%2C55.781185&mode=poi&poi%5Bpoint%5D=37.469074%2C55.781319&poi%5Buri%5D=ymapsbm1%3A%2F%2Forg%3Foid%3D19715751392%26yclid%3D10576664134676381695%26banner_id%3D1873406736351323707&tab=reviews&z=17.75'
|
||||||
const swiperSlideClassName =
|
const swiperSlideClassName = '!h-auto md:max-w-[600px]'
|
||||||
'!h-auto first:ml-0 last:mr-0 md:max-w-[600px] md:first:ml-24 md:last:mr-24'
|
|
||||||
|
|
||||||
export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
||||||
const t = useTranslations(lang)
|
const t = useTranslations(lang)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Swiper
|
<SimpleSwiper
|
||||||
modules={[Scrollbar, Mousewheel]}
|
|
||||||
slidesPerView={1}
|
slidesPerView={1}
|
||||||
spaceBetween={24}
|
freeMode={false}
|
||||||
scrollbar={{
|
|
||||||
hide: true,
|
|
||||||
draggable: true,
|
|
||||||
}}
|
|
||||||
breakpoints={{
|
breakpoints={{
|
||||||
768: {
|
768: {
|
||||||
slidesPerView: 'auto',
|
slidesPerView: 'auto',
|
||||||
|
freeMode: true,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{reviews.map(({ review, author }, index) => (
|
{reviews.map(({ review, author }, index) => (
|
||||||
<SwiperSlide key={index} className={swiperSlideClassName}>
|
<SwiperSlide key={index} className={swiperSlideClassName}>
|
||||||
<div className="flex h-full flex-col gap-6 bg-bg-light p-6 md:px-14 md:py-16">
|
<div className="card flex h-full flex-col gap-6 md:px-14 md:py-16">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{[...Array(5)].map((_, i) => (
|
{[...Array(5)].map((_, i) => (
|
||||||
<img
|
<img
|
||||||
@@ -77,7 +70,7 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
|||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
<SwiperSlide className={swiperSlideClassName}>
|
<SwiperSlide className={swiperSlideClassName}>
|
||||||
<div className="h-full bg-bg-light transition-colors hover:bg-bg-light/80">
|
<div className="card h-full transition-colors hover:bg-bg-light/80">
|
||||||
<a
|
<a
|
||||||
href={REVIEWS_LINK}
|
href={REVIEWS_LINK}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -99,6 +92,6 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
</Swiper>
|
</SimpleSwiper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,21 +11,25 @@ type Props = {
|
|||||||
const { title, description, className, id } = Astro.props
|
const { title, description, className, id } = Astro.props
|
||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section class="w-full relative min-h-fit overflow-hidden" id={id}>
|
||||||
class={cn('flex flex-col gap-6 py-12 px-4 md:py-24 md:px-16 lg:px-24 md:gap-12', className)}
|
<div
|
||||||
id={id}
|
class={cn(
|
||||||
>
|
'w-full flex flex-col gap-6 py-12 px-4 md:py-24 md:px-16 lg:px-24 md:gap-12',
|
||||||
{
|
className
|
||||||
(title || description) && (
|
)}
|
||||||
<div class="flex w-full max-w-[750px] flex-col gap-6 self-center text-center">
|
>
|
||||||
{title && (
|
{
|
||||||
<h2 class="text-4xl font-bold leading-tight md:text-5xl">
|
(title || description) && (
|
||||||
<Fragment set:html={title} />
|
<div class="flex w-full flex-col gap-6">
|
||||||
</h2>
|
{title && (
|
||||||
)}
|
<h2 class="text-4xl font-bold leading-tight md:text-5xl">
|
||||||
{description && <p>{description}</p>}
|
<Fragment set:html={title} />
|
||||||
</div>
|
</h2>
|
||||||
)
|
)}
|
||||||
}
|
{description && <p>{description}</p>}
|
||||||
<slot />
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
---
|
|
||||||
import type { servicesSchema } from '@/content.config'
|
|
||||||
import type { z } from 'astro:content'
|
|
||||||
|
|
||||||
type Props = z.infer<typeof servicesSchema> & { highlighted?: boolean }
|
|
||||||
|
|
||||||
const { title, description, charachteristics, highlighted = false, link } = Astro.props
|
|
||||||
---
|
|
||||||
|
|
||||||
<div
|
|
||||||
class:list={[
|
|
||||||
'flex h-full w-full flex-col border-2 px-5 py-6 md:min-h-[550px] md:max-w-[400px] md:px-7 md:py-8',
|
|
||||||
'border border-transparent bg-card',
|
|
||||||
{ 'border-brand': highlighted },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<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">
|
|
||||||
{
|
|
||||||
charachteristics?.map((charachteristic) => (
|
|
||||||
<li class="flex items-start justify-between space-x-2">
|
|
||||||
<span class="text-text-light">{charachteristic.text}</span>
|
|
||||||
{charachteristic.price && (
|
|
||||||
<span class="ml-2 text-nowrap text-end text-brand" set:html={charachteristic.price} />
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{
|
|
||||||
link && (
|
|
||||||
<a href={link.href} target={link.target} class="brand-btn-outline">
|
|
||||||
{link.title}
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
33
src/components/coaches/CoachCard.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { useTranslations } from '@/i18n/utils'
|
||||||
|
import type { CollectionEntry } from 'astro:content'
|
||||||
|
import rightArrowIconSrc from '@/assets/icons/RightArrowIcon.svg'
|
||||||
|
|
||||||
|
export type CoachCardProps = CollectionEntry<'coaches'> & { lang?: string }
|
||||||
|
|
||||||
|
export const CoachCard: React.FC<CoachCardProps> = ({ data, lang, slug }) => {
|
||||||
|
const { name, description, cover, coverAlt } = data
|
||||||
|
const t = useTranslations(lang)
|
||||||
|
const coachPageLink = `/coaches/${slug}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="aspect-3/4 relative h-full max-w-[262px] overflow-hidden rounded-3xl text-white md:max-w-[320px] lg:max-w-[400px]">
|
||||||
|
<img
|
||||||
|
{...cover}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="h-full w-full object-cover object-top mix-blend-luminosity"
|
||||||
|
alt={coverAlt}
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href={coachPageLink}
|
||||||
|
data-astro-prefetch
|
||||||
|
className="absolute inset-0 flex flex-col justify-end bg-gradient-to-t from-black/80 via-transparent p-2 lg:p-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<h3 className="text-xl font-bold uppercase text-text-light">{name}</h3>
|
||||||
|
<img {...rightArrowIconSrc} width={50} height={50} />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
21
src/components/coaches/Coaches.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { CollectionEntry, z } from 'astro:content'
|
||||||
|
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
||||||
|
import { SwiperSlide } from 'swiper/react'
|
||||||
|
import { CoachCard } from './CoachCard'
|
||||||
|
|
||||||
|
type CoachesProps = {
|
||||||
|
coaches: CollectionEntry<'coaches'>[]
|
||||||
|
lang?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Coaches: React.FC<CoachesProps> = ({ coaches, lang }) => {
|
||||||
|
return (
|
||||||
|
<SimpleSwiper>
|
||||||
|
{coaches.map((coach, index) => (
|
||||||
|
<SwiperSlide className="!h-auto" style={{ width: 'auto' }} key={index}>
|
||||||
|
<CoachCard {...coach} lang={lang} />
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</SimpleSwiper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,40 +3,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'
|
||||||
|
|
||||||
|
const NAV_ITEMS: Array<{ key: TranslationKey; href: string }> = [
|
||||||
|
{ 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' },
|
||||||
|
]
|
||||||
|
|
||||||
export type HeaderProps = {
|
export type HeaderProps = {
|
||||||
navItems?: Array<{ key: TranslationKey; href: string }>
|
|
||||||
lang?: string
|
lang?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Props = HeaderProps
|
type Props = HeaderProps
|
||||||
|
|
||||||
const { lang, navItems } = Astro.props
|
const { lang } = Astro.props
|
||||||
|
|
||||||
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="/" />
|
<a class="text-2xl font-bold text-text-light" set:html={t('header.logo')} href="/" />
|
||||||
{
|
<nav class="hidden md:block">
|
||||||
navItems && (
|
<ul class="grid grid-flow-col gap-6">
|
||||||
<>
|
{
|
||||||
<nav class="hidden md:block">
|
NAV_ITEMS.map((item) => (
|
||||||
<ul class="grid grid-flow-col gap-6">
|
<li>
|
||||||
{navItems.map((item) => (
|
<a href={item.href}>{t(item.key)}</a>
|
||||||
<li>
|
</li>
|
||||||
<a href={item.href}>{t(item.key)}</a>
|
))
|
||||||
</li>
|
}
|
||||||
))}
|
</ul>
|
||||||
</ul>
|
</nav>
|
||||||
</nav>
|
<MobileNavigation navItems={NAV_ITEMS} client:visible />
|
||||||
<MobileNavigation navItems={navItems} client:visible />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -66,16 +66,20 @@ const MobileNavigation: React.FC<MobileNavigationProps> = ({ navItems }) => {
|
|||||||
<AnimatedBurgerButton isOpen={isOpen} onClick={toggleMenu} />
|
<AnimatedBurgerButton isOpen={isOpen} onClick={toggleMenu} />
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'fixed left-0 top-0 z-50 w-full transform bg-bg-main p-6 pt-16 transition-transform duration-500 ease-in-out',
|
'fixed left-0 top-0 z-50 w-full h-screen transform bg-bg-main p-8 pt-16 transition-transform duration-500 ease-in-out',
|
||||||
isOpen ? 'translate-y-0' : '-translate-y-full'
|
isOpen ? '-translate-x-0' : 'translate-x-full'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<nav className="flex flex-col items-center space-y-8">
|
<nav className="flex h-full flex-col justify-between">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<a key={item.key} href={item.href} className="text-3xl" onClick={toggleMenu}>
|
<a key={item.key} href={item.href} className="text-3xl" onClick={toggleMenu}>
|
||||||
{t(item.key as any)}
|
{t(item.key as any)}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
<hr />
|
||||||
|
<a href="/#services" className="brand-btn w-full" onClick={toggleMenu}>
|
||||||
|
{t('hero.cta.getStarted')}
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
34
src/components/services/CharachteristicCard.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
type Props = {
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
price?: string
|
||||||
|
link: {
|
||||||
|
href: string
|
||||||
|
title: string
|
||||||
|
target?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>{description}</span>}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{price && (
|
||||||
|
<span
|
||||||
|
className="text-nowrap text-4xl font-bold text-brand"
|
||||||
|
dangerouslySetInnerHTML={{ __html: price }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{link && (
|
||||||
|
<a href={link.href} target={link.target} className="brand-btn-outline">
|
||||||
|
{link.title}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
49
src/components/services/Services.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { servicesSchema } from '@/content.config'
|
||||||
|
import type { z } from 'astro:content'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { SwiperSlide } from 'swiper/react'
|
||||||
|
import { CharachteristicCard } from './CharachteristicCard'
|
||||||
|
import { cn } from '@/libs/cn'
|
||||||
|
import { SimpleSwiper } from '../swiper/SimpleSwiper'
|
||||||
|
|
||||||
|
export type ServicesProps = {
|
||||||
|
services: z.infer<typeof servicesSchema>[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Services: React.FC<ServicesProps> = ({ services }) => {
|
||||||
|
const [serviceId, setServiceId] = useState(0)
|
||||||
|
const selectedService = services[serviceId]
|
||||||
|
const isSelected = (index: number) => index === serviceId
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<SimpleSwiper spaceBetween={8}>
|
||||||
|
{services.map((service, index) => (
|
||||||
|
<SwiperSlide style={{ width: 'auto' }} key={index}>
|
||||||
|
<button
|
||||||
|
className={cn('tag-btn', isSelected(index) && 'tag-btn-active')}
|
||||||
|
onClick={() => setServiceId(index)}
|
||||||
|
>
|
||||||
|
{service.title}
|
||||||
|
</button>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</SimpleSwiper>
|
||||||
|
<SimpleSwiper>
|
||||||
|
{selectedService.charachteristics?.map((charachteristic, index) => (
|
||||||
|
<SwiperSlide
|
||||||
|
className="aspect-3/4 !h-auto w-full max-w-[262px] md:max-w-[320px]"
|
||||||
|
key={index}
|
||||||
|
>
|
||||||
|
<CharachteristicCard
|
||||||
|
{...charachteristic}
|
||||||
|
title={charachteristic.text}
|
||||||
|
description={selectedService.description}
|
||||||
|
link={selectedService.link}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</SimpleSwiper>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/components/swiper/MediaSwiper.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
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 MediaSwiperProps = SwiperProps & {
|
||||||
|
images: ImportAttributes[]
|
||||||
|
imageClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MediaSwiper = React.forwardRef<SwiperRef, MediaSwiperProps>(
|
||||||
|
({ images, className, modules = [], slideClass, imageClassName, ...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} className={slideClass}>
|
||||||
|
<img
|
||||||
|
{...image}
|
||||||
|
loading={index === 0 ? 'eager' : 'lazy'}
|
||||||
|
decoding="async"
|
||||||
|
fetchPriority={index === 0 ? 'high' : 'auto'}
|
||||||
|
className={cn(
|
||||||
|
'aspect-video h-full w-full rounded-3xl object-cover object-center',
|
||||||
|
imageClassName
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
MediaSwiper.displayName = 'SimpleSwiper'
|
||||||
@@ -5,16 +5,16 @@ import 'swiper/css/navigation'
|
|||||||
import 'swiper/css/pagination'
|
import 'swiper/css/pagination'
|
||||||
import { NextButton, PrevButton } from './NavButtons'
|
import { NextButton, PrevButton } from './NavButtons'
|
||||||
import { useNavHandlers } from './hooks'
|
import { useNavHandlers } from './hooks'
|
||||||
import { SimpleSwiper, type SimpleSwiperProps } from './SimpleSwiper'
|
import { MediaSwiper, type MediaSwiperProps } from './MediaSwiper'
|
||||||
|
|
||||||
export const SwiperWithNav: React.FC<SimpleSwiperProps> = (props) => {
|
export const MediaSwiperWithNav: React.FC<MediaSwiperProps> = (props) => {
|
||||||
const swiperRef = useRef<SwiperRef | null>(null)
|
const swiperRef = useRef<SwiperRef | null>(null)
|
||||||
const { handleNext, handlePrev } = useNavHandlers(swiperRef)
|
const { handleNext, handlePrev } = useNavHandlers(swiperRef)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full items-center gap-10">
|
<div className="flex h-full w-full items-center gap-10">
|
||||||
<PrevButton className="hidden md:block" onClick={handlePrev} />
|
<PrevButton className="hidden md:block" onClick={handlePrev} />
|
||||||
<SimpleSwiper {...props} ref={swiperRef} />
|
<MediaSwiper {...props} ref={swiperRef} />
|
||||||
<NextButton className="hidden md:block" onClick={handleNext} />
|
<NextButton className="hidden md:block" onClick={handleNext} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -6,35 +6,34 @@ import 'swiper/css/navigation'
|
|||||||
import 'swiper/css/pagination'
|
import 'swiper/css/pagination'
|
||||||
import 'swiper/css/thumbs'
|
import 'swiper/css/thumbs'
|
||||||
import 'swiper/css/free-mode'
|
import 'swiper/css/free-mode'
|
||||||
import { cn } from '@/libs/cn'
|
|
||||||
import { useNavHandlers } from './hooks'
|
import { useNavHandlers } from './hooks'
|
||||||
import { PrevButton, NextButton } from './NavButtons'
|
import { PrevButton, NextButton } from './NavButtons'
|
||||||
import { SimpleSwiper } from './SimpleSwiper'
|
import { MediaSwiper, type MediaSwiperProps } from './MediaSwiper'
|
||||||
|
|
||||||
type SwiperProps = {
|
type MediaSwiperPropsWithThumbs = MediaSwiperProps & {
|
||||||
images: ImportAttributes[]
|
|
||||||
thumbs: ImportAttributes[]
|
thumbs: ImportAttributes[]
|
||||||
swiperClassName?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SwiperWithThumbs: React.FC<SwiperProps> = ({ images, thumbs, swiperClassName }) => {
|
export const MediaSwiperWithThumbs: React.FC<MediaSwiperPropsWithThumbs> = ({
|
||||||
|
thumbs,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperClass | null>(null)
|
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperClass | null>(null)
|
||||||
const swiperRef = useRef<SwiperRef | null>(null)
|
const swiperRef = useRef<SwiperRef | null>(null)
|
||||||
const { handleNext, handlePrev } = useNavHandlers(swiperRef)
|
const { handleNext, handlePrev } = useNavHandlers(swiperRef)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid w-full grid-cols-1 items-center gap-x-10 gap-y-3 md:grid-cols-[auto_1fr_auto]">
|
<div className="grid w-full grid-cols-1 items-center gap-x-10 gap-y-3 lg:grid-cols-[auto_1fr_auto]">
|
||||||
<PrevButton className="hidden md:block" onClick={handlePrev} />
|
<PrevButton className="hidden lg:block" onClick={handlePrev} />
|
||||||
<SimpleSwiper
|
<MediaSwiper
|
||||||
|
{...props}
|
||||||
modules={[Thumbs]}
|
modules={[Thumbs]}
|
||||||
thumbs={{ swiper: thumbsSwiper }}
|
thumbs={{ swiper: thumbsSwiper }}
|
||||||
watchSlidesProgress={true}
|
watchSlidesProgress={true}
|
||||||
className={cn(swiperClassName)}
|
|
||||||
images={images}
|
|
||||||
ref={swiperRef}
|
ref={swiperRef}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
/>
|
/>
|
||||||
<NextButton className="hidden md:block" onClick={handleNext} />
|
<NextButton className="hidden lg:block" onClick={handleNext} />
|
||||||
<Swiper
|
<Swiper
|
||||||
onSwiper={setThumbsSwiper}
|
onSwiper={setThumbsSwiper}
|
||||||
spaceBetween={12}
|
spaceBetween={12}
|
||||||
@@ -43,7 +42,7 @@ export const SwiperWithThumbs: React.FC<SwiperProps> = ({ images, thumbs, swiper
|
|||||||
watchSlidesProgress={true}
|
watchSlidesProgress={true}
|
||||||
modules={[Navigation, Thumbs, FreeMode]}
|
modules={[Navigation, Thumbs, FreeMode]}
|
||||||
// !!! TODO FIXME тут хардкод высоты так как если ставить в процентах, то появляется много пустого места снизу
|
// !!! TODO FIXME тут хардкод высоты так как если ставить в процентах, то появляется много пустого места снизу
|
||||||
className="relative h-24 w-full justify-center place-self-start md:col-start-2"
|
className="relative h-24 w-full justify-center place-self-start lg:col-start-2"
|
||||||
>
|
>
|
||||||
{thumbs.map((image, index) => (
|
{thumbs.map((image, index) => (
|
||||||
<SwiperSlide
|
<SwiperSlide
|
||||||
@@ -54,7 +53,7 @@ export const SwiperWithThumbs: React.FC<SwiperProps> = ({ images, thumbs, swiper
|
|||||||
{...image}
|
{...image}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
decoding="async"
|
decoding="async"
|
||||||
className="h-full w-full object-cover"
|
className="h-full w-full rounded-3xl object-cover"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
@@ -1,43 +1,21 @@
|
|||||||
import React from 'react'
|
import { FreeMode, Mousewheel } from 'swiper/modules'
|
||||||
import { Swiper, type SwiperProps, type SwiperRef, SwiperSlide } from 'swiper/react'
|
import { Swiper, type SwiperProps } from 'swiper/react'
|
||||||
import { A11y, Navigation, Pagination } from 'swiper/modules'
|
|
||||||
import { cn } from '@/libs/cn'
|
|
||||||
import 'swiper/css'
|
import 'swiper/css'
|
||||||
import 'swiper/css/navigation'
|
import 'swiper/css/free-mode'
|
||||||
import 'swiper/css/pagination'
|
import './simple-swiper.css'
|
||||||
|
import { cn } from '@/libs/cn'
|
||||||
|
|
||||||
export type SimpleSwiperProps = SwiperProps & {
|
export const SimpleSwiper: React.FC<SwiperProps> = ({ children, className, ...props }) => (
|
||||||
images: ImportAttributes[]
|
<Swiper
|
||||||
}
|
direction="horizontal"
|
||||||
|
slidesPerView="auto"
|
||||||
export const SimpleSwiper = React.forwardRef<SwiperRef, SimpleSwiperProps>(
|
freeMode={true}
|
||||||
({ images, className, modules = [], slideClass, ...otherProps }, ref) => {
|
spaceBetween={24}
|
||||||
return (
|
mousewheel={{ enabled: true, forceToAxis: true }}
|
||||||
<Swiper
|
modules={[FreeMode, Mousewheel]}
|
||||||
ref={ref}
|
{...props}
|
||||||
modules={[Navigation, Pagination, A11y, ...modules]}
|
className={cn('simple-swiper', className)}
|
||||||
slidesPerView={1}
|
>
|
||||||
pagination={{
|
{children}
|
||||||
clickable: true,
|
</Swiper>
|
||||||
}}
|
|
||||||
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 +1 @@
|
|||||||
export * from './SwiperWithNav'
|
export * from './MediaSwiperWithNav'
|
||||||
|
|||||||
4
src/components/swiper/simple-swiper.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.simple-swiper {
|
||||||
|
width: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
@@ -20,4 +20,4 @@ export const TW_SCREENS = {
|
|||||||
XL: 720,
|
XL: 720,
|
||||||
_2XL: 864,
|
_2XL: 864,
|
||||||
},
|
},
|
||||||
} as const;
|
} as const
|
||||||
|
|||||||
54
src/content/coaches/anna-suleimanova.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
cover: './assets/anna-suleimanova-cover.jpeg'
|
||||||
|
coverAlt: 'anna-suleimanova'
|
||||||
|
name: 'Александра Сулейманова'
|
||||||
|
description: 'Спортсмен и тренер. Опыт работы в фитнес индустрии 9 лет'
|
||||||
|
gallery:
|
||||||
|
- './assets/anna-suleimanova-slide-1.jpeg'
|
||||||
|
- './assets/anna-suleimanova-slide-2.jpeg'
|
||||||
|
- './assets/anna-suleimanova-slide-3.jpeg'
|
||||||
|
socialMediaLinks:
|
||||||
|
- icon: 'instagram'
|
||||||
|
href: 'https://www.instagram.com/kulikovaasasha?igsh=MjZuOXc4OTN3cGh4'
|
||||||
|
target: '_blank'
|
||||||
|
# - icon: 'telegram'
|
||||||
|
# href: 'nks://telegram.me/anna_suleimanova'
|
||||||
|
# target: '_blank'
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="coach-grid">
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Спортивные достижения
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">🏆</span> Чемпионка регионального турнира в категории "Wellness"</li>
|
||||||
|
<li><span class="coach-card-emoji">🏆</span> Призер всероссийский соревнований</li>
|
||||||
|
<li><span class="coach-card-emoji">🏆</span> КМС по плаванию</li>
|
||||||
|
<li><span class="coach-card-emoji">🏆</span> КМС по пауэрлифтингу</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Профессиональный опыт
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">👨🏫</span> Спортсмен и тренер</li>
|
||||||
|
<li><span class="coach-card-emoji">👨🏫</span> Опыт работы в фитнес индустрии 9 лет</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Образование
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> ЧГАФКиС «Физкультурно-оздоровительные технологии»</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Персональный тренер и инструктор тренажерного зала. Обучающий центр бодибилдинга и фитнеса. Пермь 2019г.</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Основы подготовки к соревнованиям в номинации «Фитнес-бикини»</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Функциональный тренинг. Обучающий центр бодибилдинга и фитнеса.</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Диетология и нутрициология.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
52
src/content/coaches/anna.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
cover: './assets/anna-cover.jpeg'
|
||||||
|
coverAlt: 'anna'
|
||||||
|
name: 'Анна'
|
||||||
|
description: 'Спортсмен и тренер. Опыт работы в фитнес индустрии более 10 лет'
|
||||||
|
gallery:
|
||||||
|
- './assets/anna-slide-1.jpeg'
|
||||||
|
- './assets/anna-slide-2.jpeg'
|
||||||
|
socialMediaLinks:
|
||||||
|
- icon: 'instagram'
|
||||||
|
href: 'https://www.instagram.com/dmitrienko_a_fit_4you?igsh=MXBpZTVlNHkxOWl6dA=='
|
||||||
|
target: '_blank'
|
||||||
|
# - icon: 'telegram'
|
||||||
|
# href: 'nks://telegram.me/anna_suleimanova'
|
||||||
|
# target: '_blank'
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="coach-grid">
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Спортивные достижения
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">🏆</span> Топ 5 Владимировкой области Фитнес Бикини</li>
|
||||||
|
<li><span class="coach-card-emoji">🏆</span> 4 место в Открытом Кубке Смоленской области</li>
|
||||||
|
<li><span class="coach-card-emoji">🏆</span> Входила в состав по легкой атлетике</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Профессиональный опыт
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">👨🏫</span> Персональный тренер и спортсмен</li>
|
||||||
|
<li><span class="coach-card-emoji">👨🏫</span> Опыт работы в фитнес индустрии более 10 лет</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Образование
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> СГУ. Институт Физкультуры и Спорта. Г. Саратов</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> АНО ДПО «УрИПКиП». Современные физкультурно-оздоровительные технологии в физическом воспитании</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> АНО ДПО «УрИПКиП». Детский фитнес в работе с детьми старшего дошкольного и младшего школьного возраста</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Персональный тренер 1,2,3 ступень</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Нутрициолог/Диетолог</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
BIN
src/content/coaches/assets/anna-cover.jpeg
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
src/content/coaches/assets/anna-slide-1.jpeg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
src/content/coaches/assets/anna-slide-2.jpeg
Normal file
|
After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
BIN
src/content/coaches/assets/anna-suleimanova-slide-1.jpeg
Normal file
|
After Width: | Height: | Size: 123 KiB |
BIN
src/content/coaches/assets/anna-suleimanova-slide-2.jpeg
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
src/content/coaches/assets/anna-suleimanova-slide-3.jpeg
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
src/content/coaches/assets/batyr-slide-1.JPG
Normal file
|
After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 86 KiB |
@@ -4,36 +4,65 @@ coverAlt: 'batyr-suleimanov'
|
|||||||
name: 'Батыр Сулейманов'
|
name: 'Батыр Сулейманов'
|
||||||
description: 'Профессиональный выступающий спортсмен и тренер. Опыт работы в фитнес индустрии более 12 лет'
|
description: 'Профессиональный выступающий спортсмен и тренер. Опыт работы в фитнес индустрии более 12 лет'
|
||||||
gallery:
|
gallery:
|
||||||
- './assets/batyr-slide-1.jpeg'
|
- './assets/batyr-slide-1.JPG'
|
||||||
- './assets/batyr-slide-2.jpeg'
|
socialMediaLinks:
|
||||||
- './assets/batyr-slide-3.jpeg'
|
- icon: 'instagram'
|
||||||
- './assets/batyr-slide-4.jpg'
|
href: 'https://www.instagram.com/therealbatyr?igsh=MXZkMHdsMDgxM3c4bA%3D%3D&utm_source=qr'
|
||||||
- './assets/batyr-slide-5.jpg'
|
target: '_blank'
|
||||||
- './assets/batyr-slide-6.jpg'
|
- icon: 'telegram'
|
||||||
|
href: 'nks://telegram.me/therealbatyr2'
|
||||||
|
target: '_blank'
|
||||||
---
|
---
|
||||||
🏆ЧЕМПИОН DUDUSHKIN FF 2022
|
|
||||||
|
|
||||||
🏆ЧЕМПИОН NPC REGIONAL CUP MOSCOW 2024
|
<div class="coach-grid">
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Спортивные достижения
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">🥇</span> ЧЕМПИОН DUDUSHKIN FF 2022</li>
|
||||||
|
<li><span class="coach-card-emoji">🥇</span> ЧЕМПИОН NPC REGIONAL CUP MOSCOW 2024</li>
|
||||||
|
<li><span class="coach-card-emoji">🥇</span> ЧЕМПИОН OLYMPIA AMATEUR 2024 SOUTH KOREA</li>
|
||||||
|
<li><span class="coach-card-emoji">🥈</span> ВИЦЕ-ЧЕМПИОН SPS 2022</li>
|
||||||
|
<li><span class="coach-card-emoji">🥈</span> ВИЦЕ-ЧЕМПИОН OKSANA GRISHINA CLASSIC 2023</li>
|
||||||
|
<li><span class="coach-card-emoji">🥈</span> ВИЦЕ-ЧЕМПИОН MUSCLE MULISHA GRANDPRIX 2023</li>
|
||||||
|
<li><span class="coach-card-emoji">🏅</span> TOP-5 DUBAI PRO 2024</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
🏆ЧЕМПИОН OLYMPIA AMATEUR 2024 SOUTH KOREA
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Профессиональный опыт
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">👨🏫</span> Профессиональный выступающий спортсмен и тренер.</li>
|
||||||
|
<li><span class="coach-card-emoji">👨🏫</span> Опыт работы в фитнес индустрии более 12 лет.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
🏆ВИЦЕ-ЧЕМПИОН SPS 2022
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
|
Образование
|
||||||
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Школа бодибилдинга и фитнеса им. Б. Вейдера 2014г.</li>
|
||||||
|
<li><span class="coach-card-emoji">🎓</span> Ассоциация профессионалов фитнеса (FPA) 2015г.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
🏆ВИЦЕ-ЧЕМПИОН OKSANA GRISHINA CLASSIC 2023
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">
|
||||||
🏆ВИЦЕ-ЧЕМПИОН
|
Специализация
|
||||||
MUSCLE MULISHA GRANDPRIX 2023
|
</h2>
|
||||||
|
<ul class="coach-card-list">
|
||||||
🏆TOP-5 DUBAI PRO 2024
|
<li><span class="coach-card-emoji">🏋️</span> Строительство тела (набор мышечной массы, сжигание подкожно-жировой клетчатки без вреда для организма)</li>
|
||||||
|
<li><span class="coach-card-emoji">📋</span> Составление плана и методики тренировочного процесса</li>
|
||||||
Профессиональный выступающий спортсмен и тренер.
|
<li><span class="coach-card-emoji">🥗</span> Разработка и контроль рациона питания (БЖУ индивидуально под каждого подопечного)</li>
|
||||||
Опыт работы в фитнес индустрии более 12 лет.
|
<li><span class="coach-card-emoji">🥤</span> Спортивное питание</li>
|
||||||
|
<li><span class="coach-card-emoji">🔬</span> Контроль анализов</li>
|
||||||
Образование:
|
<li><span class="coach-card-emoji">💊</span> Рекомендации по приему БАДов и витаминов</li>
|
||||||
1. Школа бодибилдинга и фитнеса им. Б. Вейдера 2014г.
|
<li><span class="coach-card-emoji">🏆</span> Подготовка к соревнованиям по бодибилдингу</li>
|
||||||
2. Ассоциация профессионалов фитнеса (FPA) 2015г.
|
<li><span class="coach-card-emoji">🥊</span> Опыт в подготовке бойцов ММА (весогонка, «витаминная» поддержка)</li>
|
||||||
|
</ul>
|
||||||
Специализация:
|
</div>
|
||||||
Строительство тела (набор мышечной массы, сжигание подкожно-жировой клетчатки без вреда для организма), составление плана и методики тренировочного процесса, разработка и контроль рациона питания (БЖУ индивидуально под каждого подопечного).
|
</div>
|
||||||
Спортивное питание. Контроль анализов. Рекомендации по приему БАДов и витаминов.
|
|
||||||
Подготовка к соревнованиям по бодибилдингу. Опыт в подготовке бойцов ММА (весогонка, «витаминная» поддержка).
|
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ slider:
|
|||||||
- image: './assets/slider/IMG_6520.jpg'
|
- image: './assets/slider/IMG_6520.jpg'
|
||||||
imageAlt: 'seventeenth-slide'
|
imageAlt: 'seventeenth-slide'
|
||||||
services:
|
services:
|
||||||
- title: 'Разовые Персональные тренировки с тренером'
|
- title: 'Тренировка с тренером'
|
||||||
link:
|
link:
|
||||||
title: 'Записаться'
|
title: 'Записаться'
|
||||||
href: 'https://wa.me/message/JKEYZFZBLQBIF1'
|
href: 'https://wa.me/message/JKEYZFZBLQBIF1'
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const ui = {
|
|||||||
// Footer
|
// Footer
|
||||||
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
||||||
'footer.copyright': 'Все права защищены. Политика конфиденциальности.',
|
'footer.copyright': 'Все права защищены. Политика конфиденциальности.',
|
||||||
|
'coaches.socialMedias.title': (name: string) => `${name} в соцсетях`,
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
'seo.meta.description':
|
'seo.meta.description':
|
||||||
@@ -86,5 +87,6 @@ export const ui = {
|
|||||||
// Footer
|
// Footer
|
||||||
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
||||||
'footer.copyright': 'All rights reserved. Privacy Policy.',
|
'footer.copyright': 'All rights reserved. Privacy Policy.',
|
||||||
|
'coaches.socialMedias.title': (name: string) => `${name} in social medias`,
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Primitive } from 'astro:schema'
|
||||||
import { ui, type TranslationKey } from './ui'
|
import { ui, type TranslationKey } from './ui'
|
||||||
|
|
||||||
import { i18n } from 'astro:config/client'
|
import { i18n } from 'astro:config/client'
|
||||||
@@ -11,7 +12,12 @@ export function getLangFromUrl(url: URL) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useTranslations(lang?: string) {
|
export function useTranslations(lang?: string) {
|
||||||
return function t(key: TranslationKey) {
|
return function t(key: TranslationKey, payload?: Primitive): string {
|
||||||
return ui[lang as keyof typeof ui]?.[key] || ui[i18n!.defaultLocale as keyof typeof ui][key]
|
const translation =
|
||||||
|
ui[lang as keyof typeof ui]?.[key] || ui[i18n!.defaultLocale as keyof typeof ui][key]
|
||||||
|
|
||||||
|
if (typeof translation === 'function') return translation(String(payload))
|
||||||
|
|
||||||
|
return translation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { useTranslations } from '@/i18n/utils'
|
import { useTranslations } from '@/i18n/utils'
|
||||||
import { cn } from '@/libs/cn'
|
import { cn } from '@/libs/cn'
|
||||||
import '@/styles/global.css'
|
import '@/styles/global.css'
|
||||||
import { ClientRouter } from 'astro:transitions'
|
import { ClientRouter, slide } from 'astro:transitions'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string
|
title: string
|
||||||
@@ -14,7 +14,7 @@ const t = useTranslations(Astro.currentLocale)
|
|||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang={Astro.currentLocale}>
|
<html lang={Astro.currentLocale} transition:animate={slide({ duration: '0.3s' })}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ import BaseLayout from '@/layouts/BaseLayout.astro'
|
|||||||
import { getCollection } from 'astro:content'
|
import { getCollection } from 'astro:content'
|
||||||
import { optimizeSliderImages, optimizeSliderThumbs } from '@/libs/image'
|
import { optimizeSliderImages, optimizeSliderThumbs } from '@/libs/image'
|
||||||
import Section from '@/components/Section.astro'
|
import Section from '@/components/Section.astro'
|
||||||
import { SwiperWithThumbs } from '@/components/swiper/SwiperWithThumbs'
|
import { MediaSwiperWithThumbs } from '@/components/swiper/MediaSwiperWithThumbs'
|
||||||
import Header from '@/components/header/Header.astro'
|
import Header from '@/components/header/Header.astro'
|
||||||
|
import './styles.css'
|
||||||
|
import { SocialMediaIcon } from '@/components/SocialMediaIcon'
|
||||||
|
import { useTranslations } from '@/i18n/utils'
|
||||||
|
import {slide, ViewTransitions} from 'astro:transitions'
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
export const getStaticPaths = async () => {
|
||||||
const coaches = await getCollection('coaches')
|
const coaches = await getCollection('coaches')
|
||||||
@@ -13,6 +17,7 @@ export const getStaticPaths = async () => {
|
|||||||
|
|
||||||
const { data, render } = Astro.props
|
const { data, render } = Astro.props
|
||||||
const { gallery, cover } = data
|
const { gallery, cover } = data
|
||||||
|
const t = useTranslations(Astro.currentLocale)
|
||||||
const { Content } = await render()
|
const { Content } = await render()
|
||||||
const galleryWithCover = gallery ? [cover, ...gallery] : undefined
|
const galleryWithCover = gallery ? [cover, ...gallery] : undefined
|
||||||
const [thumbs, optimizedGallery] = galleryWithCover
|
const [thumbs, optimizedGallery] = galleryWithCover
|
||||||
@@ -31,18 +36,37 @@ const [thumbs, optimizedGallery] = galleryWithCover
|
|||||||
<Header />
|
<Header />
|
||||||
{
|
{
|
||||||
optimizedGallery && (
|
optimizedGallery && (
|
||||||
<Section>
|
<Section className="mt-[var(--header-height)] md:mt-0">
|
||||||
<SwiperWithThumbs
|
<div class="grid w-full grid-cols-1 place-items-start gap-8 lg:grid-cols-2">
|
||||||
swiperClassName="h-[540px]"
|
<MediaSwiperWithThumbs
|
||||||
client:load
|
client:load
|
||||||
images={optimizedGallery}
|
images={optimizedGallery}
|
||||||
thumbs={thumbs}
|
thumbs={thumbs}
|
||||||
/>
|
className="sm:max-w-[400px] lg:max-w-none"
|
||||||
|
imageClassName="aspect-3/4 w-[400px] lg:w-auto object-top mx-auto h-auto"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-1 flex-col gap-8">
|
||||||
|
<h1 class="text-nowrap font-bold">{data.name}</h1>
|
||||||
|
{data.description && <span>{data.description}</span>}
|
||||||
|
<Content />
|
||||||
|
{data.socialMediaLinks && (
|
||||||
|
<div class="coach-card">
|
||||||
|
<h2 class="coach-card-title">{t('coaches.socialMedias.title', data.name)}</h2>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
{data.socialMediaLinks.map(({ icon, ...link }) => (
|
||||||
|
<a {...link}>
|
||||||
|
<SocialMediaIcon
|
||||||
|
type={icon}
|
||||||
|
className="fill-text hover:fill-text-light transition-all duration-300"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<Section>
|
|
||||||
<Content />
|
|
||||||
</Section>
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|||||||
29
src/pages/coaches/styles.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
.coach-grid {
|
||||||
|
@apply grid grid-cols-1 gap-8;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.coach-grid {
|
||||||
|
@apply grid-cols-[repeat(auto-fit,minmax(400px,1fr))];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-card {
|
||||||
|
@apply flex flex-col;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-card-title {
|
||||||
|
@apply mb-6 font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-card-emoji {
|
||||||
|
@apply mx-2 text-2xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-card-list {
|
||||||
|
@apply flex flex-col gap-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coach-card-list > li {
|
||||||
|
@apply flex items-center rounded-2xl bg-card px-3 py-2;
|
||||||
|
}
|
||||||
@@ -2,31 +2,23 @@
|
|||||||
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 { SwiperWithNav } from '@/components/swiper'
|
|
||||||
import Header, { type HeaderProps } 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'
|
||||||
import { getEntry } from 'astro:content'
|
import { getEntry } from 'astro:content'
|
||||||
import CoachCard from '@/components/CoachCard.astro'
|
|
||||||
import ServiceCard from '@/components/ServiceCard.astro'
|
|
||||||
import Marquee from '@/components/Marquee.astro'
|
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 { imageResultToImageAttributes, optimizeSliderImages } from '@/libs/image'
|
import { imageResultToImageAttributes, optimizeSliderImages } from '@/libs/image'
|
||||||
import { getCollection } from 'astro:content'
|
import { getCollection } from 'astro:content'
|
||||||
|
import { Services } from '@/components/services/Services'
|
||||||
|
import { GymGallery } from '@/components/GymGallery'
|
||||||
|
import { Coaches } from '@/components/coaches/Coaches'
|
||||||
|
|
||||||
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, coaches] = await Promise.all([getEntry('pages', 'main'), getCollection('coaches')])
|
const [page, coaches] = await Promise.all([getEntry('pages', 'main'), getCollection('coaches')])
|
||||||
const { slider, services, hero, marquee, reviews, footer } = page?.data || {}
|
const { slider, services, hero, marquee, reviews, footer } = page?.data || {}
|
||||||
@@ -57,7 +49,7 @@ const t = useTranslations(lang)
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={t('home.title')}>
|
<BaseLayout title={t('home.title')}>
|
||||||
<Header lang={lang} navItems={navItems} />
|
<Header lang={lang} />
|
||||||
|
|
||||||
{hero && <Hero {...hero} />}
|
{hero && <Hero {...hero} />}
|
||||||
|
|
||||||
@@ -72,11 +64,7 @@ const t = useTranslations(lang)
|
|||||||
{
|
{
|
||||||
coaches.length > 0 && (
|
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">
|
<Coaches client:load coaches={coaches} lang={lang} />
|
||||||
{coaches.map((coach) => (
|
|
||||||
<CoachCard {...coach} lang={lang} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,14 +77,7 @@ const t = useTranslations(lang)
|
|||||||
description={t('slider.subtitle')}
|
description={t('slider.subtitle')}
|
||||||
className="bg-bg-light"
|
className="bg-bg-light"
|
||||||
>
|
>
|
||||||
<div class="-ml-4 flex w-[calc(100%+(1rem*2))] md:ml-0 md:w-full">
|
<GymGallery client:visible images={optimizedSlider} />
|
||||||
<SwiperWithNav
|
|
||||||
className="h-[540px]"
|
|
||||||
slideClass="object-cover sm:object-contain"
|
|
||||||
client:visible
|
|
||||||
images={optimizedSlider}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -104,11 +85,7 @@ const t = useTranslations(lang)
|
|||||||
{
|
{
|
||||||
services && (
|
services && (
|
||||||
<Section id="services" title={t('services.title')}>
|
<Section id="services" title={t('services.title')}>
|
||||||
<div class="grid place-items-center gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-3">
|
<Services client:visible services={services} />
|
||||||
{services.map((service) => (
|
|
||||||
<ServiceCard {...service} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -116,9 +93,7 @@ const t = useTranslations(lang)
|
|||||||
{
|
{
|
||||||
optimizedReviews && (
|
optimizedReviews && (
|
||||||
<Section id="reviews" title={t('reviews.title')}>
|
<Section id="reviews" title={t('reviews.title')}>
|
||||||
<div class="-mx-4 flex w-[calc(100%+2rem)] md:-mx-16 md:w-[calc(100%+8rem)] lg:-mx-24 lg:w-[calc(100%+12rem)]">
|
<Reviews client:visible reviews={optimizedReviews} lang={lang} />
|
||||||
<Reviews client:visible reviews={optimizedReviews} lang={lang} />
|
|
||||||
</div>
|
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@
|
|||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--header-height: 4rem;
|
--header-height: 4rem;
|
||||||
|
|
||||||
.swiper-pagination-bullet-active {
|
.swiper-pagination-bullet-active {
|
||||||
@apply bg-brand;
|
@apply bg-brand;
|
||||||
}
|
}
|
||||||
@@ -37,6 +36,25 @@
|
|||||||
@apply h-full w-full bg-bg-main text-text;
|
@apply h-full w-full bg-bg-main text-text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
@apply text-4xl;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
@apply text-3xl;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
@apply text-2xl;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
@apply text-xl;
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
@apply text-lg;
|
||||||
|
}
|
||||||
|
h6 {
|
||||||
|
@apply text-base;
|
||||||
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
@@ -58,4 +76,13 @@
|
|||||||
.brand-btn-outline {
|
.brand-btn-outline {
|
||||||
@apply rounded-full border border-brand bg-transparent px-6 py-3 text-center text-text-light transition-colors duration-300 hover:bg-brand;
|
@apply rounded-full border border-brand bg-transparent px-6 py-3 text-center text-text-light transition-colors duration-300 hover:bg-brand;
|
||||||
}
|
}
|
||||||
|
.tag-btn {
|
||||||
|
@apply text-sm hover:tag-btn-active rounded-full bg-card p-2 transition-colors duration-300;
|
||||||
|
}
|
||||||
|
.tag-btn-active {
|
||||||
|
@apply bg-brand text-text-light;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
@apply rounded-3xl bg-card p-4 md:p-6;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,16 +12,19 @@ export default {
|
|||||||
DEFAULT: 'rgba(28, 28, 30, 1)',
|
DEFAULT: 'rgba(28, 28, 30, 1)',
|
||||||
},
|
},
|
||||||
brand: {
|
brand: {
|
||||||
DEFAULT: 'rgba(136,10,145, 1)',
|
DEFAULT: 'rgba(199, 14, 211, 1)',
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
light: '#FFFFFF',
|
light: '#FFFFFF',
|
||||||
DEFAULT: '#AEAEAE',
|
DEFAULT: '#C4C4C4',
|
||||||
},
|
},
|
||||||
border: {
|
border: {
|
||||||
light: '#D9D9D9',
|
light: '#D9D9D9',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
aspectRatio: {
|
||||||
|
'3/4': '3 / 4'
|
||||||
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
marquee: {
|
marquee: {
|
||||||
to: { transform: 'translateX(-50%)' },
|
to: { transform: 'translateX(-50%)' },
|
||||||
|
|||||||