diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 0000000..8912ec8 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,226 @@ +name: Lighthouse CI + +on: + pull_request: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + lighthouse: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - path: /posts + path_key: posts + perf_level: error + perf_min: "0.7" + - path: /posts/100 + path_key: posts-100 + perf_level: warn + perf_min: "0.5" + - path: /projects/1 + path_key: projects-1 + perf_level: error + perf_min: "0.8" + env: + NEXT_PUBLIC_API_BASE_URL: https://api.yonghun.me + NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} + NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Build app + run: npm run build + + - name: Prepare LHCI runtime config + run: | + node <<'EOF' + const fs = require("fs"); + const config = JSON.parse(fs.readFileSync(".lighthouserc.json", "utf8")); + config.ci.collect.url = [`http://localhost:3000${process.env.LH_PATH}`]; + config.ci.assert.assertions["categories:performance"] = [ + process.env.LH_PERF_LEVEL, + { minScore: Number(process.env.LH_PERF_MIN) }, + ]; + fs.writeFileSync(".lighthouserc.runtime.json", JSON.stringify(config, null, 2)); + EOF + env: + LH_PATH: ${{ matrix.path }} + LH_PERF_LEVEL: ${{ matrix.perf_level }} + LH_PERF_MIN: ${{ matrix.perf_min }} + + - name: Run Lighthouse CI + id: lhci + continue-on-error: true + run: npx @lhci/cli@0.13.x autorun --config=.lighthouserc.runtime.json + + - name: Prepare Lighthouse summary + if: github.event_name == 'pull_request' + run: | + node <<'EOF' + const fs = require("fs"); + const path = require("path"); + + const reportsDir = path.join(process.cwd(), ".lighthouseci"); + const outPath = path.join(process.cwd(), ".lighthouse-summary.md"); + + let lines = []; + lines.push(``); + lines.push("## Lighthouse CI 결과"); + lines.push(""); + lines.push(`- 실행 상태: **${process.env.LHCI_OUTCOME || "unknown"}**`); + lines.push(`- 대상: \`${process.env.LH_PATH}\` (3회 측정)`); + lines.push(`- 성능 기준: **${process.env.LH_PERF_LEVEL} >= ${Math.round(Number(process.env.LH_PERF_MIN) * 100)}**`); + lines.push(""); + + if (!fs.existsSync(reportsDir)) { + lines.push("리포트 디렉터리를 찾을 수 없습니다."); + fs.writeFileSync(outPath, lines.join("\n")); + process.exit(0); + } + + const jsonFiles = fs + .readdirSync(reportsDir) + .filter((name) => name.endsWith(".json")) + .map((name) => path.join(reportsDir, name)); + + const rows = []; + for (const jsonFile of jsonFiles) { + try { + const report = JSON.parse(fs.readFileSync(jsonFile, "utf8")); + if (!report || typeof report !== "object" || !report.categories) { + continue; + } + + const categories = report.categories || {}; + const formatScore = (value) => + typeof value === "number" ? Math.round(value * 100) : "-"; + + rows.push({ + url: report.finalDisplayedUrl || report.finalUrl || report.requestedUrl || "-", + performance: formatScore(categories.performance?.score), + accessibility: formatScore(categories.accessibility?.score), + bestPractices: formatScore(categories["best-practices"]?.score), + seo: formatScore(categories.seo?.score), + }); + } catch { + // ignore non-report JSON files + } + } + + if (rows.length === 0) { + lines.push("파싱 가능한 Lighthouse report JSON이 없습니다."); + lines.push(""); + lines.push(`- 감지된 JSON 파일 수: ${jsonFiles.length}`); + fs.writeFileSync(outPath, lines.join("\n")); + process.exit(0); + } + + const toNumber = (v) => (typeof v === "number" && Number.isFinite(v) ? v : null); + const median = (arr) => { + const clean = arr.filter((v) => v !== null).sort((a, b) => a - b); + if (clean.length === 0) return "-"; + return clean[Math.floor(clean.length / 2)]; + }; + + lines.push(`- 수집된 리포트 수: ${rows.length}`); + lines.push(""); + lines.push("| URL | Perf(median) | A11y(median) | BP(median) | SEO(median) |"); + lines.push("|---|---:|---:|---:|---:|"); + + const url = rows[0]?.url || `http://localhost:3000${process.env.LH_PATH}`; + const perf = median(rows.map((r) => toNumber(r.performance))); + const a11y = median(rows.map((r) => toNumber(r.accessibility))); + const bp = median(rows.map((r) => toNumber(r.bestPractices))); + const seo = median(rows.map((r) => toNumber(r.seo))); + lines.push(`| ${url} | ${perf} | ${a11y} | ${bp} | ${seo} |`); + + fs.writeFileSync(outPath, lines.join("\n")); + EOF + env: + LHCI_OUTCOME: ${{ steps.lhci.outcome }} + LH_PATH: ${{ matrix.path }} + LH_PATH_KEY: ${{ matrix.path_key }} + LH_PERF_LEVEL: ${{ matrix.perf_level }} + LH_PERF_MIN: ${{ matrix.perf_min }} + + - name: Comment PR with Lighthouse summary + if: github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require("fs"); + const marker = ``; + const baseBody = fs.readFileSync(".lighthouse-summary.md", "utf8"); + const { owner, repo } = context.repo; + const issue_number = context.issue.number; + const perfPattern = /\|\s*[^|]+\s*\|\s*(\d+)\s*\|/; + + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number, + per_page: 100, + }); + + const existing = comments.find((c) => c.body && c.body.includes(marker)); + const currentPerfMatch = baseBody.match(perfPattern); + const currentPerf = currentPerfMatch ? Number(currentPerfMatch[1]) : null; + + let deltaLine = "- 이전 대비 Perf(median): 첫 측정"; + if (existing) { + const prevPerfMatch = existing.body.match(perfPattern); + const prevPerf = prevPerfMatch ? Number(prevPerfMatch[1]) : null; + + if (currentPerf !== null && prevPerf !== null) { + const delta = currentPerf - prevPerf; + const sign = delta > 0 ? "+" : ""; + deltaLine = `- 이전 대비 Perf(median): ${sign}${delta}p (이전 ${prevPerf} -> 현재 ${currentPerf})`; + } else { + deltaLine = "- 이전 대비 Perf(median): 비교 불가"; + } + } + + const body = `${baseBody}\n${deltaLine}`; + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } + env: + LH_PATH_KEY: ${{ matrix.path_key }} + + - name: Fail when Lighthouse assertions fail + if: steps.lhci.outcome == 'failure' + run: | + echo "Lighthouse CI assertions failed." + exit 1 diff --git a/.lighthouserc.json b/.lighthouserc.json new file mode 100644 index 0000000..2525bf2 --- /dev/null +++ b/.lighthouserc.json @@ -0,0 +1,37 @@ +{ + "ci": { + "collect": { + "startServerCommand": "npm run start -- --port=3000", + "startServerReadyPattern": "Ready", + "settings": { + "preset": "desktop", + "formFactor": "desktop", + "screenEmulation": { + "mobile": false, + "width": 1350, + "height": 940, + "deviceScaleFactor": 1, + "disabled": false + }, + "throttlingMethod": "devtools" + }, + "url": [ + "http://localhost:3000/posts", + "http://localhost:3000/posts/100", + "http://localhost:3000/projects/1" + ], + "numberOfRuns": 3 + }, + "assert": { + "assertions": { + "categories:performance": ["error", {"minScore": 0.8}], + "categories:accessibility": ["warn", {"minScore": 0.9}], + "categories:best-practices": ["warn", {"minScore": 0.9}], + "categories:seo": ["warn", {"minScore": 1}] + } + }, + "upload": { + "target": "temporary-public-storage" + } + } +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..208d8aa --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,113 @@ +# AGENTS.md + +이 문서는 이 저장소 루트(`./`)에서 작업하는 사람/에이전트를 위한 실행 가이드입니다. + +## 1) 프로젝트 요약 + +- 스택: Next.js App Router + TypeScript + Tailwind CSS + Zustand +- 런타임: React 19, Next 15 +- 데이터: 외부 백엔드 API(`https://api.yonghun.me`) + Supabase Storage +- 주요 도메인: + - 포트폴리오/소개 페이지 + - 블로그 목록/상세/작성 페이지 + - SEO(메타데이터, sitemap, robots) + +## 2) 핵심 디렉터리 + +- `src/app`: 라우트 및 페이지(App Router) +- `src/app/posts`: 블로그 목록/상세/작성 UI +- `src/components`: 공통 UI, provider, 레이아웃 컴포넌트 +- `src/lib/api`: API 호출 유틸 및 도메인 API 함수 +- `src/lib/supabase`: Supabase 클라이언트 +- `src/data`: 정적 데이터 +- `src/store`: Zustand 스토어 + +## 3) 환경변수 + +현재 코드 기준으로 아래 키를 사용합니다. + +- `NEXT_PUBLIC_API_BASE_URL` +- `NEXT_PUBLIC_SUPABASE_URL` +- `NEXT_PUBLIC_SUPABASE_ANON_KEY` +- `NEXT_PUBLIC_GOOGLE_ANALYTICS` + +주의: +- `NEXT_PUBLIC_*` 값은 클라이언트에 노출됩니다. 비밀값(secret)은 절대 넣지 않습니다. +- API 기본값 fallback은 `http://localhost:8080` 입니다(`src/lib/api/apiClient.tsx`). + +## 4) 실행/검증 명령어 + +- 개발 서버: `npm run dev` +- 빌드: `npm run build` +- 프로덕션 실행: `npm run start` +- 린트: `npm run lint` + +작업 완료 전 최소 검증: +1. `npm run lint` +2. 변경한 라우트 수동 확인(특히 `posts`, `posts/[id]`, `posts/add`) +3. SEO 변경 시 메타데이터/OG/canonical 값 확인 + +## 5) 커밋 규칙 (이 저장소 표준) + +최근 커밋 로그를 보면 `feat/fix/docs/refactor/style` 타입은 잘 사용되지만, 대소문자(`Feat`, `Fix`, `Docs`)가 섞여 있습니다. +앞으로는 아래 규칙으로 통일합니다. + +형식: +- `: ` + +규칙: +- `type`은 반드시 **소문자** +- `subject`는 한국어/영어 모두 가능, 짧고 구체적으로 작성 +- 마침표(`.`) 생략 +- 한 커밋에는 하나의 목적만 담기 + +허용 타입: +- `feat`: 기능 추가/확장 +- `fix`: 버그 수정 +- `refactor`: 동작 변화 없는 구조 개선 +- `style`: UI 스타일 변경(로직 영향 없음) +- `docs`: 문서 수정 +- `chore`: 빌드/설정/의존성/기타 유지보수 + +좋은 예시: +- `feat: posts 페이지 성능 및 SEO 최적화` +- `fix: posts 목록 빈 상태 처리 오류 수정` +- `refactor: blog API 응답 타입 분리` +- `docs: README 배포 섹션 업데이트` + +피해야 할 예시: +- `Feat: 수정` (타입 대문자 + 의미 불명확) +- `fix: 이것저것 수정` (범위 과다) + +## 6) 브랜치/PR 권장 규칙 + +- 브랜치명: `feature/*`, `fix/*`, `refactor/*`, `docs/*` +- PR 제목도 커밋 규칙과 동일한 톤 권장 +- PR 본문에 반드시 포함: + - 변경 목적 + - 주요 변경 파일 + - 사용자 영향(화면/API/SEO) + - 테스트/검증 결과 + +## 7) 코드 작업 원칙 + +- TypeScript `strict`를 깨는 `any` 남발 금지 +- 기존 import alias(`@/*`) 우선 사용 +- API 레이어(`src/lib/api/*`)에서 타입 우선 정의 후 페이지에 전달 +- 네트워크 요청 실패 경로를 고려하고, 사용자 노출 메시지/상태를 처리 +- 불필요한 클라이언트 컴포넌트(`"use client"`) 증가 지양 +- 이미지/콘텐츠 변경 시 성능(LCP), 접근성(alt), SEO 메타데이터를 함께 확인 + +## 8) 변경 시 주의 포인트 (이 프로젝트 특화) + +- `next.config.ts`의 `/api/:path*` rewrites는 백엔드 연동 핵심 경로이므로 함부로 변경하지 않습니다. +- `src/lib/api/blog.tsx`의 `revalidate`(ISR) 정책 변경 시 캐시 전략을 PR에 명시합니다. +- `posts/add` 인증 흐름(`AuthForm`, `Auth`) 변경 시 Guest/Admin 분기 동작을 반드시 수동 검증합니다. +- Supabase 업로드 로직 변경 시 public URL, placeholder, blur 데이터 동작까지 함께 확인합니다. + +## 9) 작업 완료 체크리스트 + +1. 코드가 린트 오류 없이 통과한다. +2. 변경 범위의 핵심 페이지가 의도대로 동작한다. +3. 커밋 메시지가 소문자 타입 규칙을 지킨다. +4. 문서/타입/코드가 서로 모순되지 않는다. diff --git a/next.config.ts b/next.config.ts index 27100a3..ed16339 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + htmlLimitedBots: /.*/, async rewrites() { return [ { @@ -10,7 +11,15 @@ const nextConfig: NextConfig = { ]; }, images: { - domains: ["evcsbwqeetfvegvrtbny.supabase.co"], + remotePatterns: [ + { + protocol: "https", + hostname: "evcsbwqeetfvegvrtbny.supabase.co", + pathname: "/**", + }, + ], + qualities: [45, 50, 75], + formats: ["image/avif", "image/webp"], }, }; diff --git a/package-lock.json b/package-lock.json index 3d020bc..bb35d56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "clsx": "^2.1.1", "highlight.js": "^11.11.1", "motion": "^12.9.4", - "next": "15.3.1", + "next": "^15.5.12", "quill": "^2.0.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -29,7 +29,7 @@ "@types/react-dom": "^19", "@types/react-syntax-highlighter": "^15.5.13", "eslint": "^9", - "eslint-config-next": "15.3.1", + "eslint-config-next": "^15.5.12", "tailwindcss": "^4", "typescript": "^5" } @@ -69,9 +69,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", - "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", "license": "MIT", "optional": true, "dependencies": { @@ -90,9 +90,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -122,9 +122,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -293,10 +293,20 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", - "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -312,13 +322,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.1.0" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", - "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -334,13 +344,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.1.0" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", - "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -354,9 +364,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", - "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -370,9 +380,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", - "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -386,9 +396,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", - "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -402,9 +412,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", - "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -417,10 +427,26 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", - "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -434,9 +460,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", - "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -450,9 +476,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", - "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -466,9 +492,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", - "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -482,9 +508,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", - "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -500,13 +526,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.1.0" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", - "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -522,13 +548,57 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.1.0" + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", - "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -544,13 +614,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.1.0" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", - "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -566,13 +636,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.1.0" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", - "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -588,13 +658,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", - "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -610,21 +680,40 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", - "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.4.0" + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, @@ -633,9 +722,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", - "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -652,9 +741,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", - "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -684,15 +773,15 @@ } }, "node_modules/@next/env": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.1.tgz", - "integrity": "sha512-cwK27QdzrMblHSn9DZRV+DQscHXRuJv6MydlJRpFSqJWZrTYMLzKDeyueJNN9MGd8NNiUKzDQADAf+dMLXX7YQ==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.12.tgz", + "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.3.1.tgz", - "integrity": "sha512-oEs4dsfM6iyER3jTzMm4kDSbrQJq8wZw5fmT6fg2V3SMo+kgG+cShzLfEV20senZzv8VF+puNLheiGPlBGsv2A==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.12.tgz", + "integrity": "sha512-+ZRSDFTv4aC96aMb5E41rMjysx8ApkryevnvEYZvPZO52KvkqP5rNExLUXJFr9P4s0f3oqNQR6vopCZsPWKDcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -700,9 +789,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.1.tgz", - "integrity": "sha512-hjDw4f4/nla+6wysBL07z52Gs55Gttp5Bsk5/8AncQLJoisvTBP0pRIBK/B16/KqQyH+uN4Ww8KkcAqJODYH3w==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.12.tgz", + "integrity": "sha512-RnRjBtH8S8eXCpUNkQ+543DUc7ys8y15VxmFU9HRqlo9BG3CcBUiwNtF8SNoi2xvGCVJq1vl2yYq+3oISBS0Zg==", "cpu": [ "arm64" ], @@ -716,9 +805,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.1.tgz", - "integrity": "sha512-q+aw+cJ2ooVYdCEqZVk+T4Ni10jF6Fo5DfpEV51OupMaV5XL6pf3GCzrk6kSSZBsMKZtVC1Zm/xaNBFpA6bJ2g==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.12.tgz", + "integrity": "sha512-nqa9/7iQlboF1EFtNhWxQA0rQstmYRSBGxSM6g3GxvxHxcoeqVXfGNr9stJOme674m2V7r4E3+jEhhGvSQhJRA==", "cpu": [ "x64" ], @@ -732,9 +821,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.1.tgz", - "integrity": "sha512-wBQ+jGUI3N0QZyWmmvRHjXjTWFy8o+zPFLSOyAyGFI94oJi+kK/LIZFJXeykvgXUk1NLDAEFDZw/NVINhdk9FQ==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.12.tgz", + "integrity": "sha512-dCzAjqhDHwmoB2M4eYfVKqXs99QdQxNQVpftvP1eGVppamXh/OkDAwV737Zr0KPXEqRUMN4uCjh6mjO+XtF3Mw==", "cpu": [ "arm64" ], @@ -748,9 +837,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.1.tgz", - "integrity": "sha512-IIxXEXRti/AulO9lWRHiCpUUR8AR/ZYLPALgiIg/9ENzMzLn3l0NSxVdva7R/VDcuSEBo0eGVCe3evSIHNz0Hg==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.12.tgz", + "integrity": "sha512-+fpGWvQiITgf7PUtbWY1H7qUSnBZsPPLyyq03QuAKpVoTy/QUx1JptEDTQMVvQhvizCEuNLEeghrQUyXQOekuw==", "cpu": [ "arm64" ], @@ -764,9 +853,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.1.tgz", - "integrity": "sha512-bfI4AMhySJbyXQIKH5rmLJ5/BP7bPwuxauTvVEiJ/ADoddaA9fgyNNCcsbu9SlqfHDoZmfI6g2EjzLwbsVTr5A==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.12.tgz", + "integrity": "sha512-jSLvgdRRL/hrFAPqEjJf1fFguC719kmcptjNVDJl26BnJIpjL3KH5h6mzR4mAweociLQaqvt4UyzfbFjgAdDcw==", "cpu": [ "x64" ], @@ -780,9 +869,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.1.tgz", - "integrity": "sha512-FeAbR7FYMWR+Z+M5iSGytVryKHiAsc0x3Nc3J+FD5NVbD5Mqz7fTSy8CYliXinn7T26nDMbpExRUI/4ekTvoiA==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.12.tgz", + "integrity": "sha512-/uaF0WfmYqQgLfPmN6BvULwxY0dufI2mlN2JbOKqqceZh1G4hjREyi7pg03zjfyS6eqNemHAZPSoP84x17vo6w==", "cpu": [ "x64" ], @@ -796,9 +885,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.1.tgz", - "integrity": "sha512-yP7FueWjphQEPpJQ2oKmshk/ppOt+0/bB8JC8svPUZNy0Pi3KbPx2Llkzv1p8CoQa+D2wknINlJpHf3vtChVBw==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.12.tgz", + "integrity": "sha512-xhsL1OvQSfGmlL5RbOmU+FV120urrgFpYLq+6U8C6KIym32gZT6XF/SDE92jKzzlPWskkbjOKCpqk5m4i8PEfg==", "cpu": [ "arm64" ], @@ -812,9 +901,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.1.tgz", - "integrity": "sha512-3PMvF2zRJAifcRNni9uMk/gulWfWS+qVI/pagd+4yLF5bcXPZPPH2xlYRYOsUjmCJOXSTAC2PjRzbhsRzR2fDQ==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.12.tgz", + "integrity": "sha512-Z1Dh6lhFkxvBDH1FoW6OU/L6prYwPSlwjLiZkExIAh8fbP6iI/M7iGTQAJPYJ9YFlWobCZ1PHbchFhFYb2ADkw==", "cpu": [ "x64" ], @@ -883,9 +972,9 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.11.0.tgz", - "integrity": "sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", "dev": true, "license": "MIT" }, @@ -963,12 +1052,6 @@ "@supabase/storage-js": "2.7.1" } }, - "node_modules/@swc/counter": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", - "license": "Apache-2.0" - }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1348,21 +1431,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.1.tgz", - "integrity": "sha512-oUlH4h1ABavI4F0Xnl8/fOtML/eu8nI2A1nYd+f+55XI0BLu+RIqKoCiZKNo6DtqZBEQm5aNKA20G3Z5w3R6GQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.0.tgz", + "integrity": "sha512-lRyPDLzNCuae71A3t9NEINBiTn7swyOhvUj3MyUOxb8x6g6vPEFoOU+ZRmGMusNC3X3YMhqMIX7i8ShqhT74Pw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/type-utils": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/type-utils": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1372,23 +1454,33 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "@typescript-eslint/parser": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.1.tgz", - "integrity": "sha512-oU/OtYVydhXnumd0BobL9rkJg7wFJ9bFFPmSmB/bf/XWN85hlViji59ko6bSKBXyseT9V8l+CN1nwmlbiN0G7Q==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.0.tgz", + "integrity": "sha512-IgSWvLobTDOjnaxAfDTIHaECbkNlAlKv2j5SjpB2v7QHKv1FIfjwMy8FsDbVfDX/KjmCmYICcw7uGaXLhtsLNg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1398,19 +1490,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.1.tgz", - "integrity": "sha512-BMNLOElPxrtNQMIsFHE+3P0Yf1z0dJqV9zLdDxN/xLlWMlXK/ApEsVEKzpizg9oal8bAT5Sc7+ocal7AC1HCVw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.0.tgz", + "integrity": "sha512-M3rnyL1vIQOMeWxTWIW096/TtVP+8W3p/XnaFflhmcFp+U4zlxUxWj4XwNs6HbDeTtN4yun0GNTTDBw/SvufKg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1" + "@typescript-eslint/tsconfig-utils": "^8.56.0", + "@typescript-eslint/types": "^8.56.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1418,19 +1511,20 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.1.tgz", - "integrity": "sha512-fNaT/m9n0+dpSp8G/iOQ05GoHYXbxw81x+yvr7TArTuZuCA6VVKbqWYVZrV5dVagpDTtj/O8k5HBEE/p/HM5LA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.0.tgz", + "integrity": "sha512-7UiO/XwMHquH+ZzfVCfUNkIXlp/yQjjnlYUyYz7pfvlK3/EyyN6BK+emDmGNyQLBtLGaYrTAI6KOw8tFucWL2w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.31.1", - "@typescript-eslint/utils": "8.31.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1438,16 +1532,12 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.1.tgz", - "integrity": "sha512-SfepaEFUDQYRoA70DD9GtytljBePSj17qPxFHA/h3eg6lPTqGJ5mWOtbXCk1YrVU1cTJRd14nhaXWFu0l2troQ==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.0.tgz", + "integrity": "sha512-bSJoIIt4o3lKXD3xmDh9chZcjCz5Lk8xS7Rxn+6l5/pKrDpkCwtQNQQwZ2qRPk7TkUYhrq3WPIHXOXlbXP0itg==", "dev": true, "license": "MIT", "engines": { @@ -1456,23 +1546,23 @@ "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.1.tgz", - "integrity": "sha512-kaA0ueLe2v7KunYOyWYtlf/QhhZb7+qh4Yw6Ni5kgukMIG+iP773tjgBiLWIXYumWCwEq3nLW+TUywEp8uEeag==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.0.tgz", + "integrity": "sha512-qX2L3HWOU2nuDs6GzglBeuFXviDODreS58tLY/BALPC7iu3Fa+J7EOTwnX9PdNBxUI7Uh0ntP0YWGnxCkXzmfA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/visitor-keys": "8.31.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0", + "@typescript-eslint/utils": "8.56.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1482,47 +1572,60 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@typescript-eslint/types": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.0.tgz", + "integrity": "sha512-DBsLPs3GsWhX5HylbP9HNG15U0bnwut55Lx12bHB9MpXxQ+R5GC8MwQe+N1UFXxAeQDvEsEDY6ZYwX03K7Z6HQ==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.0.tgz", + "integrity": "sha512-ex1nTUMWrseMltXUHmR2GAQ4d+WjkZCT4f+4bVsps8QEdh0vlBsaCokKTPlnqBFqqGaxilDNJG7b8dolW2m43Q==", "dev": true, "license": "MIT", "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" + "@typescript-eslint/project-service": "8.56.0", + "@typescript-eslint/tsconfig-utils": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/visitor-keys": "8.56.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": ">=8.6.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" + "balanced-match": "^1.0.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { @@ -1542,16 +1645,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.1.tgz", - "integrity": "sha512-2DSI4SNfF5T4oRveQ4nUrSjUqjMND0nLq9rEkz0gfGr3tg0S5KB6DhwR+WZPCjzkZl3cH+4x2ce3EsL50FubjQ==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.0.tgz", + "integrity": "sha512-RZ3Qsmi2nFGsS+n+kjLAYDPVlrzf7UhTffrDIKr+h2yzAlYP/y5ZulU0yeDEPItos2Ph46JAL5P/On3pe7kDIQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.31.1", - "@typescript-eslint/types": "8.31.1", - "@typescript-eslint/typescript-estree": "8.31.1" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.56.0", + "@typescript-eslint/types": "8.56.0", + "@typescript-eslint/typescript-estree": "8.56.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1561,19 +1664,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.31.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.1.tgz", - "integrity": "sha512-I+/rgqOVBn6f0o7NDTmAPWWC6NuqhV174lfYvAm9fUaWeiefLdux9/YI3/nLugEn9L8fcSi0XmpKi/r5u0nmpw==", + "version": "8.56.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.0.tgz", + "integrity": "sha512-q+SL+b+05Ud6LbEE35qe4A99P+htKTKVbyiNEe45eCbJFyh/HVK9QXwlrbz+Q4L8SOW4roxSVwXYj4DMBT7Ieg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.31.1", - "eslint-visitor-keys": "^4.2.0" + "@typescript-eslint/types": "8.56.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1583,6 +1686,19 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.0.tgz", + "integrity": "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@unrs/resolver-binding-darwin-arm64": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", @@ -1915,18 +2031,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2139,17 +2257,6 @@ "node": ">=8" } }, - "node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -2211,9 +2318,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001716", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz", - "integrity": "sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw==", + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", "funding": [ { "type": "opencollective", @@ -2292,25 +2399,11 @@ "node": ">=6" } }, - "node_modules/color": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", - "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-convert": "^2.0.1", - "color-string": "^1.9.0" - }, - "engines": { - "node": ">=12.5.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2323,20 +2416,9 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "devOptional": true, + "dev": true, "license": "MIT" }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "license": "MIT", - "optional": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, "node_modules/comma-separated-tokens": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", @@ -2438,9 +2520,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { @@ -2499,9 +2581,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "devOptional": true, "license": "Apache-2.0", "engines": { @@ -2558,9 +2640,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -2568,18 +2650,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -2591,21 +2673,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -2614,7 +2699,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -2806,13 +2891,13 @@ } }, "node_modules/eslint-config-next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.3.1.tgz", - "integrity": "sha512-GnmyVd9TE/Ihe3RrvcafFhXErErtr2jS0JDeCSp3vWvy86AXwHsRBt0E3MqP/m8ACS1ivcsi5uaqjbhsG18qKw==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.12.tgz", + "integrity": "sha512-ktW3XLfd+ztEltY5scJNjxjHwtKWk6vU2iwzZqSN09UsbBmMeE/cVlJ1yESg6Yx5LW7p/Z8WzUAgYXGLEmGIpg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.3.1", + "@next/eslint-plugin-next": "15.5.12", "@rushstack/eslint-patch": "^1.10.3", "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", @@ -2891,9 +2976,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", - "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", "dev": true, "license": "MIT", "dependencies": { @@ -2919,30 +3004,30 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", - "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", "dependencies": { "@rtsao/scc": "^1.1.0", - "array-includes": "^3.1.8", - "array.prototype.findlastindex": "^1.2.5", - "array.prototype.flat": "^1.3.2", - "array.prototype.flatmap": "^1.3.2", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.12.0", + "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", - "is-core-module": "^2.15.1", + "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", - "object.values": "^1.2.0", + "object.values": "^1.2.1", "semver": "^6.3.1", - "string.prototype.trimend": "^1.0.8", + "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "engines": { @@ -3234,9 +3319,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -3545,13 +3630,6 @@ "dev": true, "license": "ISC" }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -3782,13 +3860,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "license": "MIT", - "optional": true - }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -4007,6 +4078,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4799,15 +4883,13 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz", - "integrity": "sha512-8+dDV0xNLOgHlyBxP1GwHGVaNXsmp+2NhZEYrXr24GWLHtt27YrBPbPuHvzlhi7kZNYjeJNR93IF5zfFu5UL0g==", + "version": "15.5.12", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", + "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", "license": "MIT", "dependencies": { - "@next/env": "15.3.1", - "@swc/counter": "0.1.3", + "@next/env": "15.5.12", "@swc/helpers": "0.5.15", - "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -4819,19 +4901,19 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.3.1", - "@next/swc-darwin-x64": "15.3.1", - "@next/swc-linux-arm64-gnu": "15.3.1", - "@next/swc-linux-arm64-musl": "15.3.1", - "@next/swc-linux-x64-gnu": "15.3.1", - "@next/swc-linux-x64-musl": "15.3.1", - "@next/swc-win32-arm64-msvc": "15.3.1", - "@next/swc-win32-x64-msvc": "15.3.1", - "sharp": "^0.34.1" + "@next/swc-darwin-arm64": "15.5.12", + "@next/swc-darwin-x64": "15.5.12", + "@next/swc-linux-arm64-gnu": "15.5.12", + "@next/swc-linux-arm64-musl": "15.5.12", + "@next/swc-linux-x64-gnu": "15.5.12", + "@next/swc-linux-x64-musl": "15.5.12", + "@next/swc-win32-arm64-msvc": "15.5.12", + "@next/swc-win32-x64-msvc": "15.5.12", + "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.41.2", + "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", @@ -5566,9 +5648,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "devOptional": true, "license": "ISC", "bin": { @@ -5628,16 +5710,16 @@ } }, "node_modules/sharp": { - "version": "0.34.1", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", - "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.3", - "semver": "^7.7.1" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -5646,26 +5728,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.1", - "@img/sharp-darwin-x64": "0.34.1", - "@img/sharp-libvips-darwin-arm64": "1.1.0", - "@img/sharp-libvips-darwin-x64": "1.1.0", - "@img/sharp-libvips-linux-arm": "1.1.0", - "@img/sharp-libvips-linux-arm64": "1.1.0", - "@img/sharp-libvips-linux-ppc64": "1.1.0", - "@img/sharp-libvips-linux-s390x": "1.1.0", - "@img/sharp-libvips-linux-x64": "1.1.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", - "@img/sharp-libvips-linuxmusl-x64": "1.1.0", - "@img/sharp-linux-arm": "0.34.1", - "@img/sharp-linux-arm64": "0.34.1", - "@img/sharp-linux-s390x": "0.34.1", - "@img/sharp-linux-x64": "0.34.1", - "@img/sharp-linuxmusl-arm64": "0.34.1", - "@img/sharp-linuxmusl-x64": "0.34.1", - "@img/sharp-wasm32": "0.34.1", - "@img/sharp-win32-ia32": "0.34.1", - "@img/sharp-win32-x64": "0.34.1" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -5767,16 +5853,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5803,12 +5879,18 @@ "dev": true, "license": "MIT" }, - "node_modules/streamsearch": { + "node_modules/stop-iteration-iterator": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, "engines": { - "node": ">=10.0.0" + "node": ">= 0.4" } }, "node_modules/string.prototype.includes": { @@ -6024,14 +6106,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -6041,11 +6123,14 @@ } }, "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -6056,9 +6141,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -6088,9 +6173,9 @@ "license": "MIT" }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index ae007ad..e86a09f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "clsx": "^2.1.1", "highlight.js": "^11.11.1", "motion": "^12.9.4", - "next": "15.3.1", + "next": "^15.5.12", "quill": "^2.0.3", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -30,7 +30,7 @@ "@types/react-dom": "^19", "@types/react-syntax-highlighter": "^15.5.13", "eslint": "^9", - "eslint-config-next": "15.3.1", + "eslint-config-next": "^15.5.12", "tailwindcss": "^4", "typescript": "^5" } diff --git a/src/app/about/pageClient.tsx b/src/app/about/pageClient.tsx index a823c4d..4e623b1 100644 --- a/src/app/about/pageClient.tsx +++ b/src/app/about/pageClient.tsx @@ -4,6 +4,7 @@ import Nav from "@/components/common/Nav"; import { GlassBox } from "@/components/ui/GlassBox"; import { about } from "@/data"; import { motion } from "motion/react"; +import Image from "next/image"; export default function AboutClient() { return ( @@ -20,13 +21,19 @@ export default function AboutClient() { >
{about.name} - + - github button + github button
diff --git a/src/app/globals.css b/src/app/globals.css index 5c2926a..abaa37a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -13,7 +13,7 @@ --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); --font-main: var(--font-body); - --font-nanum: "Nanum Pen Script", cursive; + --font-nanum: var(--font-nanum-pen), cursive; --animate-spotlight: spotlight 2s ease 0.75s 1 forwards; --animate-unspotlight: unspotlight 2s ease 0.75s 1 forwards; --animate-leftToRight: leftToRight 1s ease; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d039c49..4b698e6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,10 @@ import GoogleAnalytics from "@/components/provider/GoogleAnalytics"; -import InitHLJS from "@/components/provider/InitHLJS"; import PathListener from "@/components/provider/PathListener"; import NoiseBackground from "@/components/ui/NoiseBackground"; import { Spotlight } from "@/components/ui/Spotlight"; import type { Metadata } from "next"; import { Geist, Geist_Mono, Nanum_Pen_Script } from "next/font/google"; +import "highlight.js/styles/github-dark.css"; import "./globals.css"; const geistSans = Geist({ @@ -18,11 +18,15 @@ const geistMono = Geist_Mono({ }); const geistNanum = Nanum_Pen_Script({ + variable: "--font-nanum-pen", weight: "400", subsets: ["latin"], + display: "swap", + preload: false, }); export const metadata: Metadata = { + metadataBase: new URL("https://www.yonghun.me"), title: "Yonghun - 포트폴리오", description: "기억에 남는 순간을 만들고 싶은 웹 개발자 이용훈입니다.", keywords: "웹개발,프론트엔드,백엔드,포트폴리오,개발자,블로그", @@ -54,12 +58,11 @@ export default function RootLayout({ return ( {process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS ? ( ) : null} - diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx index 5ac72b7..64ff77f 100644 --- a/src/app/posts/[id]/page.tsx +++ b/src/app/posts/[id]/page.tsx @@ -5,6 +5,26 @@ import PostDetailClient from "./pageClient"; export const dynamic = "force-static"; export const revalidate = 3600; +const DEFAULT_DESCRIPTION = "Yonghun 개발 블로그의 게시글 상세 페이지입니다."; + +const stripHtml = (value: string) => + value + .replace(/<[^>]*>/g, " ") + .replace(/ /gi, " ") + .replace(/&/gi, "&") + .replace(/\s+/g, " ") + .trim(); + +const createMetaDescription = (description?: string, body?: string) => { + const preferred = description?.trim(); + if (preferred) return preferred; + + const bodyText = body ? stripHtml(body) : ""; + if (bodyText) return bodyText.slice(0, 160); + + return DEFAULT_DESCRIPTION; +}; + export const generateMetadata = async ({ params, }: { @@ -17,24 +37,27 @@ export const generateMetadata = async ({ if (!data) { return { title: `Yonghun - 개발 블로그`, + description: DEFAULT_DESCRIPTION, }; } + const metaDescription = createMetaDescription(data.description, data.body); + return { title: data.title, - description: data.description, + description: metaDescription, keywords: data.tags.join(", "), openGraph: { type: "website", url: `https://www.yonghun.me/posts/${id}`, title: data.title, - description: data.description, + description: metaDescription, images: data.thumbnail, }, twitter: { card: "summary_large_image", title: data.title, - description: data.description, + description: metaDescription, images: data.thumbnail, }, }; diff --git a/src/app/posts/[id]/pageClient.tsx b/src/app/posts/[id]/pageClient.tsx index d9fc29a..cf0da08 100644 --- a/src/app/posts/[id]/pageClient.tsx +++ b/src/app/posts/[id]/pageClient.tsx @@ -3,12 +3,14 @@ import Nav from "@/components/common/Nav"; import QuillCodeRenderer from "@/components/common/QuillCodeRenderer"; import { GlassBox } from "@/components/ui/GlassBox"; -import { type Post } from "@/lib/api/blog"; +import { deletePost, type Post } from "@/lib/api/blog"; +import { isKnownAnimatedSupabaseImage } from "@/lib/image"; import { formatDate } from "@/lib/utils"; import useImageStore from "@/store/useImageStore"; import { AnimatePresence, motion } from "motion/react"; import Image from "next/image"; -import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react"; import { BsTrash, BsWrenchAdjustable } from "react-icons/bs"; import AuthForm from "../components/AuthForm"; @@ -26,11 +28,57 @@ const buttonVariants = { tap: { scale: 0.97 }, }; +const FOCUSABLE_SELECTOR = + 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])'; + +const trapTabKey = (event: ReactKeyboardEvent) => { + if (event.key !== "Tab") return; + + const focusables = Array.from( + event.currentTarget.querySelectorAll(FOCUSABLE_SELECTOR) + ); + + if (focusables.length === 0) { + event.preventDefault(); + return; + } + + const firstEl = focusables[0]; + const lastEl = focusables[focusables.length - 1]; + const activeEl = document.activeElement as HTMLElement | null; + + if (event.shiftKey && activeEl === firstEl) { + event.preventDefault(); + lastEl.focus(); + return; + } + + if (!event.shiftKey && activeEl === lastEl) { + event.preventDefault(); + firstEl.focus(); + } +}; + +const focusDialog = (dialog: HTMLDivElement | null) => { + if (!dialog) return; + const firstFocusable = dialog.querySelector(FOCUSABLE_SELECTOR); + (firstFocusable ?? dialog).focus(); +}; + export default function PostDetailClient({ post }: { post: Post }) { + const router = useRouter(); const { curImage, setCurImage } = useImageStore(); + const isAnimatedThumbnail = isKnownAnimatedSupabaseImage(post.thumbnail); + const authDialogRef = useRef(null); + const deleteDialogRef = useRef(null); + const imageDialogRef = useRef(null); + const lastFocusedElementRef = useRef(null); const [viewAuth, setViewAuth] = useState(false); const [viewDelete, setViewDelete] = useState(false); + const [authAction, setAuthAction] = useState<"delete" | "edit" | null>(null); + const [message, setMessage] = useState(""); + const [isDeleting, setIsDeleting] = useState(false); useEffect(() => { const nodeList = @@ -39,76 +87,185 @@ export default function PostDetailClient({ post }: { post: Post }) { const target = event.currentTarget as HTMLImageElement; setCurImage(target.src); }; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Enter" && event.key !== " ") return; + event.preventDefault(); + const target = event.currentTarget as HTMLImageElement; + setCurImage(target.src); + }; nodeList.forEach((imgEl) => { + if (imgEl.closest("a, button")) return; + imgEl.setAttribute("tabindex", "0"); + imgEl.setAttribute("role", "button"); + if (!imgEl.getAttribute("aria-label")) { + imgEl.setAttribute("aria-label", "이미지 확대 보기"); + } + imgEl.classList.add("cursor-zoom-in"); imgEl.addEventListener("click", handleClick); + imgEl.addEventListener("keydown", handleKeyDown); }); return () => { setCurImage(null); nodeList.forEach((imgEl) => { imgEl.removeEventListener("click", handleClick); + imgEl.removeEventListener("keydown", handleKeyDown); }); }; - }, []); + }, [setCurImage]); + + useEffect(() => { + const isDialogOpen = viewAuth || viewDelete || Boolean(curImage); + + if (isDialogOpen) { + if (!lastFocusedElementRef.current) { + lastFocusedElementRef.current = document.activeElement as HTMLElement | null; + } + + if (viewAuth) { + focusDialog(authDialogRef.current); + return; + } + + if (viewDelete) { + focusDialog(deleteDialogRef.current); + return; + } + + if (curImage) { + focusDialog(imageDialogRef.current); + } + + return; + } + + if (lastFocusedElementRef.current) { + lastFocusedElementRef.current.focus(); + lastFocusedElementRef.current = null; + } + }, [viewAuth, viewDelete, curImage]); + + const handleDelete = async () => { + if (isDeleting) return; + setMessage(""); + setIsDeleting(true); + try { + await deletePost(post.id); + setViewDelete(false); + router.replace("/posts"); + router.refresh(); + } catch (error) { + console.error(error); + setMessage("삭제 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setIsDeleting(false); + } + }; return (
@@ -160,16 +201,17 @@ const AddPostForm = ({ }; function generateBlurredDataUrl( - file: File, - blurAmount: number = 10 + file: File ): Promise { return new Promise((resolve, reject) => { const objectUrl = URL.createObjectURL(file); const img = new Image(); img.onload = () => { - const width = img.naturalWidth; - const height = img.naturalHeight; + const targetWidth = 24; + const ratio = targetWidth / img.naturalWidth; + const width = targetWidth; + const height = Math.max(1, Math.round(img.naturalHeight * ratio)); const canvas = document.createElement("canvas"); canvas.width = width; canvas.height = height; @@ -180,10 +222,10 @@ function generateBlurredDataUrl( return reject(new Error("Canvas 2D context를 가져올 수 없습니다.")); } - ctx.filter = `blur(${blurAmount}px)`; + ctx.filter = "blur(8px)"; ctx.drawImage(img, 0, 0, width, height); - const blurredDataUrl = canvas.toDataURL(); + const blurredDataUrl = canvas.toDataURL("image/webp", 0.5); URL.revokeObjectURL(objectUrl); resolve(blurredDataUrl); @@ -199,6 +241,60 @@ function generateBlurredDataUrl( }); } +function optimizeThumbnailImage(file: File): Promise { + return new Promise((resolve, reject) => { + const objectUrl = URL.createObjectURL(file); + const img = new Image(); + + img.onload = () => { + const maxWidth = 1280; + const scale = Math.min(1, maxWidth / img.naturalWidth); + const width = Math.max(1, Math.round(img.naturalWidth * scale)); + const height = Math.max(1, Math.round(img.naturalHeight * scale)); + + const canvas = document.createElement("canvas"); + canvas.width = width; + canvas.height = height; + + const ctx = canvas.getContext("2d"); + if (!ctx) { + URL.revokeObjectURL(objectUrl); + reject(new Error("Canvas 2D context를 가져올 수 없습니다.")); + return; + } + + ctx.drawImage(img, 0, 0, width, height); + + canvas.toBlob( + (blob) => { + URL.revokeObjectURL(objectUrl); + if (!blob) { + reject(new Error("썸네일 최적화에 실패했습니다.")); + return; + } + + const nextFile = new File( + [blob], + `${file.name.replace(/\.[^.]+$/, "")}.webp`, + { type: "image/webp" } + ); + + resolve(nextFile); + }, + "image/webp", + 0.82 + ); + }; + + img.onerror = () => { + URL.revokeObjectURL(objectUrl); + reject(new Error("이미지를 로드하는데 실패했습니다.")); + }; + + img.src = objectUrl; + }); +} + async function addFileToStorage(file: File, filePath: string) { const { error: uploadError } = await supabase.storage .from("blog-img") diff --git a/src/app/posts/components/AuthForm.tsx b/src/app/posts/components/AuthForm.tsx index f37300e..4b12422 100644 --- a/src/app/posts/components/AuthForm.tsx +++ b/src/app/posts/components/AuthForm.tsx @@ -8,6 +8,8 @@ import { useState } from "react"; interface AuthFormProps { onSuccess?: VoidFunction; setRole?: (role: User) => void; + titleId?: string; + descriptionId?: string; } const formVariants = { @@ -24,22 +26,38 @@ const buttonVariants = { tap: { scale: 0.97 }, }; -const AuthForm = ({ onSuccess, setRole }: AuthFormProps) => { +const AuthForm = ({ + onSuccess, + setRole, + titleId, + descriptionId, +}: AuthFormProps) => { const [user, setUser] = useState(""); const [password, setPassword] = useState(""); const [message, setMessage] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - const data = await Auth(user, password); + if (isSubmitting) return; + setMessage(""); + setIsSubmitting(true); - setRole?.(data.role); + try { + const data = await Auth(user, password); + setRole?.(data.role); - if (data.role === "Guest") { - setMessage("사용자 정보를 확인해주세요."); - } + if (data.role === "Guest") { + setMessage("사용자 정보를 확인해주세요."); + } - if (data.role === "Admin") onSuccess?.(); + if (data.role === "Admin") onSuccess?.(); + } catch (error) { + console.error(error); + setMessage("인증 요청에 실패했습니다. 잠시 후 다시 시도해주세요."); + } finally { + setIsSubmitting(false); + } }; return ( @@ -50,19 +68,30 @@ const AuthForm = ({ onSuccess, setRole }: AuthFormProps) => { initial="hidden" animate="visible" onClick={(e) => e.stopPropagation()} + aria-busy={isSubmitting} > -

사용자 확인

+

+ 사용자 확인 +

+

+ 사용자와 비밀번호를 입력하고 확인하기 버튼을 누르세요. +

-
@@ -74,13 +103,20 @@ const AuthForm = ({ onSuccess, setRole }: AuthFormProps) => { setPassword(e.target.value)} + onChange={(e) => { + setPassword(e.target.value); + setMessage(""); + }} className="mt-1 block w-full px-4 py-2 border border-gray-400 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-400" /> -
{message}
+
+ {message} +
{ variants={buttonVariants} whileHover="hover" whileTap="tap" + disabled={isSubmitting} > - 확인하기 + {isSubmitting ? "확인 중..." : "확인하기"} diff --git a/src/app/posts/components/RecentSection.tsx b/src/app/posts/components/RecentSection.tsx index b8ffbfa..5c15ed6 100644 --- a/src/app/posts/components/RecentSection.tsx +++ b/src/app/posts/components/RecentSection.tsx @@ -2,6 +2,7 @@ import { GlassBox } from "@/components/ui/GlassBox"; import { type PostsResponse } from "@/lib/api/blog"; +import { isKnownAnimatedSupabaseImage } from "@/lib/image"; import { cn, formatDate } from "@/lib/utils"; import Image from "next/image"; import Link from "next/link"; @@ -13,20 +14,29 @@ const RecentSection = ({ data }: { data: PostsResponse }) => { useEffect(() => { setLoading(false); - }, [data]); + }, [data, setLoading]); if (isLoading) return ; return ( -
+
- {data.posts.map((post) => { + {data.posts.map((post, index) => { + const isLcpCandidate = index === 0; + const useBlurPlaceholder = Boolean(post.thumbnail_blur); + const isAnimatedLegacyImage = isKnownAnimatedSupabaseImage( + post.thumbnail + ); return ( {`thumbnail`}
diff --git a/src/app/posts/page.tsx b/src/app/posts/page.tsx index 5a5d7bb..55437f0 100644 --- a/src/app/posts/page.tsx +++ b/src/app/posts/page.tsx @@ -1,24 +1,80 @@ import { getAllPosts } from "@/lib/api/blog"; +import type { Metadata } from "next"; import { notFound } from "next/navigation"; import PostClient from "./pageClient"; type SearchParams = Promise<{ [key: string]: string | string[] | undefined }>; +const DEFAULT_DESCRIPTION = + "프론트엔드/백엔드 개발 경험, 성능 최적화, 프로젝트 회고를 기록하는 개발 블로그입니다."; + +const parsePage = (value: string | string[] | undefined) => { + const raw = Array.isArray(value) ? value[0] : value; + const page = Number(raw); + if (!Number.isFinite(page) || page < 1) return 1; + return Math.floor(page); +}; + +export const generateMetadata = async ({ + searchParams, +}: { + searchParams: SearchParams; +}): Promise => { + const params = await searchParams; + const page = parsePage(params.page); + + const canonicalPath = page === 1 ? "/posts" : `/posts?page=${page}`; + const title = + page === 1 ? "Yonghun - 개발 블로그" : `Yonghun - 개발 블로그 | ${page}페이지`; + const description = + page === 1 + ? DEFAULT_DESCRIPTION + : `${DEFAULT_DESCRIPTION} 현재 ${page}페이지입니다.`; + const url = `https://www.yonghun.me${canonicalPath}`; -export const generateMetadata = () => { return { - title: `Yonghun - 개발 블로그`, + title, + description, + alternates: { + canonical: url, + }, + openGraph: { + type: "website", + url, + title, + description, + images: "/metaimg.png", + }, + twitter: { + card: "summary_large_image", + title, + description, + images: "/metaimg.png", + }, }; }; export default async function Posts(props: { searchParams: SearchParams }) { const searchParams = await props.searchParams; - const page = Number(searchParams.page) || 1; + const page = parsePage(searchParams.page); const data = await getAllPosts(page); - if (data.posts.length === 0 || !data) { + if (!data || data.posts.length === 0) { notFound(); } - return ; + const optimizedData = { + total_count: data.total_count, + posts: data.posts.map((post, index) => ({ + id: post.id, + title: post.title, + description: post.description, + tags: post.tags, + thumbnail: post.thumbnail, + thumbnail_blur: index < 3 ? post.thumbnail_blur : "", + created_at: post.created_at, + })), + }; + + return ; } diff --git a/src/app/posts/pageClient.tsx b/src/app/posts/pageClient.tsx index d5f714d..9fb5046 100644 --- a/src/app/posts/pageClient.tsx +++ b/src/app/posts/pageClient.tsx @@ -3,6 +3,7 @@ import Nav from "@/components/common/Nav"; import { GlassBox } from "@/components/ui/GlassBox"; import { type PostsResponse } from "@/lib/api/blog"; +import Image from "next/image"; import { useRouter } from "next/navigation"; import { createContext, useContext, useRef, useState } from "react"; import { BsPencilSquare } from "react-icons/bs"; @@ -48,6 +49,7 @@ export default function PostClient({ className="relative w-screen h-screen font-mono overflow-auto pb-20" > +
); } diff --git a/src/components/ui/MaskContainer.tsx b/src/components/ui/MaskContainer.tsx index cb69a43..36e4d27 100644 --- a/src/components/ui/MaskContainer.tsx +++ b/src/components/ui/MaskContainer.tsx @@ -3,7 +3,7 @@ import { cn } from "@/lib/utils"; import useMaskRevealStore from "@/store/useMaskRevealStore"; import { motion } from "motion/react"; -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import ParticleCanvas from "./ParticleCanvas"; export const MaskContainer = ({ @@ -27,28 +27,65 @@ export const MaskContainer = ({ y: null | number; }>({ x: null, y: null }); const containerRef = useRef(null); - const updateMousePosition = (e: MouseEvent) => { + const rectRef = useRef(null); + const rafRef = useRef(null); + const pointerRef = useRef<{ x: number; y: number } | null>(null); + + const updateRect = useCallback(() => { if (!containerRef.current) return; - const rect = containerRef.current.getBoundingClientRect(); - setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top }); - }; + rectRef.current = containerRef.current.getBoundingClientRect(); + }, []); + + const flushMousePosition = useCallback(() => { + const rect = rectRef.current; + const pointer = pointerRef.current; + if (!rect || !pointer) { + rafRef.current = null; + return; + } + + setMousePosition({ x: pointer.x - rect.left, y: pointer.y - rect.top }); + rafRef.current = null; + }, []); + + const updateMousePosition = useCallback( + (e: MouseEvent) => { + pointerRef.current = { x: e.clientX, y: e.clientY }; + + if (rafRef.current !== null) return; + rafRef.current = window.requestAnimationFrame(flushMousePosition); + }, + [flushMousePosition] + ); useEffect(() => { - if (!containerRef.current) return; - containerRef.current.addEventListener("mousemove", updateMousePosition); + const container = containerRef.current; + if (!container) return; + updateRect(); + + const onEnter = () => updateRect(); + const onScroll = () => updateRect(); + const onResize = () => updateRect(); + + container.addEventListener("mousemove", updateMousePosition); + container.addEventListener("mouseenter", onEnter); + window.addEventListener("resize", onResize); + window.addEventListener("scroll", onScroll, true); + return () => { - if (containerRef.current) { - containerRef.current.removeEventListener( - "mousemove", - updateMousePosition - ); + container.removeEventListener("mousemove", updateMousePosition); + container.removeEventListener("mouseenter", onEnter); + window.removeEventListener("resize", onResize); + window.removeEventListener("scroll", onScroll, true); + if (rafRef.current !== null) { + window.cancelAnimationFrame(rafRef.current); } }; - }, []); + }, [updateMousePosition, updateRect]); useEffect(() => { setIsHover(isHovered); - }, [isHovered]); + }, [isHovered, setIsHover]); const maskSize = isHovered ? revealSize : size; diff --git a/src/components/ui/ParticleCanvas.tsx b/src/components/ui/ParticleCanvas.tsx index e212e6a..b667a44 100644 --- a/src/components/ui/ParticleCanvas.tsx +++ b/src/components/ui/ParticleCanvas.tsx @@ -8,51 +8,52 @@ const INTERVAL = 1000 / 60; const DELAY = 300; const ParticleCanvas = ({ - x = window.innerWidth / 2, - y = window.innerHeight / 2, + x, + y, }: { x?: number; y?: number; }) => { + const fallbackX = typeof window !== "undefined" ? window.innerWidth / 2 : 0; + const fallbackY = typeof window !== "undefined" ? window.innerHeight / 2 : 0; const canvasRef = useRef(null); const particlesRef = useRef([]); - const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1; + const originRef = useRef({ + x: x ?? fallbackX, + y: y ?? fallbackY, + }); - const initCanvas = () => { + useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; - const canvasWidth = window.innerWidth; - const canvasHeight = window.innerHeight; - - canvas.style.width = canvasWidth + "px"; - canvas.style.height = canvasHeight + "px"; - - canvas.width = canvasWidth * dpr; - canvas.height = canvasHeight * dpr; - - ctx.scale(dpr, dpr); - }; + const dpr = window.devicePixelRatio || 1; + let then = Date.now(); + let animationFrameId: number; + const initCanvas = () => { + const canvasWidth = window.innerWidth; + const canvasHeight = window.innerHeight; - const createParticles = () => { - particlesRef.current = []; - for (let i = 0; i < PARTICLE_NUM; i++) { - particlesRef.current.push(new Particle(x, y)); - } - }; + canvas.style.width = canvasWidth + "px"; + canvas.style.height = canvasHeight + "px"; - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; + canvas.width = canvasWidth * dpr; + canvas.height = canvasHeight * dpr; - const ctx = canvas.getContext("2d"); - if (!ctx) return; + ctx.scale(dpr, dpr); + }; - let then = Date.now(); - let animationFrameId: number; + const createParticles = () => { + particlesRef.current = []; + for (let i = 0; i < PARTICLE_NUM; i++) { + particlesRef.current.push( + new Particle(originRef.current.x, originRef.current.y) + ); + } + }; initCanvas(); diff --git a/src/components/ui/Spotlight.tsx b/src/components/ui/Spotlight.tsx index 19278c7..7121bbf 100644 --- a/src/components/ui/Spotlight.tsx +++ b/src/components/ui/Spotlight.tsx @@ -36,7 +36,7 @@ export const Spotlight = ({ className }: SpotlightProps) => { changeColor("white"); } } - }, [pathname, isHover]); + }, [pathname, isHover, changeColor]); return ( ; + export type PostsResponse = { total_count: number; - posts: Post[]; + posts: PostListItem[]; }; export type AddPostsRequest = { @@ -32,7 +34,7 @@ export async function getAllPosts(page?: number) { }); } else { return apiClient(`${BASE_URL}/posts`, { - next: { revalidate: 3600 }, + cache: "no-store", }); } } diff --git a/src/lib/highlight/hljs.ts b/src/lib/highlight/hljs.ts new file mode 100644 index 0000000..3ef173e --- /dev/null +++ b/src/lib/highlight/hljs.ts @@ -0,0 +1,16 @@ +import hljs from "highlight.js/lib/core"; +import css from "highlight.js/lib/languages/css"; +import html from "highlight.js/lib/languages/xml"; +import javascript from "highlight.js/lib/languages/javascript"; +import json from "highlight.js/lib/languages/json"; +import rust from "highlight.js/lib/languages/rust"; +import typescript from "highlight.js/lib/languages/typescript"; + +hljs.registerLanguage("javascript", javascript); +hljs.registerLanguage("typescript", typescript); +hljs.registerLanguage("rust", rust); +hljs.registerLanguage("html", html); +hljs.registerLanguage("css", css); +hljs.registerLanguage("json", json); + +export default hljs; diff --git a/src/lib/image.ts b/src/lib/image.ts new file mode 100644 index 0000000..9420b3a --- /dev/null +++ b/src/lib/image.ts @@ -0,0 +1,21 @@ +const ANIMATED_SUPABASE_IMAGE_IDS = new Set([ + "1747995748559", + "1747983130757", + "1747987828131", + "1747984646422", +]); + +export function isKnownAnimatedSupabaseImage(src: string) { + if (!src.startsWith("http")) return false; + + try { + const url = new URL(src); + if (url.hostname !== "evcsbwqeetfvegvrtbny.supabase.co") return false; + const fileId = url.pathname.split("/").pop(); + if (!fileId) return false; + + return ANIMATED_SUPABASE_IMAGE_IDS.has(fileId); + } catch { + return false; + } +} diff --git a/src/lib/quill/ImageUploader.ts b/src/lib/quill/ImageUploader.ts index dabac96..09758b4 100644 --- a/src/lib/quill/ImageUploader.ts +++ b/src/lib/quill/ImageUploader.ts @@ -5,6 +5,7 @@ import { LoadingImage } from "./LoadingImage"; export interface ImageUploaderOptions { upload: (file: File) => Promise; + onError?: (error: Error) => void; } export default class ImageUploader { @@ -146,7 +147,14 @@ export default class ImageUploader { this.options.upload(file).then( (url) => this.replaceWithFinalImage(url), - () => this.removePlaceholder() + (error) => { + this.removePlaceholder(); + if (error instanceof Error) { + this.options.onError?.(error); + } else { + this.options.onError?.(new Error("이미지 업로드에 실패했습니다.")); + } + } ); } diff --git a/tsconfig.json b/tsconfig.json index 42ebe75..456f105 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,11 @@ { "compilerOptions": { "target": "ES2017", - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, @@ -19,10 +23,22 @@ } ], "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, - "types": ["node"] + "types": [ + "node" + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] }