опять фиксим слайдеры + верстку

This commit is contained in:
Salam-vsem
2025-05-19 01:27:56 +03:00
parent 042ee91fb2
commit ce1aafa810
18 changed files with 129 additions and 89 deletions

View File

@@ -4,7 +4,7 @@ WORKDIR /app
# Install dependencies first to leverage Docker cache # Install dependencies first to leverage Docker cache
COPY package*.json ./ COPY package*.json ./
RUN npm ci --no-audit --no-fund --production && \ RUN npm ci --no-audit --no-fund --omit=dev && \
npm cache clean --force npm cache clean --force
# Copy source code and build # Copy source code and build

View File

@@ -15,14 +15,22 @@ const t = useTranslations(lang)
--- ---
<footer id="contacts" class="flex flex-col items-center gap-4 text-center md:gap-6"> <footer id="contacts" class="flex flex-col items-center gap-4 text-center md:gap-6">
<div class="flex h-[400px] w-full items-center justify-center bg-bg-light">
<script <script
script-src:unsafe-inline script-src:unsafe-inline
is:inline is:inline
type="text/javascript" type="text/javascript"
charset="utf-8" charset="utf-8"
async async
onload="document.getElementById('ymap-loader').style.display = 'none';"
src=`https://api-maps.yandex.ru/services/constructor/1.0/js/?um=constructor%3A069b17526dc83cbf87c488f13ae089eb3af0f8f74af57e1be82e183434693562&width=100%25&height=400&lang=${lang}&scroll=false` src=`https://api-maps.yandex.ru/services/constructor/1.0/js/?um=constructor%3A069b17526dc83cbf87c488f13ae089eb3af0f8f74af57e1be82e183434693562&width=100%25&height=400&lang=${lang}&scroll=false`
></script> ></script>
<div
id="ymap-loader"
class="h-16 w-16 animate-spin rounded-full border-8 border-brand border-t-transparent"
>
</div>
</div>
<div class="flex flex-col items-center gap-3 px-4 py-6 text-center md:gap-4 md:px-6 md:py-8"> <div class="flex flex-col items-center gap-3 px-4 py-6 text-center md:gap-4 md:px-6 md:py-8">
<Image src={logoSrc} alt="logo" class="h-32 w-auto md:h-48" /> <Image src={logoSrc} alt="logo" class="h-32 w-auto md:h-48" />
<p class="text-light text-sm"> <p class="text-light text-sm">

View File

@@ -1,9 +1,23 @@
import { MediaSwiperWithNav } from './swiper/MediaSwiperWithNav' import { TW_SCREENS } from '@/constants/images'
import { MediaSwiper } from './swiper/MediaSwiper'
export type GymGalleryProps = { export type GymGalleryProps = {
images: ImportAttributes[] images: ImportAttributes[]
} }
export const GymGallery: React.FC<GymGalleryProps> = ({ images }) => { export const GymGallery: React.FC<GymGalleryProps> = ({ images }) => {
return <MediaSwiperWithNav images={images} imageClassName="aspect-3/4 lg:aspect-video" /> return (
<MediaSwiper
breakpoints={{
[TW_SCREENS.MD]: {
slidesPerView: 2,
},
[TW_SCREENS.LG]: {
slidesPerView: 3,
},
}}
images={images}
imageClassName="aspect-3/4 max-w-[50vh] md:max-w-none mx-auto lg:max-h-[calc(90vh-144px)]"
/>
)
} }

View File

@@ -3,35 +3,36 @@ import type { z } from 'astro:content'
import Section from './Section.astro' import Section from './Section.astro'
import { heroSchema } from '@/content.config' import { heroSchema } from '@/content.config'
import { Image } from 'astro:assets' import { Image } from 'astro:assets'
import { cn } from '@/libs/cn'
type Props = z.infer<ReturnType<typeof heroSchema>> type Props = z.infer<ReturnType<typeof heroSchema>> & { className?: string }
const { title, description, cta, image, imageAlt } = Astro.props const { title, description, cta, image, imageAlt, className } = Astro.props
--- ---
<Section <Section id="hero" className={cn('h-full bg-bg-light', className)}>
id="hero" <div
className="mt-[var(--header-height)] p-0 md:mt-0 md:flex-col-reverse md:py-24 lg:flex-row lg:items-center" class="flex h-full w-full flex-col items-center gap-5 pt-[var(--header-height)] md:gap-16 lg:flex-row lg:gap-36"
> >
<div class="px-4 py-8 md:p-0 md:text-center lg:w-1/2 lg:text-start"> <div class="order-2 inline-flex flex-col items-start gap-5">
<h1 class="mb-4 text-4xl font-bold leading-tight lg:text-6xl" set:html={title} /> <h1 class="text-3xl font-bold leading-tight md:text-5xl" set:html={title} />
<p class="text-brand-text mb-6">{description}</p> <p class="text-brand-text">{description}</p>
{ {
cta && ( cta && (
<div class="flex w-full space-x-4"> <a href="#services" class="brand-btn w-full md:w-auto">
<a href="#services" class="brand-btn-outline lg:brand-btn w-full lg:w-auto">
{cta?.title} {cta?.title}
</a> </a>
</div>
) )
} }
</div> </div>
<div class="relative hidden md:block md:h-[400px] lg:w-1/2"> <div class="order-1 h-full w-full grow overflow-hidden rounded-3xl lg:order-3">
<Image <Image
src={image} src={image}
alt={imageAlt} alt={imageAlt}
class="absolute h-full w-full object-contain" class="h-full w-full object-cover object-top"
loading="eager" loading="eager"
/> />
</div> </div>
</div>
<slot />
</Section> </Section>

View File

@@ -5,6 +5,7 @@ import StarIconSrc from '@/assets/icons/StarIcon.svg'
import { useTranslations } from '@/i18n/utils' import { useTranslations } from '@/i18n/utils'
import { SimpleSwiper } from './swiper/SimpleSwiper' import { SimpleSwiper } from './swiper/SimpleSwiper'
import { isAboveBreakpoint } from '@/libs/breakpoint' import { isAboveBreakpoint } from '@/libs/breakpoint'
import { TW_SCREENS } from '@/constants/images'
type Review = z.infer<ReturnType<typeof reviewsSchema>> type Review = z.infer<ReturnType<typeof reviewsSchema>>
type Author = Review['author'] type Author = Review['author']
@@ -29,7 +30,7 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
slidesPerView={1} slidesPerView={1}
freeMode={isMd} freeMode={isMd}
breakpoints={{ breakpoints={{
768: { [TW_SCREENS.MD]: {
slidesPerView: 'auto', slidesPerView: 'auto',
}, },
}} }}

View File

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

View File

@@ -23,12 +23,17 @@ const t = useTranslations(lang)
--- ---
<header <header
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:list={[
'fixed left-0 right-0 top-0 z-50',
'flex h-[var(--header-height)] items-center justify-between gap-20 bg-bg-main px-6 md:justify-start md:px-10',
'transition-transform duration-300',
]}
id="main-header" id="main-header"
transition:persist
> >
<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"> <nav class="hidden flex-1 md:block">
<ul class="grid grid-flow-col gap-6"> <ul class="grid grid-flow-col place-content-end lg:place-content-start gap-10">
{ {
NAV_ITEMS.map((item) => ( NAV_ITEMS.map((item) => (
<li> <li>
@@ -38,6 +43,9 @@ const t = useTranslations(lang)
} }
</ul> </ul>
</nav> </nav>
<a href="/#services" class="brand-btn hidden lg:block">
{t('hero.cta.getStarted')}
</a>
<MobileNavigation navItems={NAV_ITEMS} client:visible /> <MobileNavigation navItems={NAV_ITEMS} client:visible />
</header> </header>
@@ -45,7 +53,7 @@ const t = useTranslations(lang)
let lastScrollY = window.scrollY let lastScrollY = window.scrollY
const header = document.getElementById('main-header') const header = document.getElementById('main-header')
window.addEventListener('scroll', () => { document.addEventListener('scroll', () => {
if (!header) return if (!header) return
const currentScrollY = window.scrollY const currentScrollY = window.scrollY

View File

@@ -5,19 +5,24 @@ import { cn } from '@/libs/cn'
import 'swiper/css' import 'swiper/css'
import 'swiper/css/navigation' import 'swiper/css/navigation'
import 'swiper/css/pagination' import 'swiper/css/pagination'
import {isAboveBreakpoint} from '@/libs/breakpoint'
export type MediaSwiperProps = SwiperProps & { export type MediaSwiperProps = SwiperProps & {
images: ImportAttributes[] images: ImportAttributes[]
imageClassName?: string imageClassName?: string
slideClassName?: string
} }
const isMd = isAboveBreakpoint('MD')
export const MediaSwiper = React.forwardRef<SwiperRef, MediaSwiperProps>( export const MediaSwiper = React.forwardRef<SwiperRef, MediaSwiperProps>(
({ images, className, modules = [], slideClass, imageClassName, ...otherProps }, ref) => { ({ images, className, modules = [], slideClassName, imageClassName, ...otherProps }, ref) => {
return ( return (
<Swiper <Swiper
ref={ref} ref={ref}
modules={[Navigation, Pagination, A11y, ...modules]} modules={[Navigation, Pagination, A11y, ...modules]}
slidesPerView={1} slidesPerView={1}
navigation={isMd}
spaceBetween={24} spaceBetween={24}
pagination={{ pagination={{
clickable: true, clickable: true,
@@ -28,7 +33,7 @@ export const MediaSwiper = React.forwardRef<SwiperRef, MediaSwiperProps>(
{...otherProps} {...otherProps}
> >
{images.map((image, index) => ( {images.map((image, index) => (
<SwiperSlide key={index} className={slideClass}> <SwiperSlide key={index} className={slideClassName}>
<img <img
{...image} {...image}
loading={index === 0 ? 'eager' : 'lazy'} loading={index === 0 ? 'eager' : 'lazy'}
@@ -39,6 +44,7 @@ export const MediaSwiper = React.forwardRef<SwiperRef, MediaSwiperProps>(
imageClassName imageClassName
)} )}
/> />
<div className="swiper-lazy-preloader border-brand"></div>
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>

View File

@@ -1,13 +1,11 @@
import React, { useRef, useState } from 'react' import React, { useState } from 'react'
import { Swiper, SwiperSlide, type SwiperClass, type SwiperRef } from 'swiper/react' import { Swiper, SwiperSlide, type SwiperClass } from 'swiper/react'
import { FreeMode, Navigation, Thumbs } from 'swiper/modules' import { FreeMode, Navigation, Thumbs } from 'swiper/modules'
import 'swiper/css' import 'swiper/css'
import 'swiper/css/navigation' 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 { useNavHandlers } from './hooks'
import { PrevButton, NextButton } from './NavButtons'
import { MediaSwiper, type MediaSwiperProps } from './MediaSwiper' import { MediaSwiper, type MediaSwiperProps } from './MediaSwiper'
type MediaSwiperPropsWithThumbs = MediaSwiperProps & { type MediaSwiperPropsWithThumbs = MediaSwiperProps & {
@@ -19,21 +17,16 @@ export const MediaSwiperWithThumbs: React.FC<MediaSwiperPropsWithThumbs> = ({
...props ...props
}) => { }) => {
const [thumbsSwiper, setThumbsSwiper] = useState<SwiperClass | null>(null) const [thumbsSwiper, setThumbsSwiper] = useState<SwiperClass | null>(null)
const swiperRef = useRef<SwiperRef | null>(null)
const { handleNext, handlePrev } = useNavHandlers(swiperRef)
return ( return (
<div className="grid w-full grid-cols-1 items-center gap-x-10 gap-y-3 lg:grid-cols-[auto_1fr_auto]"> <div className="grid w-full grid-cols-1 items-center gap-y-3">
<PrevButton className="hidden lg:block" onClick={handlePrev} />
<MediaSwiper <MediaSwiper
{...props} {...props}
modules={[Thumbs]} modules={[Thumbs]}
thumbs={{ swiper: thumbsSwiper }} thumbs={{ swiper: thumbsSwiper }}
watchSlidesProgress={true} watchSlidesProgress={true}
ref={swiperRef}
pagination={false} pagination={false}
/> />
<NextButton className="hidden lg:block" onClick={handleNext} />
<Swiper <Swiper
onSwiper={setThumbsSwiper} onSwiper={setThumbsSwiper}
spaceBetween={12} spaceBetween={12}
@@ -42,7 +35,7 @@ export const MediaSwiperWithThumbs: React.FC<MediaSwiperPropsWithThumbs> = ({
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 lg:col-start-2" className="relative h-24 w-full justify-center place-self-start"
> >
{thumbs.map((image, index) => ( {thumbs.map((image, index) => (
<SwiperSlide <SwiperSlide

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -2,7 +2,7 @@
hero: hero:
title: 'ПРЕОБРАЗИТЕ СВОЕ ТЕЛО <br /> <span class="text-brand">В СОВЕРШЕНСТВО</span>' title: 'ПРЕОБРАЗИТЕ СВОЕ ТЕЛО <br /> <span class="text-brand">В СОВЕРШЕНСТВО</span>'
description: 'Профессиональные тренеры The Real Skill помогут достичь ваших целей через персональные тренировки и индивидуальный подход.' description: 'Профессиональные тренеры The Real Skill помогут достичь ваших целей через персональные тренировки и индивидуальный подход.'
image: '../../assets/logo.png' image: './assets/hero.jpg'
imageAlt: 'hero' imageAlt: 'hero'
cta: cta:
title: 'Записаться' title: 'Записаться'

View File

@@ -33,7 +33,7 @@ export const ui = {
'coaches.link': 'Подробнее', 'coaches.link': 'Подробнее',
// Slider Section // Slider Section
'slider.title': 'slider.title':
'ПОСМОТРИТЕ, ГДЕ МЫ <br /> <span class="text-brand">ВЫПОЛНЯЕМ УПРАЖНЕНИЯ</span>', 'НАШ <span class="text-brand">ТРЕНАЖЕРНЫЙ ЗАЛ</span>',
'slider.subtitle': 'Современное оборудование в просторном зале', 'slider.subtitle': 'Современное оборудование в просторном зале',
// Services Section // Services Section
'services.title': 'НАШИ <span class="text-brand">УСЛУГИ</span>', 'services.title': 'НАШИ <span class="text-brand">УСЛУГИ</span>',

View File

@@ -1,6 +1,5 @@
--- ---
import { useTranslations } from '@/i18n/utils' import { useTranslations } from '@/i18n/utils'
import { cn } from '@/libs/cn'
import '@/styles/global.css' import '@/styles/global.css'
import type { TransitionAnimationValue } from 'astro' import type { TransitionAnimationValue } from 'astro'
import { ClientRouter, slide } from 'astro:transitions' import { ClientRouter, slide } from 'astro:transitions'
@@ -33,7 +32,21 @@ const t = useTranslations(Astro.currentLocale)
<title>{`${title} | ${t('seo.og.title')}`}</title> <title>{`${title} | ${t('seo.og.title')}`}</title>
<ClientRouter /> <ClientRouter />
</head> </head>
<body class={cn('flex flex-col', bodyClass)}> <body class={bodyClass}>
<slot /> <slot />
</body> </body>
</html> </html>
<script>
document.addEventListener('astro:after-swap', () => {
const hash = window.location.hash
if (hash) {
setTimeout(() => {
const target = document.querySelector(hash)
if (target) {
target.scrollIntoView({ behavior: 'smooth' })
}
}, 200)
}
})
</script>

View File

@@ -16,7 +16,6 @@ export const optimizeSliderImages = (images: Pick<UnresolvedImageTransform, 'src
alt, alt,
widths: [TW_SCREENS.SM, TW_SCREENS.LG], widths: [TW_SCREENS.SM, TW_SCREENS.LG],
sizes: `(max-width: ${TW_WIDTHS.LG}px) ${TW_SCREENS.SM}px, ${TW_SCREENS.LG}px`, sizes: `(max-width: ${TW_WIDTHS.LG}px) ${TW_SCREENS.SM}px, ${TW_SCREENS.LG}px`,
quality: 80,
format: 'webp', format: 'webp',
loading: 'lazy', loading: 'lazy',
}).then(imageResultToImageAttributes) }).then(imageResultToImageAttributes)

View File

@@ -8,7 +8,7 @@ import Header from '@/components/header/Header.astro'
import './styles.css' import './styles.css'
import { SocialMediaIcon } from '@/components/SocialMediaIcon' import { SocialMediaIcon } from '@/components/SocialMediaIcon'
import { useTranslations } from '@/i18n/utils' import { useTranslations } from '@/i18n/utils'
import { slide, ViewTransitions } from 'astro:transitions' import { slide } from 'astro:transitions'
export const getStaticPaths = async () => { export const getStaticPaths = async () => {
const coaches = await getCollection('coaches') const coaches = await getCollection('coaches')
@@ -39,11 +39,10 @@ const [thumbs, optimizedGallery] = galleryWithCover
<Section className="mt-[var(--header-height)] md:mt-0"> <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"> <div class="grid w-full grid-cols-1 place-items-start gap-8 lg:grid-cols-2">
<MediaSwiperWithThumbs <MediaSwiperWithThumbs
client:load client:only="react"
images={optimizedGallery} images={optimizedGallery}
thumbs={thumbs} thumbs={thumbs}
className="sm:max-w-[400px] lg:max-w-none" imageClassName="aspect-3/4 max-w-[50vh] lg:w-auto object-top mx-auto lg:max-h-[70vh]"
imageClassName="aspect-3/4 w-[400px] lg:w-auto object-top mx-auto h-auto"
/> />
<div class="flex flex-1 flex-col gap-8"> <div class="flex flex-1 flex-col gap-8">
<h1 class="font-bold lg:text-nowrap">{data.name}</h1> <h1 class="font-bold lg:text-nowrap">{data.name}</h1>

View File

@@ -60,7 +60,7 @@ const t = useTranslations(lang)
{ {
marquee && ( marquee && (
<section class="min-h-[105px] overflow-clip bg-brand"> <section class="h-[var(--marquee-height)] overflow-clip bg-brand">
<Marquee texts={marquee} /> <Marquee texts={marquee} />
</section> </section>
) )
@@ -82,7 +82,7 @@ const t = useTranslations(lang)
description={t('slider.subtitle')} description={t('slider.subtitle')}
className="bg-bg-light" className="bg-bg-light"
> >
<GymGallery client:visible images={optimizedSlider} /> <GymGallery client:only="react" images={optimizedSlider} />
</Section> </Section>
) )
} }

View File

@@ -21,9 +21,8 @@
@layer base { @layer base {
:root { :root {
--header-height: 4rem; --header-height: 4rem;
.swiper-pagination-bullet-active { --marquee-height: 105px;
@apply bg-brand; --swiper-theme-color: rgba(198, 15, 211, 1) !important;
}
} }
html { html {

View File

@@ -12,7 +12,7 @@ export default {
DEFAULT: 'rgba(28, 28, 30, 1)', DEFAULT: 'rgba(28, 28, 30, 1)',
}, },
brand: { brand: {
DEFAULT: 'rgba(199, 14, 211, 1)', DEFAULT: 'rgba(198, 15, 211, 1)',
}, },
text: { text: {
light: '#FFFFFF', light: '#FFFFFF',