summaryrefslogtreecommitdiff
path: root/apps/web/lib
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-12 01:28:18 -0800
committerFuwn <[email protected]>2026-02-12 01:52:32 -0800
commit6d8f7ea8b30e229cc0662db8dc3438828feb6880 (patch)
tree9e9270f6f7d6d3cb9c11fb5393eb44e1a44ad418 /apps/web/lib
parentfix: invalidate unread counts when toggling individual entry read state (diff)
downloadasa.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.ts12
-rw-r--r--apps/web/lib/queries/use-folder-mutations.ts12
-rw-r--r--apps/web/lib/queries/use-reorder-mutations.ts167
-rw-r--r--apps/web/lib/queries/use-subscription-mutations.ts17
-rw-r--r--apps/web/lib/queries/use-subscriptions.ts1
-rw-r--r--apps/web/lib/reorder-positions.ts34
-rw-r--r--apps/web/lib/types/subscription.ts1
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