-
+
sentry-example-page
diff --git a/app/supabase-types.ts b/app/supabase-types.ts
index 3cba2d3..55f5788 100644
--- a/app/supabase-types.ts
+++ b/app/supabase-types.ts
@@ -18,18 +18,24 @@ export type Database = {
Row: {
conversation_id: string
email: string | null
+ last_read_at: string
+ last_seen_at: string
type: string
user_id: string
}
Insert: {
conversation_id: string
email?: string | null
+ last_read_at?: string
+ last_seen_at?: string
type?: string
user_id: string
}
Update: {
conversation_id?: string
email?: string | null
+ last_read_at?: string
+ last_seen_at?: string
type?: string
user_id?: string
}
@@ -185,6 +191,57 @@ export type Database = {
}
Relationships: []
}
+ user_dashboard_snapshots: {
+ Row: {
+ active_days_7d: number
+ best_streak: number
+ consistency_percent: number
+ created_at: string
+ current_streak: number
+ id: number
+ peak_day: string | null
+ peak_day_seconds: number
+ snapshot_date: string
+ top_language: string | null
+ top_language_percent: number | null
+ total_seconds_7d: number
+ updated_at: string
+ user_id: string
+ }
+ Insert: {
+ active_days_7d?: number
+ best_streak?: number
+ consistency_percent?: number
+ created_at?: string
+ current_streak?: number
+ id?: number
+ peak_day?: string | null
+ peak_day_seconds?: number
+ snapshot_date: string
+ top_language?: string | null
+ top_language_percent?: number | null
+ total_seconds_7d?: number
+ updated_at?: string
+ user_id: string
+ }
+ Update: {
+ active_days_7d?: number
+ best_streak?: number
+ consistency_percent?: number
+ created_at?: string
+ current_streak?: number
+ id?: number
+ peak_day?: string | null
+ peak_day_seconds?: number
+ snapshot_date?: string
+ top_language?: string | null
+ top_language_percent?: number | null
+ total_seconds_7d?: number
+ updated_at?: string
+ user_id?: string
+ }
+ Relationships: []
+ }
user_flexes: {
Row: {
created_at: string
diff --git a/app/utils/badge.ts b/app/utils/badge.ts
new file mode 100644
index 0000000..e756969
--- /dev/null
+++ b/app/utils/badge.ts
@@ -0,0 +1,86 @@
+import type { IconDefinition } from "@fortawesome/fontawesome-svg-core";
+import {
+ faBolt,
+ faCrown,
+ faFire,
+ faGhost,
+ faMedal,
+ faMinus,
+ faSeedling,
+ faStar,
+} from "@fortawesome/free-solid-svg-icons";
+
+type BadgeRule = {
+ minHours: number;
+ label: string;
+ className: string;
+ icon: IconDefinition;
+};
+
+export type BadgeInfo = Omit;
+
+const BADGE_RULES: BadgeRule[] = [
+ {
+ minHours: 160,
+ label: "MISSION IMPOSSIBLE",
+ className: "badge-impossible",
+ icon: faGhost,
+ },
+ {
+ minHours: 130,
+ label: "GOD LEVEL",
+ className: "badge-god",
+ icon: faCrown,
+ },
+ {
+ minHours: 100,
+ label: "STARLIGHT",
+ className: "badge-starlight",
+ icon: faStar,
+ },
+ {
+ minHours: 50,
+ label: "ELITE",
+ className: "badge-elite",
+ icon: faFire,
+ },
+ {
+ minHours: 20,
+ label: "PRO",
+ className: "badge-pro",
+ icon: faBolt,
+ },
+ {
+ minHours: 5,
+ label: "NOVICE",
+ className: "badge-novice",
+ icon: faMedal,
+ },
+ {
+ minHours: 1,
+ label: "NEWBIE",
+ className: "badge-newbie",
+ icon: faSeedling,
+ },
+];
+
+const DEFAULT_BADGE: BadgeInfo = {
+ label: "NONE",
+ className: "badge-none",
+ icon: faMinus,
+};
+
+export const BADGE_LEGEND_HOURS = [160, 130, 100, 50, 20, 5, 1, 0];
+
+export function getBadgeInfoFromHours(hours: number): BadgeInfo {
+ const safeHours = Number.isFinite(hours) ? hours : 0;
+
+ const matched = BADGE_RULES.find((rule) => safeHours >= rule.minHours);
+ if (!matched) return DEFAULT_BADGE;
+
+ return {
+ label: matched.label,
+ className: matched.className,
+ icon: matched.icon,
+ };
+}
\ No newline at end of file
diff --git a/app/utils/media.ts b/app/utils/media.ts
new file mode 100644
index 0000000..4688dbf
--- /dev/null
+++ b/app/utils/media.ts
@@ -0,0 +1,21 @@
+export async function downloadRemoteMedia(url: string, filename = "media") {
+ try {
+ const res = await fetch(url);
+ if (!res.ok) throw new Error(`Download failed: ${res.status}`);
+
+ const blob = await res.blob();
+ const blobUrl = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+
+ link.href = blobUrl;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+
+ URL.revokeObjectURL(blobUrl);
+ } catch (err) {
+ console.error("Media download failed:", err);
+ window.open(url, "_blank", "noopener,noreferrer");
+ }
+}
\ No newline at end of file
diff --git a/app/utils/moderation.ts b/app/utils/moderation.ts
new file mode 100644
index 0000000..04f6c3d
--- /dev/null
+++ b/app/utils/moderation.ts
@@ -0,0 +1,43 @@
+const DEFAULT_REPLACEMENT_TEXT = "System says: touch grass first 🌱";
+
+function escapeRegExp(input: string) {
+ return input.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+function buildWordFilter(blocklist: string[]) {
+ const sanitizedWords = Array.from(
+ new Set(
+ blocklist
+ .map((word) => word.trim())
+ .filter(Boolean)
+ .map((word) => escapeRegExp(word)),
+ ),
+ );
+
+ if (sanitizedWords.length === 0) return null;
+
+ return new RegExp(`\\b(${sanitizedWords.join("|")})\\b`, "gi");
+}
+
+export function sanitizeTextWithBlocklist(
+ input: string,
+ blocklist: string[],
+ replacement = DEFAULT_REPLACEMENT_TEXT,
+) {
+ if (!blocklist.length) return input;
+
+ const filter = buildWordFilter(blocklist);
+ if (!filter) return input;
+
+ return input.replace(filter, replacement);
+}
+
+export function hasBlocklistedWord(input: string, blocklist: string[]) {
+ if (!input || !blocklist.length) return false;
+
+ const filter = buildWordFilter(blocklist);
+ if (!filter) return false;
+
+ filter.lastIndex = 0;
+ return filter.test(input);
+}
\ No newline at end of file
diff --git a/app/utils/slug.ts b/app/utils/slug.ts
new file mode 100644
index 0000000..f3d57a8
--- /dev/null
+++ b/app/utils/slug.ts
@@ -0,0 +1,11 @@
+export function toKebabSlug(value: string, fallback = "item") {
+ const slug = value
+ .toLowerCase()
+ .trim()
+ .replace(/\s+/g, "-")
+ .replace(/[^\w-]+/g, "")
+ .replace(/--+/g, "-")
+ .replace(/^-+|-+$/g, "");
+
+ return slug || fallback;
+}
\ No newline at end of file
diff --git a/app/utils/wakatime.ts b/app/utils/wakatime.ts
new file mode 100644
index 0000000..a247504
--- /dev/null
+++ b/app/utils/wakatime.ts
@@ -0,0 +1,63 @@
+export type DailyStat = {
+ date: string;
+ total_seconds: number;
+};
+
+export function formatDateYMD(date: Date) {
+ const y = date.getFullYear();
+ const m = String(date.getMonth() + 1).padStart(2, "0");
+ const d = String(date.getDate()).padStart(2, "0");
+ return `${y}-${m}-${d}`;
+}
+
+export function toDateKey(value: string) {
+ return value.slice(0, 10);
+}
+
+export function buildSnapshotMetrics(dailyStats: DailyStat[]) {
+ const normalized = [...dailyStats]
+ .map((entry) => ({
+ date: toDateKey(entry.date),
+ total_seconds: Math.max(0, Math.floor(entry.total_seconds || 0)),
+ }))
+ .sort((a, b) => a.date.localeCompare(b.date));
+
+ const last7 = normalized.slice(-7);
+ const totalSeconds7d = last7.reduce((sum, day) => sum + day.total_seconds, 0);
+ const activeDays7d = last7.filter((day) => day.total_seconds > 0).length;
+
+ const activeByDay = normalized.map((day) => day.total_seconds > 0);
+ const activeDays = activeByDay.filter(Boolean).length;
+ const consistencyPercent =
+ normalized.length > 0
+ ? Math.round((activeDays / normalized.length) * 100)
+ : 0;
+
+ let bestStreak = 0;
+ let runningStreak = 0;
+ for (const isActive of activeByDay) {
+ runningStreak = isActive ? runningStreak + 1 : 0;
+ if (runningStreak > bestStreak) bestStreak = runningStreak;
+ }
+
+ let currentStreak = 0;
+ for (let i = activeByDay.length - 1; i >= 0; i -= 1) {
+ if (!activeByDay[i]) break;
+ currentStreak += 1;
+ }
+
+ const peakDay = last7.reduce(
+ (max, day) => (day.total_seconds > max.total_seconds ? day : max),
+ { date: "", total_seconds: 0 },
+ );
+
+ return {
+ totalSeconds7d,
+ activeDays7d,
+ consistencyPercent,
+ currentStreak,
+ bestStreak,
+ peakDayDate: peakDay.date || null,
+ peakDaySeconds: peakDay.total_seconds,
+ };
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8b787b3..bc847e3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,13 +11,12 @@
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
- "@fortawesome/react-fontawesome": "^3.2.0",
+ "@fortawesome/react-fontawesome": "^3.3.0",
"@hcaptcha/react-hcaptcha": "^2.0.2",
"@sentry/nextjs": "^10.45.0",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.98.0",
"aos": "^2.3.4",
- "devicon": "^2.17.0",
"devtools-detector": "^2.0.25",
"next": "16.1.6",
"nextjs-toploader": "^3.9.17",
@@ -83,6 +82,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -581,6 +581,7 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz",
"integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "7.2.0"
},
@@ -613,9 +614,9 @@
}
},
"node_modules/@fortawesome/react-fontawesome": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.2.0.tgz",
- "integrity": "sha512-E9Gu1hqd6JussVO26EC4WqRZssXMnQr2ol7ZNWkkFOH8jZUaxDJ9Z9WF9wIVkC+kJGXUdY3tlffpDwEKfgQrQw==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.3.0.tgz",
+ "integrity": "sha512-EHmHeTf8WgO29sdY3iX/7ekE3gNUdlc2RW6mm/FzELlHFKfTrA9S4MlyquRR+RRCRCn8+jXfLFpLGB2l7wCWyw==",
"license": "MIT",
"engines": {
"node": ">=20"
@@ -1183,7 +1184,6 @@
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@@ -1415,6 +1415,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -1436,6 +1437,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.6.0.tgz",
"integrity": "sha512-L8UyDwqpTcbkIK5cgwDRDYDoEhQoj8wp8BwsO19w3LB1Z41yEQm2VJyNfAi9DrLP/YTqXqWpKHyZfR9/tFYo1Q==",
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
@@ -1448,6 +1450,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.6.0.tgz",
"integrity": "sha512-HLM1v2cbZ4TgYN6KEOj+Bbj8rAKriOdkF9Ed3tG25FoprSiQl7kYc+RRT6fUZGOvx0oMi5U67GoFdT+XUn8zEg==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -1856,6 +1859,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.0.tgz",
"integrity": "sha512-D4y/+OGe3JSuYUCBxtH5T9DSAWNcvCb/nQWIga8HNtXTVPQn59j0nTBAgaAXxUVBDl40mG3Tc76b46wPlZaiJQ==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@opentelemetry/core": "2.6.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -1872,6 +1876,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz",
"integrity": "sha512-g/OZVkqlxllgFM7qMKqbPV9c1DUPhQ7d4n3pgZFcrnrNft9eJXZM2TNHTPYREJBrtNdRytYyvwjgL5geDKl3EQ==",
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@opentelemetry/core": "2.6.0",
"@opentelemetry/resources": "2.6.0",
@@ -1889,6 +1894,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
"integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
"license": "Apache-2.0",
+ "peer": true,
"engines": {
"node": ">=14"
}
@@ -2045,6 +2051,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -2962,6 +2969,7 @@
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.98.0.tgz",
"integrity": "sha512-Ohc97CtInLwZyiSASz7tT9/Abm/vqnIbO9REp+PivVUII8UZsuI3bngRQnYgJdFoOIwvaEII1fX1qy8x0CyNiw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@supabase/auth-js": "2.98.0",
"@supabase/functions-js": "2.98.0",
@@ -3357,7 +3365,6 @@
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
@@ -3368,7 +3375,6 @@
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
"integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
@@ -3481,6 +3487,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -3580,6 +3587,7 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1",
@@ -4110,7 +4118,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
"integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/helper-numbers": "1.13.2",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2"
@@ -4120,29 +4127,25 @@
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
"integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
"integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
"integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
"integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
"@webassemblyjs/helper-api-error": "1.13.2",
@@ -4153,15 +4156,13 @@
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
"integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
"integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
@@ -4174,7 +4175,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
"integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
}
@@ -4184,7 +4184,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
"integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"@xtuc/long": "4.2.2"
}
@@ -4193,15 +4192,13 @@
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
"integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
"integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
@@ -4218,7 +4215,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
"integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-wasm-bytecode": "1.13.2",
@@ -4232,7 +4228,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
"integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-buffer": "1.14.1",
@@ -4245,7 +4240,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
"integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@webassemblyjs/helper-api-error": "1.13.2",
@@ -4260,7 +4254,6 @@
"resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
"integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
"@xtuc/long": "4.2.2"
@@ -4270,21 +4263,20 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
"integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
- "license": "BSD-3-Clause",
- "peer": true
+ "license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
"integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
- "license": "Apache-2.0",
- "peer": true
+ "license": "Apache-2.0"
},
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4306,7 +4298,6 @@
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.13.0"
},
@@ -4358,7 +4349,6 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ajv": "^8.0.0"
},
@@ -4376,7 +4366,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
"integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -4392,8 +4381,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/ansi-styles": {
"version": "4.3.0",
@@ -4724,6 +4712,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -4742,8 +4731,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/call-bind": {
"version": "1.0.8",
@@ -4897,7 +4885,6 @@
"resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
"integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6.0"
}
@@ -4963,8 +4950,7 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/commondir": {
"version": "1.0.1",
@@ -5305,12 +5291,6 @@
"node": ">=8"
}
},
- "node_modules/devicon": {
- "version": "2.17.0",
- "resolved": "https://registry.npmjs.org/devicon/-/devicon-2.17.0.tgz",
- "integrity": "sha512-2nKUdjobJlmRSaCHa50PGsVq0VDURnq9gVzQoJggsM/NKN0tLhC/Uq2zmy2pH36Q/1q3gvYwp/GjTgv/R0Ysbg==",
- "license": "MIT"
- },
"node_modules/devlop": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
@@ -5530,8 +5510,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
"integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
@@ -5631,6 +5610,7 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -5816,6 +5796,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -6080,7 +6061,6 @@
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.8.x"
}
@@ -6247,8 +6227,7 @@
"url": "https://opencollective.com/fastify"
}
],
- "license": "BSD-3-Clause",
- "peer": true
+ "license": "BSD-3-Clause"
},
"node_modules/fastq": {
"version": "1.20.1",
@@ -6557,8 +6536,7 @@
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
"integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
- "license": "BSD-2-Clause",
- "peer": true
+ "license": "BSD-2-Clause"
},
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.4",
@@ -7500,7 +7478,6 @@
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
"integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/node": "*",
"merge-stream": "^2.0.0",
@@ -7515,7 +7492,6 @@
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"has-flag": "^4.0.0"
},
@@ -7578,8 +7554,7 @@
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -7933,7 +7908,6 @@
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
"integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6.11.5"
},
@@ -8197,8 +8171,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/merge2": {
"version": "1.4.1",
@@ -8671,7 +8644,6 @@
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -8681,7 +8653,6 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -8778,14 +8749,14 @@
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/next": {
"version": "16.1.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz",
"integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@next/env": "16.1.6",
"@swc/helpers": "0.5.15",
@@ -9488,6 +9459,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9497,6 +9469,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -9508,7 +9481,8 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/react-markdown": {
"version": "10.1.0",
@@ -9542,6 +9516,7 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@@ -9639,7 +9614,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@@ -9748,7 +9724,6 @@
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -9829,6 +9804,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -9958,7 +9934,6 @@
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
"integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
@@ -9995,7 +9970,6 @@
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
@@ -10007,8 +9981,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/semver": {
"version": "6.3.1",
@@ -10303,7 +10276,6 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
- "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -10322,7 +10294,6 @@
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"license": "MIT",
- "peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -10623,7 +10594,6 @@
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0",
@@ -10642,7 +10612,6 @@
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz",
"integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
@@ -10718,6 +10687,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -10915,6 +10885,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -11211,7 +11182,6 @@
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
"integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.1.2"
@@ -11231,7 +11201,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz",
"integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -11280,7 +11249,6 @@
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz",
"integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10.13.0"
}
@@ -11290,7 +11258,6 @@
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
"integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
"license": "BSD-2-Clause",
- "peer": true,
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^4.1.1"
@@ -11304,7 +11271,6 @@
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
"license": "BSD-2-Clause",
- "peer": true,
"engines": {
"node": ">=4.0"
}
@@ -11494,6 +11460,7 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 4b2cd35..a3b6321 100644
--- a/package.json
+++ b/package.json
@@ -12,13 +12,12 @@
"@fortawesome/fontawesome-svg-core": "^7.2.0",
"@fortawesome/free-brands-svg-icons": "^7.2.0",
"@fortawesome/free-solid-svg-icons": "^7.2.0",
- "@fortawesome/react-fontawesome": "^3.2.0",
+ "@fortawesome/react-fontawesome": "^3.3.0",
"@hcaptcha/react-hcaptcha": "^2.0.2",
"@sentry/nextjs": "^10.45.0",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.98.0",
"aos": "^2.3.4",
- "devicon": "^2.17.0",
"devtools-detector": "^2.0.25",
"next": "16.1.6",
"nextjs-toploader": "^3.9.17",
diff --git a/supabase/migrations/20260320234731_add_enforcement_checks.sql b/supabase/migrations/20260320234731_add_enforcement_checks.sql
index 71fc6d6..84b3f89 100644
--- a/supabase/migrations/20260320234731_add_enforcement_checks.sql
+++ b/supabase/migrations/20260320234731_add_enforcement_checks.sql
@@ -10,7 +10,6 @@ alter table public.leaderboards
add constraint leaderboards_join_code_format
check (join_code ~ '^[A-Za-z0-9]{1,8}$');
-
create policy "Owner can delete leaderboard"
on public.leaderboards
for delete
diff --git a/supabase/migrations/20260327100000_cascade_conversation_delete.sql b/supabase/migrations/20260327100000_cascade_conversation_delete.sql
new file mode 100644
index 0000000..12f0195
--- /dev/null
+++ b/supabase/migrations/20260327100000_cascade_conversation_delete.sql
@@ -0,0 +1,10 @@
+DO $$
+BEGIN
+ -- Drop existing constraints if they exist
+ ALTER TABLE conversation_participants DROP CONSTRAINT IF EXISTS conversation_participants_conversation_id_fkey;
+ ALTER TABLE messages DROP CONSTRAINT IF EXISTS messages_conversation_id_fkey;
+
+ -- Add constraints with cascade
+ ALTER TABLE conversation_participants ADD CONSTRAINT conversation_participants_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE;
+ ALTER TABLE messages ADD CONSTRAINT messages_conversation_id_fkey FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE;
+END $$;
diff --git a/supabase/migrations/20260329120000_add_user_dashboard_snapshots.sql b/supabase/migrations/20260329120000_add_user_dashboard_snapshots.sql
new file mode 100644
index 0000000..babb93b
--- /dev/null
+++ b/supabase/migrations/20260329120000_add_user_dashboard_snapshots.sql
@@ -0,0 +1,42 @@
+/* ---- User Dashboard Snapshots ----- */
+create table public.user_dashboard_snapshots (
+ id bigint generated by default as identity primary key,
+ user_id uuid not null references auth.users(id) on delete cascade,
+ snapshot_date date not null,
+ total_seconds_7d bigint not null default 0,
+ active_days_7d integer not null default 0,
+ consistency_percent integer not null default 0,
+ current_streak integer not null default 0,
+ best_streak integer not null default 0,
+ peak_day date,
+ peak_day_seconds bigint not null default 0,
+ top_language text,
+ top_language_percent numeric(5,2),
+ created_at timestamp with time zone not null default now(),
+ updated_at timestamp with time zone not null default now(),
+ unique (user_id, snapshot_date),
+ check (active_days_7d between 0 and 7),
+ check (consistency_percent between 0 and 100),
+ check (peak_day_seconds >= 0),
+ check (
+ top_language_percent is null
+ or (top_language_percent >= 0 and top_language_percent <= 100)
+ )
+);
+
+alter table public.user_dashboard_snapshots enable row level security;
+
+create policy "Users can view their own dashboard snapshots"
+on public.user_dashboard_snapshots
+for select
+using (auth.uid() = user_id);
+
+create policy "Users can insert their own dashboard snapshots"
+on public.user_dashboard_snapshots
+for insert
+with check (auth.uid() = user_id);
+
+create policy "Users can update their own dashboard snapshots"
+on public.user_dashboard_snapshots
+for update
+using (auth.uid() = user_id);
diff --git a/supabase/migrations/20260329133000_add_chat_presence_and_read_tracking.sql b/supabase/migrations/20260329133000_add_chat_presence_and_read_tracking.sql
new file mode 100644
index 0000000..15849a8
--- /dev/null
+++ b/supabase/migrations/20260329133000_add_chat_presence_and_read_tracking.sql
@@ -0,0 +1,14 @@
+ALTER TABLE conversation_participants
+ADD COLUMN IF NOT EXISTS last_seen_at timestamptz NOT NULL DEFAULT to_timestamp(0),
+ADD COLUMN IF NOT EXISTS last_read_at timestamptz NOT NULL DEFAULT now();
+
+UPDATE conversation_participants
+SET
+ last_seen_at = COALESCE(last_seen_at, to_timestamp(0)),
+ last_read_at = COALESCE(last_read_at, now());
+
+CREATE INDEX IF NOT EXISTS idx_conversation_participants_user_last_seen_at
+ON conversation_participants (user_id, last_seen_at DESC);
+
+CREATE INDEX IF NOT EXISTS idx_messages_conversation_created_sender
+ON messages (conversation_id, created_at DESC, sender_id);
diff --git a/supabase/migrations/20260330103000_enable_chat_realtime_publication.sql b/supabase/migrations/20260330103000_enable_chat_realtime_publication.sql
new file mode 100644
index 0000000..9afa37f
--- /dev/null
+++ b/supabase/migrations/20260330103000_enable_chat_realtime_publication.sql
@@ -0,0 +1,33 @@
+DO $$
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_publication_tables
+ WHERE pubname = 'supabase_realtime'
+ AND schemaname = 'public'
+ AND tablename = 'conversations'
+ ) THEN
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.conversations;
+ END IF;
+
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_publication_tables
+ WHERE pubname = 'supabase_realtime'
+ AND schemaname = 'public'
+ AND tablename = 'conversation_participants'
+ ) THEN
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.conversation_participants;
+ END IF;
+
+ IF NOT EXISTS (
+ SELECT 1
+ FROM pg_publication_tables
+ WHERE pubname = 'supabase_realtime'
+ AND schemaname = 'public'
+ AND tablename = 'messages'
+ ) THEN
+ ALTER PUBLICATION supabase_realtime ADD TABLE public.messages;
+ END IF;
+END
+$$;
diff --git a/supabase/migrations/20260330104500_fix_global_chat_membership.sql b/supabase/migrations/20260330104500_fix_global_chat_membership.sql
new file mode 100644
index 0000000..42a36f5
--- /dev/null
+++ b/supabase/migrations/20260330104500_fix_global_chat_membership.sql
@@ -0,0 +1,45 @@
+-- Ensure the global conversation row exists and has the expected type.
+INSERT INTO public.conversations (id, type)
+VALUES ('00000000-0000-0000-0000-000000000001', 'global')
+ON CONFLICT (id) DO UPDATE
+SET type = EXCLUDED.type;
+
+-- Backfill global chat membership for all existing auth users.
+INSERT INTO public.conversation_participants (conversation_id, user_id, email)
+SELECT
+ '00000000-0000-0000-0000-000000000001',
+ u.id,
+ COALESCE(u.email, CONCAT(u.id::text, '@user.local'))
+FROM auth.users u
+ON CONFLICT (conversation_id, user_id) DO UPDATE
+SET email = EXCLUDED.email;
+
+-- Keep future auth users automatically enrolled in global chat.
+CREATE OR REPLACE FUNCTION public.ensure_user_in_global_chat()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM public.conversations
+ WHERE id = '00000000-0000-0000-0000-000000000001'
+ ) THEN
+ INSERT INTO public.conversation_participants (conversation_id, user_id, email)
+ VALUES (
+ '00000000-0000-0000-0000-000000000001',
+ NEW.id,
+ COALESCE(NEW.email, CONCAT(NEW.id::text, '@user.local'))
+ )
+ ON CONFLICT (conversation_id, user_id) DO UPDATE
+ SET email = EXCLUDED.email;
+ END IF;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql SECURITY DEFINER SET search_path = public;
+
+DROP TRIGGER IF EXISTS on_auth_user_join_global_chat ON auth.users;
+
+CREATE TRIGGER on_auth_user_join_global_chat
+AFTER INSERT ON auth.users
+FOR EACH ROW
+EXECUTE FUNCTION public.ensure_user_in_global_chat();