From cd04851be8556e49436f840551fdac4f51a2cc05 Mon Sep 17 00:00:00 2001 From: PoshanP Date: Wed, 14 Jan 2026 13:43:14 -0800 Subject: [PATCH 1/2] Redesign auth pages to match app theme with content JSON --- .env.example | 7 + app/auth/layout.tsx | 13 ++ app/auth/login/page.tsx | 206 ++++++++++++++------------- app/auth/signup/page.tsx | 293 ++++++++++++++++++++++----------------- lib/content/auth.json | 81 +++++++++++ lib/content/common.json | 10 ++ lib/content/index.ts | 88 ++++++++++++ 7 files changed, 469 insertions(+), 229 deletions(-) create mode 100644 .env.example create mode 100644 app/auth/layout.tsx create mode 100644 lib/content/auth.json create mode 100644 lib/content/common.json create mode 100644 lib/content/index.ts diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4ae0440 --- /dev/null +++ b/.env.example @@ -0,0 +1,7 @@ +# Supabase (required) +NEXT_PUBLIC_SUPABASE_URL=your_supabase_project_url +NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key +SUPABASE_SERVICE_ROLE_KEY=your_supabase_service_role_key + +# OpenAI (required) +OPENAI_API_KEY=your_openai_api_key diff --git a/app/auth/layout.tsx b/app/auth/layout.tsx new file mode 100644 index 0000000..5451132 --- /dev/null +++ b/app/auth/layout.tsx @@ -0,0 +1,13 @@ +export default function AuthLayout({ + children, +}: { + children: React.ReactNode; +}) { + // Auth pages handle their own full-screen layout + // This layout removes the container wrapper from the root layout + return ( +
+ {children} +
+ ); +} diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 3003a8f..d663f3a 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -3,8 +3,9 @@ import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { Mail, Lock, Loader2, FileText } from "lucide-react"; +import { Mail, Lock, Loader2 } from "lucide-react"; import { useSupabase } from "@/lib/hooks/useSupabase"; +import { brand, login } from "@/lib/content"; export default function LoginPage() { const [email, setEmail] = useState(""); @@ -20,7 +21,6 @@ export default function LoginPage() { setError(""); try { - // Sign in with Supabase const { data, error: signInError } = await supabase.auth.signInWithPassword({ email, password, @@ -31,131 +31,139 @@ export default function LoginPage() { } if (data.session) { - // Successfully logged in, redirect to dashboard without refresh to prevent flicker router.push("/"); } - } catch (err: any) { + } catch (err: unknown) { console.error("Login error:", err); - setError(err.message || "Failed to sign in. Please check your credentials."); + const errorMessage = err instanceof Error ? err.message : login.errors.default; + setError(errorMessage); } finally { setIsLoading(false); } }; return ( -
+
+ {/* Header */}
-
-
- -
-
-

- Welcome back -

-

- Sign in to continue your research +

+ {brand.name} +

+

+ {brand.tagline}

-
- {error && ( -
- {error} -
- )} + {/* Card */} +
+
+

+ {login.title} +

+

+ {login.subtitle} +

+
+ + + {error && ( +
+ {error} +
+ )} -
-
- -
-
- +
+
+ +
+
+ +
+ setEmail(e.target.value)} + className="block w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors" + placeholder={login.form.emailPlaceholder} + />
- setEmail(e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" - placeholder="you@example.com" - />
-
-
- -
-
- +
+ +
+
+ +
+ setPassword(e.target.value)} + className="block w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors" + placeholder={login.form.passwordPlaceholder} + />
+
+
+ +
+
setPassword(e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" - placeholder="••••••••" + id="remember" + type="checkbox" + className="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-600 rounded" /> +
-
-
-
-
- - + + {login.form.forgotPassword} +
- - Forgot password? - -
- - + {isLoading ? ( + <> + + {login.form.submittingButton} + + ) : ( + login.form.submitButton + )} + + -
- - Don't have an account?{" "} - - - Sign up for free - +
+

+ {login.footer.noAccount}{" "} + + {login.footer.signUpLink} + +

- +
); -} \ No newline at end of file +} diff --git a/app/auth/signup/page.tsx b/app/auth/signup/page.tsx index baa3e30..e538c0c 100644 --- a/app/auth/signup/page.tsx +++ b/app/auth/signup/page.tsx @@ -3,8 +3,9 @@ import { useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { Mail, Lock, User, Loader2, FileText } from "lucide-react"; +import { Mail, Lock, User, Loader2, CheckCircle2 } from "lucide-react"; import { useSupabase } from "@/lib/hooks/useSupabase"; +import { brand, signup } from "@/lib/content"; export default function SignupPage() { const [name, setName] = useState(""); @@ -12,6 +13,7 @@ export default function SignupPage() { const [password, setPassword] = useState(""); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); const router = useRouter(); const supabase = useSupabase(); @@ -21,14 +23,13 @@ export default function SignupPage() { setError(""); try { - // Sign up with Supabase const { data, error: signUpError } = await supabase.auth.signUp({ email, password, options: { data: { full_name: name, - name: name, // Add both for compatibility + name: name, }, }, }); @@ -38,7 +39,6 @@ export default function SignupPage() { } if (data.user) { - // Create or update profile with the name const { error: profileError } = await supabase .from('profiles') .upsert({ @@ -55,156 +55,189 @@ export default function SignupPage() { console.error('Profile creation error:', profileError); } - // Check if email confirmation is required if (data.user.email_confirmed_at) { - // Email is confirmed, redirect to dashboard router.push("/"); } else { - // Email confirmation required - setError("Please check your email to confirm your account before logging in."); - setIsLoading(false); + setSuccess(true); } } - } catch (err: any) { + } catch (err: unknown) { console.error("Signup error:", err); - setError(err.message || "Failed to create account. Please try again."); + const errorMessage = err instanceof Error ? err.message : signup.errors.default; + setError(errorMessage); + } finally { setIsLoading(false); } }; return ( -
+
+ {/* Header */}
-
-
- -
-
-

- Create your account -

-

- Start exploring research papers with AI +

+ {brand.name} +

+

+ {brand.tagline}

-
- {error && ( -
- {error} -
- )} - -
-
- -
-
- + {/* Card */} +
+ {success ? ( + /* Success State */ +
+
+
+
- setName(e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" - placeholder="John Doe" - />
+

+ {signup.success.title} +

+

+ We've sent a confirmation link to{" "} + {email}. + Click the link to activate your account. +

+ + {signup.success.backButton} +
+ ) : ( + <> +
+

+ {signup.title} +

+

+ {signup.subtitle} +

+
-
- -
-
- + + {error && ( +
+ {error} +
+ )} + +
+
+ +
+
+ +
+ setName(e.target.value)} + className="block w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors" + placeholder={signup.form.namePlaceholder} + /> +
+
+ +
+ +
+
+ +
+ setEmail(e.target.value)} + className="block w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors" + placeholder={signup.form.emailPlaceholder} + /> +
+
+ +
+ +
+
+ +
+ setPassword(e.target.value)} + className="block w-full pl-10 pr-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-900 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors" + placeholder={signup.form.passwordPlaceholder} + minLength={6} + /> +
+

+ {signup.form.passwordHint} +

+
- setEmail(e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" - placeholder="you@example.com" - /> -
-
-
- -
-
- +
+ +
- setPassword(e.target.value)} - className="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent" - placeholder="••••••••" - minLength={6} - /> + + + + +
+

+ {signup.footer.hasAccount}{" "} + + {signup.footer.signInLink} + +

-

- Must be at least 6 characters -

-
-
- -
- - -
- - - -
- - Already have an account?{" "} - - - Sign in - -
- + + )} +
); -} \ No newline at end of file +} diff --git a/lib/content/auth.json b/lib/content/auth.json new file mode 100644 index 0000000..c2e164c --- /dev/null +++ b/lib/content/auth.json @@ -0,0 +1,81 @@ +{ + "login": { + "title": "Welcome back", + "subtitle": "Sign in to continue your research", + "form": { + "emailLabel": "Email Address", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Enter your password", + "rememberMe": "Remember me", + "forgotPassword": "Forgot password?", + "submitButton": "Sign In", + "submittingButton": "Signing in..." + }, + "footer": { + "noAccount": "Don't have an account?", + "signUpLink": "Sign up for free" + }, + "features": [ + { + "title": "Chat with Your Papers", + "description": "Ask questions and get instant answers with precise citations from your research documents." + }, + { + "title": "Organize Your Library", + "description": "Keep all your research papers organized in collections for easy access and management." + }, + { + "title": "AI-Powered Insights", + "description": "Leverage advanced AI to understand complex papers and extract key information effortlessly." + } + ], + "errors": { + "default": "Failed to sign in. Please check your credentials." + } + }, + "signup": { + "title": "Create your account", + "subtitle": "Start exploring research papers with AI", + "sidebarTitle": "Start your research journey", + "form": { + "nameLabel": "Full Name", + "namePlaceholder": "John Doe", + "emailLabel": "Email Address", + "emailPlaceholder": "you@example.com", + "passwordLabel": "Password", + "passwordPlaceholder": "Create a password", + "passwordHint": "Must be at least 6 characters", + "termsPrefix": "I agree to the", + "termsLink": "Terms and Conditions", + "submitButton": "Create Account", + "submittingButton": "Creating account..." + }, + "footer": { + "hasAccount": "Already have an account?", + "signInLink": "Sign in" + }, + "benefits": [ + { + "title": "Instant Setup", + "description": "Upload your first paper and start chatting in under a minute. No complex configuration needed." + }, + { + "title": "Secure & Private", + "description": "Your research papers are encrypted and only accessible to you. We take your privacy seriously." + }, + { + "title": "Free to Start", + "description": "Get started with generous free limits. No credit card required to explore the platform." + } + ], + "success": { + "title": "Check your email", + "message": "We've sent a confirmation link to {email}. Click the link to activate your account.", + "backButton": "Back to Sign In" + }, + "errors": { + "default": "Failed to create account. Please try again." + } + } +} diff --git a/lib/content/common.json b/lib/content/common.json new file mode 100644 index 0000000..baf6634 --- /dev/null +++ b/lib/content/common.json @@ -0,0 +1,10 @@ +{ + "brand": { + "name": "ThinkFolio", + "tagline": "Reading that talks back. Think faster. Struggle less.", + "motto": "Think faster. Struggle less." + }, + "footer": { + "poweredBy": "Powered by" + } +} diff --git a/lib/content/index.ts b/lib/content/index.ts new file mode 100644 index 0000000..68d93dd --- /dev/null +++ b/lib/content/index.ts @@ -0,0 +1,88 @@ +import commonContent from './common.json'; +import authContent from './auth.json'; + +// Type definitions +export interface FeatureItem { + title: string; + description: string; +} + +export interface CommonContent { + brand: { + name: string; + tagline: string; + motto: string; + }; + footer: { + poweredBy: string; + }; +} + +export interface LoginContent { + title: string; + subtitle: string; + form: { + emailLabel: string; + emailPlaceholder: string; + passwordLabel: string; + passwordPlaceholder: string; + rememberMe: string; + forgotPassword: string; + submitButton: string; + submittingButton: string; + }; + footer: { + noAccount: string; + signUpLink: string; + }; + features: FeatureItem[]; + errors: { + default: string; + }; +} + +export interface SignupContent { + title: string; + subtitle: string; + sidebarTitle: string; + form: { + nameLabel: string; + namePlaceholder: string; + emailLabel: string; + emailPlaceholder: string; + passwordLabel: string; + passwordPlaceholder: string; + passwordHint: string; + termsPrefix: string; + termsLink: string; + submitButton: string; + submittingButton: string; + }; + footer: { + hasAccount: string; + signInLink: string; + }; + benefits: FeatureItem[]; + success: { + title: string; + message: string; + backButton: string; + }; + errors: { + default: string; + }; +} + +export interface AuthContent { + login: LoginContent; + signup: SignupContent; +} + +// Typed exports +export const common: CommonContent = commonContent; +export const auth: AuthContent = authContent; + +// Convenience exports +export const brand = common.brand; +export const login = auth.login; +export const signup = auth.signup; From a3d46cba28443cfda06abc72b0fb344167d032e6 Mon Sep 17 00:00:00 2001 From: PoshanP Date: Wed, 14 Jan 2026 14:05:28 -0800 Subject: [PATCH 2/2] Add landing page with twinkling stars background --- app/page.tsx | 19 +- frontend/components/LandingPage.tsx | 280 ++++++++++++++++++++++++++++ lib/content/index.ts | 34 ++++ lib/content/landing.json | 75 ++++++++ lib/contexts/AuthContext.tsx | 9 +- 5 files changed, 413 insertions(+), 4 deletions(-) create mode 100644 frontend/components/LandingPage.tsx create mode 100644 lib/content/landing.json diff --git a/app/page.tsx b/app/page.tsx index 112f0c8..59ced1e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -6,17 +6,34 @@ import { RecentPapers } from "@/frontend/components/RecentPapers"; import { NextReadList } from "@/frontend/components/NextReadList"; import { ProfileDialog } from "@/frontend/components/ProfileDialog"; import { CollectionsWidget } from "@/frontend/components/collections"; -import { FileText, MessageSquare, BookOpen, User, FolderOpen } from "lucide-react"; +import { LandingPage } from "@/frontend/components/LandingPage"; +import { FileText, MessageSquare, BookOpen, User, FolderOpen, Loader2 } from "lucide-react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { useStats } from "@/lib/contexts/StatsContext"; import { useCollections } from "@/lib/hooks/useCollections"; +import { useAuth } from "@/lib/contexts/AuthContext"; export default function Home() { const router = useRouter(); const [isProfileOpen, setIsProfileOpen] = useState(false); const { stats, loading } = useStats(); const { data: collections, isLoading: collectionsLoading } = useCollections(); + const { user, loading: authLoading } = useAuth(); + + // Show loading state while checking auth + if (authLoading) { + return ( +
+ +
+ ); + } + + // Show landing page for unauthenticated users + if (!user) { + return ; + } return (
diff --git a/frontend/components/LandingPage.tsx b/frontend/components/LandingPage.tsx new file mode 100644 index 0000000..e87a871 --- /dev/null +++ b/frontend/components/LandingPage.tsx @@ -0,0 +1,280 @@ +"use client"; + +import Link from "next/link"; +import { FileText, MessageSquare, FolderOpen, Search, Quote, Bookmark, Globe, Sparkles, BookOpen, Moon } from "lucide-react"; +import { brand, landing } from "@/lib/content"; + +const featureIcons = [FileText, MessageSquare, Quote, FolderOpen]; +const moreFeatureIcons = [Globe, Sparkles, Bookmark, Moon]; +const stepIcons = [FileText, BookOpen, MessageSquare]; + +export function LandingPage() { + return ( +
+ {/* Twinkling stars animation */} + + + {/* Base background */} +
+ + {/* Stars layer 1 - bright stars */} +
+ + {/* Stars layer 2 - medium stars */} +
+ + {/* Stars layer 3 - dim stars */} +
+ + {/* Header */} +
+
+

+ {brand.name} +

+

+ {brand.tagline} +

+
+ + {/* Auth Actions */} +
+ + {landing.hero.secondaryCta} + + + {landing.hero.primaryCta} + +
+
+ + {/* Hero Section */} +
+

+ {landing.hero.title} +

+

+ {landing.hero.subtitle} +

+
+ + {landing.hero.primaryCta} + + + {landing.hero.secondaryCta} + +
+
+ + {/* Features Section */} +
+

+ {landing.features.title} +

+
+ {landing.features.items.map((feature, index) => { + const Icon = featureIcons[index]; + return ( +
+
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ); + })} +
+
+ + {/* More Features Section */} +
+

+ {landing.moreFeatures.title} +

+
+ {landing.moreFeatures.items.map((feature, index) => { + const Icon = moreFeatureIcons[index]; + return ( +
+
+ +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ); + })} +
+
+ + {/* How It Works Section */} +
+

+ {landing.howItWorks.title} +

+
+ {landing.howItWorks.steps.map((step, index) => { + const Icon = stepIcons[index]; + return ( +
+
+ +
+
+ Step {step.step} +
+

+ {step.title} +

+

+ {step.description} +

+
+ ); + })} +
+
+ + {/* CTA Section */} +
+

+ {landing.cta.title} +

+

+ {landing.cta.subtitle} +

+ + {landing.cta.button} + +
+ + {/* Footer */} + +
+ ); +} diff --git a/lib/content/index.ts b/lib/content/index.ts index 68d93dd..870af32 100644 --- a/lib/content/index.ts +++ b/lib/content/index.ts @@ -1,5 +1,6 @@ import commonContent from './common.json'; import authContent from './auth.json'; +import landingContent from './landing.json'; // Type definitions export interface FeatureItem { @@ -7,6 +8,38 @@ export interface FeatureItem { description: string; } +export interface StepItem { + step: string; + title: string; + description: string; +} + +export interface LandingContent { + hero: { + title: string; + subtitle: string; + primaryCta: string; + secondaryCta: string; + }; + features: { + title: string; + items: FeatureItem[]; + }; + moreFeatures: { + title: string; + items: FeatureItem[]; + }; + howItWorks: { + title: string; + steps: StepItem[]; + }; + cta: { + title: string; + subtitle: string; + button: string; + }; +} + export interface CommonContent { brand: { name: string; @@ -81,6 +114,7 @@ export interface AuthContent { // Typed exports export const common: CommonContent = commonContent; export const auth: AuthContent = authContent; +export const landing: LandingContent = landingContent; // Convenience exports export const brand = common.brand; diff --git a/lib/content/landing.json b/lib/content/landing.json new file mode 100644 index 0000000..8494743 --- /dev/null +++ b/lib/content/landing.json @@ -0,0 +1,75 @@ +{ + "hero": { + "title": "Read and understand research papers", + "subtitle": "A smart PDF reader that lets you read your papers and ask questions about anything you don't understand. Get instant answers with precise citations.", + "primaryCta": "Get Started", + "secondaryCta": "Sign In" + }, + "features": { + "title": "Everything you need to understand research", + "items": [ + { + "title": "Read Your PDFs", + "description": "A clean, distraction-free PDF reader built for research. Read your papers comfortably with full navigation." + }, + { + "title": "Ask Questions", + "description": "Stuck on a concept? Just ask. Get clear explanations for anything you don't understand in the paper." + }, + { + "title": "Precise Citations", + "description": "Every answer comes with exact page references so you can jump right to the source and verify." + }, + { + "title": "Create Collections", + "description": "Organize your research into custom collections. Group papers by topic, project, or however you work best." + } + ] + }, + "moreFeatures": { + "title": "Designed for researchers", + "items": [ + { + "title": "Access Anywhere", + "description": "Your papers are securely stored in the cloud. Read and chat with your library from any device, anywhere." + }, + { + "title": "AI-Powered Insights", + "description": "Powered by advanced AI to understand complex academic language and break down difficult concepts for you." + }, + { + "title": "Save for Later", + "description": "Build your reading queue with papers you want to explore. Never lose track of important research again." + }, + { + "title": "Dark Mode", + "description": "Easy on the eyes during those late-night reading sessions. Full dark mode support throughout the app." + } + ] + }, + "howItWorks": { + "title": "How it works", + "steps": [ + { + "step": "1", + "title": "Upload your PDF", + "description": "Drag and drop or browse to upload your research paper. We support files up to 5MB." + }, + { + "step": "2", + "title": "Read your paper", + "description": "Open your PDF in our clean reader. Navigate pages, zoom, and read comfortably." + }, + { + "step": "3", + "title": "Ask when stuck", + "description": "Don't understand something? Ask the AI and get clear explanations with page citations." + } + ] + }, + "cta": { + "title": "Ready to read smarter?", + "subtitle": "Join researchers who understand papers faster with ThinkFolio. Free to get started.", + "button": "Create Free Account" + } +} diff --git a/lib/contexts/AuthContext.tsx b/lib/contexts/AuthContext.tsx index 0c662ad..97cb9ec 100644 --- a/lib/contexts/AuthContext.tsx +++ b/lib/contexts/AuthContext.tsx @@ -70,8 +70,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { console.error('Session error:', error); setUser(null); - // Only redirect to login if we're not already on auth pages - if (!window.location.pathname.startsWith('/auth/')) { + // Allow root path (landing page) and auth pages without redirect + const isPublicPath = window.location.pathname === '/' || window.location.pathname.startsWith('/auth/'); + if (!isPublicPath) { router.push('/auth/login'); } return; @@ -79,7 +80,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { if (!session) { setUser(null); - if (!window.location.pathname.startsWith('/auth/')) { + // Allow root path (landing page) and auth pages without redirect + const isPublicPath = window.location.pathname === '/' || window.location.pathname.startsWith('/auth/'); + if (!isPublicPath) { router.push('/auth/login'); } } else {