AppForge: migrate off Supabase to external Postgres + Better Auth + local storage
- New Express + TypeScript backend (server/) with pg, Better Auth, local file storage - De-Supabased Postgres schema (server/db) and TS reimplementations of DB functions - Frontend data layer rewired to REST (rest-client + backend-client compat shim) - Removed all Supabase references (code, config, deps, docs) - New brand assets: gradient favicon/app icons + dark/white wordmark logos Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,215 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Cookie, X, Settings, Shield } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
interface CookiePreferences {
|
||||
necessary: boolean;
|
||||
analytics: boolean;
|
||||
marketing: boolean;
|
||||
}
|
||||
|
||||
const COOKIE_CONSENT_KEY = "cookie-consent";
|
||||
const COOKIE_PREFERENCES_KEY = "cookie-preferences";
|
||||
|
||||
const CookieConsent = () => {
|
||||
const [showBanner, setShowBanner] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [preferences, setPreferences] = useState<CookiePreferences>({
|
||||
necessary: true, // Always required
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const consent = localStorage.getItem(COOKIE_CONSENT_KEY);
|
||||
if (!consent) {
|
||||
// Small delay to avoid flash on page load
|
||||
const timer = setTimeout(() => setShowBanner(true), 1000);
|
||||
return () => clearTimeout(timer);
|
||||
} else {
|
||||
const savedPreferences = localStorage.getItem(COOKIE_PREFERENCES_KEY);
|
||||
if (savedPreferences) {
|
||||
setPreferences(JSON.parse(savedPreferences));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleAcceptAll = () => {
|
||||
const allAccepted: CookiePreferences = {
|
||||
necessary: true,
|
||||
analytics: true,
|
||||
marketing: true,
|
||||
};
|
||||
saveConsent(allAccepted);
|
||||
};
|
||||
|
||||
const handleRejectAll = () => {
|
||||
const onlyNecessary: CookiePreferences = {
|
||||
necessary: true,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
};
|
||||
saveConsent(onlyNecessary);
|
||||
};
|
||||
|
||||
const handleSavePreferences = () => {
|
||||
saveConsent(preferences);
|
||||
setShowSettings(false);
|
||||
};
|
||||
|
||||
const saveConsent = (prefs: CookiePreferences) => {
|
||||
localStorage.setItem(COOKIE_CONSENT_KEY, "true");
|
||||
localStorage.setItem(COOKIE_PREFERENCES_KEY, JSON.stringify(prefs));
|
||||
setPreferences(prefs);
|
||||
setShowBanner(false);
|
||||
|
||||
// Dispatch custom event for analytics scripts to listen to
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("cookie-consent-updated", { detail: prefs })
|
||||
);
|
||||
};
|
||||
|
||||
if (!showBanner) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Cookie Banner */}
|
||||
<div className="fixed bottom-0 left-0 right-0 z-50 p-4 animate-fade-in">
|
||||
<Card className="max-w-4xl mx-auto glass-card border-border/50 shadow-2xl">
|
||||
<CardContent className="p-6">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<div className="p-2 rounded-lg bg-primary/10">
|
||||
<Cookie className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<h3 className="font-semibold text-foreground">
|
||||
We value your privacy
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
We use cookies to enhance your browsing experience, serve
|
||||
personalized content, and analyze our traffic. By clicking
|
||||
"Accept All", you consent to our use of cookies.{" "}
|
||||
<Link
|
||||
to="/privacy"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
Read our Privacy Policy
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 w-full md:w-auto">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowSettings(true)}
|
||||
className="gap-2"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
Customize
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleRejectAll}>
|
||||
Reject All
|
||||
</Button>
|
||||
<Button size="sm" onClick={handleAcceptAll}>
|
||||
Accept All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Cookie Settings Dialog */}
|
||||
<Dialog open={showSettings} onOpenChange={setShowSettings}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-primary" />
|
||||
Cookie Preferences
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Manage your cookie preferences. You can enable or disable
|
||||
different types of cookies below.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* Necessary Cookies */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-base font-medium">
|
||||
Necessary Cookies
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Essential for the website to function properly. Cannot be
|
||||
disabled.
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={true} disabled />
|
||||
</div>
|
||||
|
||||
{/* Analytics Cookies */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-base font-medium">
|
||||
Analytics Cookies
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Help us understand how visitors interact with our website to
|
||||
improve user experience.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.analytics}
|
||||
onCheckedChange={(checked) =>
|
||||
setPreferences((prev) => ({ ...prev, analytics: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Marketing Cookies */}
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-base font-medium">
|
||||
Marketing Cookies
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Used to track visitors across websites to display relevant
|
||||
advertisements.
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={preferences.marketing}
|
||||
onCheckedChange={(checked) =>
|
||||
setPreferences((prev) => ({ ...prev, marketing: checked }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={handleRejectAll}>
|
||||
Reject All
|
||||
</Button>
|
||||
<Button onClick={handleSavePreferences}>Save Preferences</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CookieConsent;
|
||||
Reference in New Issue
Block a user