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]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1766780bca
commit
b1edb5c460
@@ -85,11 +85,11 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
m.id.startsWith('temp-') &&
|
||||
m.user_id === payload.new.user_id &&
|
||||
m.content === payload.new.content
|
||||
? payload.new
|
||||
? (payload.new as Message)
|
||||
: m,
|
||||
)
|
||||
}
|
||||
return [...prev, payload.new]
|
||||
return [...prev, payload.new as Message]
|
||||
})
|
||||
},
|
||||
)
|
||||
@@ -105,6 +105,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
}, [messages])
|
||||
|
||||
async function loadMessages() {
|
||||
if (!selectedChannel) return
|
||||
const supabase = createClient()
|
||||
const { data } = await supabase
|
||||
.from('comms_messages')
|
||||
@@ -119,6 +120,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
e.preventDefault()
|
||||
const content = newMessage.trim()
|
||||
if (!content || sending) return
|
||||
if (!selectedChannel) return
|
||||
setSending(true)
|
||||
|
||||
// Optimistic insert — message appears immediately for the sender
|
||||
@@ -133,7 +135,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
content,
|
||||
created_at: new Date().toISOString(),
|
||||
}
|
||||
setMessages(prev => [...prev, optimisticMsg])
|
||||
setMessages(prev => [...prev, optimisticMsg as Message])
|
||||
setNewMessage('')
|
||||
|
||||
const supabase = createClient()
|
||||
@@ -142,7 +144,7 @@ export default function CommsClient({ member, space, channels }: Props) {
|
||||
.insert({
|
||||
channel_id: selectedChannel.id,
|
||||
space_id: space?.id ?? member.space_id,
|
||||
user_id: member.user_id,
|
||||
user_id: member.user_id as string,
|
||||
display_name: member.display_name,
|
||||
handle: member.handle,
|
||||
content,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { Tables } from '@/types/database'
|
||||
import CommsClient from './comms-client'
|
||||
|
||||
export default async function CommsPage() {
|
||||
@@ -17,5 +18,5 @@ export default async function CommsPage() {
|
||||
.order('channel_type')
|
||||
.order('name')
|
||||
|
||||
return <CommsClient member={member} space={member?.spaces} channels={channels ?? []} />
|
||||
return <CommsClient member={member} space={member?.spaces as Tables<'spaces'>} channels={channels ?? []} />
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ export function ContactsClient({ contacts: initialContacts }: { contacts: Contac
|
||||
tags: form.tags ? form.tags.split(',').map(t => t.trim()).filter(Boolean) : [],
|
||||
})
|
||||
if (result.error) { setError(result.error); setLoading(false); return }
|
||||
setContacts(prev => prev.map(c => c.id === editContact.id ? { ...c, ...form } : c))
|
||||
setContacts(prev => prev.map(c => c.id === editContact.id ? { ...c, ...form } as unknown as Contact : c))
|
||||
setEditContact(null)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ export function TiersPanel({ isAdmin, tiers: initial }: { isAdmin: boolean; tier
|
||||
onSubmit={async e => {
|
||||
e.preventDefault()
|
||||
const cents = Math.round(parseFloat(d.dollars || '0') * 100)
|
||||
const result = await createTier({ slug: d.slug.trim().toLowerCase(), name: d.name.trim(), description: d.description.trim() || undefined, monthly_price_cents: isNaN(cents) ? 0 : cents, billing_cadence: d.cadence })
|
||||
const result = await createTier({ slug: d.slug.trim().toLowerCase(), name: d.name.trim(), description: d.description.trim() || undefined, monthly_price_cents: isNaN(cents) ? 0 : cents, billing_cadence: d.cadence as 'monthly' | 'quarterly' | 'annual' | 'one_time' | 'custom' })
|
||||
if ('error' in result && result.error) { toast.error(result.error); return }
|
||||
toast.success('Tier created')
|
||||
setTiers(prev => [...prev, { id: (result as { id: string }).id, slug: d.slug.trim().toLowerCase(), name: d.name.trim(), description: d.description.trim() || null, monthly_price_cents: isNaN(cents) ? 0 : cents, billing_cadence: d.cadence, is_system: false, is_archived: false, sort_order: 100 }])
|
||||
|
||||
@@ -30,7 +30,16 @@ export default async function FormResultsPage({ params }: { params: Promise<{ id
|
||||
formId={form.id}
|
||||
title={form.title}
|
||||
fields={parseFormSchema(form.schema)}
|
||||
submissions={submissions ?? []}
|
||||
submissions={
|
||||
(submissions ?? []) as {
|
||||
id: string
|
||||
member_id: string | null
|
||||
submitter_email: string | null
|
||||
answers: Record<string, unknown> | null
|
||||
form_version: number
|
||||
created_at: string
|
||||
}[]
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ export function ResultsClient({
|
||||
toast.error(res.error || 'Export failed')
|
||||
return
|
||||
}
|
||||
if (!res.data) return
|
||||
const { filename, csv } = res.data
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
@@ -98,10 +98,12 @@ export default async function MePage() {
|
||||
}))
|
||||
|
||||
const signupRes = await getMyClassSignups()
|
||||
const classSignups: ClassSignup[] = 'data' in signupRes ? (signupRes.data as ClassSignup[]) : []
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const classSignups: ClassSignup[] = 'data' in signupRes ? (signupRes.data as unknown as ClassSignup[]) : []
|
||||
|
||||
const reservationRes = await getMyReservations()
|
||||
const reservations: Reservation[] = 'data' in reservationRes ? (reservationRes.data as Reservation[]) : []
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
const reservations: Reservation[] = 'data' in reservationRes ? (reservationRes.data as unknown as Reservation[]) : []
|
||||
|
||||
const cardsRes = await getMyCards()
|
||||
const myCards: MyCard[] = 'data' in cardsRes ? (cardsRes.data as MyCard[]) : []
|
||||
@@ -121,7 +123,7 @@ export default async function MePage() {
|
||||
|
||||
const notifsRes = await getMyNotifications()
|
||||
const notifs: MyNotif[] = 'data' in notifsRes ? (notifsRes.data as MyNotif[]) : []
|
||||
const unreadNotifs = 'unreadCount' in notifsRes ? notifsRes.unreadCount : 0
|
||||
const unreadNotifs = 'unreadCount' in notifsRes ? (notifsRes.unreadCount ?? 0) : 0
|
||||
|
||||
const notifPrefsRes = await getMyNotificationPreferences()
|
||||
const notifPrefs: PrefMap = 'data' in notifPrefsRes ? notifPrefsRes.data : {}
|
||||
|
||||
@@ -96,7 +96,7 @@ export default function KbEntryEditor({ entry, member }: Props) {
|
||||
<label className="font-sans text-xs font-medium text-muted-foreground block mb-1.5">Visibility</label>
|
||||
<select
|
||||
value={visibility}
|
||||
onChange={e => setVisibility(e.target.value)}
|
||||
onChange={e => setVisibility(e.target.value as 'board' | 'all_members' | 'admin_only')}
|
||||
className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground focus:outline-none focus:border-primary transition"
|
||||
>
|
||||
<option value="all_members">All Members</option>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import { ArrowLeft } from 'lucide-react'
|
||||
import KbEntryEditor from './kb-entry-editor'
|
||||
import type { Tables } from '@/types/database'
|
||||
|
||||
export default async function OpsEntryPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params
|
||||
@@ -19,5 +20,5 @@ export default async function OpsEntryPage({ params }: { params: Promise<{ id: s
|
||||
|
||||
if (!entry) notFound()
|
||||
|
||||
return <KbEntryEditor entry={entry} member={member} />
|
||||
return <KbEntryEditor entry={entry} member={member as Tables<'space_members'>} />
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import type { Tables } from '@/types/database'
|
||||
import KbEntryEditor from '../[id]/kb-entry-editor'
|
||||
|
||||
export default async function NewOpsEntryPage() {
|
||||
@@ -10,5 +11,5 @@ export default async function NewOpsEntryPage() {
|
||||
.from('space_members').select('space_id, role, display_name').eq('user_id', user.id).in('status', ['current', 'unverified', 'late']).single()
|
||||
if (!member) return null
|
||||
|
||||
return <KbEntryEditor member={member} />
|
||||
return <KbEntryEditor member={member as Tables<'space_members'>} />
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { revealSecret, deleteSecret } from '@/lib/actions/secrets'
|
||||
import { OpsAclEditor } from '@/components/ops/ops-acl-editor'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import { toast } from 'sonner'
|
||||
import type { Tables, TablesInsert } from '@/types/database'
|
||||
import type { Tables, TablesInsert, TablesUpdate, Enums } from '@/types/database'
|
||||
import { PageTitle } from '@/components/ui/page-title'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { useConfirm } from '@/components/ui/confirm'
|
||||
@@ -135,7 +135,7 @@ function KbModal({
|
||||
<label className="font-sans text-xs font-medium text-muted-foreground block mb-1.5">Visibility</label>
|
||||
<select
|
||||
value={visibility}
|
||||
onChange={e => setVisibility(e.target.value)}
|
||||
onChange={e => setVisibility(e.target.value as Enums<'kb_visibility'>)}
|
||||
className="w-full bg-background border border-border rounded px-3 py-2 font-sans text-sm text-foreground focus:outline-none focus:border-primary transition"
|
||||
>
|
||||
<option value="all_members">All Members</option>
|
||||
@@ -212,7 +212,11 @@ function AddSecretModal({
|
||||
label: title.trim(),
|
||||
area: area.trim() || null,
|
||||
value: value.trim(),
|
||||
}).select('id, title, area, created_at').single()
|
||||
// `satisfies` keeps the payload type-checked; the trailing `as never`
|
||||
// only bridges the typed browser client whose .insert() generic collapses
|
||||
// to `never`.
|
||||
// TODO(types): remove after regenerating types/database.ts (missing FK relationship metadata)
|
||||
} satisfies TablesInsert<'secrets'> as never).select('id, title, area, created_at').single()
|
||||
setSaving(false)
|
||||
if (error) { toast.error(error.message); return }
|
||||
toast.success('Secret saved')
|
||||
@@ -298,14 +302,16 @@ function AreaLeadModal({
|
||||
area_name: areaName.trim(),
|
||||
lead_handle: memberName.trim(),
|
||||
description: contactInfo.trim() || null,
|
||||
}).eq('id', lead.id).select('*').single()
|
||||
// TODO(types): remove `as never` after regenerating types/database.ts (missing FK relationship metadata)
|
||||
} satisfies TablesUpdate<'area_leads'> as never).eq('id', lead.id).select('*').single()
|
||||
} else {
|
||||
result = await supabase.from('area_leads').insert({
|
||||
space_id: spaceId,
|
||||
area_name: areaName.trim(),
|
||||
lead_handle: memberName.trim(),
|
||||
description: contactInfo.trim() || null,
|
||||
}).select('*').single()
|
||||
// TODO(types): remove `as never` after regenerating types/database.ts (missing FK relationship metadata)
|
||||
} satisfies TablesInsert<'area_leads'> as never).select('*').single()
|
||||
}
|
||||
setSaving(false)
|
||||
if (result.error) { toast.error(result.error.message); return }
|
||||
|
||||
@@ -55,7 +55,7 @@ export default async function OpsPage() {
|
||||
spaceId={member.space_id}
|
||||
kbEntries={kbEntries ?? []}
|
||||
areaLeads={areaLeads ?? []}
|
||||
secrets={secretsResult.data ?? []}
|
||||
secrets={(secretsResult.data ?? []) as Tables<'secrets'>[]}
|
||||
canSeeSecrets={canSeeSecrets}
|
||||
canManageAcl={canManageAcl}
|
||||
aclRoleOptions={aclRoleOptions}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ComponentProps } from 'react'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { PaymentsClient } from './payments-client'
|
||||
|
||||
@@ -27,7 +28,7 @@ export default async function PaymentsPage() {
|
||||
|
||||
return (
|
||||
<PaymentsClient
|
||||
payments={payments ?? []}
|
||||
payments={(payments ?? []) as ComponentProps<typeof PaymentsClient>['payments']}
|
||||
members={members ?? []}
|
||||
integrations={integrations ?? []}
|
||||
currentRole={member.role}
|
||||
|
||||
@@ -58,7 +58,7 @@ export function ProjectsClient({
|
||||
async function handleStatusChange(projectId: string, newStatus: string) {
|
||||
const result = await updateProjectStatus(projectId, newStatus)
|
||||
if (result?.error) { toast.error(result.error); return }
|
||||
setProjects(prev => prev.map(p => p.id === projectId ? { ...p, status: newStatus } : p))
|
||||
setProjects(prev => prev.map(p => p.id === projectId ? { ...p, status: newStatus as Project['status'] } : p))
|
||||
}
|
||||
|
||||
async function handleDelete(projectId: string) {
|
||||
|
||||
@@ -21,6 +21,8 @@ export default async function SettingsPage() {
|
||||
const { data: space } = await supabase
|
||||
.from('spaces').select('*').eq('id', member.space_id).single()
|
||||
|
||||
if (!space) redirect('/dashboard')
|
||||
|
||||
const { data: integrations } = await supabase
|
||||
.from('integrations').select('*').eq('space_id', member.space_id)
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ export default function SettingsClient({ space, isAdmin, integrations, currentRo
|
||||
const existing = integrations.find(i => i.platform === config.platform)
|
||||
const initialForm: Record<string, string> = {}
|
||||
config.fields.forEach(f => {
|
||||
initialForm[f.key] = existing?.config?.[f.key] || ''
|
||||
initialForm[f.key] = (existing?.config as Record<string, string> | undefined)?.[f.key] || ''
|
||||
})
|
||||
setIntegrationForm(initialForm)
|
||||
setEditingIntegration(config)
|
||||
|
||||
@@ -86,7 +86,7 @@ export function TasksClient({ tasks: initialTasks, members, currentUserId, space
|
||||
const member = members.find(m => m.user_id === currentUserId)
|
||||
setTasks(prev => prev.map(t =>
|
||||
t.id === taskId
|
||||
? { ...t, status: 'claimed', claimed_by: currentUserId, claimed_by_name: member?.display_name }
|
||||
? { ...t, status: 'claimed', claimed_by: currentUserId, claimed_by_name: member?.display_name } as Task
|
||||
: t
|
||||
))
|
||||
}
|
||||
|
||||
@@ -265,15 +265,18 @@ export async function POST(
|
||||
.eq('external_id', inv.id)
|
||||
.maybeSingle()
|
||||
if (!dupe) {
|
||||
await admin.from('payments').insert(
|
||||
stripeInvoiceToPaymentRow({
|
||||
inv,
|
||||
spaceId,
|
||||
memberId,
|
||||
eventId: event.id,
|
||||
nowIso: new Date().toISOString(),
|
||||
}),
|
||||
)
|
||||
const paymentRow = stripeInvoiceToPaymentRow({
|
||||
inv,
|
||||
spaceId,
|
||||
memberId,
|
||||
eventId: event.id,
|
||||
nowIso: new Date().toISOString(),
|
||||
})
|
||||
await admin.from('payments').insert({
|
||||
...paymentRow,
|
||||
status: paymentRow.status as 'linked' | 'unlinked',
|
||||
link_status: paymentRow.link_status as 'linked' | 'unlinked',
|
||||
})
|
||||
}
|
||||
if (memberId) {
|
||||
await admin
|
||||
|
||||
@@ -25,6 +25,16 @@ export default async function PublicFormPage({
|
||||
}
|
||||
|
||||
const form = res.data
|
||||
if (!form) {
|
||||
return (
|
||||
<main className="mx-auto flex min-h-screen max-w-xl flex-col items-center justify-center p-6 text-center">
|
||||
<h1 className="text-xl font-semibold">This form is not available</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
It may be unpublished, closed, or the link is wrong.
|
||||
</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
const supabase = await createClient()
|
||||
const {
|
||||
data: { user },
|
||||
|
||||
@@ -30,7 +30,7 @@ export function TrackClient({
|
||||
setError(res.error)
|
||||
return
|
||||
}
|
||||
setView(res.data)
|
||||
setView(res.data ?? null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user