147 lines
4.3 KiB
TypeScript
147 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext, useEffect, useState, useCallback } from "react";
|
|
import { api } from "@/lib/api";
|
|
import { syncPlanOnLogin } from "@/lib/planSync";
|
|
import type { UserProfile } from "@/lib/types";
|
|
|
|
interface AuthCtx {
|
|
user: UserProfile | null;
|
|
token: string | null;
|
|
favorites: string[];
|
|
planSyncStatus: "idle" | "syncing" | "synced" | "offline";
|
|
login: (email: string, password: string) => Promise<boolean>;
|
|
register: (email: string, password: string, name: string) => Promise<boolean>;
|
|
demoLogin: () => Promise<boolean>;
|
|
googleLogin: (idToken: string) => Promise<boolean>;
|
|
logout: () => void;
|
|
refreshUser: () => Promise<void>;
|
|
toggleFavorite: (slug: string) => Promise<void>;
|
|
isFavorite: (slug: string) => boolean;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthCtx | null>(null);
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<UserProfile | null>(null);
|
|
const [token, setToken] = useState<string | null>(null);
|
|
const [favorites, setFavorites] = useState<string[]>([]);
|
|
const [planSyncStatus, setPlanSyncStatus] = useState<"idle" | "syncing" | "synced" | "offline">("idle");
|
|
|
|
const loadFavorites = useCallback(async (t: string) => {
|
|
try {
|
|
const data = await api.getFavorites(t);
|
|
setFavorites(data.slugs);
|
|
} catch { /* ignore */ }
|
|
}, []);
|
|
|
|
const reconcilePlan = useCallback(async (t: string) => {
|
|
setPlanSyncStatus("syncing");
|
|
try {
|
|
await syncPlanOnLogin(t);
|
|
setPlanSyncStatus("synced");
|
|
} catch {
|
|
setPlanSyncStatus("offline");
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const saved = localStorage.getItem("nomad-token");
|
|
const savedUser = localStorage.getItem("nomad-user");
|
|
if (saved && savedUser) {
|
|
setToken(saved);
|
|
setUser(JSON.parse(savedUser));
|
|
loadFavorites(saved);
|
|
void reconcilePlan(saved);
|
|
}
|
|
}, [loadFavorites, reconcilePlan]);
|
|
|
|
const persist = async (t: string, u: UserProfile) => {
|
|
setToken(t);
|
|
setUser(u);
|
|
localStorage.setItem("nomad-token", t);
|
|
localStorage.setItem("nomad-user", JSON.stringify(u));
|
|
await loadFavorites(t);
|
|
await reconcilePlan(t);
|
|
};
|
|
|
|
const login = async (email: string, password: string) => {
|
|
try {
|
|
const data = await api.login(email, password);
|
|
await persist(data.token, data.user);
|
|
return true;
|
|
} catch { return false; }
|
|
};
|
|
|
|
const register = async (email: string, password: string, name: string) => {
|
|
try {
|
|
const data = await api.register(email, password, name);
|
|
await persist(data.token, data.user);
|
|
return true;
|
|
} catch { return false; }
|
|
};
|
|
|
|
const demoLogin = async () => {
|
|
try {
|
|
const data = await api.demoLogin();
|
|
await persist(data.token, data.user);
|
|
return true;
|
|
} catch { return false; }
|
|
};
|
|
|
|
const googleLogin = async (idToken: string) => {
|
|
try {
|
|
const data = await api.googleLogin(idToken);
|
|
await persist(data.token, data.user);
|
|
return true;
|
|
} catch { return false; }
|
|
};
|
|
|
|
const logout = () => {
|
|
setToken(null);
|
|
setUser(null);
|
|
setFavorites([]);
|
|
setPlanSyncStatus("idle");
|
|
localStorage.removeItem("nomad-token");
|
|
localStorage.removeItem("nomad-user");
|
|
};
|
|
|
|
const refreshUser = async () => {
|
|
const t = token || localStorage.getItem("nomad-token");
|
|
if (!t) return;
|
|
try {
|
|
const me = await api.getMe(t);
|
|
setUser(me as UserProfile);
|
|
localStorage.setItem("nomad-user", JSON.stringify(me));
|
|
} catch {
|
|
try {
|
|
const saved = localStorage.getItem("nomad-user");
|
|
if (saved) setUser(JSON.parse(saved));
|
|
} catch { /* ignore */ }
|
|
}
|
|
};
|
|
|
|
const toggleFavorite = async (slug: string) => {
|
|
if (!token) return;
|
|
try {
|
|
const data = await api.toggleFavorite(slug, token);
|
|
setFavorites(data.slugs);
|
|
} catch { /* ignore */ }
|
|
};
|
|
|
|
return (
|
|
<AuthContext.Provider value={{
|
|
user, token, favorites, planSyncStatus, login, register, demoLogin, googleLogin, logout, refreshUser,
|
|
toggleFavorite, isFavorite: (slug) => favorites.includes(slug),
|
|
}}>
|
|
{children}
|
|
</AuthContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useAuth() {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) throw new Error("useAuth must be used within AuthProvider");
|
|
return ctx;
|
|
}
|