diff --git a/src/components/MaintenanceGate.tsx b/src/components/MaintenanceGate.tsx new file mode 100644 index 0000000..6306a60 --- /dev/null +++ b/src/components/MaintenanceGate.tsx @@ -0,0 +1,59 @@ +import type { ReactNode } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useRouterState } from "@tanstack/react-router"; +import { Wrench } from "lucide-react"; +import { supabase } from "@/integrations/supabase/client"; + +const MAINTENANCE_SETTING_KEY = "site.maintenance_mode"; + +function isAdminOrAuthPath(pathname: string) { + return /^\/(?:auth|mfa-verify|dashboard|admin)(?:\/|$)/.test(pathname); +} + +export function MaintenanceGate({ children }: { children: ReactNode }) { + const pathname = useRouterState({ select: (state) => state.location.pathname }); + const bypass = isAdminOrAuthPath(pathname); + const maintenance = useQuery({ + queryKey: ["site-maintenance-mode"], + enabled: !bypass, + queryFn: async () => { + const { data, error } = await supabase + .from("site_settings") + .select("value") + .eq("key", MAINTENANCE_SETTING_KEY) + .maybeSingle(); + if (error) throw error; + return data?.value === "true"; + }, + refetchInterval: 15_000, + retry: 1, + }); + + if (bypass || maintenance.isError) return children; + if (maintenance.isLoading) { + return ( +
+ Checking CodeForge status… +
+ ); + } + if (!maintenance.data) return children; + + return ( +
+
+
+
+

+ CodeForge +

+

We’ll be back soon

+

+ CodeForge is temporarily unavailable while maintenance is in progress. Please check back + shortly. +

+
+
+ ); +} diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 791ff81..54cb428 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -13,6 +13,7 @@ import appCss from "../styles.css?url"; import { AuthProvider } from "@/hooks/use-auth"; import { Toaster } from "@/components/ui/sonner"; import { EditModeProvider } from "@/contexts/EditModeContext"; +import { MaintenanceGate } from "@/components/MaintenanceGate"; function NotFoundComponent() { return ( @@ -136,11 +137,13 @@ function RootComponent() { - + + + ); -} \ No newline at end of file +} diff --git a/src/routes/_authenticated/admin.tsx b/src/routes/_authenticated/admin.tsx index 40aac54..b04bb62 100644 --- a/src/routes/_authenticated/admin.tsx +++ b/src/routes/_authenticated/admin.tsx @@ -285,7 +285,17 @@ function WebsiteSettings({ settings, refetch, onActivity }: { settings: Array<{ const [key, setKey] = useState(""); const [value, setValue] = useState(""); const [editingKey, setEditingKey] = useState(null); const save = async () => { if (!key.trim()) return toast.error("Setting key is required"); const result = editingKey ? await supabase.from("site_settings").update({ value }).eq("key", editingKey) : await supabase.from("site_settings").upsert({ key: key.trim(), value }); if (result.error) return toast.error(result.error.message); await onActivity(editingKey ? "update" : "create", "site_setting", key); toast.success("Setting saved"); setKey(""); setValue(""); setEditingKey(null); await refetch(); }; const remove = async (settingKey: string) => { if (!window.confirm(`Delete setting '${settingKey}'?`)) return; const { error } = await supabase.from("site_settings").delete().eq("key", settingKey); if (error) return toast.error(error.message); await onActivity("delete", "site_setting", settingKey); toast.success("Setting deleted"); await refetch(); }; - return
setKey(e.target.value)} placeholder="homepage.hero_title" className="rounded-xl" /> setValue(e.target.value)} placeholder="Setting value" className="rounded-xl" />
{editingKey && }
{settings.map((s) =>
{s.key}
{formatSettingPreview(s.value)}
)}{settings.length === 0 && }
; + const maintenanceEnabled = settings.find((setting) => setting.key === "site.maintenance_mode")?.value === "true"; + const toggleMaintenance = async () => { + const nextEnabled = !maintenanceEnabled; + if (nextEnabled && !window.confirm("Turn on Maintenance Mode? Visitors will see the maintenance notice until you turn it off.")) return; + const { error } = await supabase.from("site_settings").upsert({ key: "site.maintenance_mode", value: String(nextEnabled) }); + if (error) return toast.error(error.message); + await onActivity("update", "site_setting", "site.maintenance_mode"); + toast.success(nextEnabled ? "Maintenance Mode is on" : "Maintenance Mode is off"); + await refetch(); + }; + return
Status: {maintenanceEnabled ? "On" : "Off"}

When on, public pages show a maintenance notice. Admin, sign-in, and MFA pages remain available so you can turn it off.

setKey(e.target.value)} placeholder="homepage.hero_title" className="rounded-xl" /> setValue(e.target.value)} placeholder="Setting value" className="rounded-xl" />
{editingKey && }
{settings.map((s) =>
{s.key}
{formatSettingPreview(s.value)}
)}{settings.length === 0 && }
; } function MediaLibrary({ files, refetch, onActivity }: { files: MediaFile[]; refetch: () => Promise; onActivity: (a: string, r: string, id?: string) => Promise; }) {