опять фиксим слайдеры + верстку
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)]"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
<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="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>
|
||||
<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>
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
BIN
src/content/pages/assets/hero.jpg
Normal file
BIN
src/content/pages/assets/hero.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
@@ -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: 'Записаться'
|
||||
|
||||
@@ -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>',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user