SkateSpots/src/components/BetaDisclaimerDialog.tsx
shakespeare.diy 6e226d5561 Add first-launch beta disclaimer dialog + footer BETA badge
- BetaDisclaimerDialog: shown once on first load, versioned storage key
  (bump DISCLAIMER_VERSION to re-show after terms change), soft single
  "Got it" button, matches dark + #ff0040 style, cannot be dismissed by
  outside click/Esc
- Permanent BETA badge in desktop footer
- Wired into App.tsx

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-01 12:14:42 +07:00

152 lines
5.2 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { useLocalStorage } from '@/hooks/useLocalStorage';
import { AlertTriangle, Code2, FlaskConical, ShieldOff, Wallet, ArrowRight } from 'lucide-react';
/**
* Bump this version to re-show the disclaimer to everyone
* (e.g. after terms change or a major release).
*/
const DISCLAIMER_VERSION = 1;
const STORAGE_KEY = `skatespots_disclaimer_accepted_v${DISCLAIMER_VERSION}`;
const REPO_URL =
'https://github.com/search?q=SkateSpots';
export function BetaDisclaimerDialog() {
const [accepted, setAccepted] = useLocalStorage<boolean>(STORAGE_KEY, false);
const [open, setOpen] = useState(false);
// Open on first launch (after mount so SSR/hydration is clean)
useEffect(() => {
if (!accepted) {
const t = setTimeout(() => setOpen(true), 400);
return () => clearTimeout(t);
}
}, [accepted]);
const handleAccept = () => {
setAccepted(true);
setOpen(false);
};
return (
<Dialog
open={open}
// Prevent closing by clicking outside / Esc — must acknowledge
onOpenChange={(next) => {
if (!next) return;
setOpen(next);
}}
>
<DialogContent
className="bg-black border-2 border-[#ff0040]/50 mx-4 max-w-[calc(100vw-2rem)] sm:max-w-md p-0 gap-0 overflow-hidden [&>button]:hidden"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
{/* Accent gradient strip */}
<div
className="h-1 w-full"
style={{
background:
'linear-gradient(90deg, #ff0040, #ff6600, #ccff00, #ff6600, #ff0040)',
}}
/>
<div className="px-6 pt-6 pb-5">
<DialogHeader className="space-y-3">
{/* Icon badge */}
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-2xl border border-[#ff0040]/40 bg-[#ff0040]/10 shadow-[0_0_25px_rgba(255,0,64,0.25)]">
<AlertTriangle className="h-7 w-7 text-[#ff0040]" />
</div>
<DialogTitle className="text-center text-xl font-black uppercase tracking-wide text-white">
Heads up — this is beta
</DialogTitle>
<DialogDescription className="text-center text-sm text-gray-400">
A quick word before you drop in.
</DialogDescription>
</DialogHeader>
{/* Bullets */}
<div className="mt-5 space-y-3">
<DisclaimerRow
icon={<Code2 className="h-4 w-4 text-[#ccff00]" />}
title="Open-source & experimental"
body="Built in the open by the community. Poke around, fork it, break it."
/>
<DisclaimerRow
icon={<FlaskConical className="h-4 w-4 text-[#ff6600]" />}
title="In beta — not audited"
body="The code has not undergone a formal security audit yet. Bugs happen."
/>
<DisclaimerRow
icon={<ShieldOff className="h-4 w-4 text-[#ff0040]" />}
title="Handles real Bitcoin & Lightning"
body="Use money on it at your own risk. Only load what you can afford to lose."
/>
</div>
{/* Fine print */}
<p className="mt-4 flex items-start gap-2 rounded-lg border border-gray-800 bg-gray-900/50 px-3 py-2.5 text-[11px] leading-relaxed text-gray-500">
<Wallet className="mt-0.5 h-3.5 w-3.5 flex-shrink-0 text-gray-600" />
<span>
No warranty of any kind. You are solely responsible for your own
funds and keys.{' '}
<a
href={REPO_URL}
target="_blank"
rel="noopener noreferrer"
className="font-medium text-[#ccff00] underline decoration-dotted underline-offset-2 hover:text-[#ccff00]/80"
>
View the code
</a>
.
</span>
</p>
</div>
<DialogFooter className="px-6 pb-6 pt-0 sm:flex-col">
<Button
onClick={handleAccept}
className="w-full bg-[#ff0040] hover:bg-[#ff0040]/90 text-white font-black uppercase tracking-wide h-12 shadow-[0_0_20px_rgba(255,0,64,0.3)] transition-all hover:shadow-[0_0_28px_rgba(255,0,64,0.45)]"
>
Got it, let me in
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function DisclaimerRow({
icon,
title,
body,
}: {
icon: React.ReactNode;
title: string;
body: string;
}) {
return (
<div className="flex items-start gap-3">
<div className="mt-0.5 flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg border border-gray-800 bg-gray-900/60">
{icon}
</div>
<div className="min-w-0">
<p className="text-sm font-bold text-white">{title}</p>
<p className="text-xs leading-relaxed text-gray-400">{body}</p>
</div>
</div>
);
}