diff --git a/apps/web-next/bun.lockb b/apps/web-next/bun.lockb index eb52e0d0..8c7e9235 100755 Binary files a/apps/web-next/bun.lockb and b/apps/web-next/bun.lockb differ diff --git a/apps/web-next/package.json b/apps/web-next/package.json index 2b346d58..4a17e086 100644 --- a/apps/web-next/package.json +++ b/apps/web-next/package.json @@ -20,7 +20,7 @@ "dependencies": { "@fontsource-variable/roboto-mono": "^5.1.0", "@knpwrs/envariant": "^1.1.1", - "@modular-forms/solid": "^0.20.0", + "@modular-forms/solid": "^0.25.0", "@sentry/browser": "^7.114.0", "@solid-primitives/autofocus": "^0.0.111", "@solid-primitives/input-mask": "^0.2.2", @@ -59,7 +59,7 @@ "tailwind-merge": "^2.5.5", "tailwindcss": "^3.4.16", "tiny-invariant": "^1.3.3", - "valibot": "^0.30.0", + "valibot": "^v1.0.0-beta.9", "video.js": "^8.21.0", "vinxi": "^0.5.1", "xss": "^1.0.15", @@ -90,7 +90,7 @@ "prettier": "^3.4.2", "prettier-plugin-tailwindcss": "^0.6.9", "type-fest": "^4.30.2", - "typescript": "^5.4.5", + "typescript": "^5.7.2", "vite-plugin-solid-svg": "^0.8.1", "wait-on": "^7.2.0" }, diff --git a/apps/web-next/src/components/churches/churches.tsx b/apps/web-next/src/components/churches/churches.tsx index a00f1e86..5d356bdf 100644 --- a/apps/web-next/src/components/churches/churches.tsx +++ b/apps/web-next/src/components/churches/churches.tsx @@ -144,8 +144,9 @@ export default function ChurchesApp(props: { } onMount(() => { + invariant(mapNode!, 'Map node should be defined'); const map = new mapboxgl.Map({ - container: mapNode, + container: mapNode!, style: 'mapbox://styles/mapbox/streets-v12', center: murica, zoom: 4, diff --git a/apps/web-next/src/components/churches/searchbox/location.tsx b/apps/web-next/src/components/churches/searchbox/location.tsx index ea074a13..21549b72 100644 --- a/apps/web-next/src/components/churches/searchbox/location.tsx +++ b/apps/web-next/src/components/churches/searchbox/location.tsx @@ -7,7 +7,9 @@ import { parse, string, unknown, - type Input, + type InferInput, + looseObject, + objectWithRest, } from 'valibot'; import { useSearchParams } from '@solidjs/router'; import { type Optional, cn, unwrapFirst } from '../../../util'; @@ -23,41 +25,32 @@ const sessionToken = window.crypto.randomUUID(); export const murica = [-97.9222112121185, 39.3812661305678] as [number, number]; const defaultRange = '100 mi'; -const suggestionSchema = object( - { - name: string(), - feature_type: string(), - address: optional(string()), - full_address: optional(string()), - place_formatted: string(), - mapbox_id: string(), - }, - unknown(), -); - -const locationSuggestSchema = object( - { - suggestions: array(suggestionSchema), - }, - unknown(), -); - -const reverseGeocodeSchema = object( - { - features: array( - object( - { - place_name: string(), - }, - unknown(), - ), +const suggestionSchema = looseObject({ + name: string(), + feature_type: string(), + address: optional(string()), + full_address: optional(string()), + place_formatted: string(), + mapbox_id: string(), +}); + +const locationSuggestSchema = looseObject({ + suggestions: array(suggestionSchema), +}); + +const reverseGeocodeSchema = looseObject({ + features: array( + objectWithRest( + { + place_name: string(), + }, + unknown(), ), - }, - unknown(), -); + ), +}); async function reverseGeocode([long, lat]: [number, number]): Promise< - Input + InferInput > { const res = await fetch( `https://api.mapbox.com/geocoding/v5/mapbox.places/${long},${lat}.json?access_token=${mbAccessToken}`, @@ -66,26 +59,25 @@ async function reverseGeocode([long, lat]: [number, number]): Promise< return parse(reverseGeocodeSchema, await res.json()); } -const retrieveSchema = object( - { - features: array( - object( - { - properties: object( - { - coordinates: object({ longitude: number(), latitude: number() }), - }, - unknown(), - ), - }, - unknown(), - ), +const retrieveSchema = looseObject({ + features: array( + objectWithRest( + { + properties: objectWithRest( + { + coordinates: object({ longitude: number(), latitude: number() }), + }, + unknown(), + ), + }, + unknown(), ), - }, - unknown(), -); + ), +}); -async function retrieve(id: string): Promise> { +async function retrieve( + id: string, +): Promise> { const res = await fetch( `https://api.mapbox.com/search/searchbox/v1/retrieve/${id}?session_token=${sessionToken}&access_token=${mbAccessToken}`, ); diff --git a/apps/web-next/src/components/churches/searchbox/result-row.tsx b/apps/web-next/src/components/churches/searchbox/result-row.tsx index ab0b9121..f54d867d 100644 --- a/apps/web-next/src/components/churches/searchbox/result-row.tsx +++ b/apps/web-next/src/components/churches/searchbox/result-row.tsx @@ -1,4 +1,5 @@ import { createEffect, type ParentProps } from 'solid-js'; +import invariant from 'tiny-invariant'; import { type Optional, cn } from '../../../util'; export default function ResultRow( @@ -11,6 +12,7 @@ export default function ResultRow( let el: HTMLLIElement; createEffect(() => { + invariant(el!, 'ResultRow: el is undefined'); if (props.activeId === props.id) { el.scrollIntoView({ block: 'nearest' }); } diff --git a/apps/web-next/src/components/churches/searchbox/searchbox.tsx b/apps/web-next/src/components/churches/searchbox/searchbox.tsx index 4a2090c7..c4166202 100644 --- a/apps/web-next/src/components/churches/searchbox/searchbox.tsx +++ b/apps/web-next/src/components/churches/searchbox/searchbox.tsx @@ -54,7 +54,7 @@ export default function Searchbox(props: { hidden?: Optional> }) { let inputEl: HTMLInputElement; function clearInput() { - if (inputEl) { + if (inputEl!) { inputEl.value = ''; inputEl.focus(); } @@ -246,7 +246,9 @@ export default function Searchbox(props: { hidden?: Optional> }) { ref={setReference} class="flex cursor-text flex-wrap gap-2 rounded-md px-3 py-1.5 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600" onClick={() => { - inputEl?.focus(); + if (inputEl!) { + inputEl.focus(); + } setFloatOpen((o) => !o); }} > diff --git a/apps/web-next/src/components/comment.tsx b/apps/web-next/src/components/comment.tsx index 6df4b3f3..4f96b4cf 100644 --- a/apps/web-next/src/components/comment.tsx +++ b/apps/web-next/src/components/comment.tsx @@ -54,7 +54,7 @@ export function CommentForm(props: { let ref: HTMLTextAreaElement; onMount(() => { - if (ref && props.autofocus) { + if (ref! && props.autofocus) { ref.focus(); ref.scrollIntoView(); } diff --git a/apps/web-next/src/components/media/player/player.tsx b/apps/web-next/src/components/media/player/player.tsx index d5d77dd2..151b5e2a 100644 --- a/apps/web-next/src/components/media/player/player.tsx +++ b/apps/web-next/src/components/media/player/player.tsx @@ -12,6 +12,7 @@ import { import type VideoJsPlayer from 'video.js/dist/types/player'; import 'video.js/dist/video-js.css'; import { isServer } from 'solid-js/web'; +import invariant from 'tiny-invariant'; import { MediaRouteRecordViewRangesMutation, MediaRouteRecordViewRangesMutationVariables, @@ -97,6 +98,8 @@ export default function Player(props: Props) { return; } + invariant(videoRef!, 'player reportTimeRanges: videoRef is undefined'); + try { const res = await recordViewRanges( id, @@ -134,6 +137,8 @@ export default function Player(props: Props) { }); } + invariant(videoRef!, 'player onMount: videoRef is undefined'); + player = videojs( videoRef, { diff --git a/apps/web-next/src/components/media/player/waveform.tsx b/apps/web-next/src/components/media/player/waveform.tsx index 01234305..aae56116 100644 --- a/apps/web-next/src/components/media/player/waveform.tsx +++ b/apps/web-next/src/components/media/player/waveform.tsx @@ -33,6 +33,7 @@ export default function Waveform(props: Props) { setBarCount(Math.floor(entry.contentRect.width / TARGET_BAR_WIDTH)); }); + invariant(container!, 'waveform: container is undefined'); rob.observe(container); }); diff --git a/apps/web-next/src/components/settings/church-form.tsx b/apps/web-next/src/components/settings/church-form.tsx index b31a0a68..36a433cb 100644 --- a/apps/web-next/src/components/settings/church-form.tsx +++ b/apps/web-next/src/components/settings/church-form.tsx @@ -12,7 +12,8 @@ import { optional, parse, string, - type Input as VInput, + type InferInput, + pipe, } from 'valibot'; import { For, @@ -54,23 +55,25 @@ import { getAuthenticatedClientOrRedirect } from '~/util/gql/server'; export const formSchema = object({ id: optional(string()), - name: string([minLength(1, 'Please enter a name for your church.')]), - slug: string([minLength(3, 'Please enter a URL name for your church.')]), + name: pipe(string(), minLength(1, 'Please enter a name for your church.')), + slug: pipe( + string(), + minLength(3, 'Please enter a URL name for your church.'), + ), description: optional(nullable(string())), tags: optional(array(string())), websiteUrl: optional(nullable(string())), - primaryEmail: optional(nullable(string([email()]))), + primaryEmail: optional(nullable(pipe(string(), email()))), primaryPhoneNumber: optional(nullable(string())), leaders: optional( array( object({ name: optional(nullable(string())), type: enum_(OrganizationLeaderType), - email: optional(nullable(string([email()]))), + email: optional(nullable(pipe(string(), email()))), phoneNumber: optional(nullable(string())), }), ), - [], ), addresses: optional( array( @@ -83,14 +86,13 @@ export const formSchema = object({ streetAddress: optional(nullable(string()), null), }), ), - [], ), - upstreamAssociations: optional(array(string()), []), + upstreamAssociations: optional(array(string())), }); const phoneNumberMask = createInputMask('(999) 999-9999'); -type FormSchema = VInput; +type FormSchema = InferInput; const upsertChurch = action(async (data: FormSchema) => { 'use server'; @@ -375,7 +377,7 @@ function LeadershipForm(props: { export default function ChurchForm(props: { initialValues?: FormSchema }) { // TODO: Why do the initial values not render properly on refresh even though they do on navigation? - const store = createFormStore({ + const store = createFormStore({ validate: valiForm(formSchema), ...(props.initialValues ? { initialValues: props.initialValues } : {}), }); diff --git a/apps/web-next/src/components/turnstile/client.tsx b/apps/web-next/src/components/turnstile/client.tsx index ade9f4a3..5a988c16 100644 --- a/apps/web-next/src/components/turnstile/client.tsx +++ b/apps/web-next/src/components/turnstile/client.tsx @@ -31,7 +31,7 @@ export default function TurnstileClient( let element: HTMLDivElement; const ready = () => { - invariant(element); + invariant(element!); window.onloadTurnstileCallback = undefined; window.turnstile.render(element, { sitekey: import.meta.env['VITE_TURNSTILE_SITEKEY'], diff --git a/apps/web-next/src/routes/(root)/upload.tsx b/apps/web-next/src/routes/(root)/upload.tsx index 2b7216c4..444495b9 100644 --- a/apps/web-next/src/routes/(root)/upload.tsx +++ b/apps/web-next/src/routes/(root)/upload.tsx @@ -457,6 +457,7 @@ export default function UploadRoute() { let formRef: HTMLFormElement; async function submitUpsert() { + invariant(formRef!, 'formRef should be defined'); const res = await upsertAction(new FormData(formRef)); setUploadRecordId(res.upsertUploadRecord.id); }