Snapshot: full project state

This commit is contained in:
2026-10-06 23:43:43 -07:00
commit fc6d00988c
13 changed files with 1283 additions and 0 deletions

109
pettalk-AuthContext.tsx Normal file
View File

@@ -0,0 +1,109 @@
import React, { createContext, useContext, useState, useEffect, useCallback } from "react";
interface User {
id: number;
username: string;
email: string;
created_at: string;
}
interface AuthContextType {
user: User | null;
token: string | null;
isLoading: boolean;
login: (email: string, password: string) => Promise<void>;
register: (username: string, email: string, password: string) => Promise<void>;
logout: () => void;
}
const AuthContext = createContext<AuthContextType | null>(null);
const API_BASE = "";
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(() => localStorage.getItem("pettalk_token"));
const [isLoading, setIsLoading] = useState(true);
const fetchMe = useCallback(async (t: string) => {
try {
const res = await fetch("/api/me", {
headers: { Authorization: `Bearer ${t}` },
});
if (res.ok) {
const data = await res.json();
setUser(data.user);
} else {
localStorage.removeItem("pettalk_token");
setToken(null);
}
} catch {
// Network error — keep token but mark as loaded
}
}, []);
useEffect(() => {
if (token) {
fetchMe(token).finally(() => setIsLoading(false));
} else {
setIsLoading(false);
}
}, [token, fetchMe]);
const login = async (email: string, password: string) => {
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Login failed");
}
localStorage.setItem("pettalk_token", data.token);
setToken(data.token);
setUser(data.user);
};
const register = async (username: string, email: string, password: string) => {
const res = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, email, password }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Registration failed");
}
localStorage.setItem("pettalk_token", data.token);
setToken(data.token);
setUser(data.user);
};
const logout = () => {
localStorage.removeItem("pettalk_token");
setToken(null);
setUser(null);
};
return (
<AuthContext.Provider value={{ user, token, isLoading, login, register, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth(): AuthContextType {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error("useAuth must be used within an AuthProvider");
}
return ctx;
}
/** Helper to get auth headers for fetch calls */
export function getAuthHeaders(): Record<string, string> {
const token = localStorage.getItem("pettalk_token");
if (!token) return {};
return { Authorization: `Bearer ${token}` };
}