From 33dd163eff28beb5adba87554844d7c529e5296c Mon Sep 17 00:00:00 2001 From: Samuel Aboderin Date: Wed, 1 Apr 2026 01:13:49 +0100 Subject: [PATCH 1/3] feat: v2 - complete product redesign across auth, dashboard, prompt editor, collections, OCR, and landing page --- README.md | 4 +- app/api/chat/route.ts | 1 - app/api/ocr/route.ts | 1 - app/auth/reset-password/page.tsx | 175 ++--- app/chains/[id]/page.tsx | 233 +++---- app/chains/new/page.tsx | 117 ++-- app/chains/page.tsx | 146 ++--- app/dashboard/page.tsx | 133 ++-- app/docs/page.tsx | 2 +- app/globals.css | 221 ++++++- app/home/page.tsx | 2 +- app/layout.tsx | 14 +- app/login/page.tsx | 175 +++-- app/ocr/page.tsx | 342 ++++++---- app/page.tsx | 1037 ++++++++++++++++-------------- app/prompts/[id]/page.tsx | 4 +- app/prompts/new/page.tsx | 21 +- app/settings/page.tsx | 367 ++++++----- app/signup/page.tsx | 196 ++++-- components/AuthProvider.tsx | 8 +- components/ChainStepCard.tsx | 248 +++---- components/Header.tsx | 443 +++++++------ components/Layout.tsx | 11 +- components/PromptCollection.tsx | 26 +- components/PromptDetail.tsx | 170 +++-- components/PromptForm.tsx | 181 ++++-- components/PromptListItem.tsx | 37 +- components/Sidebar.tsx | 131 ++-- components/ThemeProvider.tsx | 19 +- components/VersionHistory.tsx | 73 ++- lib/PromptsContext.tsx | 9 +- lib/auth.ts | 83 +-- lib/chainData.ts | 12 +- lib/supabase-health-check.ts | 30 +- lib/supabase-server.ts | 15 +- lib/supabase.ts | 12 +- lib/types.ts | 2 +- screenshots/closedNote.png | Bin 0 -> 27379 bytes 38 files changed, 2791 insertions(+), 1910 deletions(-) create mode 100644 screenshots/closedNote.png diff --git a/README.md b/README.md index 8afb38b..9da774f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -# closedNote +

+ closedNote +

> **Prompts are living documents. closedNote is the only prompt manager that remembers how they evolved.** diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 89c2651..187e470 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -76,7 +76,6 @@ async function callOpenAI( export async function POST(req: Request) { try { - // Verify the caller is an authenticated user before doing any work const user = await getUserFromRequest(req); if (!user) { return NextResponse.json( diff --git a/app/api/ocr/route.ts b/app/api/ocr/route.ts index 5e742f4..4a0d495 100644 --- a/app/api/ocr/route.ts +++ b/app/api/ocr/route.ts @@ -41,7 +41,6 @@ async function callOpenAIVision(apiKey: string, arrayBuf: ArrayBuffer, mimeType: export async function POST(request: Request) { try { - // Verify the caller is an authenticated user before doing any work const user = await getUserFromRequest(request); if (!user) { return NextResponse.json( diff --git a/app/auth/reset-password/page.tsx b/app/auth/reset-password/page.tsx index 93e14fd..122f3a1 100644 --- a/app/auth/reset-password/page.tsx +++ b/app/auth/reset-password/page.tsx @@ -8,17 +8,18 @@ type Stage = "exchanging" | "form" | "success" | "error"; const MIN_PASSWORD_LENGTH = 6; -const EyeIcon = ({ open }: { open: boolean }) => - open ? ( - +function EyeIcon({ open, style }: { open: boolean; style?: React.CSSProperties }) { + return open ? ( + ) : ( - + ); +} export default function ResetPasswordPage() { const router = useRouter(); @@ -34,22 +35,14 @@ export default function ResetPasswordPage() { const code = new URLSearchParams(window.location.search).get("code"); if (code) { - // PKCE flow: email contains ?code= supabase.auth.exchangeCodeForSession(code).then(({ error }) => { - if (error) { - setErrorMsg(error.message); - setStage("error"); - } else { - setStage("form"); - } + if (error) { setErrorMsg(error.message); setStage("error"); } + else { setStage("form"); } }); return; } - // Implicit flow: email contains #access_token=...&type=recovery - // detectSessionInUrl:true parses the hash and fires PASSWORD_RECOVERY let timeoutId: ReturnType; - const { data: { subscription } } = supabase.auth.onAuthStateChange((event) => { if (event === "PASSWORD_RECOVERY") { clearTimeout(timeoutId); @@ -64,61 +57,79 @@ export default function ResetPasswordPage() { setStage("error"); }, 10000); - return () => { - clearTimeout(timeoutId); - subscription.unsubscribe(); - }; + return () => { clearTimeout(timeoutId); subscription.unsubscribe(); }; }, []); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (password.length < MIN_PASSWORD_LENGTH) return; if (password !== confirm) return; - setSaving(true); const { error } = await supabase.auth.updateUser({ password }); setSaving(false); - - if (error) { - setErrorMsg(error.message); - setStage("error"); - } else { - setStage("success"); - setTimeout(() => router.push("/dashboard"), 2500); - } + if (error) { setErrorMsg(error.message); setStage("error"); } + else { setStage("success"); setTimeout(() => router.push("/dashboard"), 2500); } }; const meetsLength = password.length >= MIN_PASSWORD_LENGTH; const meetsMatch = password === confirm && confirm.length > 0; - const inputClass = - "w-full px-3.5 py-2.5 bg-neutral-50 dark:bg-neutral-800 border border-neutral-200 dark:border-neutral-700 rounded-lg text-sm text-neutral-900 dark:text-neutral-100 placeholder-neutral-400 focus:outline-none focus:ring-2 focus:ring-neutral-400 dark:focus:ring-neutral-500 transition-shadow"; + const inputStyle: React.CSSProperties = { + width: "100%", padding: "10px 14px", + background: "var(--cn-bg-s2)", border: "1px solid var(--cn-border)", + borderRadius: 10, fontSize: 14, color: "var(--cn-text)", + outline: "none", transition: "border-color 0.15s", + boxSizing: "border-box", + }; + + const labelStyle: React.CSSProperties = { + display: "block", fontSize: 11, fontWeight: 700, + textTransform: "uppercase", letterSpacing: "0.08em", + color: "var(--cn-muted)", marginBottom: 6, + }; return ( -
-
-
+
+
+ {/* Logo accent */} +
+ + closedNote + +
+ +
{stage === "exchanging" && ( -
-
-

Verifying link...

+
+
+

Verifying link...

)} {stage === "form" && ( <> -
-

Set new password

-

Choose a strong password for your account.

+
+

+ Set new password +

+

+ Choose a strong password for your account. +

-
+
- -
+ +
-
- +
+ {password.length > 0 && ( - meetsLength - ? - : + + {meetsLength + ? + : + } + )} - + At least {MIN_PASSWORD_LENGTH} characters
- -
+ +
{confirm.length > 0 && !meetsMatch && ( -

Passwords don't match

+

Passwords don't match

)}
@@ -195,35 +215,36 @@ export default function ResetPasswordPage() { )} {stage === "success" && ( -
-
- +
+
+
-

Password updated

-

Redirecting you to your dashboard...

+

Password updated

+

Redirecting you to your dashboard...

)} {stage === "error" && ( -
diff --git a/app/chains/[id]/page.tsx b/app/chains/[id]/page.tsx index 2d2745b..042474e 100644 --- a/app/chains/[id]/page.tsx +++ b/app/chains/[id]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { useRouter, useParams } from "next/navigation"; import { Header } from "@/components/Header"; @@ -189,96 +189,96 @@ export default function ChainDetailPage() { return null; } + const labelStyle: React.CSSProperties = { + display: "block", fontSize: 11, fontWeight: 700, + textTransform: "uppercase", letterSpacing: "0.08em", + color: "var(--cn-muted)", marginBottom: 6, + }; + const inputStyle: React.CSSProperties = { + width: "100%", padding: "9px 12px", + background: "var(--cn-bg-s2)", border: "1px solid var(--cn-border)", + borderRadius: 8, fontSize: 13, color: "var(--cn-text)", + outline: "none", transition: "border-color 0.15s", + boxSizing: "border-box", + }; + return ( - } sidebar={null}> -
- - ← Back to Chains - - - {/* Loading state */} + } sidebar={null}> +
+
+ (e.currentTarget.style.color = "var(--cn-text)")} + onMouseLeave={e => (e.currentTarget.style.color = "var(--cn-muted)")} + > + ← Back to Threads + +
+ + {/* Loading */} {loading && ( -
-
- Loading chain... -
+
+ Loading thread...
)} - {/* Error state (when chain not found) */} + {/* Error - not found */} {!loading && error && !chain && ( -
-

- {error} -

+
+

{error}

- Back to Chains + Back to Threads
)} - {/* Chain content */} + {/* Content */} {!loading && chain && ( <> - {/* Inline error banner */} {error && ( -
+
{error}
)} - {/* ====== EDIT MODE ====== */} + {/* ── EDIT MODE ── */} {editing ? ( <> -
+
- + setEditTitle(e.target.value)} - placeholder="Chain title" - className="w-full px-3 py-2 text-sm rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500 focus:outline-none focus:ring-2 focus:ring-neutral-400 dark:focus:ring-neutral-600" + placeholder="Thread title" + style={inputStyle} />
-