diff --git a/package.json b/package.json index cac845a..e809588 100644 --- a/package.json +++ b/package.json @@ -10,11 +10,13 @@ "prepare": "husky" }, "dependencies": { + "blurhash": "^2.0.5", "clsx": "^2.1.1", "focus-trap-react": "^11.0.4", "next": "15.1.7", "nodemailer": "^6.10.0", "react": "^19.0.0", + "react-blurhash": "^0.3.0", "react-dom": "^19.0.0", "react-hot-toast": "^2.5.2", "react-icons": "^5.5.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cdd2b88..78dbd10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + blurhash: + specifier: ^2.0.5 + version: 2.0.5 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -23,6 +26,9 @@ importers: react: specifier: ^19.0.0 version: 19.2.0 + react-blurhash: + specifier: ^0.3.0 + version: 0.3.0(blurhash@2.0.5)(react@19.2.0) react-dom: specifier: ^19.0.0 version: 19.2.0(react@19.2.0) @@ -1019,6 +1025,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + blurhash@2.0.5: + resolution: {integrity: sha512-cRygWd7kGBQO3VEhPiTgq4Wc43ctsM+o46urrmPOiuAe+07fzlSB9OJVdpgDL0jPqXUVQ9ht7aq7kxOeJHRK+w==} + bowser@2.12.1: resolution: {integrity: sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==} @@ -2104,6 +2113,12 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-blurhash@0.3.0: + resolution: {integrity: sha512-XlKr4Ns1iYFRnk6DkAblNbAwN/bTJvxTVoxMvmTcURdc5oLoXZwqAF9N3LZUh/HT+QFlq5n6IS6VsDGsviYAiQ==} + peerDependencies: + blurhash: ^2.0.3 + react: '>=15' + react-dom@19.2.0: resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==} peerDependencies: @@ -3803,6 +3818,8 @@ snapshots: binary-extensions@2.3.0: {} + blurhash@2.0.5: {} + bowser@2.12.1: {} brace-expansion@1.1.12: @@ -5006,6 +5023,11 @@ snapshots: queue-microtask@1.2.3: {} + react-blurhash@0.3.0(blurhash@2.0.5)(react@19.2.0): + dependencies: + blurhash: 2.0.5 + react: 19.2.0 + react-dom@19.2.0(react@19.2.0): dependencies: react: 19.2.0 diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx index cf1d9b3..0da9dd8 100644 --- a/src/components/sections/Hero.tsx +++ b/src/components/sections/Hero.tsx @@ -1,8 +1,36 @@ +"use client"; import Image from "next/image"; +import { useEffect, useState } from "react"; +import { decode } from "blurhash"; import nav_items from "@/data/nav_items.json"; +const HERO_BLURHASH = "LQFq2y}uGE5REMNawJn%RQaeso$*"; + +function blurhashToBase64(blurhash: string, width = 32, height = 32): string { + 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("image/jpeg", 0.5); +} + export default function Hero() { + const [blurDataURL, setBlurDataURL] = useState(""); + + useEffect(() => { + setBlurDataURL(blurhashToBase64(HERO_BLURHASH, 32, 21)); + }, []); + return (

{nav_items[0]}

@@ -16,6 +44,8 @@ export default function Hero() { style={{ objectFit: "cover" }} priority={true} fetchPriority="high" + placeholder={blurDataURL ? "blur" : "empty"} + blurDataURL={blurDataURL || undefined} />