diff --git a/apps/web-next/bun.lockb b/apps/web-next/bun.lockb index 72c27b98..24291619 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 161861ed..68a2bd57 100644 --- a/apps/web-next/package.json +++ b/apps/web-next/package.json @@ -27,8 +27,8 @@ "@solid-primitives/media": "^2.2.9", "@solid-primitives/scheduled": "^1.4.3", "@solidjs/meta": "^0.29.4", - "@solidjs/router": "^0.13.3", - "@solidjs/start": "^1.0.0", + "@solidjs/router": "^0.15.2", + "@solidjs/start": "^1.0.10", "@tabler/icons": "^3.3.0", "autoprefixer": "^10.4.19", "binary-search": "^1.3.6", @@ -54,14 +54,14 @@ "pretty-ms": "^9.0.0", "slugify": "^1.6.6", "solid-floating-ui": "^0.3.1", - "solid-js": "^1.8.17", + "solid-js": "^1.9.3", "solid-mdx": "^0.0.7", "tailwind-merge": "^2.3.0", "tailwindcss": "^3.4.3", "tiny-invariant": "^1.3.3", "valibot": "^0.30.0", "video.js": "^8.12.0", - "vinxi": "^0.3.11", + "vinxi": "^0.5.1", "xss": "^1.0.15", "zod": "^3.23.8" }, @@ -81,7 +81,7 @@ "@types/wait-on": "^5.3.4", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", - "@vinxi/plugin-mdx": "^3.7.1", + "@vinxi/plugin-mdx": "^3.7.2", "eslint": "^8.57.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", @@ -93,5 +93,8 @@ "typescript": "^5.4.5", "vite-plugin-solid-svg": "^0.8.1", "wait-on": "^7.2.0" + }, + "overrides": { + "vite": "5.4.10" } } diff --git a/apps/web-next/src/components/churches/data.ts b/apps/web-next/src/components/churches/data.ts index fa101b70..f6b33be7 100644 --- a/apps/web-next/src/components/churches/data.ts +++ b/apps/web-next/src/components/churches/data.ts @@ -5,6 +5,7 @@ import { ChurchesDataQueryVariables, } from './__generated__/data'; import { getAuthenticatedClient } from '~/util/gql/server'; +import { unwrapFirst } from '~/util'; export async function getChurchesData(f: Filters) { 'use server'; @@ -66,8 +67,8 @@ export async function getChurchesData(f: Filters) { { lon: f.center[0], lat: f.center[1], - range: f.range, - organization: f.organization ?? null, + range: unwrapFirst(f.range) ?? '', + organization: unwrapFirst(f.organization), tags: f.tags, }, ); diff --git a/apps/web-next/src/components/churches/searchbox/location.tsx b/apps/web-next/src/components/churches/searchbox/location.tsx index de58d544..ea074a13 100644 --- a/apps/web-next/src/components/churches/searchbox/location.tsx +++ b/apps/web-next/src/components/churches/searchbox/location.tsx @@ -10,7 +10,7 @@ import { type Input, } from 'valibot'; import { useSearchParams } from '@solidjs/router'; -import { type Optional, cn } from '../../../util'; +import { type Optional, cn, unwrapFirst } from '../../../util'; import ListHeading from './list-heading'; import { getMenuColorClass, optionId } from './util'; import ResultRow from './result-row'; @@ -101,10 +101,10 @@ export const useParsedLocation = () => { searchParams['range'] ?? (searchParams['center'] ? defaultRange : '25000 mi'), center: - (searchParams['center']?.split(',').map(parseFloat).slice(0, 2) as [ - number, - number, - ]) ?? murica, + (unwrapFirst(searchParams['center']) + ?.split(',') + .map(parseFloat) + .slice(0, 2) as [number, number]) ?? murica, })); return parsed; @@ -125,7 +125,7 @@ export function locationState(clearInput: () => unknown) { const [searchParams, setSearchParams] = useSearchParams(); onMount(async () => { - const center = searchParams['center']; + const center = unwrapFirst(searchParams['center']); if (center) { const res = await reverseGeocode( diff --git a/apps/web-next/src/components/churches/searchbox/organization.tsx b/apps/web-next/src/components/churches/searchbox/organization.tsx index 7f115431..9035d0c6 100644 --- a/apps/web-next/src/components/churches/searchbox/organization.tsx +++ b/apps/web-next/src/components/churches/searchbox/organization.tsx @@ -1,7 +1,7 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import { useSearchParams } from '@solidjs/router'; import { gql } from 'graphql-request'; -import { type Optional, cn } from '../../../util'; +import { type Optional, cn, unwrapFirst } from '../../../util'; import ListHeading from './list-heading'; import ResultRow from './result-row'; import { getMenuColorClass, optionId } from './util'; @@ -104,7 +104,7 @@ export function organizationState(clearInput: () => unknown) { const orgId = searchParams[organizationSlug]; if (orgId) { - const res = await fetchOrganization(orgId); + const res = await fetchOrganization(unwrapFirst(orgId) ?? ''); setOrganizationLabel(res.name); } }); diff --git a/apps/web-next/src/components/churches/searchbox/searchbox.tsx b/apps/web-next/src/components/churches/searchbox/searchbox.tsx index a2a931b1..4a2090c7 100644 --- a/apps/web-next/src/components/churches/searchbox/searchbox.tsx +++ b/apps/web-next/src/components/churches/searchbox/searchbox.tsx @@ -27,7 +27,7 @@ import { import { TagsMenu, useParsedTags, tagSlug, tagsState } from './tags'; import { optionId } from './util'; import { OrganizationTag } from '~/__generated__/graphql-types'; -import type { Optional } from '~/util'; +import { unwrapFirst, type Optional } from '~/util'; const hiddenOrganization = 'organization'; @@ -152,7 +152,7 @@ export default function Searchbox(props: { hidden?: Optional> }) { } else if (chiclet.slug === organizationSlug) { onClearOrganization(); } else { - const newTags = searchParams[tagSlug] + const newTags = unwrapFirst(searchParams[tagSlug]) ?.split(',') .filter((t) => t !== chiclet.slug); setSearchParams({ @@ -231,7 +231,7 @@ export default function Searchbox(props: { hidden?: Optional> }) { } else if (slug === 'organization') { onClearOrganization(); } else if (slug) { - const newTags = searchParams[tagSlug] + const newTags = unwrapFirst(searchParams[tagSlug]) ?.split(',') .filter((t) => t !== slug); setSearchParams({ diff --git a/apps/web-next/src/components/churches/searchbox/tags.tsx b/apps/web-next/src/components/churches/searchbox/tags.tsx index fbc38d3a..6a9a821d 100644 --- a/apps/web-next/src/components/churches/searchbox/tags.tsx +++ b/apps/web-next/src/components/churches/searchbox/tags.tsx @@ -1,7 +1,7 @@ import { For, Show, createMemo, createSignal, onMount } from 'solid-js'; import { useSearchParams } from '@solidjs/router'; import { gql } from 'graphql-request'; -import { type Optional, cn } from '../../../util'; +import { type Optional, cn, unwrapFirst } from '../../../util'; import ListHeading from './list-heading'; import { getMenuColorClass, getOrgTagCategoryLabel, optionId } from './util'; import ResultRow from './result-row'; @@ -18,7 +18,7 @@ export const useParsedTags = () => { const [searchParams] = useSearchParams(); const parsed = createMemo(() => ({ - tags: searchParams['tag']?.split(',') ?? [], + tags: unwrapFirst(searchParams['tag'])?.split(',') ?? [], })); return parsed; @@ -131,7 +131,7 @@ export function TagsMenu(props: { const [searchParams, setSearchParams] = useSearchParams(); function addTag(tag: OrganizationTagQueryNode) { - const tags = searchParams[tagSlug]?.split(',') ?? []; + const tags = unwrapFirst(searchParams[tagSlug])?.split(',') ?? []; tags.push(tag.slug); setSearchParams({ [tagSlug]: tags.join(',') }); props.clearInput(); diff --git a/apps/web-next/src/components/content/about-stats.tsx b/apps/web-next/src/components/content/about-stats.tsx index f8e0255b..f52fbb2c 100644 --- a/apps/web-next/src/components/content/about-stats.tsx +++ b/apps/web-next/src/components/content/about-stats.tsx @@ -1,13 +1,13 @@ import { gql } from 'graphql-request'; import humanFormat from 'human-format'; -import { cache, createAsync } from '@solidjs/router'; +import { query, createAsync } from '@solidjs/router'; import type { AboutPageDataQuery, AboutPageDataQueryVariables, } from './__generated__/about-stats'; import { getAuthenticatedClient } from '~/util/gql/server'; -const getData = cache(async () => { +const getData = query(async () => { 'use server'; const client = await getAuthenticatedClient(); diff --git a/apps/web-next/src/components/navigating-checklist.tsx b/apps/web-next/src/components/navigating-checklist.tsx index f1da34d7..5ea14998 100644 --- a/apps/web-next/src/components/navigating-checklist.tsx +++ b/apps/web-next/src/components/navigating-checklist.tsx @@ -1,5 +1,6 @@ import { useLocation, useNavigate } from '@solidjs/router'; import { For, type JSX, splitProps } from 'solid-js'; +import { unwrapFirst } from '~/util'; import { setQueryParams } from '~/util/url'; type Link = { label: string; value: string; checked: boolean }; @@ -16,7 +17,7 @@ export default function NavigatingChecklist(props: Props) { const loc = useLocation(); const currentValues = () => - loc.query[local.queryKey]?.split(',').filter(Boolean) ?? []; + unwrapFirst(loc.query[local.queryKey])?.split(',').filter(Boolean) ?? []; function onChange({ value, checked }: { value: string; checked: boolean }) { navigate( diff --git a/apps/web-next/src/components/navigating-date-range.tsx b/apps/web-next/src/components/navigating-date-range.tsx index 45dd9d5c..70d1fdb2 100644 --- a/apps/web-next/src/components/navigating-date-range.tsx +++ b/apps/web-next/src/components/navigating-date-range.tsx @@ -2,6 +2,7 @@ import { type JSX, splitProps } from 'solid-js'; import { useLocation, useNavigate } from '@solidjs/router'; import { Button, Input } from './form'; import { setQueryParams } from '~/util/url'; +import { unwrapFirst } from '~/util'; export type Props = JSX.IntrinsicElements['div'] & { min?: Date | null; @@ -32,7 +33,7 @@ export default function NavigatingDateRange(props: Props) { const currentValues = () => { const [min = dateToIso(local.min), max = dateToIso(local.max)] = - loc.query[local.queryKey]?.split('/').filter(Boolean) ?? []; + unwrapFirst(loc.query[local.queryKey])?.split('/').filter(Boolean) ?? []; return { min, max }; }; diff --git a/apps/web-next/src/routes/(root).tsx b/apps/web-next/src/routes/(root).tsx index efad39f2..ae0764c3 100644 --- a/apps/web-next/src/routes/(root).tsx +++ b/apps/web-next/src/routes/(root).tsx @@ -1,6 +1,6 @@ import { gql } from 'graphql-request'; import { ParentProps } from 'solid-js'; -import { type RouteDefinition, cache, createAsync } from '@solidjs/router'; +import { type RouteDefinition, createAsync, query } from '@solidjs/router'; import { Title } from '@solidjs/meta'; import type { MeQuery } from './__generated__/(root)'; import Footer from '~/components/footer'; @@ -11,7 +11,7 @@ import MediaHeader from '~/components/media/header'; import { isChurchesPage } from '~/util/routing'; import { cn } from '~/util'; -const getMe = cache(async function () { +const getMe = query(async function () { 'use server'; const client = await getAuthenticatedClient(); diff --git a/apps/web-next/src/routes/(root)/(watch).tsx b/apps/web-next/src/routes/(root)/(watch).tsx index 7fae9887..47d60fac 100644 --- a/apps/web-next/src/routes/(root)/(watch).tsx +++ b/apps/web-next/src/routes/(root)/(watch).tsx @@ -1,6 +1,6 @@ import { Show } from 'solid-js'; import { gql } from 'graphql-request'; -import { A, cache, createAsync } from '@solidjs/router'; +import { A, createAsync, query } from '@solidjs/router'; import { Link } from '@solidjs/meta'; import SubscribeIcon from '@tabler/icons/outline/rss.svg?component-solid'; import type { @@ -15,7 +15,7 @@ import Newsletter from '~/components/newsletter'; import { useUser } from '~/util/user-context'; import Og from '~/components/og'; -const getHomepageData = cache(async function () { +const getHomepageData = query(async function () { 'use server'; const client = await getAuthenticatedClient(); const res = await client.request< diff --git a/apps/web-next/src/routes/(root)/admin/channels/edit.tsx b/apps/web-next/src/routes/(root)/admin/channels/edit.tsx index cc267125..025d1dcf 100644 --- a/apps/web-next/src/routes/(root)/admin/channels/edit.tsx +++ b/apps/web-next/src/routes/(root)/admin/channels/edit.tsx @@ -4,11 +4,11 @@ import { Show } from 'solid-js'; import { type RouteDefinition, action, - cache, createAsync, redirect, useSubmission, useLocation, + query, } from '@solidjs/router'; import { AdminChannelEditRouteDataQuery, @@ -19,6 +19,7 @@ import { import { UpsertForm } from '~/components/admin/upsert-form'; import { PageHeading } from '~/components/page-heading'; import { getAdminClientOrRedirect } from '~/util/gql/server'; +import { unwrapFirst } from '~/util'; const UpsertChannelSchema = z.object({ channelId: z.string().nullable(), @@ -27,7 +28,7 @@ const UpsertChannelSchema = z.object({ description: z.string().nullable(), }); -const loadChannel = cache(async (id: string | null) => { +const loadChannel = query(async (id: string | null) => { 'use server'; const client = await getAdminClientOrRedirect(); @@ -56,7 +57,7 @@ const loadChannel = cache(async (id: string | null) => { export const route = { load: ({ location }) => { - void loadChannel(location.query['id'] ?? null); + void loadChannel(unwrapFirst(location.query['id'])); }, } satisfies RouteDefinition; @@ -99,7 +100,9 @@ const upsertChannel = action(async (form: FormData) => { export default function AdminChannelsEditRoute() { const location = useLocation(); - const data = createAsync(() => loadChannel(location.query['id'] ?? null)); + const data = createAsync(() => + loadChannel(unwrapFirst(location.query['id'])), + ); const submission = useSubmission(upsertChannel); return ( diff --git a/apps/web-next/src/routes/(root)/admin/organizations/edit.tsx b/apps/web-next/src/routes/(root)/admin/organizations/edit.tsx index 21f491ca..528dc9a6 100644 --- a/apps/web-next/src/routes/(root)/admin/organizations/edit.tsx +++ b/apps/web-next/src/routes/(root)/admin/organizations/edit.tsx @@ -1,13 +1,13 @@ import { gql } from 'graphql-request'; import * as z from 'zod'; import { Show } from 'solid-js'; -import { decodeJwt } from 'jose'; +// import { decodeJwt } from 'jose'; import { action, - cache, createAsync, redirect, - useSubmission, + // useSubmission, + query, } from '@solidjs/router'; import { getRequestEvent } from 'solid-js/web'; import { @@ -16,7 +16,7 @@ import { AdminUpsertOrganizationMutation, AdminUpsertOrganizationMutationVariables, } from './__generated__/edit'; -import { UpsertForm } from '~/components/admin/upsert-form'; +// import { UpsertForm } from '~/components/admin/upsert-form'; import { PageHeading } from '~/components/page-heading'; import { getAdminClientOrRedirect } from '~/util/gql/server'; @@ -28,16 +28,16 @@ const UpsertOrganizationSchema = z.object({ addressJwt: z.string().nullable(), }); -const ParseJwtSchema = z.object({ - label: z.string(), -}); - -function renderLabelFromJwt(jwt: string) { - const decoded = decodeJwt(jwt); - return ParseJwtSchema.parse(decoded).label; -} +// const ParseJwtSchema = z.object({ +// label: z.string(), +// }); -const loadOrganization = cache(async () => { +// function renderLabelFromJwt(jwt: string) { +// const decoded = decodeJwt(jwt); +// return ParseJwtSchema.parse(decoded).label; +// } +// +const loadOrganization = query(async () => { 'use server'; const event = getRequestEvent(); const url = new URL(event?.request.url ?? ''); @@ -129,7 +129,7 @@ const upsertOrganization = action(async (form: FormData) => { export default function AdminOrganizationsEditRoute() { const data = createAsync(() => loadOrganization()); - const submission = useSubmission(upsertOrganization); + // const submission = useSubmission(upsertOrganization); return ( <> @@ -141,41 +141,42 @@ export default function AdminOrganizationsEditRoute() { {(id) => } - [], - // TODO: maybe just one prop: valueAsString? - renderValue: (val) => (val ? renderLabelFromJwt(val) : ''), - renderMenuValue: (val) => renderLabelFromJwt(val), - }, - ], - }, - ]} - defaultValues={{ - name: data()?.organizationById?.name ?? '', - type: data()?.organizationById?.type ?? 'MINISTRY', - slug: data()?.organizationById?.slug ?? '', - }} - submitting={submission.pending} - /> +

TODO

+ {/* [], */} + {/* // TODO: maybe just one prop: valueAsString? */} + {/* renderValue: (val) => (val ? renderLabelFromJwt(val) : ''), */} + {/* renderMenuValue: (val) => renderLabelFromJwt(val), */} + {/* }, */} + {/* ], */} + {/* }, */} + {/* ]} */} + {/* defaultValues={{ */} + {/* name: data()?.organizationById?.name ?? '', */} + {/* type: data()?.organizationById?.type ?? 'MINISTRY', */} + {/* slug: data()?.organizationById?.slug ?? '', */} + {/* }} */} + {/* submitting={submission.pending} */} + {/* /> */} ); diff --git a/apps/web-next/src/routes/(root)/admin/users/(users).tsx b/apps/web-next/src/routes/(root)/admin/users/(users).tsx index a95a37e6..539e68d6 100644 --- a/apps/web-next/src/routes/(root)/admin/users/(users).tsx +++ b/apps/web-next/src/routes/(root)/admin/users/(users).tsx @@ -1,5 +1,5 @@ import { gql } from 'graphql-request'; -import { type RouteDefinition, cache, createAsync, A } from '@solidjs/router'; +import { type RouteDefinition, A, createAsync, query } from '@solidjs/router'; import { getRequestEvent } from 'solid-js/web'; import { AdminUsersRouteQuery, @@ -13,7 +13,7 @@ import Pagination from '~/components/pagination'; const PAGE_SIZE = 60; -const loadUsers = cache(async () => { +const loadUsers = query(async () => { 'use server'; const event = getRequestEvent(); const url = new URL(event?.request.url ?? ''); diff --git a/apps/web-next/src/routes/(root)/admin/users/edit.tsx b/apps/web-next/src/routes/(root)/admin/users/edit.tsx index 7fd1e742..5e2c26d2 100644 --- a/apps/web-next/src/routes/(root)/admin/users/edit.tsx +++ b/apps/web-next/src/routes/(root)/admin/users/edit.tsx @@ -4,10 +4,10 @@ import { Show } from 'solid-js'; import { type RouteDefinition, createAsync, - cache, action, redirect, useSubmission, + query, } from '@solidjs/router'; import { getRequestEvent } from 'solid-js/web'; import { @@ -30,7 +30,7 @@ const UpsertUserSchema = z.object({ newPassword: z.string().nullable(), }); -const loadUser = cache(async () => { +const loadUser = query(async () => { 'use server'; const event = getRequestEvent(); const url = new URL(event?.request.url ?? ''); diff --git a/apps/web-next/src/routes/(root)/auth/reset-password.tsx b/apps/web-next/src/routes/(root)/auth/reset-password.tsx index c43d87e7..9011696b 100644 --- a/apps/web-next/src/routes/(root)/auth/reset-password.tsx +++ b/apps/web-next/src/routes/(root)/auth/reset-password.tsx @@ -15,13 +15,14 @@ import { Button, LabeledInput } from '~/components/form'; import { Turnstile } from '~/components/turnstile'; import validateTurnstile from '~/util/server/validate-turnstile'; import { getAuthenticatedClient } from '~/util/gql/server'; +import { unwrapFirst } from '~/util'; const ResetPasswordSchema = z.object({ id: z.string().uuid(), password: z.string(), }); -const routeData = async (id?: string) => { +const routeData = async (id?: string | null) => { 'use server'; if (!id) { throw redirect('/'); @@ -60,7 +61,7 @@ const resetPassword = action(async (form: FormData) => { export default function ResetPasswordRoute() { const location = useLocation(); - const data = createAsync(() => routeData(location.query['id'])); + const data = createAsync(() => routeData(unwrapFirst(location.query['id']))); const submission = useSubmission(resetPassword); return ( diff --git a/apps/web-next/src/routes/(root)/channel/[slug]/(slug).tsx b/apps/web-next/src/routes/(root)/channel/[slug]/(slug).tsx index 44d846a6..943dcc11 100644 --- a/apps/web-next/src/routes/(root)/channel/[slug]/(slug).tsx +++ b/apps/web-next/src/routes/(root)/channel/[slug]/(slug).tsx @@ -3,9 +3,9 @@ import { For } from 'solid-js'; import { gql } from 'graphql-request'; import { type RouteDefinition, - cache, createAsync, useParams, + query, } from '@solidjs/router'; import { getRequestEvent } from 'solid-js/web'; import { Link } from '@solidjs/meta'; @@ -21,7 +21,7 @@ import { UploadCardFields } from '~/util/gql/fragments'; const PAGE_SIZE = 60; -const loadChannel = cache(async (slug: string) => { +const loadChannel = query(async (slug: string) => { 'use server'; const event = getRequestEvent(); const url = new URL(event?.request.url ?? ''); diff --git a/apps/web-next/src/routes/(root)/churches/[slug].tsx b/apps/web-next/src/routes/(root)/churches/[slug].tsx index 7020bfa8..fce2771f 100644 --- a/apps/web-next/src/routes/(root)/churches/[slug].tsx +++ b/apps/web-next/src/routes/(root)/churches/[slug].tsx @@ -1,8 +1,8 @@ import { type RouteDefinition, - cache, useParams, createAsync, + query, } from '@solidjs/router'; import WorldIcon from '@tabler/icons/outline/world.svg?component-solid'; import MailIcon from '@tabler/icons/outline/mail.svg?component-solid'; @@ -19,7 +19,7 @@ import { Avatar } from '~/components/avatar'; import Chiclet from '~/components/churches/searchbox/chiclet'; import { OrganizationAddressType } from '~/__generated__/graphql-types'; -const loadChurch = cache(async (slug: string) => { +const loadChurch = query(async (slug: string) => { 'use server'; invariant(slug, 'Missing slug'); diff --git a/apps/web-next/src/routes/(root)/media/[id].tsx b/apps/web-next/src/routes/(root)/media/[id].tsx index 8cf4326c..7f7da121 100644 --- a/apps/web-next/src/routes/(root)/media/[id].tsx +++ b/apps/web-next/src/routes/(root)/media/[id].tsx @@ -12,14 +12,14 @@ import { gql } from 'graphql-request'; import { type RouteDefinition, action, - cache, createAsync, redirect, useLocation, useParams, useSubmission, - A, useAction, + query, + A, } from '@solidjs/router'; import { Title } from '@solidjs/meta'; import type { @@ -73,7 +73,7 @@ const recordView = async (id: string) => { return res; }; -const loadMediaMetadata = cache(async (id: string) => { +const loadMediaMetadata = query(async (id: string) => { 'use server'; const event = getRequestEvent(); const url = new URL(event?.request.url ?? ''); diff --git a/apps/web-next/src/routes/(root)/profile/(profile).tsx b/apps/web-next/src/routes/(root)/profile/(profile).tsx index ad78924a..52860bec 100644 --- a/apps/web-next/src/routes/(root)/profile/(profile).tsx +++ b/apps/web-next/src/routes/(root)/profile/(profile).tsx @@ -4,9 +4,9 @@ import delay from 'delay'; import { gql } from 'graphql-request'; import { action, - cache, createAsync, type RouteDefinition, + query, } from '@solidjs/router'; import type { CreateAvatarUploadMutation, @@ -45,7 +45,7 @@ const fields: Array = [ }, ]; -const loadData = cache(async () => { +const loadData = query(async () => { 'use server'; const client = await getAuthenticatedClientOrRedirect(); return await client.request< diff --git a/apps/web-next/src/routes/(root)/profile/channels/(channels).tsx b/apps/web-next/src/routes/(root)/profile/channels/(channels).tsx index f45daa25..7fe2e247 100644 --- a/apps/web-next/src/routes/(root)/profile/channels/(channels).tsx +++ b/apps/web-next/src/routes/(root)/profile/channels/(channels).tsx @@ -1,12 +1,12 @@ import { For } from 'solid-js'; import { gql } from 'graphql-request'; -import { type RouteDefinition, cache, createAsync } from '@solidjs/router'; +import { type RouteDefinition, createAsync, query } from '@solidjs/router'; import type { MyChannelsQuery } from './__generated__/(channels)'; import ChannelCard from '~/components/channel-card'; import { PageHeading } from '~/components/page-heading'; import { getAuthenticatedClientOrRedirect } from '~/util/gql/server'; -const loadChannels = cache(async () => { +const loadChannels = query(async () => { 'use server'; const client = await getAuthenticatedClientOrRedirect(); diff --git a/apps/web-next/src/routes/(root)/profile/channels/[id]/(index).tsx b/apps/web-next/src/routes/(root)/profile/channels/[id]/(index).tsx index 6160108e..0095d5f3 100644 --- a/apps/web-next/src/routes/(root)/profile/channels/[id]/(index).tsx +++ b/apps/web-next/src/routes/(root)/profile/channels/[id]/(index).tsx @@ -1,7 +1,7 @@ import { For } from 'solid-js'; import invariant from 'tiny-invariant'; import { gql } from 'graphql-request'; -import { type RouteDefinition, cache, createAsync } from '@solidjs/router'; +import { type RouteDefinition, createAsync, query } from '@solidjs/router'; import { getRequestEvent } from 'solid-js/web'; import type { ProfileChannelsQuery, @@ -15,7 +15,7 @@ import { UploadCardFields } from '~/util/gql/fragments'; const PAGE_SIZE = 60; -const loadChannel = cache(async function () { +const loadChannel = query(async function () { 'use server'; const event = getRequestEvent(); const url = new URL(event?.request.url ?? ''); diff --git a/apps/web-next/src/routes/(root)/profile/channels/[id]/edit.tsx b/apps/web-next/src/routes/(root)/profile/channels/[id]/edit.tsx index 02c4b847..1216e881 100644 --- a/apps/web-next/src/routes/(root)/profile/channels/[id]/edit.tsx +++ b/apps/web-next/src/routes/(root)/profile/channels/[id]/edit.tsx @@ -5,9 +5,9 @@ import { gql } from 'graphql-request'; import { getRequestEvent } from 'solid-js/web'; import { action, - cache, createAsync, type RouteDefinition, + query, } from '@solidjs/router'; import type { CreateChannelFileUploadMutation, @@ -28,7 +28,7 @@ import { UploadPostProcess } from '~/__generated__/graphql-types'; import { doMultipartUpload } from '~/util/multipart-upload'; import { Avatar } from '~/components/avatar'; -const loadChannel = cache(async () => { +const loadChannel = query(async () => { 'use server'; const event = getRequestEvent(); const url = new URL(event?.request.url ?? ''); diff --git a/apps/web-next/src/routes/(root)/profile/churches/(churches).tsx b/apps/web-next/src/routes/(root)/profile/churches/(churches).tsx index 9191c2b9..a6f01ab0 100644 --- a/apps/web-next/src/routes/(root)/profile/churches/(churches).tsx +++ b/apps/web-next/src/routes/(root)/profile/churches/(churches).tsx @@ -1,5 +1,5 @@ import { For } from 'solid-js'; -import { type RouteDefinition, cache, createAsync, A } from '@solidjs/router'; +import { type RouteDefinition, A, createAsync, query } from '@solidjs/router'; import { gql } from 'graphql-request'; import type { MyChurchesQuery, @@ -8,7 +8,7 @@ import type { import { PageHeading } from '~/components/page-heading'; import { getAuthenticatedClientOrRedirect } from '~/util/gql/server'; -const loadChurches = cache(async () => { +const loadChurches = query(async () => { 'use server'; const client = await getAuthenticatedClientOrRedirect(); diff --git a/apps/web-next/src/routes/(root)/profile/churches/edit.tsx b/apps/web-next/src/routes/(root)/profile/churches/edit.tsx index 42b73578..b5a8cceb 100644 --- a/apps/web-next/src/routes/(root)/profile/churches/edit.tsx +++ b/apps/web-next/src/routes/(root)/profile/churches/edit.tsx @@ -1,9 +1,9 @@ import { Show } from 'solid-js'; import { type RouteDefinition, - cache, createAsync, useLocation, + query, } from '@solidjs/router'; import { gql } from 'graphql-request'; import invariant from 'tiny-invariant'; @@ -14,8 +14,9 @@ import { import { getAuthenticatedClientOrRedirect } from '~/util/gql/server'; import { PageHeading } from '~/components/page-heading'; import ChurchForm from '~/components/settings/church-form'; +import { unwrapFirst } from '~/util'; -const loadChurch = cache(async (id: string) => { +const loadChurch = query(async (id: string) => { 'use server'; invariant(id, 'No ID provided'); @@ -96,7 +97,7 @@ const loadChurch = cache(async (id: string) => { export const route = { load: ({ location }) => { - const id = location.query['id']; + const id = unwrapFirst(location.query['id']); invariant(id, 'No ID provided'); void loadChurch(id); }, @@ -104,7 +105,9 @@ export const route = { export default function AdminOrganizationsEditRoute() { const location = useLocation(); - const data = createAsync(() => loadChurch(location.query['id'] ?? '')); + const data = createAsync(() => + loadChurch(unwrapFirst(location.query['id']) ?? ''), + ); return ( <> diff --git a/apps/web-next/src/routes/(root)/search.tsx b/apps/web-next/src/routes/(root)/search.tsx index a2643592..3c982550 100644 --- a/apps/web-next/src/routes/(root)/search.tsx +++ b/apps/web-next/src/routes/(root)/search.tsx @@ -17,14 +17,14 @@ import { A, type RouteDefinition, type Location, - cache, - useLocation, createAsync, + useLocation, + query, } from '@solidjs/router'; import type { SearchQuery, SearchQueryVariables } from './__generated__/search'; import Pagination from '~/components/pagination'; import { SearchFocus, SearchOrder } from '~/__generated__/graphql-types'; -import { cn, formatTime } from '~/util'; +import { cn, formatTime, unwrapFirst } from '~/util'; import FloatingDiv from '~/components/floating-div'; import NavigatingBooleans from '~/components/navigating-booleans'; import NavigatingChecklist from '~/components/navigating-checklist'; @@ -40,7 +40,7 @@ import { getAuthenticatedClient } from '~/util/gql/server'; const PAGE_SIZE = 20; -const loadData = cache(async function ( +const loadData = query(async function ( q = '', focus = 'uploads', after: string | null = null, @@ -173,13 +173,14 @@ const loadData = cache(async function ( function getRouteArgs(location: Location) { return [ - location.query['q'], - location.query['focus'], - location.query['after'], - location.query['before'], - location.query['publishedAt'], - location.query['orderBy'], - location.query['channels']?.split(',').filter(Boolean), + unwrapFirst(location.query['q']), + unwrapFirst(location.query['focus']), + unwrapFirst(location.query['after']), + unwrapFirst(location.query['before']), + unwrapFirst(location.query['publishedAt']), + unwrapFirst(location.query['orderBy']), + // TODO: use standard query parsing + unwrapFirst(location.query['channels'])?.split(',').filter(Boolean), (location.query['transcriptPhraseSearch'] ?? 'true') === 'true', ] as const; } @@ -348,7 +349,8 @@ export default function SearchRoute() { new URLSearchParams(location.search).get('channels')?.split(',').length ?? 0; const channelsValues = () => - location.query['channels']?.split(',').filter(Boolean) ?? []; + // TODO: use standard query parsing + unwrapFirst(location.query['channels'])?.split(',').filter(Boolean) ?? []; const channelsOptions = () => data()?.search.aggs.channels.map(({ channel }) => ({ label: channel.name, @@ -404,7 +406,7 @@ export default function SearchRoute() { > {({ title, focus, count }) => ( { void loadData( - location.query['after'] ?? null, - location.query['before'] ?? null, + unwrapFirst(location.query['after']), + unwrapFirst(location.query['before']), ); }, } satisfies RouteDefinition; @@ -76,7 +77,10 @@ export const route = { export default function SubscriptionsRoute() { const location = useLocation(); const data = createAsync(() => - loadData(location.query['after'] ?? null, location.query['before'] ?? null), + loadData( + unwrapFirst(location.query['after']), + unwrapFirst(location.query['before']), + ), ); return ( diff --git a/apps/web-next/src/routes/(root)/trending.tsx b/apps/web-next/src/routes/(root)/trending.tsx index d1b626ec..8a448528 100644 --- a/apps/web-next/src/routes/(root)/trending.tsx +++ b/apps/web-next/src/routes/(root)/trending.tsx @@ -1,9 +1,9 @@ import { gql } from 'graphql-request'; import { type RouteDefinition, - cache, - useLocation, createAsync, + useLocation, + query, } from '@solidjs/router'; import type { TrendingRouteDataQuery, @@ -13,10 +13,11 @@ import { getAuthenticatedClientOrRedirect } from '~/util/gql/server'; import { UploadCardFields } from '~/util/gql/fragments'; import { UploadGrid } from '~/components/upload-grid'; import Pagination from '~/components/pagination'; +import { unwrapFirst } from '~/util'; const PAGE_SIZE = 60; -const loadData = cache(async function ( +const loadData = query(async function ( after: string | null, before: string | null, ) { @@ -68,8 +69,8 @@ const loadData = cache(async function ( export const route = { load: ({ location }) => { void loadData( - location.query['after'] ?? null, - location.query['before'] ?? null, + unwrapFirst(location.query['after']), + unwrapFirst(location.query['before']), ); }, } satisfies RouteDefinition; @@ -77,7 +78,10 @@ export const route = { export default function TrendingRoute() { const location = useLocation(); const data = createAsync(() => - loadData(location.query['after'] ?? null, location.query['before'] ?? null), + loadData( + unwrapFirst(location.query['after']), + unwrapFirst(location.query['before']), + ), ); return ( diff --git a/apps/web-next/src/routes/(root)/upload.tsx b/apps/web-next/src/routes/(root)/upload.tsx index f4cc45e6..2b7216c4 100644 --- a/apps/web-next/src/routes/(root)/upload.tsx +++ b/apps/web-next/src/routes/(root)/upload.tsx @@ -11,7 +11,6 @@ import { z } from 'zod'; import invariant from 'tiny-invariant'; import { gql } from 'graphql-request'; import { - cache, redirect, createAsync, action, @@ -19,6 +18,7 @@ import { type RouteDefinition, useLocation, useAction, + query, } from '@solidjs/router'; import type { UploadRouteDataQuery, @@ -38,7 +38,7 @@ import { UploadVisibility, type Channel, } from '~/__generated__/graphql-types'; -import { notEmpty, type Optional } from '~/util'; +import { notEmpty, unwrapFirst, type Optional } from '~/util'; import { doMultipartUpload } from '~/util/multipart-upload'; import { Input, Select, Button, Radios, Textarea } from '~/components/form'; import { dateToIso8601 } from '~/util/date'; @@ -248,7 +248,7 @@ function getSections( ]; } -const routeData = cache(async (id: string | null) => { +const routeData = query(async (id: string | null) => { 'use server'; const client = await getAuthenticatedClientOrRedirect(); const res = await client.request< @@ -304,7 +304,7 @@ const routeData = cache(async (id: string | null) => { export const route = { load: ({ location }) => { - void routeData(location.query['id'] ?? null); + void routeData(unwrapFirst(location.query['id'])); }, } satisfies RouteDefinition; @@ -447,7 +447,7 @@ async function finalizeUpload(variables: FinalizeMediaUploadMutationVariables) { export default function UploadRoute() { const location = useLocation(); - const data = createAsync(() => routeData(location.query['id'] ?? null)); + const data = createAsync(() => routeData(unwrapFirst(location.query['id']))); const upsertAction = useAction(upsert); const upsertSubmission = useSubmission(upsert); diff --git a/apps/web-next/src/routes/embed/churches.tsx b/apps/web-next/src/routes/embed/churches.tsx index 07cead86..6b64c1e8 100644 --- a/apps/web-next/src/routes/embed/churches.tsx +++ b/apps/web-next/src/routes/embed/churches.tsx @@ -1,6 +1,7 @@ import { useSearchParams } from '@solidjs/router'; import { clientOnly } from '@solidjs/start'; import { Delay } from '~/components/delay'; +import { unwrapFirst } from '~/util'; const Client = clientOnly(async () => { return import('~/components/churches/churches'); @@ -9,7 +10,7 @@ const Client = clientOnly(async () => { export default function EmbedChurchesRoute() { const [searchParams] = useSearchParams(); - const hidden = searchParams['hidden']?.split(',') ?? []; + const hidden = unwrapFirst(searchParams['hidden'])?.split(',') ?? []; return ( { +const loadMediaMetadata = query(async (id: string) => { 'use server'; invariant(id, 'Missing id'); diff --git a/apps/web-next/src/util/index.ts b/apps/web-next/src/util/index.ts index fb63bf2c..bfbd53bf 100644 --- a/apps/web-next/src/util/index.ts +++ b/apps/web-next/src/util/index.ts @@ -63,3 +63,7 @@ export function useLoginLocation() { export function easeOutExpo(x: number): number { return x === 1 ? 1 : 1 - Math.pow(2, -10 * x); } + +export function unwrapFirst(val?: string | string[] | null): string | null { + return Array.isArray(val) ? (val[0] ?? null) : (val ?? null); +} diff --git a/apps/web-next/src/util/url.ts b/apps/web-next/src/util/url.ts index 27628229..8d2c9536 100644 --- a/apps/web-next/src/util/url.ts +++ b/apps/web-next/src/util/url.ts @@ -1,8 +1,11 @@ +import { type SearchParams } from '@solidjs/router/dist/types'; + export function setQueryParams( - current: ConstructorParameters[0], + current: SearchParams | string, params: Record>, ) { - const searchParams = new URLSearchParams(current); + const searchParams = new URLSearchParams(current.toString()); + for (const [key, value] of Object.entries(params)) { if (value && value.length > 0) { searchParams.set(key, Array.isArray(value) ? value.join(',') : value);