Skip to content

Commit 014c037

Browse files
Sonra0claude
andcommitted
feat: update login with plan selection and subscription state
Login reads ?plan= query param, stores in context/localStorage. Sidebar reads plan from context. Login page uses CSS variable theming. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 43ee861 commit 014c037

3 files changed

Lines changed: 110 additions & 42 deletions

File tree

src/app/login/page.tsx

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
11
"use client";
22

3-
import { useState } from "react";
4-
import { useRouter } from "next/navigation";
5-
import { signInWithPopup, GoogleAuthProvider, signInWithEmailAndPassword, createUserWithEmailAndPassword } from "firebase/auth";
3+
import { useState, useEffect } from "react";
4+
import { useRouter, useSearchParams } from "next/navigation";
5+
import {
6+
signInWithPopup,
7+
GoogleAuthProvider,
8+
signInWithEmailAndPassword,
9+
createUserWithEmailAndPassword,
10+
} from "firebase/auth";
611
import { auth } from "@/lib/firebase";
712
import { useAuth } from "@/contexts/AuthContext";
8-
import { useEffect } from "react";
13+
import { motion } from "framer-motion";
14+
import { Button } from "@/components/ui/Button";
915

1016
export default function LoginPage() {
11-
const { user, loading } = useAuth();
17+
const { user, loading, setPlan } = useAuth();
1218
const router = useRouter();
19+
const searchParams = useSearchParams();
1320
const [email, setEmail] = useState("");
1421
const [password, setPassword] = useState("");
1522
const [isSignUp, setIsSignUp] = useState(false);
1623
const [error, setError] = useState("");
1724
const [submitting, setSubmitting] = useState(false);
1825

26+
// Read plan from URL param
27+
useEffect(() => {
28+
const planParam = searchParams.get("plan");
29+
if (planParam === "premium" || planParam === "free") {
30+
setPlan(planParam);
31+
}
32+
}, [searchParams, setPlan]);
33+
1934
useEffect(() => {
2035
if (!loading && user) {
2136
router.replace("/dashboard");
@@ -27,8 +42,8 @@ export default function LoginPage() {
2742
setError("");
2843
await signInWithPopup(auth, new GoogleAuthProvider());
2944
router.replace("/dashboard");
30-
} catch (err: any) {
31-
setError(err.message || "Google sign-in failed");
45+
} catch (err: unknown) {
46+
setError(err instanceof Error ? err.message : "Google sign-in failed");
3247
}
3348
};
3449

@@ -43,102 +58,138 @@ export default function LoginPage() {
4358
await signInWithEmailAndPassword(auth, email, password);
4459
}
4560
router.replace("/dashboard");
46-
} catch (err: any) {
47-
setError(err.message || "Authentication failed");
61+
} catch (err: unknown) {
62+
setError(err instanceof Error ? err.message : "Authentication failed");
4863
} finally {
4964
setSubmitting(false);
5065
}
5166
};
5267

5368
if (loading) {
5469
return (
55-
<div className="flex min-h-screen items-center justify-center bg-gray-50">
56-
<div className="h-8 w-8 animate-spin rounded-full border-4 border-indigo-600 border-t-transparent" />
70+
<div className="flex min-h-screen items-center justify-center bg-[var(--bg-secondary)]">
71+
<div className="h-8 w-8 animate-spin rounded-full border-4 border-[var(--accent)] border-t-transparent" />
5772
</div>
5873
);
5974
}
6075

6176
return (
62-
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-indigo-50 via-white to-purple-50 px-4">
63-
<div className="w-full max-w-md space-y-8">
77+
<div className="flex min-h-screen items-center justify-center bg-[var(--bg-secondary)] px-4">
78+
<motion.div
79+
initial={{ opacity: 0, y: 20 }}
80+
animate={{ opacity: 1, y: 0 }}
81+
transition={{ duration: 0.4, ease: "easeOut" }}
82+
className="w-full max-w-md space-y-8"
83+
>
6484
<div className="text-center">
65-
<h1 className="text-3xl font-bold text-indigo-600">Seniora</h1>
66-
<p className="mt-2 text-sm text-gray-600">
67-
{isSignUp ? "Create your account" : "Sign in to your account"}
85+
<div className="inline-flex items-center justify-center w-12 h-12 rounded-2xl bg-[var(--accent)] text-white font-bold text-xl mb-4">
86+
S
87+
</div>
88+
<h1 className="text-2xl font-bold text-[var(--text-primary)]">
89+
{isSignUp ? "Create your account" : "Welcome back"}
90+
</h1>
91+
<p className="mt-1 text-sm text-[var(--text-secondary)]">
92+
{isSignUp
93+
? "Get started with Seniora"
94+
: "Sign in to continue to Seniora"}
6895
</p>
6996
</div>
7097

71-
<div className="rounded-xl border border-gray-200 bg-white p-8 shadow-sm space-y-6">
98+
<div className="rounded-2xl border border-[var(--card-border)] bg-[var(--card-bg)] p-8 shadow-[var(--card-shadow)] space-y-6">
7299
{error && (
73-
<p className="rounded-lg bg-red-50 p-3 text-sm text-red-600">{error}</p>
100+
<p className="rounded-xl bg-[var(--danger-light)] p-3 text-sm text-red-700">
101+
{error}
102+
</p>
74103
)}
75104

76105
<button
77106
onClick={handleGoogle}
78-
className="flex w-full items-center justify-center gap-3 rounded-lg border border-gray-300 px-4 py-2.5 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-colors"
107+
className="flex w-full items-center justify-center gap-3 rounded-xl border border-[var(--border-default)] px-4 py-2.5 text-sm font-medium text-[var(--text-primary)] hover:bg-[var(--bg-tertiary)] transition-colors"
79108
>
80109
<svg className="h-5 w-5" viewBox="0 0 24 24">
81-
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z" fill="#4285F4" />
82-
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853" />
83-
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05" />
84-
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335" />
110+
<path
111+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"
112+
fill="#4285F4"
113+
/>
114+
<path
115+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
116+
fill="#34A853"
117+
/>
118+
<path
119+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
120+
fill="#FBBC05"
121+
/>
122+
<path
123+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
124+
fill="#EA4335"
125+
/>
85126
</svg>
86127
Continue with Google
87128
</button>
88129

89130
<div className="relative">
90131
<div className="absolute inset-0 flex items-center">
91-
<div className="w-full border-t border-gray-200" />
132+
<div className="w-full border-t border-[var(--border-default)]" />
92133
</div>
93134
<div className="relative flex justify-center text-sm">
94-
<span className="bg-white px-4 text-gray-500">or</span>
135+
<span className="bg-[var(--card-bg)] px-4 text-[var(--text-muted)]">
136+
or
137+
</span>
95138
</div>
96139
</div>
97140

98141
<form onSubmit={handleEmailAuth} className="space-y-4">
99142
<div>
100-
<label className="block text-sm font-medium text-gray-700 mb-1">Email</label>
143+
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
144+
Email
145+
</label>
101146
<input
102147
type="email"
103148
value={email}
104149
onChange={(e) => setEmail(e.target.value)}
105150
required
106-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
151+
className="w-full rounded-xl border border-[var(--border-default)] bg-[var(--bg-primary)] px-3 py-2.5 text-sm text-[var(--text-primary)] placeholder-[var(--text-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-1 focus:ring-[var(--accent)]"
107152
placeholder="you@example.com"
108153
/>
109154
</div>
110155
<div>
111-
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
156+
<label className="block text-sm font-medium text-[var(--text-secondary)] mb-1">
157+
Password
158+
</label>
112159
<input
113160
type="password"
114161
value={password}
115162
onChange={(e) => setPassword(e.target.value)}
116163
required
117164
minLength={6}
118-
className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
165+
className="w-full rounded-xl border border-[var(--border-default)] bg-[var(--bg-primary)] px-3 py-2.5 text-sm text-[var(--text-primary)] placeholder-[var(--text-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-1 focus:ring-[var(--accent)]"
119166
placeholder="••••••••"
120167
/>
121168
</div>
122-
<button
169+
<Button
123170
type="submit"
124-
disabled={submitting}
125-
className="w-full rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-indigo-700 transition-colors disabled:opacity-50"
171+
loading={submitting}
172+
className="w-full"
173+
size="lg"
126174
>
127-
{submitting ? "Please wait..." : isSignUp ? "Create Account" : "Sign In"}
128-
</button>
175+
{isSignUp ? "Create Account" : "Sign In"}
176+
</Button>
129177
</form>
130178

131-
<p className="text-center text-sm text-gray-500">
179+
<p className="text-center text-sm text-[var(--text-secondary)]">
132180
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
133181
<button
134-
onClick={() => { setIsSignUp(!isSignUp); setError(""); }}
135-
className="font-medium text-indigo-600 hover:text-indigo-500"
182+
onClick={() => {
183+
setIsSignUp(!isSignUp);
184+
setError("");
185+
}}
186+
className="font-medium text-[var(--accent)] hover:underline"
136187
>
137188
{isSignUp ? "Sign In" : "Sign Up"}
138189
</button>
139190
</p>
140191
</div>
141-
</div>
192+
</motion.div>
142193
</div>
143194
);
144195
}

src/components/layout/Sidebar.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import { auth } from "@/lib/firebase";
88
import { useAuth } from "@/contexts/AuthContext";
99
import { Badge } from "@/components/ui/Badge";
1010

11-
// TODO: fetch real subscription plan from API
12-
const plan = "premium";
1311

1412
const navItems = [
1513
{
@@ -36,7 +34,7 @@ const navItems = [
3634
export function Sidebar() {
3735
const [collapsed, setCollapsed] = useState(false);
3836
const pathname = usePathname();
39-
const { user } = useAuth();
37+
const { user, plan } = useAuth();
4038

4139
const displayName = user?.displayName || user?.email?.split("@")[0] || "User";
4240
const initial = displayName.charAt(0).toUpperCase();

src/contexts/AuthContext.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@ import { createContext, useContext, useEffect, useState, ReactNode } from "react
44
import { User, onAuthStateChanged } from "firebase/auth";
55
import { auth } from "@/lib/firebase";
66

7+
type Plan = "free" | "premium";
8+
79
interface AuthContextType {
810
user: User | null;
911
loading: boolean;
12+
plan: Plan;
13+
setPlan: (plan: Plan) => void;
1014
getIdToken: () => Promise<string | null>;
1115
}
1216

1317
const AuthContext = createContext<AuthContextType>({
1418
user: null,
1519
loading: true,
20+
plan: "free",
21+
setPlan: () => {},
1622
getIdToken: async () => null,
1723
});
1824

1925
export function AuthProvider({ children }: { children: ReactNode }) {
2026
const [user, setUser] = useState<User | null>(null);
2127
const [loading, setLoading] = useState(true);
28+
const [plan, setPlanState] = useState<Plan>("free");
29+
30+
useEffect(() => {
31+
const stored = localStorage.getItem("seniora_plan");
32+
if (stored === "premium" || stored === "free") {
33+
setPlanState(stored);
34+
}
35+
}, []);
2236

2337
useEffect(() => {
2438
const unsubscribe = onAuthStateChanged(auth, (user) => {
@@ -28,13 +42,18 @@ export function AuthProvider({ children }: { children: ReactNode }) {
2842
return unsubscribe;
2943
}, []);
3044

45+
const setPlan = (p: Plan) => {
46+
setPlanState(p);
47+
localStorage.setItem("seniora_plan", p);
48+
};
49+
3150
const getIdToken = async () => {
3251
if (!user) return null;
3352
return user.getIdToken();
3453
};
3554

3655
return (
37-
<AuthContext.Provider value={{ user, loading, getIdToken }}>
56+
<AuthContext.Provider value={{ user, loading, plan, setPlan, getIdToken }}>
3857
{children}
3958
</AuthContext.Provider>
4059
);

0 commit comments

Comments
 (0)