Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
SUPABASE_URL=https://cuwxnggcavvzbqbidaml.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImN1d3huZ2djYXZ2emJxYmlkYW1sIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjMzMTA5NzAsImV4cCI6MjA3ODg4Njk3MH0.X0BjHpf6hR3v3Iwv7BN5wkJhewLTzCZnbTq090vuGi0
SIMULATE_SUPABASE_DOWN=false
Comment on lines 1 to +3
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .env file should not be committed to version control as it contains sensitive credentials. Add .env to .gitignore and create a .env.example file with placeholder values to document required environment variables.

Copilot uses AI. Check for mistakes.
2 changes: 1 addition & 1 deletion .github/workflows/test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ yarn-error.log*
/src/data/readmeSections.json
/.vscode/

/src/data/cachedSignatories.json
1 change: 1 addition & 0 deletions docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
};

Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
79 changes: 79 additions & 0 deletions scripts/cache-signatories.cjs
Original file line number Diff line number Diff line change
@@ -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(
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The script uses the global fetch API which is only available in Node.js 18+. Consider adding a comment documenting the minimum Node.js version requirement, or add a runtime check with a helpful error message if fetch is not available.

Copilot uses AI. Check for mistakes.
`${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);
});
11 changes: 11 additions & 0 deletions src/components/SignManifest.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
53 changes: 45 additions & 8 deletions src/components/SignManifest.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<Session | null>(null);
const [loading, setLoading] = useState(true);
const [signature, setSignature] = useState<Signature | null>(null);
const [checkingSignature, setCheckingSignature] = useState(false);
const [supabaseUnavailable, setSupabaseUnavailable] = useState(false);

// Name-only signature state
const [showNameOnlyForm, setShowNameOnlyForm] = useState(false);
Expand All @@ -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);
};
Expand Down Expand Up @@ -377,6 +400,11 @@ export function SignManifest() {

return (
<>
{supabaseUnavailable && (
<div className={styles.warningBanner}>
Service temporarily unavailable. Signing is currently disabled. Please try again later.
</div>
)}
{showReLoginNotice && (
<div className={styles.reLoginNotice}>
You previously signed. Log in again to withdraw or update your signature.
Expand All @@ -386,15 +414,24 @@ export function SignManifest() {
{!showSignedState ? (
<>
<div className={styles.signButtons}>
<button className="button button--primary" onClick={() => handleLogin('github')}>
<button
className="button button--primary"
onClick={() => handleLogin('github')}
disabled={supabaseUnavailable}
>
Sign with GitHub
</button>
<button className="button button--secondary" onClick={() => handleLogin('linkedin_oidc')}>
<button
className="button button--secondary"
onClick={() => handleLogin('linkedin_oidc')}
disabled={supabaseUnavailable}
>
Sign with LinkedIn
</button>
<button
className={`button button--secondary ${styles.nameOnlyButton}`}
onClick={() => setShowNameOnlyForm(!showNameOnlyForm)}
disabled={supabaseUnavailable}
>
{showNameOnlyForm ? 'Cancel' : 'Sign with name only'}
</button>
Expand Down
17 changes: 17 additions & 0 deletions src/components/SignersList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
81 changes: 65 additions & 16 deletions src/components/SignersList.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<Signer[]>(() => (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<Signer[]>(() => (shouldUseMockData ? (mockSignatures as Signer[]) : []));
const [loading, setLoading] = useState(() => !shouldUseMockData && !!supabase);
const [error, setError] = useState<string | null>(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;
}
Expand All @@ -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]);
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency array is missing shouldUseMockData. Since shouldUseMockData is calculated from USE_MOCK_DATA and simulateDown, and simulateDown is included in the dependency array, consider adding shouldUseMockData to the dependency array or simplifying by using simulateDown directly in the condition on line 45.

Copilot uses AI. Check for mistakes.

if (loading) return <div>Loading signers…</div>;

Expand All @@ -75,6 +113,17 @@ export default function SignersList({ variant = 'compact' }: SignersListProps) {

return (
<>
{showErrorBanner && error && usingCache && (
<div className={styles.errorBanner}>
{error}. Showing cached signatories
{cachedSignatories.cached_at && (
<span className={styles.cacheDate}>
{' '}
(as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')})
</span>
Comment on lines +116 to +123
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error banner only displays when both error AND usingCache are true. This means if there's an error but no cached data is available (lines 89-91), no error message is shown to the user. Consider showing an error message even when cached data is not available, so users are informed of the issue.

Suggested change
{showErrorBanner && error && usingCache && (
<div className={styles.errorBanner}>
{error}. Showing cached signatories
{cachedSignatories.cached_at && (
<span className={styles.cacheDate}>
{' '}
(as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')})
</span>
{showErrorBanner && error && (
<div className={styles.errorBanner}>
{error}.
{usingCache ? (
<>
{' '}Showing cached signatories
{cachedSignatories.cached_at && (
<span className={styles.cacheDate}>
{' '}
(as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')})
</span>
)}
</>
) : (
<> No cached data available.</>

Copilot uses AI. Check for mistakes.
)}
Comment on lines +119 to +124
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling if cachedSignatories.cached_at contains an invalid date string. Consider wrapping the new Date() call in a try-catch or validating the date before rendering to prevent potential runtime errors.

Suggested change
{cachedSignatories.cached_at && (
<span className={styles.cacheDate}>
{' '}
(as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')})
</span>
)}
{cachedSignatories.cached_at && (() => {
const cacheDate = new Date(cachedSignatories.cached_at);
if (!isNaN(cacheDate.getTime())) {
return (
<span className={styles.cacheDate}>
{' '}
(as of {cacheDate.toLocaleString('en-US')})
</span>
);
}
return (
<span className={styles.cacheDate}>
{' '}
(as of unknown date)
</span>
);
})()}

Copilot uses AI. Check for mistakes.
</div>
)}
<h3 className={styles.heading}></h3>
<div className={styles.container}>
<ul className={gridClass}>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/signatories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function SignatoriesPage() {
</div>

<div className={styles.signersSection}>
<SignersList variant="full" />
<SignersList variant="full" showErrorBanner={true} />
</div>
</main>
</Layout>
Expand Down