API Reference¶
🪝 Custom Hooks¶
useCollection()¶
Subscribe to real-time Firestore collection updates.
Syntax¶
const { data, loading, error } = useCollection<T>(
collectionName: string,
...constraints: QueryConstraint[]
)
Parameters¶
collectionName: Firestore collection path (e.g., "agenda", "members")...constraints: Optional Firestore query constraints (where, orderBy, limit)
Returns¶
{
data: T[]; // Array of documents
loading: boolean; // True while fetching
error: Error | null; // Error if failed
}
Examples¶
Basic Usage
const { data: members, loading } = useCollection('members');
return (
<div>
{loading && <p>Loading...</p>}
{members.map(m => <div key={m.id}>{m.name}</div>)}
</div>
);
With Constraints
import { where, orderBy, limit } from 'firebase/firestore';
const { data: speakers, loading } = useCollection(
'speakers',
where('agendaItemId', '==', activeItemId),
orderBy('timestamp', 'desc'),
limit(10)
);
Multiple Constraints
const { data: votedItems } = useCollection(
'votes',
where('agendaItemId', 'in', ['item1', 'item2']),
where('voteOption', '==', 'FOR')
);
useDocument()¶
Subscribe to real-time single document updates.
Syntax¶
const { data, loading, error, exists } = useDocument<T>(
collectionName: string,
documentId: string
)
Parameters¶
collectionName: Firestore collection pathdocumentId: Document ID (e.g., user's Golf ID)
Returns¶
{
data: T | null; // Document data or null
loading: boolean; // True while fetching
error: Error | null; // Error if failed
exists: boolean; // True if document exists
}
Examples¶
Basic Usage
const { data: member, loading, exists } = useDocument('members', golfId);
if (!exists) {
return <div>Member not found</div>;
}
return <div>{member?.name}</div>;
With Type Safety
interface Member {
id: string;
name: string;
canVote: boolean;
}
const { data: member } = useDocument<Member>('members', golfId);
// member.name is typed as string
🔥 Firebase Services¶
firebaseConfig.ts¶
Initialize Firebase SDK and export services.
Exports¶
import { db, auth, storage, functions } from '../services/firebaseConfig';
Usage¶
Firestore
import { collection, query, where } from 'firebase/firestore';
import { db } from '../services/firebaseConfig';
const membersRef = collection(db, 'members');
const q = query(membersRef, where('canVote', '==', true));
Authentication
import { auth } from '../services/firebaseConfig';
import { onAuthStateChanged } from 'firebase/auth';
onAuthStateChanged(auth, (user) => {
if (user) {
console.log('User:', user.uid);
}
});
Cloud Functions
import { httpsCallable } from 'firebase/functions';
import { functions } from '../services/firebaseConfig';
const castVote = httpsCallable(functions, 'castVote');
const result = await castVote({ agendaItemId, voteOption: 'FOR' });
📊 Firestore Collections¶
members¶
Member data with voting rights.
interface Member {
id: string; // Golf-ID (document ID)
name: string;
email: string;
phone: string;
role: string; // e.g., "member", "styret"
verification: string; // Post code for login verification
canVote: boolean;
joinDate: string; // ISO timestamp
}
Access:
const { data: member } = useDocument<Member>('members', golfId);
agenda¶
Meeting agenda items.
interface AgendaItem {
id: string;
number: number; // §1, §2, etc.
title: string;
description: string;
status: AgendaItemStatus; // PENDING | ACTIVE | VOTING_OPEN | VOTING_CLOSED | FINISHED
isVotable: boolean;
requiresSecretBallot: boolean;
decision?: string; // Secretary's decision note
proposals?: Proposal[]; // Amendments
voteResults?: {
for: number;
against: number;
abstain: number;
total: number;
};
}
type AgendaItemStatus =
| 'PENDING'
| 'ACTIVE'
| 'VOTING_OPEN'
| 'VOTING_CLOSED'
| 'FINISHED';
Access:
const { data: agenda } = useCollection<AgendaItem>('agenda');
speakers¶
Speaker queue for each agenda item.
interface Speaker {
id: string;
agendaItemId: string;
userId: string; // Golf-ID or member ID
userName: string;
type: 'INNLEGG' | 'REPLIKK'; // Speech or reply
timestamp: number; // Firestore timestamp
}
Access:
import { where, orderBy } from 'firebase/firestore';
const { data: speakers } = useCollection<Speaker>(
'speakers',
where('agendaItemId', '==', itemId),
orderBy('timestamp', 'asc')
);
votes¶
Anonymous votes (never directly readable).
interface Vote {
id: string; // Auto-generated
agendaItemId: string;
voteOption: 'FOR' | 'MOT' | 'AVSTAR';
timestamp: number;
// NOTE: votes collection is write-only via Cloud Function
// Users cannot read individual votes
}
Write via Cloud Function:
const castVote = httpsCallable(functions, 'castVote');
await castVote({
agendaItemId: 'item-123',
voteOption: 'FOR'
});
vote_receipts¶
Proof of participation (prevents double voting).
interface VoteReceipt {
id: string; // Document ID = userId
agendaItemId: string;
hasVoted: boolean;
timestamp: number;
}
Access:
const { data: receipt } = useDocument<VoteReceipt>(
'vote_receipts',
userId
);
if (!receipt?.hasVoted) {
// User can vote
}
🛠️ Cloud Functions (Planned)¶
processMemberUpload()¶
Import members from CSV file.
const importMembers = httpsCallable(functions, 'processMemberUpload');
const result = await importMembers({
fileName: 'members.csv', // File path in Cloud Storage
bucketName: 'gkit-meeting-suite.firebasestorage.app'
});
// result: { imported: 150, skipped: 2, errors: [] }
castVote()¶
Cast an anonymous vote (backend validations).
const castVote = httpsCallable(functions, 'castVote');
const result = await castVote({
agendaItemId: 'agenda-item-123',
voteOption: 'FOR' // 'FOR' | 'MOT' | 'AVSTAR'
});
// Backend:
// 1. Verify user is authenticated
// 2. Check user has canVote permission
// 3. Check voting is open for this item
// 4. Prevent double voting
// 5. Create anonymous vote document
// 6. Create vote receipt for user
generateMeetingReport()¶
Generate meeting protocol/report.
const generateReport = httpsCallable(functions, 'generateMeetingReport');
const result = await generateReport({
meetingId: 'meeting-2025-01',
format: 'pdf' // 'pdf' | 'docx' | 'html'
});
// result: { reportUrl: 'https://...' }
🔐 Type Definitions¶
User (Delegate)¶
interface User {
id: string; // Golf-ID
name: string;
postCode: string; // For login verification
canVote: boolean;
isCheckedIn: boolean;
role: 'DELEGATE' | 'ADMIN' | 'OBSERVER';
}
MeetingState¶
interface MeetingState {
meetingId: string;
status: MeetingStatus;
activeAgendaItemId?: string;
registeredVoters: number;
speakerListOpen: boolean;
startTime: number;
endTime?: number;
}
type MeetingStatus =
| 'NOT_STARTED'
| 'REGISTRATION'
| 'ACTIVE'
| 'PAUSED'
| 'ENDED';
Proposal (Amendment)¶
interface Proposal {
id: string;
text: string;
author: string; // Member name
isMainProposal?: boolean;
}
🔍 Common Queries¶
Get all voting items
const { data } = useCollection(
'agenda',
where('isVotable', '==', true)
);
Get active speakers
const { data } = useCollection(
'speakers',
where('agendaItemId', '==', activeId),
orderBy('timestamp', 'asc')
);
Get members by role
const { data } = useCollection(
'members',
where('role', '==', 'styret')
);
Get vote count (via Cloud Function)
const getVoteCount = httpsCallable(functions, 'getVoteCount');
const result = await getVoteCount({ agendaItemId });
// { for: 45, against: 12, abstain: 3 }
🚨 Error Handling¶
const { data, error } = useCollection('agenda');
if (error) {
return (
<div className="text-red-500">
Feil: {error.message}
</div>
);
}
Common Errors:
- permission-denied - User lacks read/write permission
- not-found - Document/collection doesn't exist
- unavailable - Firebase service temporarily down