diff options
| author | Fuwn <[email protected]> | 2026-02-07 01:42:57 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-07 01:42:57 -0800 |
| commit | 5c5b1993edd890a80870ee05607ac5f088191d4e (patch) | |
| tree | a721b76bcd49ba10826c53efc87302c7a689512f /apps/web/lib/queries/use-entry-state-mutations.ts | |
| download | asa.news-5c5b1993edd890a80870ee05607ac5f088191d4e.tar.xz asa.news-5c5b1993edd890a80870ee05607ac5f088191d4e.zip | |
feat: asa.news RSS reader with developer tier, REST API, and webhooks
Full-stack RSS reader SaaS: Supabase + Next.js + Go worker.
Includes three subscription tiers (free/pro/developer), API key auth,
read-only REST API, webhook push notifications, Stripe billing with
proration, and PWA support.
Diffstat (limited to 'apps/web/lib/queries/use-entry-state-mutations.ts')
| -rw-r--r-- | apps/web/lib/queries/use-entry-state-mutations.ts | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/apps/web/lib/queries/use-entry-state-mutations.ts b/apps/web/lib/queries/use-entry-state-mutations.ts new file mode 100644 index 0000000..5f79fc0 --- /dev/null +++ b/apps/web/lib/queries/use-entry-state-mutations.ts @@ -0,0 +1,133 @@ +"use client" + +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { createSupabaseBrowserClient } from "@/lib/supabase/client" +import { queryKeys } from "./query-keys" +import type { TimelineEntry } from "@/lib/types/timeline" +import type { InfiniteData } from "@tanstack/react-query" + +export function useToggleEntryReadState() { + const supabaseClient = createSupabaseBrowserClient() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ + entryIdentifier, + isRead, + }: { + entryIdentifier: string + isRead: boolean + }) => { + const { + data: { user }, + } = await supabaseClient.auth.getUser() + + if (!user) throw new Error("Not authenticated") + + const { error } = await supabaseClient + .from("user_entry_states") + .upsert( + { + user_id: user.id, + entry_id: entryIdentifier, + read: isRead, + read_at: isRead ? new Date().toISOString() : null, + }, + { onConflict: "user_id,entry_id" } + ) + + if (error) throw error + }, + onMutate: async ({ entryIdentifier, isRead }) => { + await queryClient.cancelQueries({ queryKey: queryKeys.timeline.all }) + + const previousTimeline = queryClient.getQueriesData< + InfiniteData<TimelineEntry[]> + >({ queryKey: queryKeys.timeline.all }) + + queryClient.setQueriesData<InfiniteData<TimelineEntry[]>>( + { queryKey: queryKeys.timeline.all }, + (existingData) => { + if (!existingData) return existingData + + return { + ...existingData, + pages: existingData.pages.map((page) => + page.map((entry) => + entry.entryIdentifier === entryIdentifier + ? { ...entry, isRead } + : entry + ) + ), + } + } + ) + + return { previousTimeline } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.timeline.all }) + queryClient.invalidateQueries({ queryKey: queryKeys.savedEntries.all }) + }, + }) +} + +export function useToggleEntrySavedState() { + const supabaseClient = createSupabaseBrowserClient() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ + entryIdentifier, + isSaved, + }: { + entryIdentifier: string + isSaved: boolean + }) => { + const { + data: { user }, + } = await supabaseClient.auth.getUser() + + if (!user) throw new Error("Not authenticated") + + const { error } = await supabaseClient + .from("user_entry_states") + .upsert( + { + user_id: user.id, + entry_id: entryIdentifier, + saved: isSaved, + saved_at: isSaved ? new Date().toISOString() : null, + }, + { onConflict: "user_id,entry_id" } + ) + + if (error) throw error + }, + onMutate: async ({ entryIdentifier, isSaved }) => { + await queryClient.cancelQueries({ queryKey: queryKeys.timeline.all }) + + queryClient.setQueriesData<InfiniteData<TimelineEntry[]>>( + { queryKey: queryKeys.timeline.all }, + (existingData) => { + if (!existingData) return existingData + + return { + ...existingData, + pages: existingData.pages.map((page) => + page.map((entry) => + entry.entryIdentifier === entryIdentifier + ? { ...entry, isSaved } + : entry + ) + ), + } + } + ) + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.timeline.all }) + queryClient.invalidateQueries({ queryKey: queryKeys.savedEntries.all }) + }, + }) +} |