Initial commit

Next.js + Express event management app for Hope Family Church.
This commit is contained in:
2026-07-23 15:26:47 +02:00
commit 3d381944d2
246 changed files with 57565 additions and 0 deletions
+101
View File
@@ -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);
}