{step.title}
+{step.description}
+diff --git a/app/(auth)/login/page.tsx b/app/(auth)/login/page.tsx index affe353..05cd1c5 100644 --- a/app/(auth)/login/page.tsx +++ b/app/(auth)/login/page.tsx @@ -2,6 +2,8 @@ import Link from "next/link"; import Image from "next/image"; import LoginForm from "@/app/components/auth/LoginForm"; import { Metadata } from "next"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faChevronLeft } from "@fortawesome/free-solid-svg-icons"; export const metadata: Metadata = { title: "Login - DevPulse", @@ -63,7 +65,15 @@ export default async function Login(props: { : undefined; return ( -
@@ -112,17 +115,12 @@ export default function JoinButton({
>
{joining ? (
<>
-
+
- Manage your account settings and including your WakaTime API key. -
++ Manage profile details, WakaTime connection, and account security. +
+{activeSublabel}
++ It's quiet here +
++ Select a conversation from the left sidebar or start a new one to begin chatting. +
+{isGlobalActive ? "Community channel" : activeOtherUser?.email}
+ +No media shared yet
+- No Conversation Selected -
-- Select a conversation from the top or start a new one. -
No users found.
+No users found.
No messages yet. Say hello!
+ Keep your profile details accurate for a better dashboard experience. +
+{originalName}
+{user.email || "No email"}
++ Profile fields are locked. Click Edit to make changes. +
+ )}+ Keep your token updated to sync coding activity accurately. +
++ Current status:{" "} + + {isConnected ? "Connected" : "Not connected"} + + {isConnected && displayMaskedKey ? ( + ({displayMaskedKey}) + ) : null} +
+ + {isEditing ? ( + <> + setKey(e.target.value)} + disabled={saving} + /> + ++ API key is locked. Click Edit to replace it. +
+ )} + ++ Find your key in{" "} + + WakaTime account settings + + . +
+- Connect your WakaTime account to DevPulse to visualize your coding - activity -
++ Account Setup +
++ Connect WakaTime to unlock your dashboard insights, rankings, and + coding activity trends. +
+- Welcome {email}. Enter - your WakaTime API key to activate your DevPulse dashboard. -
- - setKey(e.target.value)} - /> - -- You can get your WakaTime API key from your{" "} - +
+ Account Email +
+{email}
++ Get your API key from{" "} + + WakaTime account settings + + . +
+Daily and weekly coding performance charts.
+Language, editor, machine, and category insights.
+Leaderboard participation and progress tracking.
++ Security Note +
++ Your key is used only to sync your coding data and can be updated + anytime in settings. +
++ New account setup is almost done. Connect WakaTime to finish onboarding and start using your full dashboard. +
+ No category data. +
+ )}Last {days} days
+{summaryText}
+Learn how we count contributions
++ Contribution Stats +
++ Active days: {activeDays} +
++ Period days: {periodCells.length} +
++ Consistency: {consistencyScore}% +
++ Current streak: {currentStreak} +
++ Best streak: {bestStreak} +
++ No editor data. +
+ )}{point.subject}
+{point.percent}% share
+{formatHours(point.seconds)}
+No language data available yet.
++ No operating system data. +
+ )}- We welcome contributions! You can help by adding new features, fixing - bugs, or improving the project. Just make sure to follow our - guidelines to avoid conflicts. -
-
- {`## Contribution Guidelines
-
-Contributions to devpulse are welcome! Please follow these guidelines:
+ const contributionSteps = [
+ {
+ title: "Align First",
+ description:
+ "Open an issue or discussion before changing established behavior.",
+ },
+ {
+ title: "Build Focused PRs",
+ description:
+ "Keep pull requests scoped to one improvement so review stays fast.",
+ },
+ {
+ title: "Ship with Context",
+ description:
+ "Include problem statement, change summary, and verification details.",
+ },
+ ];
-1. Before modifying any existing design or logic that is already working or in use, **contact us first** to avoid conflicts.
-2. You are welcome to contribute new features, bug fixes, or improvements.
-3. Check the Issues tab (if available) before starting to avoid duplicate work.
-4. Follow the existing code style and conventions.
-5. Submit a pull request with a clear description of your changes and the problem it solves.
+ return (
+
+
+ Open Source
+
+
+ Want to contribute?
+
+
+ We welcome thoughtful contributions across features, fixes, and
+ documentation. A quick review of the contribution flow helps avoid
+ collisions and keeps delivery fast for everyone.
+
-> ⚠️ Pull requests that modify existing working features without prior discussion may not be merged.
+
+ {contributionSteps.map((step, i) => (
+
+
+ {i + 1}
+
+ {step.title}
+ {step.description}
+
+ ))}
+
-Help us keep the codebase ("DevPulse") clean, stable, and maintainable.`}
-
- - A big thank you to all the amazing contributors who have helped make - DevPulse better! Your support and contributions are what drive this - project forward. -
++ Maintainers, builders, and reviewers powering DevPulse. +
++ {contributor.login} +
++ Spotlight contributor +
++ Showing the top {visibleContributors.length} contributors by public + activity. +
+ )} + +- Top most "dedicated" developers who have spent the least - amount of time coding. Remember, its not about how much you code, - but how effective you are! 😉 -
- - {(!losser_members || losser_members.length) === 0 && ( -+ Team Insight +
++ Surface developers who may need support, context, or better + task flow. +
++ Management View +
++ Use this view to identify blockers early and support developers + before momentum drops across the week. +
++ Activity signal: {getActivitySignal(member.total_seconds)} +
+- Join the top engineering communities today. -
-+ Board Arena +
++ Live competition spaces where teams and individuals race on + coding consistency. +
++ Most recently launched board with open competition access. +
++ Status +
+Live
++ Access +
+Public
++ Action +
+Enter
++ New boards will appear here as your community creates more + competition arenas. +
- Celebrating the most dedicated coders in our community. -
++ Real Leaderboard +
++ Ranked by weekly coding time with clear podium cutoffs and + progress to the top. +
+{member.email.split("@")[0]} - +
++ {rank <= 3 ? "Podium zone" : "Chasing podium"} +
++ Showing top {rankedTopMembers.length} developers for a cleaner + weekly snapshot. +
+ )} + + )} > diff --git a/app/components/landing-page/VibeCoders.tsx b/app/components/landing-page/VibeCoders.tsx index 12ffff8..2571a8b 100644 --- a/app/components/landing-page/VibeCoders.tsx +++ b/app/components/landing-page/VibeCoders.tsx @@ -1,60 +1,105 @@ import { TopMember } from "./TopLeaderbord"; -export default async function VibeCoders({ +function formatDuration(totalSeconds: number) { + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + return `${hours}h ${minutes}m`; +} + +export default function VibeCoders({ vibe_coders, }: { vibe_coders: TopMember[]; }) { + const visibleVibeCoders = vibe_coders.slice(0, 6); + return ( <> {vibe_coders && vibe_coders.length > 0 && ( -- Meet the vibe coders of the week - those who spent the least - amount of time coding and more time prompting. -
- {(!vibe_coders || vibe_coders.length) === 0 && ( -+ AI Category +
++ Snapshot of developers investing the most time in AI Coding. +
++ Category Focus +
++ Measure how teams are adopting AI tooling without losing coding + consistency and technical depth. +
++ AI coding contribution +
+- Join developers and teams competing on DevPulse. -
- - Create Free Account - ++ Launch DevPulse +
++ Connect your data source, onboard your team, and turn raw coding + time into actionable performance insights. +
+ ++ Setup Time +
+~5 minutes
++ Sync Source +
+WakaTime
++ Visibility +
+Team + Public
+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/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/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();