images optimizations

This commit is contained in:
Salam-vsem
2025-05-14 01:15:30 +03:00
parent 5d9b7c27ae
commit 2f26ad5464
14 changed files with 130 additions and 63 deletions

View File

@@ -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,
}, },
}, },
}); })

View File

@@ -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>
) )

View File

@@ -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>

View File

@@ -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>
))} ))}

View File

@@ -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}
> >
{ {

View File

@@ -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>
)) ))
} }

View File

@@ -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
)} )}

View File

@@ -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>

View File

@@ -1 +1 @@
export * from './Swiper'; export * from './Swiper'

23
src/constants/images.ts Normal file
View 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;

View File

@@ -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
View 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,
})

View File

@@ -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>
) )

View File

@@ -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"