- 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" 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}

View File

@@ -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" />
</> </>
)) ))
} }

View File

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

View File

@@ -11,13 +11,16 @@ 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) && ( (title || description) && (
<div class="flex w-full max-w-[750px] flex-col gap-6 self-center text-center"> <div class="flex w-full flex-col gap-6">
{title && ( {title && (
<h2 class="text-4xl font-bold leading-tight md:text-5xl"> <h2 class="text-4xl font-bold leading-tight md:text-5xl">
<Fragment set:html={title} /> <Fragment set:html={title} />
@@ -28,4 +31,5 @@ const { title, description, className, id } = Astro.props
) )
} }
<slot /> <slot />
</div>
</section> </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 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="/" />
{
navItems && (
<>
<nav class="hidden md:block"> <nav class="hidden md:block">
<ul class="grid grid-flow-col gap-6"> <ul class="grid grid-flow-col gap-6">
{navItems.map((item) => ( {
NAV_ITEMS.map((item) => (
<li> <li>
<a href={item.href}>{t(item.key)}</a> <a href={item.href}>{t(item.key)}</a>
</li> </li>
))} ))
}
</ul> </ul>
</nav> </nav>
<MobileNavigation navItems={navItems} client:visible /> <MobileNavigation navItems={NAV_ITEMS} client:visible />
</>
)
}
</header> </header>
<script> <script>

View File

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

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

View File

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

View File

@@ -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[]
}
export const SimpleSwiper = React.forwardRef<SwiperRef, SimpleSwiperProps>(
({ images, className, modules = [], slideClass, ...otherProps }, ref) => {
return (
<Swiper <Swiper
ref={ref} direction="horizontal"
modules={[Navigation, Pagination, A11y, ...modules]} slidesPerView="auto"
slidesPerView={1} freeMode={true}
pagination={{ spaceBetween={24}
clickable: true, mousewheel={{ enabled: true, forceToAxis: true }}
}} modules={[FreeMode, Mousewheel]}
loop={true} {...props}
watchSlidesProgress={true} className={cn('simple-swiper', className)}
className={cn('h-full w-full', className)}
{...otherProps}
> >
{images.map((image, index) => ( {children}
<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> </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, XL: 720,
_2XL: 864, _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: 'Батыр Сулейманов' 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>
Спортивное питание. Контроль анализов. Рекомендации по приему БАДов и витаминов.
Подготовка к соревнованиям по бодибилдингу. Опыт в подготовке бойцов ММА (весогонка, «витаминная» поддержка).

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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%)' },