fill most content + improve ux
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 749 B |
4
src/assets/icons/WideCrossIcon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="50" height="16" viewBox="0 0 50 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="49.4934" height="4.23718" transform="matrix(0.975813 -0.218607 0.297462 0.954734 0 11.2129)" fill="#FBFBFB"/>
|
||||
<rect width="49.4934" height="4.23718" transform="matrix(0.975813 0.218607 -0.297462 0.954734 1.52344 0.462891)" fill="#FBFBFB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
BIN
src/assets/logo.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
@@ -1,22 +1,28 @@
|
||||
---
|
||||
import type { coachesSchema } from '@/content.config'
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
import { Image } from 'astro:assets'
|
||||
import type { z } from 'astro:content'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
description?: string
|
||||
image: ImageMetadata
|
||||
imageAlt: string
|
||||
}
|
||||
type Props = z.infer<ReturnType<typeof coachesSchema>> & { lang?: string }
|
||||
|
||||
const { name, description, image, imageAlt } = Astro.props
|
||||
const { lang, name, description, image, imageAlt, link } = Astro.props
|
||||
const t = useTranslations(lang)
|
||||
---
|
||||
|
||||
<div class="flex h-full w-full flex-col items-center justify-start gap-4">
|
||||
<div class="relative h-[390px] w-[300px]">
|
||||
<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={link} target="_blank">
|
||||
<Image src={image} class="absolute h-full w-full object-cover" alt={imageAlt} />
|
||||
</div>
|
||||
</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>}
|
||||
{
|
||||
link && (
|
||||
<a href={link} target="_blank" class="text-brand hover:text-brand/80 mt-2">
|
||||
{t('coaches.link')}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
29
src/components/Footer.astro
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
import { Image } from 'astro:assets'
|
||||
import logoSrc from '@/assets/logo.png'
|
||||
|
||||
type Props = {
|
||||
lang?: string
|
||||
}
|
||||
|
||||
const { lang } = Astro.props
|
||||
const t = useTranslations(lang)
|
||||
---
|
||||
|
||||
<footer id="contacts" class="flex flex-col items-center gap-6 text-center">
|
||||
<script
|
||||
is:inline
|
||||
type="text/javascript"
|
||||
charset="utf-8"
|
||||
async
|
||||
src="https://api-maps.yandex.ru/services/constructor/1.0/js/?um=constructor%3A069b17526dc83cbf87c488f13ae089eb3af0f8f74af57e1be82e183434693562&width=100%25&height=400&lang=ru_RU&scroll=true"
|
||||
></script>
|
||||
<div class="flex flex-col items-center gap-4 px-6 py-8 text-center">
|
||||
<Image src={logoSrc} alt="logo" class="h-32 w-auto md:h-48" />
|
||||
<p class="text-light text-sm">
|
||||
© {new Date().getFullYear()}
|
||||
{t('footer.copyright')}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
29
src/components/Hero.astro
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import type { z } from 'astro:content'
|
||||
import Section from './Section.astro'
|
||||
import { heroSchema } from '@/content.config'
|
||||
import { Image } from 'astro:assets'
|
||||
|
||||
type Props = z.infer<ReturnType<typeof heroSchema>>
|
||||
|
||||
const { title, description, cta, image, imageAlt } = Astro.props
|
||||
---
|
||||
|
||||
<Section id="hero" className="mt-16 p-0 md:mt-0 md:flex-row md:p-0">
|
||||
<div class="mb-8 px-6 py-10 md:mb-0 md:w-1/2 md:p-24">
|
||||
<h1 class="mb-4 text-4xl font-bold leading-tight md:text-6xl" set:html={title} />
|
||||
<p class="text-brand-text mb-6">{description}</p>
|
||||
{
|
||||
cta && (
|
||||
<div class="flex w-full space-x-4">
|
||||
<a href="#services" class="brand-btn w-full md:w-auto">
|
||||
{cta?.title}
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div class="relative hidden md:block md:w-1/2">
|
||||
<Image src={image} alt={imageAlt} class="absolute h-full w-full object-cover" />
|
||||
</div>
|
||||
</Section>
|
||||
@@ -1,12 +1,23 @@
|
||||
---
|
||||
import { Image } from 'astro:assets'
|
||||
import wideCrossIconSrc from '@/assets/icons/WideCrossIcon.svg'
|
||||
|
||||
type Props = {
|
||||
texts: string[]
|
||||
}
|
||||
|
||||
const { texts } = Astro.props
|
||||
const repeatedTexts = texts.length < 10 ? Array.from({ length: 10 / texts.length }, () => texts).flat() : texts;
|
||||
const repeatedTexts =
|
||||
texts.length < 10 ? Array.from({ length: 10 / texts.length }, () => texts).flat() : texts
|
||||
---
|
||||
|
||||
<div class="flex h-full w-max animate-marquee items-center gap-10 text-5xl font-bold uppercase">
|
||||
{repeatedTexts.map((text) => <h2>{text}</h2>)}
|
||||
{
|
||||
repeatedTexts.map((text) => (
|
||||
<>
|
||||
<h2>{text}</h2>
|
||||
<Image src={wideCrossIconSrc} alt="cross-icon" />
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -23,7 +23,7 @@ const { title, charachteristics, highlighted = false, link } = Astro.props
|
||||
charachteristics?.map((charachteristic) => (
|
||||
<li class="flex items-start space-x-2 justify-between">
|
||||
<span class="text-text-light">{charachteristic.text}</span>
|
||||
{charachteristic.price && <span class="ml-2 text-brand text-nowrap">{charachteristic.price}</span>}
|
||||
{charachteristic.price && <span class="ml-2 text-brand text-nowrap text-end" set:html={charachteristic.price} />}
|
||||
</li>
|
||||
))
|
||||
}
|
||||
|
||||
60
src/components/header/Header.astro
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
import MobileNavigation from './MobileNavigation'
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
|
||||
type Props = {
|
||||
lang?: string
|
||||
}
|
||||
|
||||
const { lang } = Astro.props
|
||||
|
||||
const 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' },
|
||||
] as const
|
||||
|
||||
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"
|
||||
id="main-header"
|
||||
>
|
||||
<a class="text-2xl font-bold text-text-light" set:html={t('header.logo')} href="#hero" />
|
||||
<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 />
|
||||
</header>
|
||||
|
||||
<script>
|
||||
let lastScrollY = window.scrollY
|
||||
const header = document.getElementById('main-header')
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!header) return
|
||||
|
||||
const currentScrollY = window.scrollY
|
||||
|
||||
if (Math.abs(currentScrollY - lastScrollY) < 10) return
|
||||
|
||||
if (currentScrollY > lastScrollY && currentScrollY > 100) {
|
||||
header.style.transform = 'translateY(-100%)'
|
||||
} else {
|
||||
header.style.transform = 'translateY(0)'
|
||||
}
|
||||
|
||||
lastScrollY = currentScrollY
|
||||
})
|
||||
</script>
|
||||
@@ -1,28 +1,46 @@
|
||||
import { defineCollection, z, type ImageFunction } from 'astro:content'
|
||||
|
||||
export const heroSchema = (image: ImageFunction) =>
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
image: image(),
|
||||
imageAlt: z.string(),
|
||||
cta: z
|
||||
.object({
|
||||
title: z.string(),
|
||||
href: z.string(),
|
||||
target: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const coachesSchema = (image: ImageFunction) =>
|
||||
z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
image: image(),
|
||||
imageAlt: z.string(),
|
||||
link: z.string().optional(),
|
||||
})
|
||||
|
||||
export const servicesSchema = z.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
price: z.string(),
|
||||
description: z.string().optional(),
|
||||
price: z.string().optional(),
|
||||
link: z.object({
|
||||
title: z.string(),
|
||||
href: z.string(),
|
||||
target: z.string().optional(),
|
||||
}),
|
||||
charachteristics: z.array(
|
||||
charachteristics: z
|
||||
.array(
|
||||
z.object({
|
||||
text: z.string(),
|
||||
price: z.string().optional(),
|
||||
})
|
||||
),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const reviewsSchema = (image: ImageFunction) =>
|
||||
@@ -40,21 +58,7 @@ export const reviewsSchema = (image: ImageFunction) =>
|
||||
const pages = defineCollection({
|
||||
schema: ({ image }) =>
|
||||
z.object({
|
||||
hero: z
|
||||
.object({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
image: image(),
|
||||
imageAlt: z.string(),
|
||||
cta: z
|
||||
.object({
|
||||
title: z.string(),
|
||||
href: z.string(),
|
||||
target: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.nullable(),
|
||||
hero: heroSchema(image).optional(),
|
||||
marquee: z.array(z.string()).optional(),
|
||||
coaches: z.array(coachesSchema(image)).optional(),
|
||||
reviews: z.array(reviewsSchema(image)).optional(),
|
||||
|
||||
BIN
src/content/pages/assets/coaches/1.jpeg
Normal file
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 107 KiB |
BIN
src/content/pages/assets/coaches/2.jpeg
Normal file
|
After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 63 KiB |
BIN
src/content/pages/assets/slider/IMG_6491.jpg
Normal file
|
After Width: | Height: | Size: 2.8 MiB |
BIN
src/content/pages/assets/slider/IMG_6492.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
BIN
src/content/pages/assets/slider/IMG_6493.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
src/content/pages/assets/slider/IMG_6494.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
src/content/pages/assets/slider/IMG_6495.jpg
Normal file
|
After Width: | Height: | Size: 2.7 MiB |
BIN
src/content/pages/assets/slider/IMG_6496.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
src/content/pages/assets/slider/IMG_6498.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/content/pages/assets/slider/IMG_6499.jpg
Normal file
|
After Width: | Height: | Size: 2.3 MiB |
BIN
src/content/pages/assets/slider/IMG_6500.jpg
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
src/content/pages/assets/slider/IMG_6506.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/content/pages/assets/slider/IMG_6507.jpg
Normal file
|
After Width: | Height: | Size: 887 KiB |
BIN
src/content/pages/assets/slider/IMG_6509.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/content/pages/assets/slider/IMG_6510.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/content/pages/assets/slider/IMG_6514.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
src/content/pages/assets/slider/IMG_6515.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/content/pages/assets/slider/IMG_6518.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
src/content/pages/assets/slider/IMG_6520.jpg
Normal file
|
After Width: | Height: | Size: 808 KiB |
@@ -8,67 +8,101 @@ hero:
|
||||
title: 'Записаться'
|
||||
href: '#services'
|
||||
marquee:
|
||||
- ВОТ И ВСЕ ЕБАТЬ
|
||||
- ТЫ СЛАБ
|
||||
- ДОСТИГАЙТЕ ЦЕЛЕЙ
|
||||
- БУДЬТЕ СИЛЬНЕЕ
|
||||
- ТРЕНИРУЙТЕСЬ С НАМИ
|
||||
- МЕНЯЙТЕСЬ К ЛУЧШЕМУ
|
||||
- СТАНОВИТЕСЬ ЗДОРОВЕЕ
|
||||
slider:
|
||||
- image: './assets/slider/1.jpeg'
|
||||
- image: './assets/slider/IMG_6491.jpg'
|
||||
imageAlt: 'first-slide'
|
||||
- image: './assets/slider/2.jpeg'
|
||||
- image: './assets/slider/IMG_6492.jpg'
|
||||
imageAlt: 'second-slide'
|
||||
- image: './assets/slider/3.jpeg'
|
||||
- image: './assets/slider/IMG_6493.jpg'
|
||||
imageAlt: 'third-slide'
|
||||
- image: './assets/slider/IMG_6494.jpg'
|
||||
imageAlt: 'fourth-slide'
|
||||
- image: './assets/slider/IMG_6495.jpg'
|
||||
imageAlt: 'fifth-slide'
|
||||
- image: './assets/slider/IMG_6496.jpg'
|
||||
imageAlt: 'sixth-slide'
|
||||
- image: './assets/slider/IMG_6498.jpg'
|
||||
imageAlt: 'seventh-slide'
|
||||
- image: './assets/slider/IMG_6499.jpg'
|
||||
imageAlt: 'eighth-slide'
|
||||
- image: './assets/slider/IMG_6500.jpg'
|
||||
imageAlt: 'ninth-slide'
|
||||
- image: './assets/slider/IMG_6506.jpg'
|
||||
imageAlt: 'tenth-slide'
|
||||
- image: './assets/slider/IMG_6507.jpg'
|
||||
imageAlt: 'eleventh-slide'
|
||||
- image: './assets/slider/IMG_6509.jpg'
|
||||
imageAlt: 'twelfth-slide'
|
||||
- image: './assets/slider/IMG_6510.jpg'
|
||||
imageAlt: 'thirteenth-slide'
|
||||
- image: './assets/slider/IMG_6514.jpg'
|
||||
imageAlt: 'fourteenth-slide'
|
||||
- image: './assets/slider/IMG_6515.jpg'
|
||||
imageAlt: 'fifteenth-slide'
|
||||
- image: './assets/slider/IMG_6518.jpg'
|
||||
imageAlt: 'sixteenth-slide'
|
||||
- image: './assets/slider/IMG_6520.jpg'
|
||||
imageAlt: 'seventeenth-slide'
|
||||
coaches:
|
||||
- image: './assets/coaches/1.jpg'
|
||||
- image: './assets/coaches/1.jpeg'
|
||||
imageAlt: 'first-coach'
|
||||
name: 'Иванов Иван Иванович'
|
||||
description: 'Тренер'
|
||||
- image: './assets/coaches/2.jpg'
|
||||
name: 'БАТЫР СУЛЕЙМАНОВ'
|
||||
description: 'Профессиональный выступающий спортсмен и тренер. Опыт работы в фитнес индустрии более 12 лет'
|
||||
link: 'https://t.me/therealskill_studio/4'
|
||||
- image: './assets/coaches/2.jpeg'
|
||||
imageAlt: 'second-coach'
|
||||
name: 'Петров Петр Петрович'
|
||||
description: 'Тренер'
|
||||
- image: './assets/coaches/3.jpg'
|
||||
imageAlt: 'third-coach'
|
||||
name: 'Сидоров Сидор Сидорович'
|
||||
name: 'СУЛЕЙМАНОВА АЛЕКСАНДРА'
|
||||
description: 'Спортсмен и тренер. Опыт работы в фитнес индустрии 9 лет'
|
||||
link: 'https://t.me/therealskill_studio/7'
|
||||
services:
|
||||
- title: 'Пробный день'
|
||||
description: 'Идеально подходит для знакомства с нашим залом и услугами'
|
||||
price: '1500 ₽'
|
||||
- title: 'Разовые Персональные тренировки с тренером'
|
||||
link:
|
||||
title: 'Начать'
|
||||
title: 'Записаться'
|
||||
href: '/checkout/day-pass'
|
||||
charachteristics:
|
||||
- text: 'Доступ ко всему оборудованию'
|
||||
price: '500 ₽'
|
||||
- text: 'Персональный шкафчик и душ'
|
||||
price: '300 ₽'
|
||||
- text: 'Базовая оценка физической подготовки'
|
||||
price: '700 ₽'
|
||||
- title: 'Месячный абонемент'
|
||||
description: 'Гибкая программа тренировок на месяц'
|
||||
price: '5900 ₽'
|
||||
- text: 'Разовая тренировка'
|
||||
price: '5 000 ₽'
|
||||
- text: 'Разовая СПЛИТ (2 человека)'
|
||||
price: '8 000 ₽'
|
||||
- text: 'Блок из 5 тренировок'
|
||||
price: '22 500 ₽'
|
||||
- text: 'Блок из 5 тренировок СПЛИТ'
|
||||
price: '38 500 ₽'
|
||||
- text: 'Блок из 10 тренировок'
|
||||
price: '42 500 ₽'
|
||||
- text: 'Блок из 10 тренировок СПЛИТ'
|
||||
price: '75 000 ₽'
|
||||
- text: 'Блок из 15 тренировок'
|
||||
price: '60 000 ₽'
|
||||
- text: 'Блок из 15 тренировок СПЛИТ'
|
||||
price: '110 000 ₽'
|
||||
- title: 'Безлимитные Абонементы на месяц'
|
||||
description: '30 календарных дней занятий с тренером'
|
||||
link:
|
||||
title: 'Оформить'
|
||||
title: 'Записаться'
|
||||
href: '/checkout/monthly'
|
||||
charachteristics:
|
||||
- text: 'Полный доступ к залу и оборудованию'
|
||||
price: '2500 ₽'
|
||||
- text: '2 занятия с сертифицированным тренером'
|
||||
price: '2000 ₽'
|
||||
- text: 'Доступ ко всем групповым занятиям'
|
||||
price: '1400 ₽'
|
||||
- title: 'Годовой абонемент'
|
||||
description: 'Лучшее предложение для постоянных клиентов'
|
||||
price: '49900 ₽'
|
||||
- text: 'Персональный безлимит'
|
||||
price: '50 000 ₽'
|
||||
- text: 'СПЛИТ безлимит'
|
||||
price: '80 000 ₽'
|
||||
- text: 'Мини-группа (3-5 человек) безлимит'
|
||||
price: '30 000 ₽<br />c человека'
|
||||
- title: 'Аренда зала / Разовые посещения'
|
||||
description: 'Присутствие тренера БЕСПЛАТНО'
|
||||
link:
|
||||
title: 'Присоединиться'
|
||||
title: 'Записаться'
|
||||
href: '/checkout/annual'
|
||||
charachteristics:
|
||||
- text: 'Доступ ко всем зонам и премиум-оборудованию'
|
||||
price: '20000 ₽'
|
||||
- text: 'Ежемесячные персональные тренировки'
|
||||
price: '18000 ₽'
|
||||
- text: 'Индивидуальная консультация по питанию'
|
||||
price: '11900 ₽'
|
||||
- text: 'Разовое посещение Время 75 минут'
|
||||
price: '1500 ₽<br />c человека'
|
||||
- text: '«Приходящий тренер» Время 75 минут'
|
||||
price: '1500 ₽<br />c человека'
|
||||
reviews:
|
||||
- review: 'Место силы. Комфортная студия. Для тренировок есть всё необходимое . Раздевалки уютные , так же всё продумано до мелочей . Можно после тренировки насладиться кофе . По месторасположению очень комфортно , для парковки места есть всегда. Уютно. Спортивно . Красиво . Стильно . Профессионально! От души рекомендую'
|
||||
author:
|
||||
|
||||
@@ -5,7 +5,14 @@ export const languages = {
|
||||
|
||||
export const ui = {
|
||||
ru: {
|
||||
'home.title': 'The real skill studio',
|
||||
'seo.meta.description':
|
||||
'The Real Skill Studio - профессиональная фитнес-студия в России. Персональные тренировки, групповые занятия, опытные тренеры.',
|
||||
'seo.meta.keywords':
|
||||
'фитнес, тренировки, спортзал, персональный тренер, групповые занятия, здоровье, спорт, Real Skill Studio"',
|
||||
'seo.og.title': 'THEREALSKILL',
|
||||
'seo.og.description':
|
||||
'The Real Skill Studio - профессиональная фитнес-студия в России. Персональные тренировки, групповые занятия, опытные тренеры.',
|
||||
'home.title': 'Фитнес-студия',
|
||||
// Header
|
||||
'header.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
||||
'nav.coaches': 'Тренера',
|
||||
@@ -20,7 +27,8 @@ export const ui = {
|
||||
'ticker.shapeBody': 'ТЫ СЛАБ',
|
||||
// Coaches Section
|
||||
'coaches.title': 'НАШИ <span class="text-brand">ТРЕНЕРА</span>',
|
||||
'coaches.subtitle': 'Профессиональные тренеры с многолетним опытом',
|
||||
'coaches.subtitle': 'Вашему вниманию! Представляем наш тренерский состав',
|
||||
'coaches.link': 'Подробнее',
|
||||
// Slider Section
|
||||
'slider.title':
|
||||
'ПОСМОТРИТЕ, ГДЕ МЫ <br /> <span class="text-brand">ВЫПОЛНЯЕМ УПРАЖНЕНИЯ</span>',
|
||||
@@ -38,7 +46,14 @@ export const ui = {
|
||||
'footer.copyright': 'Все права защищены. Политика конфиденциальности.',
|
||||
},
|
||||
en: {
|
||||
'home.title': 'Home Page',
|
||||
'seo.meta.description':
|
||||
'The Real Skill Studio - professional fitness studio in Russia. Personal training, group classes, experienced trainers.',
|
||||
'seo.meta.keywords':
|
||||
'fitness, training, gym, personal trainer, group classes, health, sports, Real Skill Studio"',
|
||||
'seo.og.title': 'THEREALSKILL',
|
||||
'seo.og.description':
|
||||
'The Real Skill Studio - professional fitness studio in Russia. Personal training, group classes, experienced trainers.',
|
||||
'home.title': 'Fitness Studio',
|
||||
// Header
|
||||
'header.logo': 'THEREAL<span class="text-brand">SKILL</span>',
|
||||
'nav.coaches': 'Coaches',
|
||||
@@ -54,6 +69,7 @@ export const ui = {
|
||||
// Coaches Section
|
||||
'coaches.title': 'OUR <span class="text-brand">COACHES</span>',
|
||||
'coaches.subtitle': 'Professional trainers with years of experience',
|
||||
'coaches.link': 'More info',
|
||||
// Slider Section
|
||||
'slider.title': 'TAKE A LOOK WHERE WE <br /> <span class="text-brand">EXERCISE</span>',
|
||||
'slider.subtitle': 'Modern equipment in a spacious gym',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
---
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
import '@/styles/global.css'
|
||||
|
||||
type Props = {
|
||||
@@ -6,6 +7,7 @@ type Props = {
|
||||
}
|
||||
|
||||
const { title } = Astro.props
|
||||
const t = useTranslations(Astro.currentLocale)
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
@@ -13,9 +15,17 @@ const { title } = Astro.props
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> -->
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={t('seo.meta.description')} />
|
||||
<meta name="keywords" content={t('seo.meta.keywords')} />
|
||||
<meta property="og:title" content={`${title} | ${t('seo.og.title')}`} />
|
||||
<meta property="og:description" content={t('seo.og.description')} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content={Astro.currentLocale} />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<link rel="canonical" href={Astro.url} />
|
||||
<title>{`${title} | ${t('seo.og.title')}`}</title>
|
||||
</head>
|
||||
<body class="flex flex-col">
|
||||
<slot />
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
---
|
||||
import MobileNavigation from '@/components/MobileNavigation.tsx'
|
||||
import MobileNavigation from '@/components/header/MobileNavigation'
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
import BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import Section from '@/components/Section.astro'
|
||||
import { Swiper } from '@/components/swiper'
|
||||
import Header 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 { Image } from 'astro:assets'
|
||||
import Marquee from '@/components/Marquee.astro'
|
||||
import { Reviews } from '@/components/Reviews.tsx'
|
||||
import { Image } from 'astro:assets'
|
||||
import Hero from '@/components/Hero.astro'
|
||||
|
||||
type Props = CollectionEntry<'pages'>['data']
|
||||
|
||||
const 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' },
|
||||
] as const
|
||||
|
||||
const { lang } = Astro.params
|
||||
const page = await getEntry('pages', 'main')
|
||||
const { slider, coaches, services, hero, marquee, reviews } = page?.data || {}
|
||||
@@ -30,44 +25,9 @@ const t = useTranslations(lang)
|
||||
---
|
||||
|
||||
<BaseLayout title={t('home.title')}>
|
||||
<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 md:px-10"
|
||||
>
|
||||
<a class="text-2xl font-bold text-text-light" set:html={t('header.logo')} href="#hero" />
|
||||
<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 />
|
||||
</header>
|
||||
<Header lang={lang} />
|
||||
|
||||
{
|
||||
hero && (
|
||||
<Section id="hero" className="md:flex-row mt-16 md:mt-0 p-0 md:p-0">
|
||||
<div class="mb-8 px-6 py-10 md:mb-0 md:w-1/2 md:p-24">
|
||||
<h1 class="mb-4 text-4xl font-bold leading-tight md:text-6xl" set:html={hero.title} />
|
||||
<p class="text-brand-text mb-6">{hero.description}</p>
|
||||
{hero.cta && (
|
||||
<div class="flex space-x-4">
|
||||
<a href="#services" class="brand-btn">
|
||||
{hero.cta?.title}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div class="relative hidden md:block md:w-1/2">
|
||||
<Image src={hero.image} alt={hero.imageAlt} class="absolute h-full w-full object-cover" />
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
{hero && <Hero {...hero} />}
|
||||
|
||||
{
|
||||
marquee && (
|
||||
@@ -80,9 +40,9 @@ const t = useTranslations(lang)
|
||||
{
|
||||
coaches && (
|
||||
<Section id="coaches" title={t('coaches.title')} description={t('coaches.subtitle')}>
|
||||
<div class="grid w-full grid-flow-col place-items-center gap-6 overflow-x-scroll md:gap-14">
|
||||
<div class="grid place-items-center gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-3">
|
||||
{coaches.map((coach) => (
|
||||
<CoachCard {...coach} />
|
||||
<CoachCard {...coach} lang={lang} />
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
@@ -97,11 +57,13 @@ const t = useTranslations(lang)
|
||||
description={t('slider.subtitle')}
|
||||
className="bg-bg-light"
|
||||
>
|
||||
<div class="-ml-6 flex w-[calc(100%+(1.5rem*2))] md:ml-0 md:w-full">
|
||||
<Swiper
|
||||
className="h-[540px]"
|
||||
client:visible
|
||||
images={slider.map(({ image, imageAlt }) => ({ ...image, alt: imageAlt }))}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
}
|
||||
@@ -128,22 +90,5 @@ const t = useTranslations(lang)
|
||||
)
|
||||
}
|
||||
|
||||
<Section className="px-0 md:px-0" id="contacts">
|
||||
<script
|
||||
type="text/javascript"
|
||||
charset="utf-8"
|
||||
async
|
||||
src="https://api-maps.yandex.ru/services/constructor/1.0/js/?um=constructor%3A069b17526dc83cbf87c488f13ae089eb3af0f8f74af57e1be82e183434693562&width=100%25&height=400&lang=ru_RU&scroll=true"
|
||||
></script>
|
||||
</Section>
|
||||
|
||||
<footer class="py-8 text-center">
|
||||
<div class="container mx-auto px-6 md:px-10">
|
||||
<div class="mb-4 text-2xl font-bold text-text-light" set:html={t('footer.logo')} />
|
||||
<p class="text-light text-sm">
|
||||
© {new Date().getFullYear()}
|
||||
{t('footer.copyright')}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
<Footer lang={lang} />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
html {
|
||||
font-family: system-ui, sans-serif;
|
||||
scroll-behavior: smooth;
|
||||
@apply scroll-pt-[var(--header-height)];
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -34,9 +33,9 @@
|
||||
|
||||
@layer components {
|
||||
.brand-btn {
|
||||
@apply rounded-full bg-brand px-4 py-2 text-center text-text-light transition-colors duration-300 hover:bg-brand/80;
|
||||
@apply rounded-full bg-brand px-6 py-3 text-center text-text-light transition-colors duration-300 hover:bg-brand/80;
|
||||
}
|
||||
.brand-btn-outline {
|
||||
@apply rounded-full border border-brand bg-transparent px-4 py-2 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;
|
||||
}
|
||||
}
|
||||
|
||||