Skip to content

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 path
  • documentId: 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