images optimizations
This commit is contained in:
@@ -12,14 +12,23 @@ const t = useTranslations(lang)
|
||||
|
||||
<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} />
|
||||
<Image
|
||||
src={image}
|
||||
class="absolute h-full w-full object-cover"
|
||||
alt={imageAlt}
|
||||
width={300}
|
||||
height={390}
|
||||
format="webp"
|
||||
fit="cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
</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">
|
||||
<a href={link} target="_blank" class="mt-2 text-brand hover:text-brand/80">
|
||||
{t('coaches.link')}
|
||||
</a>
|
||||
)
|
||||
|
||||
@@ -27,6 +27,11 @@ const { title, description, cta, image, imageAlt } = Astro.props
|
||||
}
|
||||
</div>
|
||||
<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>
|
||||
</Section>
|
||||
|
||||
@@ -6,10 +6,15 @@ import 'swiper/css/scrollbar'
|
||||
import { Mousewheel, Scrollbar } from 'swiper/modules'
|
||||
import StarIconSrc from '@/assets/icons/StarIcon.svg'
|
||||
import { useTranslations } from '@/i18n/utils'
|
||||
import type { GetImageResult } from 'astro'
|
||||
|
||||
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 = {
|
||||
reviews: Review[]
|
||||
reviews: OptimizedReview[]
|
||||
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}>
|
||||
<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">
|
||||
@@ -51,20 +56,24 @@ export const Reviews: React.FC<ReviewsProps> = ({ reviews, lang }) => {
|
||||
))}
|
||||
</div>
|
||||
<blockquote className="break-words text-lg font-semibold text-text-light">
|
||||
{review.review}
|
||||
{review}
|
||||
</blockquote>
|
||||
<div className="mt-auto flex items-center gap-4">
|
||||
<img
|
||||
{...review.author.image}
|
||||
alt={review.author.imageAlt}
|
||||
loading="lazy"
|
||||
className="h-16 w-16 rounded-full object-cover"
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-text-light">{review.author.name}</span>
|
||||
<span>{review.author.description}</span>
|
||||
{author && (
|
||||
<div className="mt-auto flex items-center gap-4">
|
||||
{author.image && (
|
||||
<img
|
||||
{...author.image}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
className="h-16 w-16 rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="font-semibold text-text-light">{author.name}</span>
|
||||
<span>{author.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
|
||||
@@ -12,10 +12,7 @@ const { title, description, className, id } = Astro.props
|
||||
---
|
||||
|
||||
<section
|
||||
class={cn(
|
||||
'flex flex-col gap-6 py-12 px-4 md:py-24 md:px-16 lg:px-24 md:gap-12',
|
||||
className
|
||||
)}
|
||||
class={cn('flex flex-col gap-6 py-12 px-4 md:py-24 md:px-16 lg:px-24 md:gap-12', className)}
|
||||
id={id}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -9,7 +9,7 @@ const { title, charachteristics, highlighted = false, link } = Astro.props
|
||||
|
||||
<div
|
||||
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-brand': highlighted },
|
||||
]}
|
||||
@@ -21,9 +21,11 @@ const { title, charachteristics, highlighted = false, link } = Astro.props
|
||||
<ul class="mb-16 flex-grow space-y-4">
|
||||
{
|
||||
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>
|
||||
{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>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export const NavButton: React.FC<Props> = ({ iconSrc, ...buttonProps }) => {
|
||||
<button
|
||||
{...buttonProps}
|
||||
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.className
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React from '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 'swiper/css'
|
||||
import 'swiper/css/navigation'
|
||||
import 'swiper/css/pagination'
|
||||
import './styles.css'
|
||||
import { NextButton, PrevButton } from './NavButtons'
|
||||
import type { GetImageResult } from 'astro'
|
||||
|
||||
type SwiperProps = {
|
||||
images: GetImageResult[]
|
||||
images: ImportAttributes[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
@@ -22,16 +21,16 @@ export const Swiper: React.FC<SwiperProps> = ({ images, className }) => {
|
||||
pagination={{
|
||||
clickable: true,
|
||||
}}
|
||||
watchSlidesProgress={true}
|
||||
className={cn('h-full w-full', className)}
|
||||
>
|
||||
{images.map((image, index) => (
|
||||
<SwiperSlide key={index}>
|
||||
<img
|
||||
{...image.attributes}
|
||||
src={image.src}
|
||||
srcSet={image.srcSet.attribute}
|
||||
loading="lazy"
|
||||
{...image}
|
||||
loading={index === 0 ? 'eager' : 'lazy'}
|
||||
decoding="async"
|
||||
fetchPriority={index === 0 ? 'high' : 'auto'}
|
||||
className="absolute h-full w-full object-cover md:px-[calc(50px+2rem)]"
|
||||
/>
|
||||
</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) {
|
||||
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) {
|
||||
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 BaseLayout from '../layouts/BaseLayout.astro'
|
||||
import Section from '@/components/Section.astro'
|
||||
@@ -12,9 +11,11 @@ import { getEntry } from 'astro:content'
|
||||
import CoachCard from '@/components/CoachCard.astro'
|
||||
import ServiceCard from '@/components/ServiceCard.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 { getImage } from 'astro:assets'
|
||||
import { TW_SCREENS, TW_WIDTHS } from '@/constants/images'
|
||||
import { imageResultToImageAttributes } from '@/libs/image'
|
||||
|
||||
type Props = CollectionEntry<'pages'>['data']
|
||||
|
||||
@@ -27,13 +28,35 @@ const optimizedSlider = slider
|
||||
getImage({
|
||||
src: slide.image,
|
||||
alt: slide.imageAlt,
|
||||
widths: [600, 1200],
|
||||
quality: 90,
|
||||
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: 'eager',
|
||||
}).then(imageResultToImageAttributes)
|
||||
)
|
||||
)
|
||||
: 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)
|
||||
---
|
||||
|
||||
@@ -53,7 +76,7 @@ const t = useTranslations(lang)
|
||||
{
|
||||
coaches && (
|
||||
<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) => (
|
||||
<CoachCard {...coach} lang={lang} />
|
||||
))}
|
||||
@@ -90,10 +113,10 @@ const t = useTranslations(lang)
|
||||
}
|
||||
|
||||
{
|
||||
reviews && (
|
||||
optimizedReviews && (
|
||||
<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)]">
|
||||
<Reviews client:visible reviews={reviews} lang={lang} />
|
||||
<Reviews client:visible reviews={optimizedReviews} lang={lang} />
|
||||
</div>
|
||||
</Section>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user