From c46c911ce6abe710719d99d77dcb418aacf48575 Mon Sep 17 00:00:00 2001 From: Bissbert <43237892+Bissbert@users.noreply.github.com> Date: Tue, 5 May 2026 17:44:56 +0700 Subject: [PATCH] feat(ux): remove lock gate, fix CSP, raise crystal-chip contrast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 1 of the 2026-05-05 UX review implementation. W1 — Remove lock gate entirely - Delete LockGate.tsx, all Protected* wrappers (10 files), useAuth.ts - Inline page chrome into Astro pages for tools/* and gallery - Extract ProtectedQuiz logic into non-gated QuizPage component - Extract ProtectedTools UI into ToolsHub component - Strip LockGate from StudyReviewScreen and StudySettingsScreen - All Tier-1 surfaces (gallery, playground, tools, quiz, study) now render directly with no gate W2 — Fix CSP headers - Allow cdn.jsdelivr.net (Monaco editor) - Allow static.cloudflareinsights.com (analytics beacon) - Allow api.gemmology.dev (playground render endpoint) - Add explicit script-src-elem to satisfy strict CSP parsers W5 — Raise crystal-system chip contrast - Bump crystalSystemColors.bg from -500 to -700 across all seven crystal systems so white-on-coloured chips reach WCAG AA 4.5:1 --- public/_headers | 2 +- src/components/auth/LockGate.tsx | 237 ------------------ src/components/auth/ProtectedAdvanced.tsx | 24 -- src/components/auth/ProtectedConversions.tsx | 24 -- src/components/auth/ProtectedGallery.tsx | 33 --- .../auth/ProtectedIdentification.tsx | 24 -- src/components/auth/ProtectedLab.tsx | 24 -- src/components/auth/ProtectedMeasurement.tsx | 28 --- src/components/auth/ProtectedOptical.tsx | 24 -- src/components/auth/ProtectedPlayground.tsx | 18 -- src/components/auth/ProtectedQuiz.tsx | 93 ------- src/components/auth/useAuth.ts | 171 ------------- src/components/learn/icons.ts | 14 +- src/components/quiz/QuizPage.tsx | 84 +++++++ .../quiz/study/StudyReviewScreen.tsx | 53 ++-- .../quiz/study/StudySettingsScreen.tsx | 43 ++-- .../ProtectedTools.tsx => tools/ToolsHub.tsx} | 73 +++--- src/pages/gallery/index.astro | 23 +- src/pages/playground.astro | 4 +- src/pages/quiz/index.astro | 8 +- src/pages/study/review.astro | 2 - src/pages/study/settings.astro | 2 +- src/pages/tools/advanced.astro | 16 +- src/pages/tools/conversions.astro | 17 +- src/pages/tools/identification.astro | 16 +- src/pages/tools/index.astro | 5 +- src/pages/tools/lab.astro | 16 +- src/pages/tools/measurement.astro | 15 +- src/pages/tools/optical.astro | 16 +- 29 files changed, 271 insertions(+), 838 deletions(-) delete mode 100644 src/components/auth/LockGate.tsx delete mode 100644 src/components/auth/ProtectedAdvanced.tsx delete mode 100644 src/components/auth/ProtectedConversions.tsx delete mode 100644 src/components/auth/ProtectedGallery.tsx delete mode 100644 src/components/auth/ProtectedIdentification.tsx delete mode 100644 src/components/auth/ProtectedLab.tsx delete mode 100644 src/components/auth/ProtectedMeasurement.tsx delete mode 100644 src/components/auth/ProtectedOptical.tsx delete mode 100644 src/components/auth/ProtectedPlayground.tsx delete mode 100644 src/components/auth/ProtectedQuiz.tsx delete mode 100644 src/components/auth/useAuth.ts create mode 100644 src/components/quiz/QuizPage.tsx rename src/components/{auth/ProtectedTools.tsx => tools/ToolsHub.tsx} (58%) diff --git a/public/_headers b/public/_headers index 655d1d2..4a31ddc 100644 --- a/public/_headers +++ b/public/_headers @@ -3,7 +3,7 @@ X-Frame-Options: DENY Referrer-Policy: strict-origin-when-cross-origin Permissions-Policy: camera=(), microphone=(), geolocation=(), interest-cohort=() - Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self'; worker-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self' + Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://static.cloudflareinsights.com; script-src-elem 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data: blob:; font-src 'self' data: https://cdn.jsdelivr.net; connect-src 'self' https://api.gemmology.dev https://cloudflareinsights.com https://cdn.jsdelivr.net; worker-src 'self' blob:; frame-ancestors 'none'; base-uri 'self'; form-action 'self' Strict-Transport-Security: max-age=31536000; includeSubDomains; preload # Long-cache versioned static assets (Astro hashed bundle) diff --git a/src/components/auth/LockGate.tsx b/src/components/auth/LockGate.tsx deleted file mode 100644 index c3d5837..0000000 --- a/src/components/auth/LockGate.tsx +++ /dev/null @@ -1,237 +0,0 @@ -/** - * LockGate Component - Protects page content with invite code authentication - */ - -import { useState, type ReactNode, type FormEvent } from 'react'; -import { useAuth } from './useAuth'; -import { Button } from '../ui/Button'; -import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '../ui/Card'; -import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/Tabs'; -import { cn } from '../ui/cn'; - -interface LockGateProps { - children: ReactNode; -} - -export function LockGate({ children }: LockGateProps) { - const { isAuthenticated, isLoading, verifyCode, requestAccess } = useAuth(); - - // Code entry state - const [code, setCode] = useState(''); - const [codeError, setCodeError] = useState(null); - const [codeLoading, setCodeLoading] = useState(false); - - // Request form state - const [email, setEmail] = useState(''); - const [reason, setReason] = useState(''); - const [requestError, setRequestError] = useState(null); - const [requestSuccess, setRequestSuccess] = useState(false); - const [requestLoading, setRequestLoading] = useState(false); - - // Show loading state while checking session - if (isLoading) { - return ( -
-
Loading...
-
- ); - } - - // If authenticated, render children - if (isAuthenticated) { - return <>{children}; - } - - // Handle code submission - const handleCodeSubmit = async (e: FormEvent) => { - e.preventDefault(); - setCodeError(null); - setCodeLoading(true); - - const result = await verifyCode(code.trim()); - - if (!result.success) { - setCodeError(result.error || 'Invalid code'); - } - setCodeLoading(false); - }; - - // Handle request submission - const handleRequestSubmit = async (e: FormEvent) => { - e.preventDefault(); - setRequestError(null); - setRequestLoading(true); - - const result = await requestAccess(email.trim(), reason.trim()); - - if (result.success) { - setRequestSuccess(true); - } else { - setRequestError(result.error || 'Failed to submit request'); - } - setRequestLoading(false); - }; - - // Render lock overlay - return ( -
- - -
- - - -
- Coming Soon - - This feature is still under development. Access is limited to early testers while we work on finishing it. - -
- - - - - - I have a code - - - Request access - - - - -
-
- - setCode(e.target.value.toUpperCase())} - placeholder="GEMSTONE-XXXX-XXXX" - className={cn( - 'w-full px-3 py-2 rounded-lg border text-center font-mono tracking-wider', - 'focus:outline-none focus:ring-2 focus:ring-crystal-500 focus:border-transparent', - codeError ? 'border-red-300 bg-red-50' : 'border-slate-300' - )} - autoComplete="off" - autoFocus - /> - {codeError && ( -

{codeError}

- )} -
- - -
-
- - - {requestSuccess ? ( -
-
- - - -
-

- Request Submitted -

-

- We'll review your request and send you an invite code by email. -

-
- ) : ( -
-
- - setEmail(e.target.value)} - placeholder="you@example.com" - className={cn( - 'w-full px-3 py-2 rounded-lg border', - 'focus:outline-none focus:ring-2 focus:ring-crystal-500 focus:border-transparent', - requestError ? 'border-red-300 bg-red-50' : 'border-slate-300' - )} - required - /> -
- -
- -