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

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
COPY package*.json ./
RUN npm ci --no-audit --no-fund --production && \
RUN npm ci --no-audit --no-fund --omit=dev && \
npm cache clean --force
# 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">
<div class="flex h-[400px] w-full items-center justify-center bg-bg-light">
<script
script-src:unsafe-inline
is:inline
type="text/javascript"
charset="utf-8"
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`
></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">
<Image src={logoSrc} alt="logo" class="h-32 w-auto md:h-48" />
<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 = {
images: ImportAttributes[]
}
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 { heroSchema } from '@/content.config'
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
id="hero"
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 lg:text-6xl" set:html={title} />
<p class="text-brand-text mb-6">{description}</p>
<Section id="hero" className={cn('h-full bg-bg-light', className)}>
<div
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="order-2 inline-flex flex-col items-start gap-5">
<h1 class="text-3xl font-bold leading-tight md:text-5xl" set:html={title} />
<p class="text-brand-text">{description}</p>
{
cta && (
<div class="flex w-full space-x-4">
<a href="#services" class="brand-btn-outline lg:brand-btn w-full lg:w-auto">
<a href="#services" class="brand-btn w-full md:w-auto">
{cta?.title}
</a>
</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
src={image}
alt={imageAlt}
class="absolute h-full w-full object-contain"
class="h-full w-full object-cover object-top"
loading="eager"
/>
</div>
</div>
<slot />
</Section>

View File

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

View File

@@ -11,18 +11,18 @@ type Props = {
const { title, description, className, id } = Astro.props
---
<section id={id}>
<div
<section
id={id}
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
)}
>
>
{
(title || description) && (
<div class="flex w-full flex-col gap-6">
<div class="flex w-full flex-col gap-4 md:gap-5">
{title && (
<h2 class="text-4xl font-bold leading-tight md:text-5xl">
<h2 class="text-4xl font-bold leading-tight">
<Fragment set:html={title} />
</h2>
)}
@@ -31,5 +31,4 @@ const { title, description, className, id } = Astro.props
)
}
<slot />
</div>
</section>

View File

@@ -23,12 +23,17 @@ const t = useTranslations(lang)
---
<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"
transition:persist
>
<a class="text-2xl font-bold text-text-light" set:html={t('header.logo')} href="/" />
<nav class="hidden md:block">
<ul class="grid grid-flow-col gap-6">
<nav class="hidden flex-1 md:block">
<ul class="grid grid-flow-col place-content-end lg:place-content-start gap-10">
{
NAV_ITEMS.map((item) => (
<li>
@@ -38,6 +43,9 @@ const t = useTranslations(lang)
}
</ul>
</nav>
<a href="/#services" class="brand-btn hidden lg:block">
{t('hero.cta.getStarted')}
</a>
<MobileNavigation navItems={NAV_ITEMS} client:visible />
</header>
@@ -45,7 +53,7 @@ const t = useTranslations(lang)
let lastScrollY = window.scrollY
const header = document.getElementById('main-header')
window.addEventListener('scroll', () => {
document.addEventListener('scroll', () => {
if (!header) return
const currentScrollY = window.scrollY

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

View File

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

View File

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

View File

@@ -1,6 +1,5 @@
---
import { useTranslations } from '@/i18n/utils'
import { cn } from '@/libs/cn'
import '@/styles/global.css'
import type { TransitionAnimationValue } from 'astro'
import { ClientRouter, slide } from 'astro:transitions'
@@ -33,7 +32,21 @@ const t = useTranslations(Astro.currentLocale)
<title>{`${title} | ${t('seo.og.title')}`}</title>
<ClientRouter />
</head>
<body class={cn('flex flex-col', bodyClass)}>
<body class={bodyClass}>
<slot />
</body>
</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,
widths: [TW_SCREENS.SM, TW_SCREENS.LG],
sizes: `(max-width: ${TW_WIDTHS.LG}px) ${TW_SCREENS.SM}px, ${TW_SCREENS.LG}px`,
quality: 80,
format: 'webp',
loading: 'lazy',
}).then(imageResultToImageAttributes)

View File

@@ -8,7 +8,7 @@ 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'
import { slide } from 'astro:transitions'
export const getStaticPaths = async () => {
const coaches = await getCollection('coaches')
@@ -39,11 +39,10 @@ const [thumbs, optimizedGallery] = galleryWithCover
<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
client:only="react"
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"
imageClassName="aspect-3/4 max-w-[50vh] lg:w-auto object-top mx-auto lg:max-h-[70vh]"
/>
<div class="flex flex-1 flex-col gap-8">
<h1 class="font-bold lg:text-nowrap">{data.name}</h1>

View File

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

View File

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

View File

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