Files
hackerspace.sh/components/classes/session-attendance.tsx
T
Moheeb ZaraandClaude Opus 4.8 b1edb5c460 types: clear remaining tsc errors across 41 files (76→14, behavior-preserving)
Per-file type-only fixes surfaced after the Supabase client was correctly
typed. All fixes preserve runtime behavior; no any/ts-ignore/ts-expect-error.

- lib/supabase/client.ts: annotate the browser client as SupabaseClient<Database>
  (same ssr/js drift as server.ts; fixes it centrally for all callers).
- Supabase embedded-select joins whose FK metadata is missing from the
  generated types are bridged with `as unknown as <target>` + a TODO to remove
  after regenerating types/database.ts.
- setState optimistic-update variance and raw <select> enum values cast to the
  precise element/union type; .insert payload fields (Json, enums) given the
  precise column type via satisfies/field casts.
- Added narrowing guards where a value was legitimately nullable (comms channel,
  public form page) without changing the intended path.

The 14 remaining errors are confined to the four files fixed in Phase B (real
bugs + the tautological actions.test rewrite). 639 tests still green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-01 18:56:11 -07:00

115 lines
3.8 KiB
TypeScript

'use client'
import { useEffect, useState, useCallback } from 'react'
import { toast } from 'sonner'
import { listSessionSignups, markAttendance, completeSession } from '@/lib/actions'
import { SIGNUP_STATUS_LABEL } from '@/lib/classes-logic'
type Signup = {
id: string
member_id: string
status: string
attended: boolean
signed_up_at: string
space_members: { display_name: string | null; email: string | null } | null
}
export function SessionAttendance({
sessionId,
sessionStatus,
onCompleted,
}: {
sessionId: string
sessionStatus: string
onCompleted: () => void
}) {
const [loading, setLoading] = useState(false)
const [rows, setRows] = useState<Signup[]>([])
const [busy, setBusy] = useState(false)
const load = useCallback(async () => {
setLoading(true)
const res = await listSessionSignups({ sessionId })
setLoading(false)
if ('error' in res && res.error) {
toast.error(res.error)
return
}
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
setRows(((res as unknown as { data: Signup[] }).data) ?? [])
}, [sessionId])
useEffect(() => {
load()
}, [load])
async function toggle(s: Signup) {
const res = await markAttendance({ signupId: s.id, attended: !s.attended })
if ('error' in res && res.error) {
toast.error(res.error)
return
}
setRows(prev => prev.map(r => (r.id === s.id ? { ...r, attended: !r.attended } : r)))
}
async function onComplete() {
setBusy(true)
const res = await completeSession({ sessionId })
setBusy(false)
if ('error' in res && res.error) {
toast.error(res.error)
return
}
const d = (res as { data: { certificatesIssued: number; certificatesSkipped: boolean } }).data
if (d.certificatesSkipped) {
toast.success('Session completed. Certificates were NOT issued (requires the certifications.grant permission).')
} else if (d.certificatesIssued > 0) {
toast.success(`Session completed. ${d.certificatesIssued} certificate(s) issued.`)
} else {
toast.success('Session completed.')
}
onCompleted()
}
return (
<div className="mt-3 rounded border border-border bg-background p-3">
<div className="flex items-center justify-between mb-2">
<p className="font-mono text-[10px] uppercase tracking-widest text-muted-foreground">Attendees</p>
{sessionStatus === 'scheduled' && (
<button
onClick={onComplete}
disabled={busy}
className="font-mono text-[10px] border border-border px-2 py-1 rounded hover:border-primary hover:text-primary transition disabled:opacity-60"
>
Complete session
</button>
)}
</div>
{loading ? (
<p className="font-mono text-[10px] text-muted-foreground">Loading…</p>
) : rows.length === 0 ? (
<p className="font-mono text-[10px] text-muted-foreground">No signups yet.</p>
) : (
<ul className="divide-y divide-border">
{rows.map(s => (
<li key={s.id} className="py-2 flex items-center justify-between gap-3">
<div className="min-w-0">
<span className="font-sans text-sm text-foreground">
{s.space_members?.display_name ?? s.space_members?.email ?? 'Member'}
</span>
<span className="font-mono text-[10px] text-muted-foreground ml-2">
{SIGNUP_STATUS_LABEL[s.status] ?? s.status}
</span>
</div>
<label className="flex items-center gap-1.5 font-mono text-[10px] text-muted-foreground cursor-pointer">
<input type="checkbox" checked={s.attended} onChange={() => toggle(s)} />
attended
</label>
</li>
))}
</ul>
)}
</div>
)
}