diff options
| author | Fuwn <[email protected]> | 2026-02-12 01:28:18 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-02-12 01:52:32 -0800 |
| commit | 6d8f7ea8b30e229cc0662db8dc3438828feb6880 (patch) | |
| tree | 9e9270f6f7d6d3cb9c11fb5393eb44e1a44ad418 /apps/web/lib | |
| parent | fix: invalidate unread counts when toggling individual entry read state (diff) | |
| download | asa.news-6d8f7ea8b30e229cc0662db8dc3438828feb6880.tar.xz asa.news-6d8f7ea8b30e229cc0662db8dc3438828feb6880.zip | |
feat: add drag-and-drop reordering for feeds, folders, and custom feeds
Diffstat (limited to 'apps/web/lib')
| -rw-r--r-- | apps/web/lib/queries/use-custom-feed-mutations.ts | 12 | ||||
| -rw-r--r-- | apps/web/lib/queries/use-folder-mutations.ts | 12 | ||||
| -rw-r--r-- | apps/web/lib/queries/use-reorder-mutations.ts | 167 | ||||
| -rw-r--r-- | apps/web/lib/queries/use-subscription-mutations.ts | 17 | ||||
| -rw-r--r-- | apps/web/lib/queries/use-subscriptions.ts | 1 | ||||
| -rw-r--r-- | apps/web/lib/reorder-positions.ts | 34 | ||||
| -rw-r--r-- | apps/web/lib/types/subscription.ts | 1 |
7 files changed, 241 insertions, 3 deletions
diff --git a/apps/web/lib/queries/use-custom-feed-mutations.ts b/apps/web/lib/queries/use-custom-feed-mutations.ts index 66c2a19..34120db 100644 --- a/apps/web/lib/queries/use-custom-feed-mutations.ts +++ b/apps/web/lib/queries/use-custom-feed-mutations.ts @@ -27,13 +27,23 @@ export function useCreateCustomFeed() { if (!user) throw new Error("not authenticated") + const { data: maxRow } = await supabaseClient + .from("custom_feeds") + .select("position") + .eq("user_id", user.id) + .order("position", { ascending: false }) + .limit(1) + .maybeSingle() + + const nextPosition = (maxRow?.position ?? 0) + 1000 + const { error } = await supabaseClient.from("custom_feeds").insert({ user_id: user.id, name, query, match_mode: matchMode, source_folder_id: sourceFolderIdentifier, - position: 0, + position: nextPosition, }) if (error) throw error diff --git a/apps/web/lib/queries/use-folder-mutations.ts b/apps/web/lib/queries/use-folder-mutations.ts index 4bc1247..cc84580 100644 --- a/apps/web/lib/queries/use-folder-mutations.ts +++ b/apps/web/lib/queries/use-folder-mutations.ts @@ -17,10 +17,20 @@ export function useCreateFolder() { if (!user) throw new Error("not authenticated") + const { data: maxRow } = await supabaseClient + .from("folders") + .select("position") + .eq("user_id", user.id) + .order("position", { ascending: false }) + .limit(1) + .maybeSingle() + + const nextPosition = (maxRow?.position ?? 0) + 1000 + const { error } = await supabaseClient.from("folders").insert({ user_id: user.id, name, - position: 0, + position: nextPosition, }) if (error) throw error diff --git a/apps/web/lib/queries/use-reorder-mutations.ts b/apps/web/lib/queries/use-reorder-mutations.ts new file mode 100644 index 0000000..8ed32e9 --- /dev/null +++ b/apps/web/lib/queries/use-reorder-mutations.ts @@ -0,0 +1,167 @@ +"use client" + +import { useMutation, useQueryClient } from "@tanstack/react-query" +import { createSupabaseBrowserClient } from "@/lib/supabase/client" +import { queryKeys } from "./query-keys" +import type { Subscription, Folder } from "@/lib/types/subscription" +import type { CustomFeed } from "@/lib/types/custom-feed" + +interface ReorderInput { + itemIdentifiers: string[] + newPositions: number[] +} + +export function useReorderSubscriptions() { + const supabaseClient = createSupabaseBrowserClient() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ itemIdentifiers, newPositions }: ReorderInput) => { + const { error } = await supabaseClient.rpc("reorder_items", { + target_table: "subscriptions", + item_ids: itemIdentifiers, + new_positions: newPositions, + }) + + if (error) throw error + }, + onMutate: async ({ itemIdentifiers, newPositions }) => { + await queryClient.cancelQueries({ queryKey: queryKeys.subscriptions.all }) + + const previous = queryClient.getQueryData<{ + subscriptions: Subscription[] + folders: Folder[] + }>(queryKeys.subscriptions.all) + + if (previous) { + const positionMap = new Map( + itemIdentifiers.map((identifier, positionIndex) => [identifier, newPositions[positionIndex]]) + ) + + queryClient.setQueryData(queryKeys.subscriptions.all, { + ...previous, + subscriptions: previous.subscriptions + .map((subscription) => ({ + ...subscription, + position: + positionMap.get(subscription.subscriptionIdentifier) ?? subscription.position, + })) + .sort((first, second) => first.position - second.position), + }) + } + + return { previous } + }, + onError: (_error, _variables, context) => { + if (context?.previous) { + queryClient.setQueryData(queryKeys.subscriptions.all, context.previous) + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.subscriptions.all }) + }, + }) +} + +export function useReorderFolders() { + const supabaseClient = createSupabaseBrowserClient() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ itemIdentifiers, newPositions }: ReorderInput) => { + const { error } = await supabaseClient.rpc("reorder_items", { + target_table: "folders", + item_ids: itemIdentifiers, + new_positions: newPositions, + }) + + if (error) throw error + }, + onMutate: async ({ itemIdentifiers, newPositions }) => { + await queryClient.cancelQueries({ queryKey: queryKeys.subscriptions.all }) + + const previous = queryClient.getQueryData<{ + subscriptions: Subscription[] + folders: Folder[] + }>(queryKeys.subscriptions.all) + + if (previous) { + const positionMap = new Map( + itemIdentifiers.map((identifier, positionIndex) => [identifier, newPositions[positionIndex]]) + ) + + queryClient.setQueryData(queryKeys.subscriptions.all, { + ...previous, + folders: previous.folders + .map((folder) => ({ + ...folder, + position: + positionMap.get(folder.folderIdentifier) ?? folder.position, + })) + .sort((first, second) => first.position - second.position), + }) + } + + return { previous } + }, + onError: (_error, _variables, context) => { + if (context?.previous) { + queryClient.setQueryData(queryKeys.subscriptions.all, context.previous) + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.subscriptions.all }) + }, + }) +} + +export function useReorderCustomFeeds() { + const supabaseClient = createSupabaseBrowserClient() + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ itemIdentifiers, newPositions }: ReorderInput) => { + const { error } = await supabaseClient.rpc("reorder_items", { + target_table: "custom_feeds", + item_ids: itemIdentifiers, + new_positions: newPositions, + }) + + if (error) throw error + }, + onMutate: async ({ itemIdentifiers, newPositions }) => { + await queryClient.cancelQueries({ queryKey: queryKeys.customFeeds.all }) + + const previous = queryClient.getQueryData<CustomFeed[]>( + queryKeys.customFeeds.all + ) + + if (previous) { + const positionMap = new Map( + itemIdentifiers.map((identifier, positionIndex) => [identifier, newPositions[positionIndex]]) + ) + + queryClient.setQueryData( + queryKeys.customFeeds.all, + previous + .map((feed) => ({ + ...feed, + position: + positionMap.get(feed.identifier) ?? feed.position, + })) + .sort((first, second) => first.position - second.position) + ) + } + + return { previous } + }, + onError: (_error, _variables, context) => { + if (context?.previous) { + queryClient.setQueryData(queryKeys.customFeeds.all, context.previous) + } + }, + onSettled: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.customFeeds.all }) + }, + }) +} diff --git a/apps/web/lib/queries/use-subscription-mutations.ts b/apps/web/lib/queries/use-subscription-mutations.ts index 1570167..df4fe32 100644 --- a/apps/web/lib/queries/use-subscription-mutations.ts +++ b/apps/web/lib/queries/use-subscription-mutations.ts @@ -80,9 +80,24 @@ export function useMoveSubscriptionToFolder() { sourceFolderName?: string folderName?: string }) => { + let query = supabaseClient + .from("subscriptions") + .select("position") + .order("position", { ascending: false }) + .limit(1) + + if (folderIdentifier) { + query = query.eq("folder_id", folderIdentifier) + } else { + query = query.is("folder_id", null) + } + + const { data: maxRow } = await query.maybeSingle() + const nextPosition = (maxRow?.position ?? 0) + 1000 + const { error } = await supabaseClient .from("subscriptions") - .update({ folder_id: folderIdentifier }) + .update({ folder_id: folderIdentifier, position: nextPosition }) .eq("id", subscriptionIdentifier) if (error) throw error diff --git a/apps/web/lib/queries/use-subscriptions.ts b/apps/web/lib/queries/use-subscriptions.ts index 5dc6076..4185638 100644 --- a/apps/web/lib/queries/use-subscriptions.ts +++ b/apps/web/lib/queries/use-subscriptions.ts @@ -58,6 +58,7 @@ export function useSubscriptions() { feedIdentifier: row.feed_id, folderIdentifier: row.folder_id, customTitle: row.custom_title, + position: row.position, feedTitle: row.feeds?.title ?? "", feedUrl: row.feeds?.url ?? "", consecutiveFailures: row.feeds?.consecutive_failures ?? 0, diff --git a/apps/web/lib/reorder-positions.ts b/apps/web/lib/reorder-positions.ts new file mode 100644 index 0000000..928a00b --- /dev/null +++ b/apps/web/lib/reorder-positions.ts @@ -0,0 +1,34 @@ +const POSITION_GAP = 1000 + +export function computeReorderPositions< + T extends { identifier: string; position: number }, +>( + items: T[], + activeIdentifier: string, + overIdentifier: string +): { itemIdentifiers: string[]; newPositions: number[] } | null { + const oldIndex = items.findIndex((item) => item.identifier === activeIdentifier) + const newIndex = items.findIndex((item) => item.identifier === overIdentifier) + + if (oldIndex === -1 || newIndex === -1 || oldIndex === newIndex) return null + + const reordered = [...items] + const [moved] = reordered.splice(oldIndex, 1) + reordered.splice(newIndex, 0, moved) + + const itemIdentifiers: string[] = [] + const newPositions: number[] = [] + + for (let index = 0; index < reordered.length; index++) { + const newPosition = (index + 1) * POSITION_GAP + + if (reordered[index].position !== newPosition) { + itemIdentifiers.push(reordered[index].identifier) + newPositions.push(newPosition) + } + } + + if (itemIdentifiers.length === 0) return null + + return { itemIdentifiers, newPositions } +} diff --git a/apps/web/lib/types/subscription.ts b/apps/web/lib/types/subscription.ts index 96f314d..d59556a 100644 --- a/apps/web/lib/types/subscription.ts +++ b/apps/web/lib/types/subscription.ts @@ -10,6 +10,7 @@ export interface Subscription { feedIdentifier: string folderIdentifier: string | null customTitle: string | null + position: number feedTitle: string feedUrl: string consecutiveFailures: number |