From 2d6ec22a7b48c36c527e379fd44103cd397ebb12 Mon Sep 17 00:00:00 2001 From: jekabolt Date: Sun, 11 Jan 2026 15:12:18 +0300 Subject: [PATCH 1/7] wip --- next.config.ts | 26 ++++++++++++ src/app/[locale]/_components/ads.tsx | 10 +++++ .../[locale]/_components/featured-items.tsx | 12 ++++-- src/app/[locale]/_components/main-ads.tsx | 4 ++ .../_components/mobile-featured-items.tsx | 3 +- src/app/[locale]/_components/product-grid.tsx | 18 +++++--- src/app/[locale]/_components/product-item.tsx | 4 ++ .../_components/single-featured-item.tsx | 8 ++-- src/app/[locale]/layout.tsx | 6 +++ .../_components/mobile-image-carousel.tsx | 4 ++ .../_components/product-images-carousel.tsx | 42 +++++++++++-------- .../_components/page-component.tsx | 32 ++++++++------ src/components/ui/image.tsx | 6 +++ src/fonts/index.ts | 2 +- 14 files changed, 134 insertions(+), 43 deletions(-) diff --git a/next.config.ts b/next.config.ts index 3a3e8a5a4..139a78bcf 100644 --- a/next.config.ts +++ b/next.config.ts @@ -20,8 +20,34 @@ const nextConfig: NextConfig = { hostname: "cdn.builder.io", }, ], + formats: ["image/avif", "image/webp"], + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + minimumCacheTTL: 31536000, // 1 year in seconds }, pageExtensions: ["mdx", "ts", "tsx"], + async headers() { + return [ + { + source: '/:all*(svg|jpg|jpeg|png|webp|avif|gif|ico)', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + ], + }, + { + source: '/_next/static/:path*', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + ], + }, + ]; + }, }; export default withNextIntl(nextConfig); diff --git a/src/app/[locale]/_components/ads.tsx b/src/app/[locale]/_components/ads.tsx index 772fd40ac..bed57ead8 100644 --- a/src/app/[locale]/_components/ads.tsx +++ b/src/app/[locale]/_components/ads.tsx @@ -22,6 +22,8 @@ export function Ads({ return (
{entities?.map((e, i) => { + // Prioritize first ad for better LCP + const isPriorityAd = i === 0; switch (e.type) { case "HERO_TYPE_SINGLE": const currentTranslation = e.single?.translations?.find( @@ -48,6 +50,8 @@ export function Ads({ e.single?.mediaLandscape?.media?.fullSize?.height, )} fit="cover" + priority={isPriorityAd} + loading={isPriorityAd ? "eager" : "lazy"} />
@@ -61,6 +65,8 @@ export function Ads({ e.single?.mediaPortrait?.media?.fullSize?.height, )} fit="cover" + priority={isPriorityAd} + loading={isPriorityAd ? "eager" : "lazy"} />
@@ -112,6 +118,8 @@ export function Ads({ e.double?.left?.mediaLandscape?.media?.fullSize?.height, )} fit="contain" + priority={isPriorityAd} + loading={isPriorityAd ? "eager" : "lazy"} />
- {products?.map((p) => ( + {products?.map((p, index) => ( ))}
@@ -101,8 +102,13 @@ function FourFeaturedItems({ - {products?.map((p) => ( - + {products?.map((p, index) => ( + ))}
diff --git a/src/app/[locale]/_components/main-ads.tsx b/src/app/[locale]/_components/main-ads.tsx index 6ffd9e097..b5733a99a 100644 --- a/src/app/[locale]/_components/main-ads.tsx +++ b/src/app/[locale]/_components/main-ads.tsx @@ -34,6 +34,8 @@ export function MainAds({ main }: { main?: common_HeroMainWithTranslations }) { )} alt="main hero image" fit="cover" + priority={true} + loading="eager" />
@@ -45,6 +47,8 @@ export function MainAds({ main }: { main?: common_HeroMainWithTranslations }) { )} alt="main hero image" fit="cover" + priority={true} + loading="eager" />
diff --git a/src/app/[locale]/_components/mobile-featured-items.tsx b/src/app/[locale]/_components/mobile-featured-items.tsx index 3a051f87b..b41b1f771 100644 --- a/src/app/[locale]/_components/mobile-featured-items.tsx +++ b/src/app/[locale]/_components/mobile-featured-items.tsx @@ -46,7 +46,7 @@ export function MobileFeaturedItems({ "grid grid-cols-2 gap-4": itemsQuantity === 4, })} > - {products?.map((p) => ( + {products?.map((p, index) => ( = 3 && itemsQuantity !== 4, })} product={p} + imagePriority={index === 0} /> ))} diff --git a/src/app/[locale]/_components/product-grid.tsx b/src/app/[locale]/_components/product-grid.tsx index fecec5f36..2202d00ac 100644 --- a/src/app/[locale]/_components/product-grid.tsx +++ b/src/app/[locale]/_components/product-grid.tsx @@ -17,11 +17,19 @@ export default function ProductsGridSection({ return (
- {products.map((v) => ( -
- -
- ))} + {products.map((v, index) => { + // Prioritize first 4 items (2 rows on mobile, 1 row on desktop) + const isPriority = index < 4; + return ( +
+ +
+ ); + })} {isLoading && Array.from({ length: Math.min(CATALOG_LIMIT, total - products.length), diff --git a/src/app/[locale]/_components/product-item.tsx b/src/app/[locale]/_components/product-item.tsx index 684edeaf7..ad5c072ea 100644 --- a/src/app/[locale]/_components/product-item.tsx +++ b/src/app/[locale]/_components/product-item.tsx @@ -21,10 +21,12 @@ export function ProductItem({ product, className, isInfoVisible = true, + imagePriority = false, }: { product: common_Product; className: string; isInfoVisible?: boolean; + imagePriority?: boolean; }) { const tCatalog = useTranslations("catalog"); const t = useTranslations("categories"); @@ -94,6 +96,8 @@ export function ProductItem({ product.productDisplay?.thumbnail?.media?.thumbnail?.height, )} fit="contain" + priority={imagePriority} + loading={imagePriority ? "eager" : "lazy"} />
diff --git a/src/app/[locale]/_components/single-featured-item.tsx b/src/app/[locale]/_components/single-featured-item.tsx index 83e71566a..d8d8ff8a9 100644 --- a/src/app/[locale]/_components/single-featured-item.tsx +++ b/src/app/[locale]/_components/single-featured-item.tsx @@ -1,12 +1,12 @@ -import { useState } from "react"; import { common_Product } from "@/api/proto-http/frontend"; import { currencySymbols } from "@/constants"; +import { useState } from "react"; -import { useTranslationsStore } from "@/lib/stores/translations/store-provider"; -import { calculateAspectRatio, calculatePriceWithSale } from "@/lib/utils"; import { AnimatedButton } from "@/components/ui/animated-button"; import Image from "@/components/ui/image"; import { Text } from "@/components/ui/text"; +import { useTranslationsStore } from "@/lib/stores/translations/store-provider"; +import { calculateAspectRatio, calculatePriceWithSale } from "@/lib/utils"; export function SingleFeaturedItem({ products, @@ -94,6 +94,8 @@ export function SingleFeaturedItem({ p.productDisplay?.thumbnail?.media?.thumbnail?.height, )} fit="contain" + priority={true} + loading="eager" />
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 16455658d..a1017a05a 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -58,6 +58,12 @@ export default async function RootLayout({ children, params }: Props) { return ( + + + + + + diff --git a/src/app/[locale]/product/[...productParams]/_components/mobile-image-carousel.tsx b/src/app/[locale]/product/[...productParams]/_components/mobile-image-carousel.tsx index 757aecf6a..f5c1b301c 100644 --- a/src/app/[locale]/product/[...productParams]/_components/mobile-image-carousel.tsx +++ b/src/app/[locale]/product/[...productParams]/_components/mobile-image-carousel.tsx @@ -85,6 +85,8 @@ export function MobileImageCarousel({ media }: { media: common_MediaFull[] }) {
{media.map((m, index) => { const fullSize = m?.media?.fullSize; + // Prioritize first image for mobile LCP + const isPriority = index === 0; return (
); diff --git a/src/app/[locale]/product/[...productParams]/_components/product-images-carousel.tsx b/src/app/[locale]/product/[...productParams]/_components/product-images-carousel.tsx index f7805ac0a..b3523474b 100644 --- a/src/app/[locale]/product/[...productParams]/_components/product-images-carousel.tsx +++ b/src/app/[locale]/product/[...productParams]/_components/product-images-carousel.tsx @@ -1,13 +1,13 @@ "use client"; -import { useRef } from "react"; import { common_MediaFull } from "@/api/proto-http/frontend"; +import { useRef } from "react"; -import { calculateAspectRatio } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Carousel, CarouselRef } from "@/components/ui/carousel"; import ImageComponent from "@/components/ui/image"; import { Text } from "@/components/ui/text"; +import { calculateAspectRatio } from "@/lib/utils"; export function ProductImagesCarousel({ productMedia }: Props) { const carouselRef = useRef(null); @@ -37,22 +37,28 @@ export function ProductImagesCarousel({ productMedia }: Props) { className="flex h-screen w-full pt-14" scrollOnClick={true} > - {mediaForCarousel.map((m, index) => ( -
- -
- ))} + {mediaForCarousel.map((m, index) => { + // Prioritize first 2 images for LCP optimization + const isPriority = index < 2; + return ( +
+ +
+ ); + })}
); diff --git a/src/app/[locale]/timeline/[...archiveParams]/_components/page-component.tsx b/src/app/[locale]/timeline/[...archiveParams]/_components/page-component.tsx index 63080ca56..fa5aac09e 100644 --- a/src/app/[locale]/timeline/[...archiveParams]/_components/page-component.tsx +++ b/src/app/[locale]/timeline/[...archiveParams]/_components/page-component.tsx @@ -60,6 +60,8 @@ export default function PageComponent({ archive?.mainMedia?.media?.thumbnail?.width, archive?.mainMedia?.media?.thumbnail?.height, )} + priority={true} + loading="eager" /> )} @@ -91,18 +93,24 @@ export default function PageComponent({ )}
- {archive?.media?.map((item, id) => ( -
- -
- ))} + {archive?.media?.map((item, id) => { + // Prioritize first 4 images in grid (above fold on desktop) + const isPriority = id < 4; + return ( +
+ +
+ ); + })}
); diff --git a/src/components/ui/image.tsx b/src/components/ui/image.tsx index 13986e1dc..d5e2f6ee3 100644 --- a/src/components/ui/image.tsx +++ b/src/components/ui/image.tsx @@ -20,6 +20,8 @@ type ImageProps = { aspectRatio: string; sizes?: string; fit?: "cover" | "contain" | "fill" | "scale-down"; + priority?: boolean; + loading?: "lazy" | "eager"; }; export default function ImageComponent({ @@ -28,6 +30,8 @@ export default function ImageComponent({ alt, sizes = "(max-width: 1280px) 100vw, 1280px", fit, + priority = false, + loading = "lazy", }: ImageProps) { return ( @@ -37,6 +41,8 @@ export default function ImageComponent({ alt={alt} className="h-full w-full" sizes={sizes} + priority={priority} + loading={priority ? undefined : loading} style={{ objectFit: fit, }} diff --git a/src/fonts/index.ts b/src/fonts/index.ts index 998370daf..f1618ef58 100644 --- a/src/fonts/index.ts +++ b/src/fonts/index.ts @@ -19,6 +19,6 @@ export const FeatureMono = localFont({ }, ], display: "swap", - preload: false, + preload: true, fallback: ["system-ui", "sans-serif"], }); From 2052f4b18e99fb700d7762cb00a6c6ee766c4284 Mon Sep 17 00:00:00 2001 From: jekabolt Date: Sun, 11 Jan 2026 15:17:26 +0300 Subject: [PATCH 2/7] fix performance --- src/app/[locale]/layout.tsx | 4 ++-- src/app/robots.ts | 15 +++++++++++++++ src/app/sitemap.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 src/app/robots.ts create mode 100644 src/app/sitemap.ts diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index a1017a05a..499775ecf 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,7 +1,7 @@ -import { Metadata } from "next"; import { FeatureMono } from "@/fonts"; import { routing } from "@/i18n/routing"; import { GoogleTagManager } from "@next/third-parties/google"; +import { Metadata } from "next"; import { NextIntlClientProvider } from "next-intl"; import { getMessages, @@ -9,11 +9,11 @@ import { setRequestLocale, } from "next-intl/server"; -import { generateCommonMetadata } from "@/lib/common-metadata"; import { PageTransition } from "@/components/page-transition"; import { CookieBanner } from "@/components/ui/cookie-banner"; import { GeoSuggestWrapper } from "@/components/ui/geo-suggest-wrapper"; import { ToastProvider } from "@/components/ui/toaster"; +import { generateCommonMetadata } from "@/lib/common-metadata"; import "../globals.css"; diff --git a/src/app/robots.ts b/src/app/robots.ts new file mode 100644 index 000000000..5da5fc30d --- /dev/null +++ b/src/app/robots.ts @@ -0,0 +1,15 @@ +import { MetadataRoute } from 'next' + +export default function robots(): MetadataRoute.Robots { + return { + rules: [ + { + userAgent: '*', + allow: '/', + disallow: ['/api/', '/admin/'], + }, + ], + sitemap: 'https://grbpwr.com/sitemap.xml', + } +} + diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts new file mode 100644 index 000000000..f0a2e5aaf --- /dev/null +++ b/src/app/sitemap.ts @@ -0,0 +1,27 @@ +import { MetadataRoute } from 'next' + +export default function sitemap(): MetadataRoute.Sitemap { + const baseUrl = 'https://grbpwr.com' + + // Static pages + const staticPages = [ + '', + '/en', + '/en/catalog', + '/en/catalog/men', + '/en/catalog/women', + '/en/catalog/objects', + '/en/timeline', + '/en/about', + '/en/shipping', + '/en/contacts', + ].map((route) => ({ + url: `${baseUrl}${route}`, + lastModified: new Date(), + changeFrequency: 'daily' as const, + priority: route === '' || route === '/en' ? 1 : 0.8, + })) + + return staticPages +} + From c2e03785b2fa2d993cb875d895cdd670214e8f24 Mon Sep 17 00:00:00 2001 From: jekabolt Date: Sun, 11 Jan 2026 15:23:38 +0300 Subject: [PATCH 3/7] add vercel speed insigts --- .../projects/2cf64b0d0cb5b023d4e92b3311a82de8 | 1 + package.json | 1 + pnpm-lock.yaml | 239 ++++++++++-------- src/app/[locale]/layout.tsx | 2 + 4 files changed, 139 insertions(+), 104 deletions(-) create mode 120000 .pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 diff --git a/.pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 b/.pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 new file mode 120000 index 000000000..a8a4f8c21 --- /dev/null +++ b/.pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 @@ -0,0 +1 @@ +../../.. \ No newline at end of file diff --git a/package.json b/package.json index e4c087696..e2d181d56 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@stripe/stripe-js": "^4.6.0", "@tanstack/react-query": "^5.72.0", "@uidotdev/usehooks": "^2.4.1", + "@vercel/speed-insights": "^1.3.1", "babel-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301", "class-variance-authority": "^0.7.0", "clsx": "2.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 634ca1de9..4244dd1b8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,6 +72,9 @@ importers: '@uidotdev/usehooks': specifier: ^2.4.1 version: 2.4.1(react-dom@0.0.0-experimental-443b7ff2-20250303(react@0.0.0-experimental-443b7ff2-20250303))(react@0.0.0-experimental-443b7ff2-20250303) + '@vercel/speed-insights': + specifier: ^1.3.1 + version: 1.3.1(next@15.5.7(@babel/core@7.24.7)(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@0.0.0-experimental-443b7ff2-20250303(react@0.0.0-experimental-443b7ff2-20250303))(react@0.0.0-experimental-443b7ff2-20250303))(react@0.0.0-experimental-443b7ff2-20250303) babel-plugin-react-compiler: specifier: 19.0.0-beta-40c6c23-20250301 version: 19.0.0-beta-40c6c23-20250301 @@ -637,8 +640,8 @@ packages: '@radix-ui/react-accordion@1.2.1': resolution: {integrity: sha512-bg/l7l5QzUjgsh8kjwDFommzAshnUsuVMV5NM56QVCm+7ZckYdd9P/ExR8xG/Oup0OajVxNLaHJ1tb8mXk+nzQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -650,8 +653,8 @@ packages: '@radix-ui/react-arrow@1.1.0': resolution: {integrity: sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -663,8 +666,8 @@ packages: '@radix-ui/react-checkbox@1.1.2': resolution: {integrity: sha512-/i0fl686zaJbDQLNKrkCbMyDm6FQMt4jg323k7HuqitoANm9sE23Ql8yOK3Wusk34HSLKDChhMux05FnP6KUkw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -676,8 +679,8 @@ packages: '@radix-ui/react-collapsible@1.1.1': resolution: {integrity: sha512-1///SnrfQHJEofLokyczERxQbWfCGQlQ2XsCZMucVs6it+lq9iw4vXy+uDn1edlb58cOZOWSldnfPAYcT4O/Yg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -689,8 +692,8 @@ packages: '@radix-ui/react-collection@1.1.0': resolution: {integrity: sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -702,8 +705,8 @@ packages: '@radix-ui/react-collection@1.1.7': resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -715,7 +718,7 @@ packages: '@radix-ui/react-compose-refs@1.1.0': resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -724,7 +727,7 @@ packages: '@radix-ui/react-compose-refs@1.1.1': resolution: {integrity: sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -733,7 +736,7 @@ packages: '@radix-ui/react-compose-refs@1.1.2': resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -742,7 +745,7 @@ packages: '@radix-ui/react-context@1.1.0': resolution: {integrity: sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -751,7 +754,7 @@ packages: '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -760,7 +763,7 @@ packages: '@radix-ui/react-context@1.1.2': resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -769,8 +772,8 @@ packages: '@radix-ui/react-dialog@1.1.4': resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -782,7 +785,7 @@ packages: '@radix-ui/react-direction@1.1.0': resolution: {integrity: sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -791,8 +794,8 @@ packages: '@radix-ui/react-dismissable-layer@1.1.1': resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -804,8 +807,8 @@ packages: '@radix-ui/react-dismissable-layer@1.1.10': resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -817,8 +820,8 @@ packages: '@radix-ui/react-dismissable-layer@1.1.3': resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -830,7 +833,7 @@ packages: '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -839,8 +842,8 @@ packages: '@radix-ui/react-focus-scope@1.1.0': resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -852,8 +855,8 @@ packages: '@radix-ui/react-focus-scope@1.1.1': resolution: {integrity: sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -865,7 +868,7 @@ packages: '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -874,8 +877,8 @@ packages: '@radix-ui/react-label@2.1.0': resolution: {integrity: sha512-peLblDlFw/ngk3UWq0VnYaOLy6agTZZ+MUO/WhVfm14vJGML+xH4FAl2XQGLqdefjNb7ApRg6Yn7U42ZhmYXdw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -887,8 +890,8 @@ packages: '@radix-ui/react-navigation-menu@1.2.1': resolution: {integrity: sha512-egDo0yJD2IK8L17gC82vptkvW1jLeni1VuqCyzY727dSJdk5cDjINomouLoNk8RVF7g2aNIfENKWL4UzeU9c8Q==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -900,8 +903,8 @@ packages: '@radix-ui/react-popover@1.1.2': resolution: {integrity: sha512-u2HRUyWW+lOiA2g0Le0tMmT55FGOEWHwPFt1EPfbLly7uXQExFo5duNKqG2DzmFXIdqOeNd+TpE8baHWJCyP9w==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -913,8 +916,8 @@ packages: '@radix-ui/react-popper@1.2.0': resolution: {integrity: sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -926,8 +929,8 @@ packages: '@radix-ui/react-portal@1.1.2': resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -939,8 +942,8 @@ packages: '@radix-ui/react-portal@1.1.3': resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -952,8 +955,8 @@ packages: '@radix-ui/react-portal@1.1.9': resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -965,8 +968,8 @@ packages: '@radix-ui/react-presence@1.1.1': resolution: {integrity: sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -978,8 +981,8 @@ packages: '@radix-ui/react-presence@1.1.2': resolution: {integrity: sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -991,8 +994,8 @@ packages: '@radix-ui/react-presence@1.1.4': resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1004,8 +1007,8 @@ packages: '@radix-ui/react-primitive@2.0.0': resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1017,8 +1020,8 @@ packages: '@radix-ui/react-primitive@2.0.1': resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1030,8 +1033,8 @@ packages: '@radix-ui/react-primitive@2.0.2': resolution: {integrity: sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1043,8 +1046,8 @@ packages: '@radix-ui/react-primitive@2.1.3': resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1056,8 +1059,8 @@ packages: '@radix-ui/react-radio-group@1.2.1': resolution: {integrity: sha512-kdbv54g4vfRjja9DNWPMxKvXblzqbpEC8kspEkZ6dVP7kQksGCn+iZHkcCz2nb00+lPdRvxrqy4WrvvV1cNqrQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1069,8 +1072,8 @@ packages: '@radix-ui/react-roving-focus@1.1.0': resolution: {integrity: sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1082,8 +1085,8 @@ packages: '@radix-ui/react-select@2.1.2': resolution: {integrity: sha512-rZJtWmorC7dFRi0owDmoijm6nSJH1tVw64QGiNIZ9PNLyBDtG+iAq+XGsya052At4BfarzY/Dhv9wrrUr6IMZA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1095,7 +1098,7 @@ packages: '@radix-ui/react-slot@1.1.0': resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1104,7 +1107,7 @@ packages: '@radix-ui/react-slot@1.1.1': resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1113,7 +1116,7 @@ packages: '@radix-ui/react-slot@1.1.2': resolution: {integrity: sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1122,7 +1125,7 @@ packages: '@radix-ui/react-slot@1.2.3': resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1131,8 +1134,8 @@ packages: '@radix-ui/react-switch@1.1.3': resolution: {integrity: sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1144,8 +1147,8 @@ packages: '@radix-ui/react-toast@1.2.14': resolution: {integrity: sha512-nAP5FBxBJGQ/YfUB+r+O6USFVkWq3gAInkxyEnmvEV5jtSbfDhfa4hwX8CraCnbjMLsE7XSf/K75l9xXY7joWg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1157,7 +1160,7 @@ packages: '@radix-ui/react-use-callback-ref@1.1.0': resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1166,7 +1169,7 @@ packages: '@radix-ui/react-use-callback-ref@1.1.1': resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1175,7 +1178,7 @@ packages: '@radix-ui/react-use-controllable-state@1.1.0': resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1184,7 +1187,7 @@ packages: '@radix-ui/react-use-controllable-state@1.2.2': resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1193,7 +1196,7 @@ packages: '@radix-ui/react-use-effect-event@0.0.2': resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1202,7 +1205,7 @@ packages: '@radix-ui/react-use-escape-keydown@1.1.0': resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1211,7 +1214,7 @@ packages: '@radix-ui/react-use-escape-keydown@1.1.1': resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1220,7 +1223,7 @@ packages: '@radix-ui/react-use-layout-effect@1.1.0': resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1229,7 +1232,7 @@ packages: '@radix-ui/react-use-layout-effect@1.1.1': resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1238,7 +1241,7 @@ packages: '@radix-ui/react-use-previous@1.1.0': resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1247,7 +1250,7 @@ packages: '@radix-ui/react-use-rect@1.1.0': resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1256,7 +1259,7 @@ packages: '@radix-ui/react-use-size@1.1.0': resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -1265,8 +1268,8 @@ packages: '@radix-ui/react-visually-hidden@1.1.0': resolution: {integrity: sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1278,8 +1281,8 @@ packages: '@radix-ui/react-visually-hidden@1.2.3': resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 - '@types/react-dom': npm:types-react-dom@19.0.0-rc.1 + '@types/react': '*' + '@types/react-dom': '*' react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: @@ -1306,7 +1309,7 @@ packages: '@react-input/core@1.0.12': resolution: {integrity: sha512-lZDQjphsJenWCD0mcflsyneLnb2a7bFAgAVzih9diEcUjKopcK+QOXhz0a0PHEp8k5LEKpYadjwyltAGOOhL2g==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '>=16.8' react: '>=16.8' react-dom: '>=16.8' peerDependenciesMeta: @@ -1316,7 +1319,7 @@ packages: '@react-input/mask@1.2.5': resolution: {integrity: sha512-xouBATnitQqhgKLgu6/J2IZRu7kzqm/tFw2ToFnk6EkRzWs5dawlcF8asASMu7LCDrCqSkjwN+/sscEMG7IXog==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '>=16.8' react: '>=16.8' react-dom: '>=16.8' peerDependenciesMeta: @@ -1486,6 +1489,29 @@ packages: '@ungap/structured-clone@1.2.0': resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + '@vercel/speed-insights@1.3.1': + resolution: {integrity: sha512-PbEr7FrMkUrGYvlcLHGkXdCkxnylCWePx7lPxxq36DNdfo9mcUjLOmqOyPDHAOgnfqgGGdmE3XI9L/4+5fr+vQ==} + peerDependencies: + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + next: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2983,7 +3009,7 @@ packages: react-markdown@10.1.0: resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '>=18' react: '>=18' react-photo-view@1.2.4: @@ -2996,7 +3022,7 @@ packages: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -3006,7 +3032,7 @@ packages: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 peerDependenciesMeta: '@types/react': @@ -3016,7 +3042,7 @@ packages: resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -3026,7 +3052,7 @@ packages: resolution: {integrity: sha512-KmONPx5fnlXYJQqC62Q+lwIeAk64ws/cUw6omIumRzMRPqgnYqhSSti99nbj0Ry13bv7dF+BKn7NB+OqkdZGTw==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -3036,7 +3062,7 @@ packages: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -3046,7 +3072,7 @@ packages: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -3412,7 +3438,7 @@ packages: resolution: {integrity: sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -3422,7 +3448,7 @@ packages: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '*' react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -3437,7 +3463,7 @@ packages: resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==} engines: {node: '>=10'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 peerDependenciesMeta: '@types/react': @@ -3533,7 +3559,7 @@ packages: resolution: {integrity: sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==} engines: {node: '>=12.20.0'} peerDependencies: - '@types/react': npm:types-react@19.0.0-rc.1 + '@types/react': '>=18.0.0' immer: '>=9.0.6' react: '>=18.0.0' use-sync-external-store: '>=1.2.0' @@ -4828,6 +4854,11 @@ snapshots: '@ungap/structured-clone@1.2.0': {} + '@vercel/speed-insights@1.3.1(next@15.5.7(@babel/core@7.24.7)(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@0.0.0-experimental-443b7ff2-20250303(react@0.0.0-experimental-443b7ff2-20250303))(react@0.0.0-experimental-443b7ff2-20250303))(react@0.0.0-experimental-443b7ff2-20250303)': + optionalDependencies: + next: 15.5.7(@babel/core@7.24.7)(babel-plugin-react-compiler@19.0.0-beta-40c6c23-20250301)(react-dom@0.0.0-experimental-443b7ff2-20250303(react@0.0.0-experimental-443b7ff2-20250303))(react@0.0.0-experimental-443b7ff2-20250303) + react: 0.0.0-experimental-443b7ff2-20250303 + acorn-jsx@5.3.2(acorn@8.11.3): dependencies: acorn: 8.11.3 diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 499775ecf..2a526c38b 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -1,6 +1,7 @@ import { FeatureMono } from "@/fonts"; import { routing } from "@/i18n/routing"; import { GoogleTagManager } from "@next/third-parties/google"; +import { SpeedInsights } from "@vercel/speed-insights/next"; import { Metadata } from "next"; import { NextIntlClientProvider } from "next-intl"; import { @@ -76,6 +77,7 @@ export default async function RootLayout({ children, params }: Props) {
+ ); From c4daa23a9ef4bdfa423a3cb995cc9f188faea6c4 Mon Sep 17 00:00:00 2001 From: jekabolt Date: Sun, 11 Jan 2026 15:30:09 +0300 Subject: [PATCH 4/7] fix --- .gitignore | 1 + .pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 120000 .pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 diff --git a/.gitignore b/.gitignore index 66bbee8d4..486b391d7 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /.pnp .pnp.js .yarn/install-state.gz +.pnpm-store # testing /coverage diff --git a/.pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 b/.pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 deleted file mode 120000 index a8a4f8c21..000000000 --- a/.pnpm-store/v10/projects/2cf64b0d0cb5b023d4e92b3311a82de8 +++ /dev/null @@ -1 +0,0 @@ -../../.. \ No newline at end of file From d61589c103136741a16cbb09f3494fa275e4827b Mon Sep 17 00:00:00 2001 From: jekabolt Date: Sun, 11 Jan 2026 23:36:37 +0300 Subject: [PATCH 5/7] wip --- next.config.ts | 19 ++++++++++-- src/app/[locale]/_components/main-ads.tsx | 2 ++ .../[locale]/_components/page-background.tsx | 29 ++++++++++++------- .../_components/single-featured-item.tsx | 1 + src/app/[locale]/layout.tsx | 9 +++--- src/app/[locale]/loading.tsx | 8 +++++ src/app/globals.css | 9 ++++++ src/app/template.tsx | 7 +++-- src/components/page-transition.tsx | 28 +++++++++--------- src/components/ui/image.tsx | 7 ++++- src/fonts/index.ts | 4 ++- 11 files changed, 90 insertions(+), 33 deletions(-) create mode 100644 src/app/[locale]/loading.tsx diff --git a/next.config.ts b/next.config.ts index 139a78bcf..bf5f5a4c4 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,7 +7,8 @@ const nextConfig: NextConfig = { experimental: { reactCompiler: { compilationMode: 'annotation' - } + }, + optimizePackageImports: ['framer-motion', '@radix-ui/react-dialog', '@radix-ui/react-popover'], }, images: { remotePatterns: [ @@ -21,11 +22,16 @@ const nextConfig: NextConfig = { }, ], formats: ["image/avif", "image/webp"], - deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + deviceSizes: [640, 750, 828, 1080, 1200, 1920], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], minimumCacheTTL: 31536000, // 1 year in seconds + dangerouslyAllowSVG: false, + contentDispositionType: 'inline', + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", }, pageExtensions: ["mdx", "ts", "tsx"], + compress: true, + poweredByHeader: false, async headers() { return [ { @@ -46,6 +52,15 @@ const nextConfig: NextConfig = { }, ], }, + { + source: '/_next/image', + headers: [ + { + key: 'Cache-Control', + value: 'public, max-age=31536000, immutable', + }, + ], + }, ]; }, }; diff --git a/src/app/[locale]/_components/main-ads.tsx b/src/app/[locale]/_components/main-ads.tsx index b5733a99a..f57ccc23a 100644 --- a/src/app/[locale]/_components/main-ads.tsx +++ b/src/app/[locale]/_components/main-ads.tsx @@ -36,6 +36,7 @@ export function MainAds({ main }: { main?: common_HeroMainWithTranslations }) { fit="cover" priority={true} loading="eager" + fetchPriority="high" />
@@ -49,6 +50,7 @@ export function MainAds({ main }: { main?: common_HeroMainWithTranslations }) { fit="cover" priority={true} loading="eager" + fetchPriority="high" />
diff --git a/src/app/[locale]/_components/page-background.tsx b/src/app/[locale]/_components/page-background.tsx index 637934160..7b53a368b 100644 --- a/src/app/[locale]/_components/page-background.tsx +++ b/src/app/[locale]/_components/page-background.tsx @@ -177,17 +177,26 @@ export function PageBackground({ if (backgroundColor) { applyBackground(backgroundColor); } - // Extract color from image + // Extract color from image - defer to avoid blocking else if (imageUrl) { - const nextImageUrl = `/_next/image?url=${encodeURIComponent(imageUrl)}&w=1920&q=75`; - - extractAverageColorWithOverlay(nextImageUrl) - .then((hexColor) => { - if (hexColor) applyBackground(hexColor); - }) - .catch((error) => - console.error("HeroBackground color extraction error:", error), - ); + // Use requestIdleCallback to defer color extraction + const extractColor = () => { + const nextImageUrl = `/_next/image?url=${encodeURIComponent(imageUrl)}&w=128&q=50`; + + extractAverageColorWithOverlay(nextImageUrl) + .then((hexColor) => { + if (hexColor) applyBackground(hexColor); + }) + .catch((error) => + console.error("HeroBackground color extraction error:", error), + ); + }; + + if ('requestIdleCallback' in window) { + requestIdleCallback(extractColor, { timeout: 2000 }); + } else { + setTimeout(extractColor, 100); + } } return () => { diff --git a/src/app/[locale]/_components/single-featured-item.tsx b/src/app/[locale]/_components/single-featured-item.tsx index d8d8ff8a9..259964d22 100644 --- a/src/app/[locale]/_components/single-featured-item.tsx +++ b/src/app/[locale]/_components/single-featured-item.tsx @@ -96,6 +96,7 @@ export function SingleFeaturedItem({ fit="contain" priority={true} loading="eager" + fetchPriority="high" /> diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 2a526c38b..73a3b3ec2 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -60,12 +60,12 @@ export default async function RootLayout({ children, params }: Props) { return ( - + - + + - @@ -77,8 +77,9 @@ export default async function RootLayout({ children, params }: Props) { - + + ); } diff --git a/src/app/[locale]/loading.tsx b/src/app/[locale]/loading.tsx new file mode 100644 index 000000000..5a2b5da27 --- /dev/null +++ b/src/app/[locale]/loading.tsx @@ -0,0 +1,8 @@ +export default function Loading() { + return ( +
+
+
+ ); +} + diff --git a/src/app/globals.css b/src/app/globals.css index d5f8b3e6d..dca6bb675 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -54,11 +54,20 @@ body { touch-action: manipulation; background-color: var(--background); + /* Optimize rendering */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; ::selection { @apply bg-acidColor text-bgColor; } } + + /* Optimize images for better LCP */ + img { + content-visibility: auto; + } } @layer utilities { diff --git a/src/app/template.tsx b/src/app/template.tsx index 735712b2a..10db1687e 100644 --- a/src/app/template.tsx +++ b/src/app/template.tsx @@ -18,8 +18,11 @@ export default async function Template({ }: { children: React.ReactNode; }) { - const heroData = await serviceClient.GetHero({}); - const initialTranslationState = await getInitialTranslationState(); + // Fetch data in parallel for better performance + const [heroData, initialTranslationState] = await Promise.all([ + serviceClient.GetHero({}), + getInitialTranslationState(), + ]); return ( diff --git a/src/components/page-transition.tsx b/src/components/page-transition.tsx index c58af8cbc..5c025ef29 100644 --- a/src/components/page-transition.tsx +++ b/src/components/page-transition.tsx @@ -1,6 +1,6 @@ "use client"; -import { motion } from "framer-motion"; +import { LazyMotion, domAnimation, m } from "framer-motion"; import { usePathname } from "next/navigation"; import { ReactNode } from "react"; @@ -12,18 +12,20 @@ export function PageTransition({ children }: PageTransitionProps) { const pathname = usePathname(); return ( - - {children} - + + + {children} + + ); } diff --git a/src/components/ui/image.tsx b/src/components/ui/image.tsx index d5e2f6ee3..008dbe9f3 100644 --- a/src/components/ui/image.tsx +++ b/src/components/ui/image.tsx @@ -22,16 +22,18 @@ type ImageProps = { fit?: "cover" | "contain" | "fill" | "scale-down"; priority?: boolean; loading?: "lazy" | "eager"; + fetchPriority?: "high" | "low" | "auto"; }; export default function ImageComponent({ aspectRatio, src, alt, - sizes = "(max-width: 1280px) 100vw, 1280px", + sizes = "(max-width: 768px) 100vw, (max-width: 1280px) 50vw, 1280px", fit, priority = false, loading = "lazy", + fetchPriority, }: ImageProps) { return ( @@ -43,9 +45,12 @@ export default function ImageComponent({ sizes={sizes} priority={priority} loading={priority ? undefined : loading} + quality={priority ? 85 : 75} + fetchPriority={fetchPriority || (priority ? "high" : "auto")} style={{ objectFit: fit, }} + unoptimized={false} /> ); diff --git a/src/fonts/index.ts b/src/fonts/index.ts index f1618ef58..879d8e2e9 100644 --- a/src/fonts/index.ts +++ b/src/fonts/index.ts @@ -20,5 +20,7 @@ export const FeatureMono = localFont({ ], display: "swap", preload: true, - fallback: ["system-ui", "sans-serif"], + fallback: ["ui-monospace", "Menlo", "Monaco", "Cascadia Mono", "Courier New", "monospace"], + adjustFontFallback: false, + variable: "--font-feature-mono", }); From ef3b40dc78b3e1393651e44e4c163a515e588305 Mon Sep 17 00:00:00 2001 From: jekabolt Date: Sun, 11 Jan 2026 23:50:09 +0300 Subject: [PATCH 6/7] wip --- package.json | 159 +++++++++--------- pnpm-lock.yaml | 8 + src/app/[locale]/_components/ads.tsx | 10 +- src/app/[locale]/_components/archive-item.tsx | 5 +- src/app/[locale]/_components/main-ads.tsx | 2 + src/app/[locale]/_components/product-item.tsx | 1 + .../_components/single-featured-item.tsx | 1 + .../images-carousel/ArchiveMediaItem.tsx | 3 +- .../images-carousel/ProductMediaItem.tsx | 3 +- src/components/ui/image.tsx | 36 ++++ 10 files changed, 142 insertions(+), 86 deletions(-) diff --git a/package.json b/package.json index e2d181d56..af5302091 100644 --- a/package.json +++ b/package.json @@ -1,81 +1,82 @@ { - "name": "grbpwr.com", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev --turbo", - "build": "next build", - "start": "next start", - "lint": "next lint", - "format": "pnpm prettier --write .", - "gen": "cd proto && git pull origin main && cd .. && make proto" - }, - "dependencies": { - "@formatjs/intl-localematcher": "^0.5.4", - "@hookform/resolvers": "^3.6.0", - "@next/third-parties": "^15.5.2", - "@radix-ui/react-accordion": "^1.2.1", - "@radix-ui/react-checkbox": "^1.1.2", - "@radix-ui/react-dialog": "^1.1.4", - "@radix-ui/react-label": "^2.1.0", - "@radix-ui/react-navigation-menu": "^1.2.1", - "@radix-ui/react-popover": "1.1.2", - "@radix-ui/react-radio-group": "^1.2.1", - "@radix-ui/react-select": "^2.1.2", - "@radix-ui/react-slot": "^1.1.0", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-toast": "^1.2.14", - "@react-google-maps/api": "^2.20.6", - "@react-input/mask": "^1.2.5", - "@stripe/react-stripe-js": "^2.8.0", - "@stripe/stripe-js": "^4.6.0", - "@tanstack/react-query": "^5.72.0", - "@uidotdev/usehooks": "^2.4.1", - "@vercel/speed-insights": "^1.3.1", - "babel-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301", - "class-variance-authority": "^0.7.0", - "clsx": "2.1.0", - "embla-carousel-react": "^8.5.1", - "embla-carousel-wheel-gestures": "^8.0.1", - "framer-motion": "^12.23.12", - "negotiator": "^0.6.3", - "next": "15.5.7", - "next-intl": "^4.3.9", - "qrcode": "^1.5.3", - "react": "0.0.0-experimental-443b7ff2-20250303", - "react-dom": "0.0.0-experimental-443b7ff2-20250303", - "react-hook-form": "^7.52.0", - "react-intersection-observer": "^9.13.0", - "react-markdown": "^10.1.0", - "react-photo-view": "^1.2.4", - "react-zoom-pan-pinch": "^3.7.0", - "vaul": "^1.1.2", - "zod": "^3.23.8", - "zustand": "^5.0.0" - }, - "devDependencies": { - "@ianvs/prettier-plugin-sort-imports": "^4.3.1", - "@tailwindcss/typography": "^0.5.13", - "@types/mdx": "^2.0.13", - "@types/node": "20", - "@types/qrcode": "^1.5.5", - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", - "autoprefixer": "10.4.20", - "eslint": "8.57.0", - "eslint-config-next": "15.0.0", - "postcss": "^8.4.47", - "postcss-nesting": "^12.1.5", - "prettier": "3.2.5", - "prettier-plugin-tailwindcss": "0.6.11", - "tailwind-merge": "^2.3.0", - "tailwindcss": "3.4.14", - "typescript": "5.4.3" - }, - "pnpm": { - "overrides": { - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" + "name": "grbpwr.com", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbo", + "build": "next build", + "start": "next start", + "lint": "next lint", + "format": "pnpm prettier --write .", + "gen": "cd proto && git pull origin main && cd .. && make proto" + }, + "dependencies": { + "@formatjs/intl-localematcher": "^0.5.4", + "@hookform/resolvers": "^3.6.0", + "@next/third-parties": "^15.5.2", + "@radix-ui/react-accordion": "^1.2.1", + "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-navigation-menu": "^1.2.1", + "@radix-ui/react-popover": "1.1.2", + "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-select": "^2.1.2", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-toast": "^1.2.14", + "@react-google-maps/api": "^2.20.6", + "@react-input/mask": "^1.2.5", + "@stripe/react-stripe-js": "^2.8.0", + "@stripe/stripe-js": "^4.6.0", + "@tanstack/react-query": "^5.72.0", + "@uidotdev/usehooks": "^2.4.1", + "@vercel/speed-insights": "^1.3.1", + "babel-plugin-react-compiler": "19.0.0-beta-40c6c23-20250301", + "blurhash": "^2.0.5", + "class-variance-authority": "^0.7.0", + "clsx": "2.1.0", + "embla-carousel-react": "^8.5.1", + "embla-carousel-wheel-gestures": "^8.0.1", + "framer-motion": "^12.23.12", + "negotiator": "^0.6.3", + "next": "15.5.7", + "next-intl": "^4.3.9", + "qrcode": "^1.5.3", + "react": "0.0.0-experimental-443b7ff2-20250303", + "react-dom": "0.0.0-experimental-443b7ff2-20250303", + "react-hook-form": "^7.52.0", + "react-intersection-observer": "^9.13.0", + "react-markdown": "^10.1.0", + "react-photo-view": "^1.2.4", + "react-zoom-pan-pinch": "^3.7.0", + "vaul": "^1.1.2", + "zod": "^3.23.8", + "zustand": "^5.0.0" + }, + "devDependencies": { + "@ianvs/prettier-plugin-sort-imports": "^4.3.1", + "@tailwindcss/typography": "^0.5.13", + "@types/mdx": "^2.0.13", + "@types/node": "20", + "@types/qrcode": "^1.5.5", + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", + "autoprefixer": "10.4.20", + "eslint": "8.57.0", + "eslint-config-next": "15.0.0", + "postcss": "^8.4.47", + "postcss-nesting": "^12.1.5", + "prettier": "3.2.5", + "prettier-plugin-tailwindcss": "0.6.11", + "tailwind-merge": "^2.3.0", + "tailwindcss": "3.4.14", + "typescript": "5.4.3" + }, + "pnpm": { + "overrides": { + "@types/react": "npm:types-react@19.0.0-rc.1", + "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" + } } - } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4244dd1b8..d91d7a4b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: babel-plugin-react-compiler: specifier: 19.0.0-beta-40c6c23-20250301 version: 19.0.0-beta-40c6c23-20250301 + blurhash: + specifier: ^2.0.5 + version: 2.0.5 class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -1637,6 +1640,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + blurhash@2.0.5: + resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -5000,6 +5006,8 @@ snapshots: binary-extensions@2.3.0: {} + blurhash@2.0.5: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 diff --git a/src/app/[locale]/_components/ads.tsx b/src/app/[locale]/_components/ads.tsx index bed57ead8..20b4529b4 100644 --- a/src/app/[locale]/_components/ads.tsx +++ b/src/app/[locale]/_components/ads.tsx @@ -2,13 +2,13 @@ import type { common_HeroEntityWithTranslations } from "@/api/proto-http/frontend"; -import { sendHeroEvent } from "@/lib/analitycs/hero"; -import { useTranslationsStore } from "@/lib/stores/translations/store-provider"; -import { calculateAspectRatio, cn } from "@/lib/utils"; import { AnimatedButton } from "@/components/ui/animated-button"; import Image from "@/components/ui/image"; import { Overlay } from "@/components/ui/overlay"; import { Text } from "@/components/ui/text"; +import { sendHeroEvent } from "@/lib/analitycs/hero"; +import { useTranslationsStore } from "@/lib/stores/translations/store-provider"; +import { calculateAspectRatio, cn } from "@/lib/utils"; import { FeaturedItems } from "./featured-items"; import { HeroArchive } from "./hero-archive"; @@ -52,6 +52,7 @@ export function Ads({ fit="cover" priority={isPriorityAd} loading={isPriorityAd ? "eager" : "lazy"} + blurhash={e.single?.mediaLandscape?.media?.blurhash} />
@@ -67,6 +68,7 @@ export function Ads({ fit="cover" priority={isPriorityAd} loading={isPriorityAd ? "eager" : "lazy"} + blurhash={e.single?.mediaPortrait?.media?.blurhash} />
@@ -120,6 +122,7 @@ export function Ads({ fit="contain" priority={isPriorityAd} loading={isPriorityAd ? "eager" : "lazy"} + blurhash={e.double?.left?.mediaLandscape?.media?.blurhash} />
{id} diff --git a/src/app/[locale]/_components/main-ads.tsx b/src/app/[locale]/_components/main-ads.tsx index e2669e442..49fceb1aa 100644 --- a/src/app/[locale]/_components/main-ads.tsx +++ b/src/app/[locale]/_components/main-ads.tsx @@ -37,6 +37,7 @@ export function MainAds({ main }: { main?: common_HeroMainWithTranslations }) { priority={true} loading="eager" fetchPriority="high" + blurhash={main.single?.mediaLandscape?.media?.blurhash} />
@@ -51,6 +52,7 @@ export function MainAds({ main }: { main?: common_HeroMainWithTranslations }) { priority={true} loading="eager" fetchPriority="high" + blurhash={main.single?.mediaPortrait?.media?.blurhash} />
diff --git a/src/app/[locale]/_components/product-item.tsx b/src/app/[locale]/_components/product-item.tsx index 09a8026bf..5b33374f2 100644 --- a/src/app/[locale]/_components/product-item.tsx +++ b/src/app/[locale]/_components/product-item.tsx @@ -100,6 +100,7 @@ export function ProductItem({ fit="contain" priority={imagePriority} loading={imagePriority ? "eager" : "lazy"} + blurhash={product.productDisplay?.thumbnail?.media?.blurhash} /> {!disableAnimations && ( diff --git a/src/app/[locale]/_components/single-featured-item.tsx b/src/app/[locale]/_components/single-featured-item.tsx index 259964d22..dbe693f15 100644 --- a/src/app/[locale]/_components/single-featured-item.tsx +++ b/src/app/[locale]/_components/single-featured-item.tsx @@ -97,6 +97,7 @@ export function SingleFeaturedItem({ priority={true} loading="eager" fetchPriority="high" + blurhash={p.productDisplay?.thumbnail?.media?.blurhash} />
diff --git a/src/components/images-carousel/ArchiveMediaItem.tsx b/src/components/images-carousel/ArchiveMediaItem.tsx index e09c318fa..17e2217da 100644 --- a/src/components/images-carousel/ArchiveMediaItem.tsx +++ b/src/components/images-carousel/ArchiveMediaItem.tsx @@ -3,8 +3,8 @@ import { common_MediaFull } from "@/api/proto-http/frontend"; import { PhotoView } from "react-photo-view"; -import { calculateAspectRatio } from "@/lib/utils"; import GlobalImage from "@/components/ui/image"; +import { calculateAspectRatio } from "@/lib/utils"; export function ArchiveMediaItem({ singleMedia, @@ -22,6 +22,7 @@ export function ArchiveMediaItem({ singleMedia?.media?.fullSize?.height, )} fit="contain" + blurhash={singleMedia?.media?.blurhash} />
diff --git a/src/components/images-carousel/ProductMediaItem.tsx b/src/components/images-carousel/ProductMediaItem.tsx index f218f56a9..6d45f6d2a 100644 --- a/src/components/images-carousel/ProductMediaItem.tsx +++ b/src/components/images-carousel/ProductMediaItem.tsx @@ -4,8 +4,8 @@ import { common_MediaFull } from "@/api/proto-http/frontend"; // import { PhotoProvider, PhotoView } from "react-photo-view"; -import { calculateAspectRatio, cn } from "@/lib/utils"; import GlobalImage from "@/components/ui/image"; +import { calculateAspectRatio } from "@/lib/utils"; export function ProductMediaItem({ singleMedia, @@ -25,6 +25,7 @@ export function ProductMediaItem({ singleMedia?.media?.fullSize?.height, )} fit="contain" + blurhash={singleMedia?.media?.blurhash} /> {/* */} {/* */} diff --git a/src/components/ui/image.tsx b/src/components/ui/image.tsx index 008dbe9f3..6a2902253 100644 --- a/src/components/ui/image.tsx +++ b/src/components/ui/image.tsx @@ -1,4 +1,8 @@ +"use client"; + import Image from "next/image"; +import { decode } from "blurhash"; +import { useEffect, useState } from "react"; function ImageContainer({ aspectRatio, @@ -23,8 +27,28 @@ type ImageProps = { priority?: boolean; loading?: "lazy" | "eager"; fetchPriority?: "high" | "low" | "auto"; + blurhash?: string; }; +// Helper function to convert blurhash to data URL +function blurhashToDataUrl(blurhash: string, width = 32, height = 32): string { + try { + const pixels = decode(blurhash, width, height); + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext("2d"); + if (!ctx) return ""; + + const imageData = ctx.createImageData(width, height); + imageData.data.set(pixels); + ctx.putImageData(imageData, 0, 0); + return canvas.toDataURL(); + } catch { + return ""; + } +} + export default function ImageComponent({ aspectRatio, src, @@ -34,7 +58,17 @@ export default function ImageComponent({ priority = false, loading = "lazy", fetchPriority, + blurhash, }: ImageProps) { + const [blurDataURL, setBlurDataURL] = useState(""); + + useEffect(() => { + if (blurhash && typeof window !== "undefined") { + const dataUrl = blurhashToDataUrl(blurhash); + setBlurDataURL(dataUrl); + } + }, [blurhash]); + return ( From 02de10753f61de2592fdff142931318b682da7f3 Mon Sep 17 00:00:00 2001 From: jekabolt Date: Mon, 12 Jan 2026 00:03:44 +0300 Subject: [PATCH 7/7] blurhash --- next.config.ts | 2 +- src/app/[locale]/_components/product-item.tsx | 2 ++ src/components/page-transition.tsx | 28 +++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/next.config.ts b/next.config.ts index bf5f5a4c4..2618b645d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -8,7 +8,7 @@ const nextConfig: NextConfig = { reactCompiler: { compilationMode: 'annotation' }, - optimizePackageImports: ['framer-motion', '@radix-ui/react-dialog', '@radix-ui/react-popover'], + optimizePackageImports: ['@radix-ui/react-dialog', '@radix-ui/react-popover'], }, images: { remotePatterns: [ diff --git a/src/app/[locale]/_components/product-item.tsx b/src/app/[locale]/_components/product-item.tsx index 5b33374f2..292653bb6 100644 --- a/src/app/[locale]/_components/product-item.tsx +++ b/src/app/[locale]/_components/product-item.tsx @@ -22,11 +22,13 @@ export function ProductItem({ className, isInfoVisible = true, imagePriority = false, + disableAnimations = false, }: { product: common_Product; className: string; isInfoVisible?: boolean; imagePriority?: boolean; + disableAnimations?: boolean; }) { const tCatalog = useTranslations("catalog"); const t = useTranslations("categories"); diff --git a/src/components/page-transition.tsx b/src/components/page-transition.tsx index 5c025ef29..b88ae1750 100644 --- a/src/components/page-transition.tsx +++ b/src/components/page-transition.tsx @@ -1,6 +1,6 @@ "use client"; -import { LazyMotion, domAnimation, m } from "framer-motion"; +import { motion } from "framer-motion"; import { usePathname } from "next/navigation"; import { ReactNode } from "react"; @@ -12,20 +12,18 @@ export function PageTransition({ children }: PageTransitionProps) { const pathname = usePathname(); return ( - - - {children} - - + + {children} + ); }