From b5ade5b077c67739506ff92753588aa08f49d7f8 Mon Sep 17 00:00:00 2001 From: Ben Davis Date: Fri, 12 Dec 2025 23:47:31 -0800 Subject: [PATCH 01/18] moved over to traditional load functions --- AGENTS.md | 10 + .../src/lib/components/ChannelHeader.svelte | 5 +- .../components/ChannelLastSevenVids.svelte | 14 +- .../components/ChannelNotifications.svelte | 13 +- .../components/ChannelSponsorsTable.svelte | 16 +- .../src/lib/components/ChannelVideos.svelte | 7 +- .../src/lib/components/ChannelsList.svelte | 11 +- .../components/SponsorMentionsTable.svelte | 26 +- .../src/lib/components/SponsorStats.svelte | 18 +- .../lib/components/SponsorVideosTable.svelte | 23 +- .../lib/components/VideoCommentsTable.svelte | 30 +- .../components/VideoNotificationsTable.svelte | 19 +- apps/web/src/lib/remote/auth.remote.ts | 43 --- apps/web/src/lib/remote/channels.remote.ts | 73 +--- apps/web/src/lib/server/runner.ts | 122 +++++++ apps/web/src/lib/stores/AuthStore.svelte.ts | 51 --- apps/web/src/routes/+layout.svelte | 3 - apps/web/src/routes/+page.server.ts | 39 +++ apps/web/src/routes/+page.svelte | 49 +-- apps/web/src/routes/app/+layout.server.ts | 19 + apps/web/src/routes/app/+layout.svelte | 67 ++-- apps/web/src/routes/app/+page.server.ts | 21 ++ apps/web/src/routes/app/+page.svelte | 21 +- .../routes/app/channel/create/+page.server.ts | 38 ++ .../routes/app/channel/create/+page.svelte | 47 ++- .../src/routes/app/view/2025/+page.server.ts | 30 ++ .../web/src/routes/app/view/2025/+page.svelte | 329 +++++++++--------- .../routes/app/view/channel/+page.server.ts | 27 ++ .../src/routes/app/view/channel/+page.svelte | 46 +-- .../routes/app/view/sponsor/+page.server.ts | 20 ++ .../src/routes/app/view/sponsor/+page.svelte | 20 +- .../src/routes/app/view/video/+page.server.ts | 20 ++ .../src/routes/app/view/video/+page.svelte | 20 +- 33 files changed, 721 insertions(+), 576 deletions(-) delete mode 100644 apps/web/src/lib/remote/auth.remote.ts create mode 100644 apps/web/src/lib/server/runner.ts delete mode 100644 apps/web/src/lib/stores/AuthStore.svelte.ts create mode 100644 apps/web/src/routes/+page.server.ts create mode 100644 apps/web/src/routes/app/+layout.server.ts create mode 100644 apps/web/src/routes/app/+page.server.ts create mode 100644 apps/web/src/routes/app/channel/create/+page.server.ts create mode 100644 apps/web/src/routes/app/view/2025/+page.server.ts create mode 100644 apps/web/src/routes/app/view/channel/+page.server.ts create mode 100644 apps/web/src/routes/app/view/sponsor/+page.server.ts create mode 100644 apps/web/src/routes/app/view/video/+page.server.ts diff --git a/AGENTS.md b/AGENTS.md index 031d315..9604ee5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -37,3 +37,13 @@ The Effect Solutions CLI provides curated best practices and patterns for Effect - `effect-solutions search ` - Search topics by keyword **Local Effect Source:** The Effect repository is cloned to `~/.local/share/effect-solutions/effect` for reference. Use this to explore APIs, find usage examples, and understand implementation details when the documentation isn't enough. + +## btca + +Trigger: user says "use btca" (for codebase/docs questions). + +Run: + +- btca ask -t -q "" + +Available : svelte, tailwindcss diff --git a/apps/web/src/lib/components/ChannelHeader.svelte b/apps/web/src/lib/components/ChannelHeader.svelte index d98682e..4164e61 100644 --- a/apps/web/src/lib/components/ChannelHeader.svelte +++ b/apps/web/src/lib/components/ChannelHeader.svelte @@ -1,12 +1,11 @@ diff --git a/apps/web/src/lib/components/ChannelLastSevenVids.svelte b/apps/web/src/lib/components/ChannelLastSevenVids.svelte index 6a6b396..ed0031d 100644 --- a/apps/web/src/lib/components/ChannelLastSevenVids.svelte +++ b/apps/web/src/lib/components/ChannelLastSevenVids.svelte @@ -1,5 +1,4 @@ {#if channels.length === 0} diff --git a/apps/web/src/lib/components/SponsorMentionsTable.svelte b/apps/web/src/lib/components/SponsorMentionsTable.svelte index 99dc312..51b866c 100644 --- a/apps/web/src/lib/components/SponsorMentionsTable.svelte +++ b/apps/web/src/lib/components/SponsorMentionsTable.svelte @@ -19,22 +19,26 @@ } from '@tanstack/table-core'; import { formatDate } from '$lib/utils'; - const { - sponsorData, - channelId - }: { - sponsorData: Awaited< - ReturnType - >; - channelId: string; - } = $props(); + type Comment = { + ytVideoId: string; + ytCommentId: string; + text: string; + videoTitle: string; + author: string; + likeCount: number; + publishedAt: Date | string; + }; + + type SponsorData = { + sponsorMentionComments: Comment[]; + }; + + const { sponsorData, channelId }: { sponsorData: SponsorData; channelId: string } = $props(); const getYouTubeCommentUrl = (videoId: string, commentId: string) => { return `https://www.youtube.com/watch?v=${videoId}&lc=${commentId}`; }; - type Comment = (typeof sponsorData.sponsorMentionComments)[number]; - const columns: ColumnDef[] = [ { accessorKey: 'text', diff --git a/apps/web/src/lib/components/SponsorStats.svelte b/apps/web/src/lib/components/SponsorStats.svelte index 55e31ae..214f03a 100644 --- a/apps/web/src/lib/components/SponsorStats.svelte +++ b/apps/web/src/lib/components/SponsorStats.svelte @@ -2,13 +2,17 @@ import { formatNumber, formatDate } from '$lib/utils'; import { Eye, Video, Calendar, TrendingUp } from '@lucide/svelte'; - const { - sponsorData - }: { - sponsorData: Awaited< - ReturnType - >; - } = $props(); + type SponsorData = { + sponsor: { sponsorId: string; name: string; sponsorKey: string }; + stats: { + totalViews: number; + totalAds: number; + avgViewsPerVideo: number; + lastPublishDate: Date | string | number | null; + }; + }; + + const { sponsorData }: { sponsorData: SponsorData } = $props();
diff --git a/apps/web/src/lib/components/SponsorVideosTable.svelte b/apps/web/src/lib/components/SponsorVideosTable.svelte index 1a5791a..b44f497 100644 --- a/apps/web/src/lib/components/SponsorVideosTable.svelte +++ b/apps/web/src/lib/components/SponsorVideosTable.svelte @@ -18,17 +18,20 @@ } from '@tanstack/table-core'; import { formatNumber, formatDate } from '$lib/utils'; - const { - sponsorData, - channelId - }: { - sponsorData: Awaited< - ReturnType - >; - channelId: string; - } = $props(); + type VideoType = { + ytVideoId: string; + title: string; + thumbnailUrl: string; + viewCount: number; + likeCount: number; + publishedAt: Date | string; + }; - type VideoType = (typeof sponsorData.videos)[number]; + type SponsorData = { + videos: VideoType[]; + }; + + const { sponsorData, channelId }: { sponsorData: SponsorData; channelId: string } = $props(); const columns: ColumnDef[] = [ { diff --git a/apps/web/src/lib/components/VideoCommentsTable.svelte b/apps/web/src/lib/components/VideoCommentsTable.svelte index f343f00..c4330d5 100644 --- a/apps/web/src/lib/components/VideoCommentsTable.svelte +++ b/apps/web/src/lib/components/VideoCommentsTable.svelte @@ -23,13 +23,25 @@ } from '@tanstack/table-core'; import { formatNumber, formatDate } from '$lib/utils'; - const { - videoData - }: { - videoData: Awaited< - ReturnType - >; - } = $props(); + type Comment = { + ytCommentId: string; + author: string; + text: string; + likeCount: number; + replyCount: number; + publishedAt: Date; + isQuestion: boolean; + isSponsorMention: boolean; + isEditingMistake: boolean; + isPositiveComment: boolean; + }; + + type VideoData = { + video: { ytVideoId: string }; + comments: Comment[]; + }; + + const { videoData }: { videoData: VideoData } = $props(); let showQuestions = $state(false); let showSponsorMentions = $state(false); @@ -49,9 +61,7 @@ }); }); - type Comments = ColumnDef<(typeof videoData.comments)[number]>[]; - - const columns: Comments = [ + const columns: ColumnDef[] = [ { accessorKey: 'author', header: 'Author', diff --git a/apps/web/src/lib/components/VideoNotificationsTable.svelte b/apps/web/src/lib/components/VideoNotificationsTable.svelte index 906b0da..999a455 100644 --- a/apps/web/src/lib/components/VideoNotificationsTable.svelte +++ b/apps/web/src/lib/components/VideoNotificationsTable.svelte @@ -4,13 +4,18 @@ import { formatRelativeTime } from '$lib/utils'; import { Bell } from '@lucide/svelte'; - const { - videoData - }: { - videoData: Awaited< - ReturnType - >; - } = $props(); + type Notification = { + type: string; + success: boolean; + message: string; + createdAt: Date; + }; + + type VideoData = { + notifications?: Notification[]; + }; + + const { videoData }: { videoData: VideoData } = $props(); const getNotificationTypeLabel = (type: string) => { return type diff --git a/apps/web/src/lib/remote/auth.remote.ts b/apps/web/src/lib/remote/auth.remote.ts deleted file mode 100644 index f94fcbe..0000000 --- a/apps/web/src/lib/remote/auth.remote.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { command, getRequestEvent } from '$app/server'; -import z from 'zod'; -import { Effect } from 'effect'; -import { remoteRunner } from './helpers'; -import { AuthService } from '$lib/services/auth'; - -export const remoteSignOut = command(async () => { - return await remoteRunner( - Effect.gen(function* () { - const auth = yield* AuthService; - const event = yield* Effect.sync(() => getRequestEvent()); - return yield* auth.signOut(event); - }) - ); -}); - -export const remoteSignIn = command( - z.object({ - authPassword: z.string() - }), - async ({ authPassword }) => { - return await remoteRunner( - Effect.gen(function* () { - const auth = yield* AuthService; - const event = yield* Effect.sync(() => getRequestEvent()); - return yield* auth.signIn({ - event, - authPassword - }); - }) - ); - } -); - -export const remoteCheckAuth = command(async () => { - return await remoteRunner( - Effect.gen(function* () { - const event = yield* Effect.sync(() => getRequestEvent()); - const auth = yield* AuthService; - return yield* auth.checkAuth(event); - }) - ); -}); diff --git a/apps/web/src/lib/remote/channels.remote.ts b/apps/web/src/lib/remote/channels.remote.ts index bea7d1c..f106e1c 100644 --- a/apps/web/src/lib/remote/channels.remote.ts +++ b/apps/web/src/lib/remote/channels.remote.ts @@ -1,61 +1,7 @@ -import { form, query } from '$app/server'; -import { Effect } from 'effect'; +import { query } from '$app/server'; import z from 'zod'; import { authedRemoteRunner } from './helpers'; -export const remoteGetAllChannels = query(async () => { - return authedRemoteRunner(({ db }) => db.getAllChannels()); -}); - -export const remoteCreateChannel = form( - z.object({ - channelName: z.string(), - findSponsorPrompt: z.string(), - ytChannelId: z.string() - }), - async (data) => { - return authedRemoteRunner(({ db }) => - Effect.gen(function* () { - yield* db.createChannel(data); - return { success: true }; - }) - ); - } -); - -export const remoteGetChannelsWithStats = query(async () => { - return authedRemoteRunner(({ db }) => db.getChannelsWithStats()); -}); - -export const remoteGetLast7VideosByViews = query(z.string(), async (ytChannelId) => { - return authedRemoteRunner(({ db }) => db.getLast7VideosByViews(ytChannelId)); -}); - -export const remoteGetChannelDetails = query(z.string(), async (ytChannelId) => { - return authedRemoteRunner(({ db }) => db.getChannel(ytChannelId)); -}); - -export const remoteGetChannelVideos = query(z.string(), async (ytChannelId) => { - return authedRemoteRunner(({ db }) => - db.getChannelVideos({ - ytChannelId, - limit: 20 - }) - ); -}); - -export const remoteGetChannelNotifications = query(z.string(), async (ytChannelId) => { - return authedRemoteRunner(({ db }) => db.getChannelNotifications(ytChannelId)); -}); - -export const remoteGetChannelSponsors = query(z.string(), async (ytChannelId) => { - return authedRemoteRunner(({ db }) => db.getChannelSponsors(ytChannelId)); -}); - -export const remoteGetSponsorDetails = query(z.string(), async (sponsorId) => { - return authedRemoteRunner(({ db }) => db.getSponsorDetails(sponsorId)); -}); - export const remoteSearchVideosAndSponsors = query( z.object({ searchQuery: z.string(), @@ -65,20 +11,3 @@ export const remoteSearchVideosAndSponsors = query( return authedRemoteRunner(({ db }) => db.searchVideosAndSponsors(args)); } ); - -export const remoteGetVideoDetails = query(z.string(), async (ytVideoId) => { - return authedRemoteRunner(({ db }) => db.getVideoDetails(ytVideoId)); -}); - -export const remoteGetChannel2025Data = query(z.string(), async (ytChannelId) => { - return authedRemoteRunner(({ db }) => - Effect.gen(function* () { - const [videos, sponsors] = yield* Effect.all( - [db.getChannelVideos2025(ytChannelId), db.getChannelSponsors2025(ytChannelId)], - { concurrency: 'unbounded' } - ); - - return { videos, sponsors }; - }) - ); -}); diff --git a/apps/web/src/lib/server/runner.ts b/apps/web/src/lib/server/runner.ts new file mode 100644 index 0000000..97c7792 --- /dev/null +++ b/apps/web/src/lib/server/runner.ts @@ -0,0 +1,122 @@ +import { NodeContext } from '@effect/platform-node'; +import { DbError, DbService } from '$lib/services/db'; +import { error, type RequestEvent } from '@sveltejs/kit'; +import { Effect, Cause, ManagedRuntime, Layer } from 'effect'; +import { AuthError, AuthService } from '$lib/services/auth'; +import { TaggedError } from 'effect/Data'; + +export class AppError extends TaggedError('AppError') { + status: number; + body: App.Error; + constructor(body: App.Error, status = 500) { + super(); + this.message = body.message; + this.cause = body.cause; + this.body = body; + this.status = status; + } +} + +const appLayer = Layer.mergeAll(NodeContext.layer, DbService.Default, AuthService.Default); + +const runtime = ManagedRuntime.make(appLayer); + +const shutdown = async () => { + console.log('sveltekit:shutdown'); + await runtime.dispose(); + process.exit(0); +}; + +process.on('SIGTERM', shutdown); +process.on('SIGINT', shutdown); + +export const runner = async ( + effect: Effect.Effect +) => { + const result = await effect.pipe( + Effect.catchTag('DbError', (err) => + Effect.fail( + new AppError({ + type: 'db', + message: err.message, + cause: err.cause + }) + ) + ), + Effect.catchTag('AuthError', (err) => + Effect.fail( + new AppError( + { + type: 'auth', + message: err.message, + cause: err.cause + }, + 401 + ) + ) + ), + Effect.matchCause({ + onSuccess: (res): { _type: 'success'; value: A } => ({ + _type: 'success', + value: res + }), + onFailure: (cause): { _type: 'failure'; value: AppError } => { + console.error(cause.toString()); + + const failures = Array.from(Cause.failures(cause)); + + if (failures.length > 0) { + failures.forEach((failure) => { + console.error(failure.toString()); + console.error('CAUSE', failure.cause); + }); + const first = failures[0]; + if (first) { + return { + _type: 'failure', + value: first + }; + } + } + + return { + _type: 'failure', + value: new AppError( + { + type: 'unknown', + message: 'An unexpected error occurred', + cause: cause.toString() + }, + 500 + ) + }; + } + }), + runtime.runPromise + ); + + if (result._type === 'failure') { + return error(result.value.status, result.value.body); + } + + return result.value; +}; + +export const authedRunner = async ( + event: RequestEvent, + fn: (ctx: { + auth: AuthService; + db: DbService; + }) => Effect.Effect +) => { + return runner( + Effect.gen(function* () { + const auth = yield* AuthService; + const db = yield* DbService; + + yield* auth.checkAuthAndFail(event); + + return yield* fn({ auth, db }); + }) + ); +}; diff --git a/apps/web/src/lib/stores/AuthStore.svelte.ts b/apps/web/src/lib/stores/AuthStore.svelte.ts deleted file mode 100644 index 4e9323b..0000000 --- a/apps/web/src/lib/stores/AuthStore.svelte.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createContext, onMount } from 'svelte'; -import { remoteCheckAuth, remoteSignIn, remoteSignOut } from '../remote/auth.remote'; - -class AuthStore { - isAuthenticated = $state(false); - isLoading = $state(true); - - constructor() { - onMount(async () => { - const isAuthenticated = await remoteCheckAuth(); - console.log('result', isAuthenticated); - - this.isAuthenticated = isAuthenticated; - this.isLoading = false; - }); - } - - handleSignIn = async (authPassword: string) => { - this.isLoading = true; - const result = await remoteSignIn({ authPassword }); - if (result.success) { - this.isAuthenticated = true; - } - this.isLoading = false; - return result.success; - }; - - handleSignOut = async () => { - this.isLoading = true; - const result = await remoteSignOut(); - if (result.success) { - this.isAuthenticated = false; - } - this.isLoading = false; - }; -} - -const [internalGetAuthStore, internalSetAuthStore] = createContext(); - -const getAuthStore = () => { - const authStore = internalGetAuthStore(); - if (!authStore) throw new Error('AuthStore not found'); - return authStore; -}; - -const setAuthStore = () => { - const authStore = new AuthStore(); - return internalSetAuthStore(authStore); -}; - -export { getAuthStore, setAuthStore }; diff --git a/apps/web/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte index 037c7dd..65c3fd2 100644 --- a/apps/web/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -1,11 +1,8 @@ diff --git a/apps/web/src/routes/+page.server.ts b/apps/web/src/routes/+page.server.ts new file mode 100644 index 0000000..df16bfe --- /dev/null +++ b/apps/web/src/routes/+page.server.ts @@ -0,0 +1,39 @@ +import { fail, redirect } from '@sveltejs/kit'; +import { runner } from '$lib/server/runner'; +import { Effect } from 'effect'; +import { AuthService } from '$lib/services/auth'; + +export const load = async (event) => { + const isAuthenticated = await runner( + Effect.gen(function* () { + const auth = yield* AuthService; + return yield* auth.checkAuth(event); + }) + ); + + return { isAuthenticated }; +}; + +export const actions = { + signin: async (event) => { + const formData = await event.request.formData(); + const authPassword = formData.get('authPassword'); + + if (!authPassword || typeof authPassword !== 'string') { + return fail(400, { error: 'Password is required' }); + } + + const result = await runner( + Effect.gen(function* () { + const auth = yield* AuthService; + return yield* auth.signIn({ event, authPassword }); + }) + ); + + if (!result.success) { + return fail(401, { error: 'Invalid password' }); + } + + redirect(302, '/app'); + } +}; diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte index 4e352ea..f345edb 100644 --- a/apps/web/src/routes/+page.svelte +++ b/apps/web/src/routes/+page.svelte @@ -1,24 +1,13 @@ @@ -29,9 +18,7 @@ /> -{#if authStore.isLoading} - -{:else if authStore.isAuthenticated} +{#if data.isAuthenticated}
@@ -48,18 +35,38 @@

r8y 3.0

Sign in to access your dashboard

-
+ { + isSubmitting = true; + return async ({ update }) => { + await update(); + isSubmitting = false; + }; + }} + >
- + {#if form?.error} +

{form.error}

+ {/if} +
diff --git a/apps/web/src/routes/app/+layout.server.ts b/apps/web/src/routes/app/+layout.server.ts new file mode 100644 index 0000000..4d10e61 --- /dev/null +++ b/apps/web/src/routes/app/+layout.server.ts @@ -0,0 +1,19 @@ +import { redirect } from '@sveltejs/kit'; +import { runner } from '$lib/server/runner'; +import { Effect } from 'effect'; +import { AuthService } from '$lib/services/auth'; + +export const load = async (event) => { + const isAuthenticated = await runner( + Effect.gen(function* () { + const auth = yield* AuthService; + return yield* auth.checkAuth(event); + }) + ); + + if (!isAuthenticated) { + redirect(302, '/'); + } + + return { isAuthenticated }; +}; diff --git a/apps/web/src/routes/app/+layout.svelte b/apps/web/src/routes/app/+layout.svelte index 27b67a0..d9ac0a6 100644 --- a/apps/web/src/routes/app/+layout.svelte +++ b/apps/web/src/routes/app/+layout.svelte @@ -1,57 +1,40 @@
- {#if authStore.isAuthenticated} - - {:else} - - {/if} +
diff --git a/apps/web/src/routes/app/+page.server.ts b/apps/web/src/routes/app/+page.server.ts new file mode 100644 index 0000000..74c26d7 --- /dev/null +++ b/apps/web/src/routes/app/+page.server.ts @@ -0,0 +1,21 @@ +import { redirect } from '@sveltejs/kit'; +import { authedRunner, runner } from '$lib/server/runner'; +import { Effect } from 'effect'; +import { AuthService } from '$lib/services/auth'; + +export const load = async (event) => { + const channels = await authedRunner(event, ({ db }) => db.getChannelsWithStats()); + return { channels }; +}; + +export const actions = { + signout: async (event) => { + await runner( + Effect.gen(function* () { + const auth = yield* AuthService; + return yield* auth.signOut(event); + }) + ); + redirect(302, '/'); + } +}; diff --git a/apps/web/src/routes/app/+page.svelte b/apps/web/src/routes/app/+page.svelte index 89bfe87..0f02fe0 100644 --- a/apps/web/src/routes/app/+page.svelte +++ b/apps/web/src/routes/app/+page.svelte @@ -3,6 +3,8 @@ import Button from '$lib/components/ui/button/button.svelte'; import ChannelsList from '$lib/components/ChannelsList.svelte'; import { Plus } from '@lucide/svelte'; + + let { data } = $props(); @@ -33,22 +35,5 @@
- - {#snippet pending()} -
- {#each Array(3) as _} -
-
-
-
-
-
-
-
-
- {/each} -
- {/snippet} - -
+
diff --git a/apps/web/src/routes/app/channel/create/+page.server.ts b/apps/web/src/routes/app/channel/create/+page.server.ts new file mode 100644 index 0000000..3fb11e3 --- /dev/null +++ b/apps/web/src/routes/app/channel/create/+page.server.ts @@ -0,0 +1,38 @@ +import { fail, redirect } from '@sveltejs/kit'; +import { authedRunner } from '$lib/server/runner'; +import { Effect } from 'effect'; +import z from 'zod'; + +const createChannelSchema = z.object({ + channelName: z.string().min(1, 'Channel name is required'), + ytChannelId: z.string().min(1, 'YouTube Channel ID is required'), + findSponsorPrompt: z.string() +}); + +export const actions = { + default: async (event) => { + const formData = await event.request.formData(); + const data = { + channelName: formData.get('channelName')?.toString() ?? '', + ytChannelId: formData.get('ytChannelId')?.toString() ?? '', + findSponsorPrompt: formData.get('findSponsorPrompt')?.toString() ?? '' + }; + + const parsed = createChannelSchema.safeParse(data); + if (!parsed.success) { + return fail(400, { + error: parsed.error.issues[0]?.message ?? 'Invalid data', + ...data + }); + } + + await authedRunner(event, ({ db }) => + Effect.gen(function* () { + yield* db.createChannel(parsed.data); + return { success: true }; + }) + ); + + redirect(302, `/app/view/channel?channelId=${parsed.data.ytChannelId}`); + } +}; diff --git a/apps/web/src/routes/app/channel/create/+page.svelte b/apps/web/src/routes/app/channel/create/+page.svelte index 8319d8e..1bcba95 100644 --- a/apps/web/src/routes/app/channel/create/+page.svelte +++ b/apps/web/src/routes/app/channel/create/+page.svelte @@ -1,15 +1,14 @@ @@ -22,39 +21,49 @@
{ - try { - isCreating = true; - await submit(); - form.reset(); - await goto(`/app/view/channel?channelId=${data.ytChannelId}`); - } catch (error) { - console.error(error); - } finally { - isCreating = false; - } - })} + method="POST" class="flex w-96 flex-col gap-6" + use:enhance={() => { + isCreating = true; + return async ({ update }) => { + await update(); + isCreating = false; + }; + }} >

Create Channel

+ {#if form?.error} +

{form.error}

+ {/if}