- one more time rework slider

- rework design system
- fill trainer page
This commit is contained in:
Salam-vsem
2025-05-16 15:08:36 +03:00
parent 2c1ebe5dae
commit 24899dc686
46 changed files with 576 additions and 274 deletions

View File

@@ -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>

View 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} />
}

View File

@@ -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"
>
<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>
{
cta && (
@@ -26,7 +26,7 @@ const { title, description, cta, image, imageAlt } = Astro.props
)
}
</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
src={image}
alt={imageAlt}

View File

@@ -16,7 +16,7 @@ const repeatedTexts =
repeatedTexts.map((text) => (
<>
<h2>{text}</h2>
<Image src={wideCrossIconSrc} alt="cross-icon" />
<Image src={wideCrossIconSrc} alt="cross-icon" loading="eager" />
</>
))
}

View File

@@ -1,11 +1,9 @@
import type { reviewsSchema } from '@/content.config'
import type { z } from 'astro:content'
import { Swiper, SwiperSlide } from 'swiper/react'
import 'swiper/css'
import 'swiper/css/scrollbar'
import { Mousewheel, Scrollbar } from 'swiper/modules'
import { SwiperSlide } from 'swiper/react'
import StarIconSrc from '@/assets/icons/StarIcon.svg'
import { useTranslations } from '@/i18n/utils'
import { SimpleSwiper } from './swiper/SimpleSwiper'
type Review = z.infer<ReturnType<typeof reviewsSchema>>
type Author = Review['author']
@@ -19,30 +17,25 @@ type ReviewsProps = {
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'
const swiperSlideClassName =
'!h-auto first:ml-0 last:mr-0 md:max-w-[600px] md:first:ml-24 md:last:mr-24'
const swiperSlideClassName = '!h-auto md:max-w-[600px]'
export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
const t = useTranslations(lang)
return (
<Swiper
modules={[Scrollbar, Mousewheel]}
<SimpleSwiper
slidesPerView={1}
spaceBetween={24}
scrollbar={{
hide: true,
draggable: true,
}}
freeMode={false}
breakpoints={{
768: {
slidesPerView: 'auto',
freeMode: true,
},
}}
>
{reviews.map(({ review, author }, index) => (
<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">
{[...Array(5)].map((_, i) => (
<img
@@ -77,7 +70,7 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
</SwiperSlide>
))}
<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
href={REVIEWS_LINK}
target="_blank"
@@ -99,6 +92,6 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
</a>
</div>
</SwiperSlide>
</Swiper>
</SimpleSwiper>
)
}

View File

@@ -11,21 +11,25 @@ type Props = {
const { title, description, className, id } = Astro.props
---
<section
class={cn('flex flex-col gap-6 py-12 px-4 md:py-24 md:px-16 lg:px-24 md:gap-12', className)}
id={id}
>
{
(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">
<Fragment set:html={title} />
</h2>
)}
{description && <p>{description}</p>}
</div>
)
}
<slot />
<section class="w-full relative min-h-fit overflow-hidden" id={id}>
<div
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 flex-col gap-6">
{title && (
<h2 class="text-4xl font-bold leading-tight md:text-5xl">
<Fragment set:html={title} />
</h2>
)}
{description && <p>{description}</p>}
</div>
)
}
<slot />
</div>
</section>

View File

@@ -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>

View 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>
)
}

View 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>
)
}

View File

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

View File

@@ -66,16 +66,20 @@ const MobileNavigation: React.FC<MobileNavigationProps> = ({ navItems }) => {
<AnimatedBurgerButton isOpen={isOpen} onClick={toggleMenu} />
<div
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',
isOpen ? 'translate-y-0' : '-translate-y-full'
'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-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) => (
<a key={item.key} href={item.href} className="text-3xl" onClick={toggleMenu}>
{t(item.key as any)}
</a>
))}
<hr />
<a href="/#services" className="brand-btn w-full" onClick={toggleMenu}>
{t('hero.cta.getStarted')}
</a>
</nav>
</div>
</div>

View 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>
)
}

View 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>
)
}

View 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'

View File

@@ -5,16 +5,16 @@ import 'swiper/css/navigation'
import 'swiper/css/pagination'
import { NextButton, PrevButton } from './NavButtons'
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 { 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} />
<MediaSwiper {...props} ref={swiperRef} />
<NextButton className="hidden md:block" onClick={handleNext} />
</div>
)

View File

@@ -6,35 +6,34 @@ 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'
import { MediaSwiper, type MediaSwiperProps } from './MediaSwiper'
type SwiperProps = {
images: ImportAttributes[]
type MediaSwiperPropsWithThumbs = MediaSwiperProps & {
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 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
<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 lg:block" onClick={handlePrev} />
<MediaSwiper
{...props}
modules={[Thumbs]}
thumbs={{ swiper: thumbsSwiper }}
watchSlidesProgress={true}
className={cn(swiperClassName)}
images={images}
ref={swiperRef}
pagination={false}
/>
<NextButton className="hidden md:block" onClick={handleNext} />
<NextButton className="hidden lg:block" onClick={handleNext} />
<Swiper
onSwiper={setThumbsSwiper}
spaceBetween={12}
@@ -43,7 +42,7 @@ export const SwiperWithThumbs: React.FC<SwiperProps> = ({ images, thumbs, swiper
watchSlidesProgress={true}
modules={[Navigation, Thumbs, FreeMode]}
// !!! 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) => (
<SwiperSlide
@@ -54,7 +53,7 @@ export const SwiperWithThumbs: React.FC<SwiperProps> = ({ images, thumbs, swiper
{...image}
loading="lazy"
decoding="async"
className="h-full w-full object-cover"
className="h-full w-full rounded-3xl object-cover"
/>
</SwiperSlide>
))}

View File

@@ -1,43 +1,21 @@
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 { FreeMode, Mousewheel } from 'swiper/modules'
import { Swiper, type SwiperProps } from 'swiper/react'
import 'swiper/css'
import 'swiper/css/navigation'
import 'swiper/css/pagination'
import 'swiper/css/free-mode'
import './simple-swiper.css'
import { cn } from '@/libs/cn'
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>
)
}
export const SimpleSwiper: React.FC<SwiperProps> = ({ children, className, ...props }) => (
<Swiper
direction="horizontal"
slidesPerView="auto"
freeMode={true}
spaceBetween={24}
mousewheel={{ enabled: true, forceToAxis: true }}
modules={[FreeMode, Mousewheel]}
{...props}
className={cn('simple-swiper', className)}
>
{children}
</Swiper>
)
SimpleSwiper.displayName = 'SimpleSwiper'

View File

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

View File

@@ -0,0 +1,4 @@
.simple-swiper {
width: 100%;
overflow: visible;
}

View File

@@ -20,4 +20,4 @@ export const TW_SCREENS = {
XL: 720,
_2XL: 864,
},
} as const;
} as const

View 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>

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -4,36 +4,65 @@ 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'
- './assets/batyr-slide-1.JPG'
socialMediaLinks:
- icon: 'instagram'
href: 'https://www.instagram.com/therealbatyr?igsh=MXZkMHdsMDgxM3c4bA%3D%3D&utm_source=qr'
target: '_blank'
- 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
🏆ВИЦЕ-ЧЕМПИОН
MUSCLE MULISHA GRANDPRIX 2023
🏆TOP-5 DUBAI PRO 2024
Профессиональный выступающий спортсмен и тренер.
Опыт работы в фитнес индустрии более 12 лет.
Образование:
1. Школа бодибилдинга и фитнеса им. Б. Вейдера 2014г.
2. Ассоциация профессионалов фитнеса (FPA) 2015г.
Специализация:
Строительство тела (набор мышечной массы, сжигание подкожно-жировой клетчатки без вреда для организма), составление плана и методики тренировочного процесса, разработка и контроль рациона питания (БЖУ индивидуально под каждого подопечного).
Спортивное питание. Контроль анализов. Рекомендации по приему БАДов и витаминов.
Подготовка к соревнованиям по бодибилдингу. Опыт в подготовке бойцов ММА (весогонка, «витаминная» поддержка).
<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> Спортивное питание</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>
<li><span class="coach-card-emoji">🥊</span> Опыт в подготовке бойцов ММА (весогонка, «витаминная» поддержка)</li>
</ul>
</div>
</div>

View File

@@ -45,7 +45,7 @@ slider:
- image: './assets/slider/IMG_6520.jpg'
imageAlt: 'seventeenth-slide'
services:
- title: 'Разовые Персональные тренировки с тренером'
- title: 'Тренировка с тренером'
link:
title: 'Записаться'
href: 'https://wa.me/message/JKEYZFZBLQBIF1'

View File

@@ -46,6 +46,7 @@ export const ui = {
// Footer
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
'footer.copyright': 'Все права защищены. Политика конфиденциальности.',
'coaches.socialMedias.title': (name: string) => `${name} в соцсетях`,
},
en: {
'seo.meta.description':
@@ -86,5 +87,6 @@ export const ui = {
// Footer
'footer.logo': 'THEREAL<span class="text-brand">SKILL</span>',
'footer.copyright': 'All rights reserved. Privacy Policy.',
'coaches.socialMedias.title': (name: string) => `${name} in social medias`,
},
} as const

View File

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

View File

@@ -2,7 +2,7 @@
import { useTranslations } from '@/i18n/utils'
import { cn } from '@/libs/cn'
import '@/styles/global.css'
import { ClientRouter } from 'astro:transitions'
import { ClientRouter, slide } from 'astro:transitions'
type Props = {
title: string
@@ -14,7 +14,7 @@ const t = useTranslations(Astro.currentLocale)
---
<!doctype html>
<html lang={Astro.currentLocale}>
<html lang={Astro.currentLocale} transition:animate={slide({ duration: '0.3s' })}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />

View File

@@ -3,8 +3,12 @@ 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 { MediaSwiperWithThumbs } from '@/components/swiper/MediaSwiperWithThumbs'
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 () => {
const coaches = await getCollection('coaches')
@@ -13,6 +17,7 @@ export const getStaticPaths = async () => {
const { data, render } = Astro.props
const { gallery, cover } = data
const t = useTranslations(Astro.currentLocale)
const { Content } = await render()
const galleryWithCover = gallery ? [cover, ...gallery] : undefined
const [thumbs, optimizedGallery] = galleryWithCover
@@ -31,18 +36,37 @@ const [thumbs, optimizedGallery] = galleryWithCover
<Header />
{
optimizedGallery && (
<Section>
<SwiperWithThumbs
swiperClassName="h-[540px]"
client:load
images={optimizedGallery}
thumbs={thumbs}
/>
<Section className="mt-[var(--header-height)] md:mt-0">
<div class="grid w-full grid-cols-1 place-items-start gap-8 lg:grid-cols-2">
<MediaSwiperWithThumbs
client:load
images={optimizedGallery}
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>
<Content />
</Section>
</BaseLayout>

View 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;
}

View File

@@ -2,31 +2,23 @@
import { useTranslations } from '@/i18n/utils'
import BaseLayout from '../layouts/BaseLayout.astro'
import Section from '@/components/Section.astro'
import { SwiperWithNav } from '@/components/swiper'
import Header, { type HeaderProps } from '@/components/header/Header.astro'
import Footer from '@/components/Footer.astro'
import type { CollectionEntry } from 'astro:content'
import { getEntry } from 'astro:content'
import CoachCard from '@/components/CoachCard.astro'
import ServiceCard from '@/components/ServiceCard.astro'
import Marquee from '@/components/Marquee.astro'
import { Reviews, type OptimizedReview } from '@/components/Reviews.tsx'
import Hero from '@/components/Hero.astro'
import { getImage } from 'astro:assets'
import { imageResultToImageAttributes, optimizeSliderImages } from '@/libs/image'
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']
const navItems: HeaderProps['navItems'] = [
{ key: 'nav.coaches', href: '#coaches' },
{ key: 'nav.slider', href: '#image-slider' },
{ key: 'nav.services', href: '#services' },
{ key: 'nav.reviews', href: '#reviews' },
{ key: 'nav.contact', href: '#contacts' },
]
const { lang } = Astro.params
const [page, coaches] = await Promise.all([getEntry('pages', 'main'), getCollection('coaches')])
const { slider, services, hero, marquee, reviews, footer } = page?.data || {}
@@ -57,7 +49,7 @@ const t = useTranslations(lang)
---
<BaseLayout title={t('home.title')}>
<Header lang={lang} navItems={navItems} />
<Header lang={lang} />
{hero && <Hero {...hero} />}
@@ -72,11 +64,7 @@ const t = useTranslations(lang)
{
coaches.length > 0 && (
<Section id="coaches" title={t('coaches.title')} description={t('coaches.subtitle')}>
<div class="grid place-items-center gap-4 sm:grid-cols-2 md:gap-8 lg:grid-cols-3">
{coaches.map((coach) => (
<CoachCard {...coach} lang={lang} />
))}
</div>
<Coaches client:load coaches={coaches} lang={lang} />
</Section>
)
}
@@ -89,14 +77,7 @@ const t = useTranslations(lang)
description={t('slider.subtitle')}
className="bg-bg-light"
>
<div class="-ml-4 flex w-[calc(100%+(1rem*2))] md:ml-0 md:w-full">
<SwiperWithNav
className="h-[540px]"
slideClass="object-cover sm:object-contain"
client:visible
images={optimizedSlider}
/>
</div>
<GymGallery client:visible images={optimizedSlider} />
</Section>
)
}
@@ -104,11 +85,7 @@ const t = useTranslations(lang)
{
services && (
<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.map((service) => (
<ServiceCard {...service} />
))}
</div>
<Services client:visible services={services} />
</Section>
)
}
@@ -116,9 +93,7 @@ const t = useTranslations(lang)
{
optimizedReviews && (
<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} />
</div>
<Reviews client:visible reviews={optimizedReviews} lang={lang} />
</Section>
)
}

View File

@@ -21,7 +21,6 @@
@layer base {
:root {
--header-height: 4rem;
.swiper-pagination-bullet-active {
@apply bg-brand;
}
@@ -37,6 +36,25 @@
@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,
h2,
h3,
@@ -58,4 +76,13 @@
.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;
}
.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;
}
}