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>
This commit is contained in:
parent
4bef95132b
commit
6e226d5561
3 changed files with 160 additions and 1 deletions
|
|
@ -15,6 +15,7 @@ import { NWCProvider } from '@/contexts/NWCContext';
|
|||
import { LanguageProvider } from '@/contexts/LanguageContext';
|
||||
import { AppConfig } from '@/contexts/AppContext';
|
||||
import { PWAInstallPrompt } from '@/components/PWAInstallPrompt';
|
||||
import { BetaDisclaimerDialog } from '@/components/BetaDisclaimerDialog';
|
||||
import { AmberCallbackHandler } from '@/components/auth/AmberCallbackHandler';
|
||||
import AppRouter from './AppRouter';
|
||||
|
||||
|
|
@ -63,6 +64,7 @@ export function App() {
|
|||
<TooltipProvider>
|
||||
<Toaster />
|
||||
<PWAInstallPrompt />
|
||||
<BetaDisclaimerDialog />
|
||||
<Suspense>
|
||||
<AppRouter />
|
||||
</Suspense>
|
||||
|
|
|
|||
152
src/components/BetaDisclaimerDialog.tsx
Normal file
152
src/components/BetaDisclaimerDialog.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -863,7 +863,12 @@ export function Layout({ children, noPadding }: LayoutProps) {
|
|||
<footer className="hidden md:block border-t border-gray-800 bg-black/95 py-6">
|
||||
<div className="container px-4">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4 text-sm text-gray-400">
|
||||
<span>Made with <span className="text-red-500">❤️</span> for the skateboarding community</span>
|
||||
<span className="flex items-center gap-2">
|
||||
Made with <span className="text-red-500">❤️</span> for the skateboarding community
|
||||
<span className="inline-flex items-center rounded-full border border-[#ff0040]/50 bg-[#ff0040]/10 px-2 py-0.5 text-[10px] font-black uppercase tracking-wider text-[#ff0040]">
|
||||
Beta
|
||||
</span>
|
||||
</span>
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500">
|
||||
<Link
|
||||
to="/newsletter"
|
||||
|
|
|
|||
Loading…
Reference in a new issue