Initial commit
Next.js + Express event management app for Hope Family Church.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { apiFetch, ApiError } from "@/lib/api";
|
||||
import { AuthResponse, AuthUser, clearToken, getToken, saveToken } from "@/lib/auth";
|
||||
|
||||
export type AuthContextValue = {
|
||||
user: AuthUser | null;
|
||||
token: string | null;
|
||||
loading: boolean;
|
||||
login: (email: string, password: string) => Promise<AuthUser>;
|
||||
register: (data: { name: string; email: string; password: string; phoneNumber?: string; notificationPreference?: "email" | "whatsapp" | "both" }) => Promise<void>;
|
||||
logout: () => void;
|
||||
updateToken: (token: string) => void;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
try {
|
||||
const t = getToken();
|
||||
if (t) {
|
||||
// Validate the stored token by fetching the user profile.
|
||||
// Only update React state after a successful response so we never
|
||||
// hold a half-initialised (token set, user null) state.
|
||||
const me = await apiFetch<AuthUser>("/api/users/profile", { authToken: t });
|
||||
setToken(t);
|
||||
setUser(me);
|
||||
}
|
||||
} catch (e) {
|
||||
// Only wipe the stored token when the server explicitly rejects it (401).
|
||||
// Transient failures (network down, 5xx) should NOT log the user out — the
|
||||
// token stays in localStorage so the next page load can retry.
|
||||
if (e instanceof ApiError && e.status === 401) {
|
||||
clearToken();
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
const login = useCallback(async (email: string, password: string) => {
|
||||
const res = await apiFetch<AuthResponse>("/api/users/login", {
|
||||
method: "POST",
|
||||
body: { email, password },
|
||||
});
|
||||
const { token, ...loggedInUser } = res;
|
||||
saveToken(token);
|
||||
setToken(token);
|
||||
setUser(loggedInUser);
|
||||
return loggedInUser;
|
||||
}, []);
|
||||
|
||||
const register = useCallback(async (data: { name: string; email: string; password: string; phoneNumber?: string; notificationPreference?: "email" | "whatsapp" | "both" }) => {
|
||||
const res = await apiFetch<AuthResponse>("/api/users", {
|
||||
method: "POST",
|
||||
body: data,
|
||||
});
|
||||
const { token, ...registeredUser } = res;
|
||||
saveToken(token);
|
||||
setToken(token);
|
||||
setUser(registeredUser);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
clearToken();
|
||||
setToken(null);
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
// Some endpoints (profile update, password change, session revocation) issue a
|
||||
// fresh token mid-session. Persist it AND update in-memory state so the app
|
||||
// keeps using a valid token without requiring a page refresh.
|
||||
const updateToken = useCallback((t: string) => {
|
||||
saveToken(t);
|
||||
setToken(t);
|
||||
}, []);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ user, token, loading, login, register, logout, updateToken }),
|
||||
[loading, login, logout, register, token, updateToken, user]
|
||||
);
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuthContext() {
|
||||
const ctx = useContext(AuthContext);
|
||||
if (!ctx) throw new Error("useAuthContext must be used within AuthProvider");
|
||||
return ctx;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import React, { createContext, useContext, useEffect, useState } from "react";
|
||||
import { apiFetch } from "@/lib/api";
|
||||
|
||||
export type SiteSettings = {
|
||||
org_name?: string;
|
||||
org_tagline?: string;
|
||||
org_email?: string;
|
||||
org_phone?: string;
|
||||
org_address?: string;
|
||||
accent_color?: string;
|
||||
logo_url?: string;
|
||||
setup_complete?: string;
|
||||
// Legal pages
|
||||
legal_operator_name?: string;
|
||||
legal_io_name?: string;
|
||||
legal_io_email?: string;
|
||||
legal_website_url?: string;
|
||||
legal_effective_date?: string;
|
||||
};
|
||||
|
||||
type SiteSettingsContextValue = {
|
||||
settings: SiteSettings;
|
||||
loading: boolean;
|
||||
reload: () => void;
|
||||
};
|
||||
|
||||
const SiteSettingsContext = createContext<SiteSettingsContextValue>({
|
||||
settings: {},
|
||||
loading: true,
|
||||
reload: () => {},
|
||||
});
|
||||
|
||||
export function SiteSettingsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [settings, setSettings] = useState<SiteSettings>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const load = async () => {
|
||||
try {
|
||||
const data = await apiFetch<SiteSettings>("/api/settings");
|
||||
setSettings(data || {});
|
||||
} catch {
|
||||
// Non-fatal — use defaults
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => { load(); }, []);
|
||||
|
||||
return (
|
||||
<SiteSettingsContext.Provider value={{ settings, loading, reload: load }}>
|
||||
{children}
|
||||
</SiteSettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useSiteSettings() {
|
||||
return useContext(SiteSettingsContext);
|
||||
}
|
||||
Reference in New Issue
Block a user