diff --git a/.env b/.env index 319b206..e3b5c29 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ SUPABASE_URL=https://cuwxnggcavvzbqbidaml.supabase.co SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImN1d3huZ2djYXZ2emJxYmlkYW1sIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjMzMTA5NzAsImV4cCI6MjA3ODg4Njk3MH0.X0BjHpf6hR3v3Iwv7BN5wkJhewLTzCZnbTq090vuGi0 +SIMULATE_SUPABASE_DOWN=false \ No newline at end of file diff --git a/.github/workflows/test-deploy.yml b/.github/workflows/test-deploy.yml index 5508ab6..0106d8e 100644 --- a/.github/workflows/test-deploy.yml +++ b/.github/workflows/test-deploy.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v3 with: - node-version: 18.x + node-version: 20.x cache: npm - name: Install dependencies diff --git a/.gitignore b/.gitignore index 95e3f07..9c3a63d 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ yarn-error.log* /src/data/readmeSections.json /.vscode/ +/src/data/cachedSignatories.json diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 1595d35..7da69ab 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -100,6 +100,7 @@ const config: Config = { customFields: { supabaseUrl: process.env.SUPABASE_URL, supabaseAnonKey: process.env.SUPABASE_ANON_KEY, + simulateSupabaseDown: process.env.SIMULATE_SUPABASE_DOWN === 'true', }, }; diff --git a/package.json b/package.json index 7223182..91a4a67 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,15 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", - "prestart": "npm run extract:readme", + "prestart": "npm run extract:readme && npm run cache:signatories", "build": "docusaurus build", - "prebuild": "npm run extract:readme", + "prebuild": "npm run extract:readme && npm run cache:signatories", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "extract:readme": "node scripts/extract-readme.cjs", + "cache:signatories": "node scripts/cache-signatories.cjs", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc", diff --git a/scripts/cache-signatories.cjs b/scripts/cache-signatories.cjs new file mode 100644 index 0000000..605bf11 --- /dev/null +++ b/scripts/cache-signatories.cjs @@ -0,0 +1,79 @@ +#!/usr/bin/env node +// Load .env variables +require('dotenv').config(); + +const fs = require('fs'); +const path = require('path'); + +const outFile = path.resolve(__dirname, '..', 'src', 'data', 'cachedSignatories.json'); + +async function fetchSignatories() { + // Try to load Supabase credentials from environment + const supabaseUrl = process.env.SUPABASE_URL; + const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + console.warn('⚠️ SUPABASE_URL or SUPABASE_ANON_KEY not found in environment'); + console.warn('⚠️ Skipping signatories cache generation'); + return null; + } + + try { + console.log('🔄 Fetching signatories from Supabase...'); + + // Use fetch API to query Supabase REST API directly + const response = await fetch( + `${supabaseUrl}/rest/v1/signatures?select=id,name,avatar_url,profile_url,created_at,privacy_level,auth_provider,name_only_location&order=created_at.desc&limit=100`, + { + headers: { + apikey: supabaseAnonKey, + Authorization: `Bearer ${supabaseAnonKey}`, + }, + } + ); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + console.log(`✅ Successfully fetched ${data.length} signatories`); + return data; + } catch (error) { + console.error('❌ Failed to fetch signatories from Supabase:', error.message); + return null; + } +} + +async function run() { + const signatories = await fetchSignatories(); + + // Create output directory + fs.mkdirSync(path.dirname(outFile), { recursive: true }); + + if (signatories) { + // Write successful cache with metadata + const cacheData = { + cached_at: new Date().toISOString(), + count: signatories.length, + signatories: signatories, + }; + fs.writeFileSync(outFile, JSON.stringify(cacheData, null, 2), 'utf8'); + console.log(`✅ Cached ${signatories.length} signatories to ${outFile}`); + } else { + // Write empty cache with error flag + const cacheData = { + cached_at: new Date().toISOString(), + count: 0, + error: 'Failed to fetch from Supabase', + signatories: [], + }; + fs.writeFileSync(outFile, JSON.stringify(cacheData, null, 2), 'utf8'); + console.log('⚠️ Created empty cache file'); + } +} + +run().catch((err) => { + console.error('❌ Cache script failed:', err); + process.exit(1); +}); diff --git a/src/components/SignManifest.module.css b/src/components/SignManifest.module.css index 5d73a6d..041d5b9 100644 --- a/src/components/SignManifest.module.css +++ b/src/components/SignManifest.module.css @@ -53,6 +53,17 @@ color: var(--ifm-color-success-dark); } +.warningBanner { + margin-bottom: 0.75rem; + padding: 0.75rem; + background: rgba(255, 193, 7, 0.1); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: 6px; + font-size: 0.9rem; + color: rgba(255, 193, 7, 0.95); + line-height: 1.5; +} + .nameOnlyForm { margin-top: 1.5rem; padding: 1.5rem; diff --git a/src/components/SignManifest.tsx b/src/components/SignManifest.tsx index a0ee654..dc9e409 100644 --- a/src/components/SignManifest.tsx +++ b/src/components/SignManifest.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { useSupabase } from '@site/src/utils/supabase'; import PrivacySettings from './PrivacySettings'; import ShareButtons from './ShareButtons'; @@ -23,11 +24,17 @@ interface CaptchaChallenge { } export function SignManifest() { + const { siteConfig } = useDocusaurusContext(); const supabase = useSupabase(); + + // Check if we should simulate Supabase being down (for testing) + const simulateDown = Boolean(siteConfig.customFields?.simulateSupabaseDown); + const [session, setSession] = useState(null); const [loading, setLoading] = useState(true); const [signature, setSignature] = useState(null); const [checkingSignature, setCheckingSignature] = useState(false); + const [supabaseUnavailable, setSupabaseUnavailable] = useState(false); // Name-only signature state const [showNameOnlyForm, setShowNameOnlyForm] = useState(false); @@ -45,14 +52,30 @@ export function SignManifest() { // Load session useEffect(() => { - if (!supabase) return; + if (!supabase) { + setLoading(false); + return; + } const fetchSession = async () => { - const { data, error } = await supabase.auth.getSession(); - if (error) { - console.error('Error fetching session:', error); - } else { - setSession(data.session); + try { + // Simulate Supabase being down for testing + if (simulateDown) { + console.warn('🧪 [TEST] Simulating Supabase outage (set SIMULATE_SUPABASE_DOWN=false in .env to disable)'); + throw new Error('Simulated Supabase outage'); + } + + const { data, error } = await supabase.auth.getSession(); + if (error) { + console.error('Error fetching session:', error); + setSupabaseUnavailable(true); + } else { + setSession(data.session); + setSupabaseUnavailable(false); + } + } catch (err) { + console.error('Supabase connection error:', err); + setSupabaseUnavailable(true); } setLoading(false); }; @@ -377,6 +400,11 @@ export function SignManifest() { return ( <> + {supabaseUnavailable && ( +
+ Service temporarily unavailable. Signing is currently disabled. Please try again later. +
+ )} {showReLoginNotice && (
You previously signed. Log in again to withdraw or update your signature. @@ -386,15 +414,24 @@ export function SignManifest() { {!showSignedState ? ( <>
- - diff --git a/src/components/SignersList.module.css b/src/components/SignersList.module.css index b60e5f6..5c405a4 100644 --- a/src/components/SignersList.module.css +++ b/src/components/SignersList.module.css @@ -164,3 +164,20 @@ font-size: 0.7rem; } } + +/* Error banner for when Supabase is unavailable */ +.errorBanner { + background: rgba(255, 193, 7, 0.15); + border: 1px solid rgba(255, 193, 7, 0.3); + border-radius: 8px; + padding: 12px 16px; + margin-bottom: 16px; + color: rgba(255, 193, 7, 0.9); + font-size: 0.9rem; + line-height: 1.5; +} + +.cacheDate { + opacity: 0.8; + font-size: 0.85em; +} diff --git a/src/components/SignersList.tsx b/src/components/SignersList.tsx index f7b56bf..1c86fff 100644 --- a/src/components/SignersList.tsx +++ b/src/components/SignersList.tsx @@ -1,7 +1,9 @@ import React, { useEffect, useState } from 'react'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { useSupabase } from '@site/src/utils/supabase'; import styles from './SignersList.module.css'; import mockSignatures from '@site/src/data/mockSignatures.json'; +import cachedSignatories from '@site/src/data/cachedSignatories.json'; interface Signer { id: string; @@ -22,15 +24,25 @@ const USE_MOCK_DATA = process.env.NODE_ENV === 'development'; interface SignersListProps { variant?: 'compact' | 'full'; + showErrorBanner?: boolean; } -export default function SignersList({ variant = 'compact' }: SignersListProps) { +export default function SignersList({ variant = 'compact', showErrorBanner = false }: SignersListProps) { + const { siteConfig } = useDocusaurusContext(); const supabase = useSupabase(); - const [signers, setSigners] = useState(() => (USE_MOCK_DATA ? (mockSignatures as Signer[]) : [])); - const [loading, setLoading] = useState(() => !USE_MOCK_DATA && !!supabase); + + // Check if we should simulate Supabase being down (for testing) + const simulateDown = Boolean(siteConfig.customFields?.simulateSupabaseDown); + const shouldUseMockData = USE_MOCK_DATA && !simulateDown; + + const [signers, setSigners] = useState(() => (shouldUseMockData ? (mockSignatures as Signer[]) : [])); + const [loading, setLoading] = useState(() => !shouldUseMockData && !!supabase); + const [error, setError] = useState(null); + const [usingCache, setUsingCache] = useState(false); useEffect(() => { - if (USE_MOCK_DATA) { + // Query parameter takes precedence over USE_MOCK_DATA + if (shouldUseMockData) { console.log('Loading mock signatures:', mockSignatures.length, 'entries'); return; } @@ -41,26 +53,52 @@ export default function SignersList({ variant = 'compact' }: SignersListProps) { const load = async () => { setLoading(true); - const { data, error } = await supabase - .from('signatures') - .select( - 'id, name, avatar_url, profile_url, created_at, privacy_level, auth_provider, name_only_location' - ) - .order('created_at', { ascending: false }) - .limit(100); - if (error) { - console.error('Failed to load signatures', error); - } else { + setError(null); + setUsingCache(false); + + try { + // Simulate Supabase being down for testing + if (simulateDown) { + console.warn('🧪 [TEST] Simulating Supabase outage (set SIMULATE_SUPABASE_DOWN=false in .env to disable)'); + throw new Error('Simulated Supabase outage'); + } + + const { data, error: supabaseError } = await supabase + .from('signatures') + .select( + 'id, name, avatar_url, profile_url, created_at, privacy_level, auth_provider, name_only_location' + ) + .order('created_at', { ascending: false }) + .limit(100); + + if (supabaseError) { + throw supabaseError; + } + setSigners(data || []); + console.log('Loaded', data?.length || 0, 'signatures from Supabase'); + } catch (err) { + console.error('Failed to load signatures from Supabase:', err); + setError('Connection to Supabase failed'); + + // Fallback to cached data + if (cachedSignatories && cachedSignatories.signatories) { + console.log('Falling back to cached data:', cachedSignatories.signatories.length, 'entries'); + setSigners(cachedSignatories.signatories as Signer[]); + setUsingCache(true); + } else { + console.error('No cached data available'); + } + } finally { + setLoading(false); } - setLoading(false); }; load(); const handler = () => load(); window.addEventListener('signature-changed', handler); return () => window.removeEventListener('signature-changed', handler); - }, [supabase]); + }, [supabase, simulateDown]); if (loading) return
Loading signers…
; @@ -75,6 +113,17 @@ export default function SignersList({ variant = 'compact' }: SignersListProps) { return ( <> + {showErrorBanner && error && usingCache && ( +
+ {error}. Showing cached signatories + {cachedSignatories.cached_at && ( + + {' '} + (as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')}) + + )} +
+ )}

    diff --git a/src/pages/signatories.tsx b/src/pages/signatories.tsx index b7deb13..5cf879d 100644 --- a/src/pages/signatories.tsx +++ b/src/pages/signatories.tsx @@ -20,7 +20,7 @@ export default function SignatoriesPage() {
- +