images optimizations
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
// @ts-check
|
// @ts-check
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config'
|
||||||
import tailwind from '@astrojs/tailwind';
|
import tailwind from '@astrojs/tailwind'
|
||||||
|
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react'
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -14,4 +14,4 @@ export default defineConfig({
|
|||||||
prefixDefaultLocale: false,
|
prefixDefaultLocale: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|||||||
@@ -12,14 +12,23 @@ const t = useTranslations(lang)
|
|||||||
|
|
||||||
<div class="flex h-full w-full flex-col items-center justify-start gap-5">
|
<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">
|
<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} />
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="absolute h-full w-full object-cover"
|
||||||
|
alt={imageAlt}
|
||||||
|
width={300}
|
||||||
|
height={390}
|
||||||
|
format="webp"
|
||||||
|
fit="cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
<div class="inline-flex w-full max-w-[300px] flex-col gap-1">
|
<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>
|
<h3 class="text-xl font-bold uppercase text-text-light">{name}</h3>
|
||||||
{description && <p>{description}</p>}
|
{description && <p>{description}</p>}
|
||||||
{
|
{
|
||||||
link && (
|
link && (
|
||||||
<a href={link} target="_blank" class="text-brand hover:text-brand/80 mt-2">
|
<a href={link} target="_blank" class="mt-2 text-brand hover:text-brand/80">
|
||||||
{t('coaches.link')}
|
{t('coaches.link')}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ 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-[450px] lg:w-1/2">
|
||||||
<Image src={image} alt={imageAlt} class="absolute h-full w-full object-contain" />
|
<Image
|
||||||
|
src={image}
|
||||||
|
alt={imageAlt}
|
||||||
|
class="absolute h-full w-full object-contain"
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|||||||
@@ -6,10 +6,15 @@ import 'swiper/css/scrollbar'
|
|||||||
import { Mousewheel, Scrollbar } from 'swiper/modules'
|
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 type { GetImageResult } from 'astro'
|
||||||
|
|
||||||
type Review = z.infer<ReturnType<typeof reviewsSchema>>
|
type Review = z.infer<ReturnType<typeof reviewsSchema>>
|
||||||
|
type Author = Review['author']
|
||||||
|
export type OptimizedReview = Omit<Review, 'author'> & {
|
||||||
|
author: Omit<Author, 'image' | 'imageAlt'> & { image?: ImportAttributes }
|
||||||
|
}
|
||||||
type ReviewsProps = {
|
type ReviewsProps = {
|
||||||
reviews: Review[]
|
reviews: OptimizedReview[]
|
||||||
lang?: string
|
lang?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +41,7 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{reviews.map((review, 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="flex h-full flex-col gap-6 bg-bg-light p-6 md:px-14 md:py-16">
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@@ -51,20 +56,24 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<blockquote className="break-words text-lg font-semibold text-text-light">
|
<blockquote className="break-words text-lg font-semibold text-text-light">
|
||||||
{review.review}
|
{review}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
<div className="mt-auto flex items-center gap-4">
|
{author && (
|
||||||
<img
|
<div className="mt-auto flex items-center gap-4">
|
||||||
{...review.author.image}
|
{author.image && (
|
||||||
alt={review.author.imageAlt}
|
<img
|
||||||
loading="lazy"
|
{...author.image}
|
||||||
className="h-16 w-16 rounded-full object-cover"
|
loading="lazy"
|
||||||
/>
|
decoding="async"
|
||||||
<div className="flex flex-col">
|
className="h-16 w-16 rounded-full object-cover"
|
||||||
<span className="font-semibold text-text-light">{review.author.name}</span>
|
/>
|
||||||
<span>{review.author.description}</span>
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-semibold text-text-light">{author.name}</span>
|
||||||
|
<span>{author.description}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ const { title, description, className, id } = Astro.props
|
|||||||
---
|
---
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class={cn(
|
class={cn('flex flex-col gap-6 py-12 px-4 md:py-24 md:px-16 lg:px-24 md:gap-12', className)}
|
||||||
'flex flex-col gap-6 py-12 px-4 md:py-24 md:px-16 lg:px-24 md:gap-12',
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { title, charachteristics, highlighted = false, link } = Astro.props
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class:list={[
|
class:list={[
|
||||||
'flex h-full w-full flex-col border-2 px-5 py-6 md:px-7 md:py-8 md:max-w-[400px] md:min-h-[550px]',
|
'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 border-transparent bg-card',
|
||||||
{ 'border-brand': highlighted },
|
{ 'border-brand': highlighted },
|
||||||
]}
|
]}
|
||||||
@@ -21,9 +21,11 @@ const { title, charachteristics, highlighted = false, link } = Astro.props
|
|||||||
<ul class="mb-16 flex-grow space-y-4">
|
<ul class="mb-16 flex-grow space-y-4">
|
||||||
{
|
{
|
||||||
charachteristics?.map((charachteristic) => (
|
charachteristics?.map((charachteristic) => (
|
||||||
<li class="flex items-start space-x-2 justify-between">
|
<li class="flex items-start justify-between space-x-2">
|
||||||
<span class="text-text-light">{charachteristic.text}</span>
|
<span class="text-text-light">{charachteristic.text}</span>
|
||||||
{charachteristic.price && <span class="ml-2 text-brand text-nowrap text-end" set:html={charachteristic.price} />}
|
{charachteristic.price && (
|
||||||
|
<span class="ml-2 text-nowrap text-end text-brand" set:html={charachteristic.price} />
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const NavButton: React.FC<Props> = ({ iconSrc, ...buttonProps }) => {
|
|||||||
<button
|
<button
|
||||||
{...buttonProps}
|
{...buttonProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
'border-border-light hidden rounded-full border border-solid md:block',
|
'hidden rounded-full border border-solid border-border-light md:block',
|
||||||
buttonProps.disabled && 'border-none',
|
buttonProps.disabled && 'border-none',
|
||||||
buttonProps.className
|
buttonProps.className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Swiper as SwiperBase, SwiperSlide } from 'swiper/react'
|
import { Swiper as SwiperBase, SwiperSlide } from 'swiper/react'
|
||||||
import { A11y, Navigation, Pagination } from 'swiper/modules'
|
import { A11y, Navigation, Pagination } from 'swiper/modules' // Added Lazy module
|
||||||
import { cn } from '@/libs/cn'
|
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 './styles.css'
|
import './styles.css'
|
||||||
import { NextButton, PrevButton } from './NavButtons'
|
import { NextButton, PrevButton } from './NavButtons'
|
||||||
import type { GetImageResult } from 'astro'
|
|
||||||
|
|
||||||
type SwiperProps = {
|
type SwiperProps = {
|
||||||
images: GetImageResult[]
|
images: ImportAttributes[]
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,16 +21,16 @@ export const Swiper: React.FC<SwiperProps> = ({ images, className }) => {
|
|||||||
pagination={{
|
pagination={{
|
||||||
clickable: true,
|
clickable: true,
|
||||||
}}
|
}}
|
||||||
|
watchSlidesProgress={true}
|
||||||
className={cn('h-full w-full', className)}
|
className={cn('h-full w-full', className)}
|
||||||
>
|
>
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
<SwiperSlide key={index}>
|
<SwiperSlide key={index}>
|
||||||
<img
|
<img
|
||||||
{...image.attributes}
|
{...image}
|
||||||
src={image.src}
|
loading={index === 0 ? 'eager' : 'lazy'}
|
||||||
srcSet={image.srcSet.attribute}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
decoding="async"
|
||||||
|
fetchPriority={index === 0 ? 'high' : 'auto'}
|
||||||
className="absolute h-full w-full object-cover md:px-[calc(50px+2rem)]"
|
className="absolute h-full w-full object-cover md:px-[calc(50px+2rem)]"
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './Swiper';
|
export * from './Swiper'
|
||||||
|
|||||||
23
src/constants/images.ts
Normal file
23
src/constants/images.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export const TW_WIDTHS = {
|
||||||
|
/** added, for Image, bellow xs */
|
||||||
|
XXS: 320,
|
||||||
|
XS: 475,
|
||||||
|
SM: 640,
|
||||||
|
MD: 768,
|
||||||
|
LG: 1024,
|
||||||
|
XL: 1280,
|
||||||
|
_2XL: 1536,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const TW_SCREENS = {
|
||||||
|
...TW_WIDTHS,
|
||||||
|
HEIGHTS: {
|
||||||
|
XXS: 180,
|
||||||
|
XS: 268,
|
||||||
|
SM: 360,
|
||||||
|
MD: 432,
|
||||||
|
LG: 576,
|
||||||
|
XL: 720,
|
||||||
|
_2XL: 864,
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import { ui } from './ui';
|
import { ui } from './ui'
|
||||||
|
|
||||||
import { i18n } from 'astro:config/client';
|
import { i18n } from 'astro:config/client'
|
||||||
|
|
||||||
export function getLangFromUrl(url: URL) {
|
export function getLangFromUrl(url: URL) {
|
||||||
const [, lang] = url.pathname.split('/');
|
const [, lang] = url.pathname.split('/')
|
||||||
|
|
||||||
if (lang in ui) return lang as keyof typeof ui;
|
if (lang in ui) return lang as keyof typeof ui
|
||||||
|
|
||||||
return i18n!.defaultLocale;
|
return i18n!.defaultLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTranslations(lang?: string) {
|
export function useTranslations(lang?: string) {
|
||||||
return function t(key: keyof (typeof ui)[keyof typeof ui]) {
|
return function t(key: keyof (typeof ui)[keyof typeof ui]) {
|
||||||
return ui[lang as keyof typeof ui]?.[key] || ui[i18n!.defaultLocale as keyof typeof ui][key];
|
return ui[lang as keyof typeof ui]?.[key] || ui[i18n!.defaultLocale as keyof typeof ui][key]
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
src/libs/image.ts
Normal file
7
src/libs/image.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { GetImageResult } from 'astro'
|
||||||
|
|
||||||
|
export const imageResultToImageAttributes = (imageResult: GetImageResult): ImportAttributes => ({
|
||||||
|
src: imageResult.src,
|
||||||
|
srcSet: imageResult.srcSet?.attribute,
|
||||||
|
...imageResult.attributes,
|
||||||
|
})
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
---
|
---
|
||||||
import MobileNavigation from '@/components/header/MobileNavigation'
|
|
||||||
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'
|
||||||
@@ -12,9 +11,11 @@ import { getEntry } from 'astro:content'
|
|||||||
import CoachCard from '@/components/CoachCard.astro'
|
import CoachCard from '@/components/CoachCard.astro'
|
||||||
import ServiceCard from '@/components/ServiceCard.astro'
|
import ServiceCard from '@/components/ServiceCard.astro'
|
||||||
import Marquee from '@/components/Marquee.astro'
|
import Marquee from '@/components/Marquee.astro'
|
||||||
import { Reviews } 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 { TW_SCREENS, TW_WIDTHS } from '@/constants/images'
|
||||||
|
import { imageResultToImageAttributes } from '@/libs/image'
|
||||||
|
|
||||||
type Props = CollectionEntry<'pages'>['data']
|
type Props = CollectionEntry<'pages'>['data']
|
||||||
|
|
||||||
@@ -27,13 +28,35 @@ const optimizedSlider = slider
|
|||||||
getImage({
|
getImage({
|
||||||
src: slide.image,
|
src: slide.image,
|
||||||
alt: slide.imageAlt,
|
alt: slide.imageAlt,
|
||||||
widths: [600, 1200],
|
widths: [TW_SCREENS.SM, TW_SCREENS.LG],
|
||||||
quality: 90,
|
sizes: `(max-width: ${TW_WIDTHS.LG}px) ${TW_SCREENS.SM}px, ${TW_SCREENS.LG}px`,
|
||||||
|
quality: 80,
|
||||||
format: 'webp',
|
format: 'webp',
|
||||||
})
|
loading: 'eager',
|
||||||
|
}).then(imageResultToImageAttributes)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
const optimizedReviews = reviews
|
||||||
|
? await Promise.all(
|
||||||
|
reviews.map(async (review): Promise<OptimizedReview> => {
|
||||||
|
if (!review.author?.image)
|
||||||
|
return { ...review, author: { ...review.author, image: undefined } }
|
||||||
|
|
||||||
|
const optimizedImage = await getImage({
|
||||||
|
src: review.author?.image,
|
||||||
|
alt: review.author?.imageAlt,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
format: 'webp',
|
||||||
|
loading: 'lazy',
|
||||||
|
}).then(imageResultToImageAttributes)
|
||||||
|
|
||||||
|
return { ...review, author: { ...review.author, image: optimizedImage } }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
const t = useTranslations(lang)
|
const t = useTranslations(lang)
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,7 +76,7 @@ const t = useTranslations(lang)
|
|||||||
{
|
{
|
||||||
coaches && (
|
coaches && (
|
||||||
<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 md:grid-cols-2 md:gap-8 lg:grid-cols-3">
|
<div class="grid place-items-center gap-4 sm:grid-cols-2 md:gap-8 lg:grid-cols-3">
|
||||||
{coaches.map((coach) => (
|
{coaches.map((coach) => (
|
||||||
<CoachCard {...coach} lang={lang} />
|
<CoachCard {...coach} lang={lang} />
|
||||||
))}
|
))}
|
||||||
@@ -90,10 +113,10 @@ const t = useTranslations(lang)
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
reviews && (
|
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)]">
|
<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={reviews} lang={lang} />
|
<Reviews client:visible reviews={optimizedReviews} lang={lang} />
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
{
|
{
|
||||||
"extends": "astro/tsconfigs/strict",
|
"extends": "astro/tsconfigs/strict",
|
||||||
"include": [
|
"include": [".astro/types.d.ts", "**/*"],
|
||||||
".astro/types.d.ts",
|
"exclude": ["dist"],
|
||||||
"**/*"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./src/*"]
|
||||||
"./src/*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"jsxImportSource": "react"
|
"jsxImportSource": "react"
|
||||||
|
|||||||
Reference in New Issue
Block a user