"use client"
import { useState } from "react"
import { useSubscriptions } from "@/lib/queries/use-subscriptions"
import {
useUpdateSubscriptionTitle,
useUpdateSubscriptionHiddenFromTimeline,
useUpdateFeedUrl,
useUpdateFeedCredentials,
useAddFeedCredentials,
useMoveSubscriptionToFolder,
useUnsubscribe,
useRequestFeedRefresh,
} from "@/lib/queries/use-subscription-mutations"
import { useUserProfile } from "@/lib/queries/use-user-profile"
import { useUserInterfaceStore } from "@/lib/stores/user-interface-store"
import { TIER_LIMITS } from "@asa-news/shared"
import type { Subscription } from "@/lib/types/subscription"
function formatRelativeTime(isoString: string | null): string {
if (!isoString) return "never"
const date = new Date(isoString)
const now = new Date()
const differenceSeconds = Math.floor((now.getTime() - date.getTime()) / 1000)
if (differenceSeconds < 60) return "just now"
if (differenceSeconds < 3600) return `${Math.floor(differenceSeconds / 60)}m ago`
if (differenceSeconds < 86400) return `${Math.floor(differenceSeconds / 3600)}h ago`
return `${Math.floor(differenceSeconds / 86400)}d ago`
}
function formatRefreshInterval(seconds: number): string {
if (seconds < 3600) return `${Math.round(seconds / 60)} min`
return `${Math.round(seconds / 3600)} hr`
}
function SubscriptionRow({
subscription,
folderOptions,
}: {
subscription: Subscription
folderOptions: { identifier: string; name: string }[]
}) {
const [isEditingTitle, setIsEditingTitle] = useState(false)
const [editedTitle, setEditedTitle] = useState(
subscription.customTitle ?? ""
)
const [isEditingUrl, setIsEditingUrl] = useState(false)
const [editedUrl, setEditedUrl] = useState(subscription.feedUrl)
const [showUnsubscribeConfirm, setShowUnsubscribeConfirm] = useState(false)
const [isEditingCredentials, setIsEditingCredentials] = useState(false)
const [editedAuthType, setEditedAuthType] = useState("bearer")
const [editedCredential, setEditedCredential] = useState("")
const updateTitle = useUpdateSubscriptionTitle()
const updateHiddenFromTimeline = useUpdateSubscriptionHiddenFromTimeline()
const updateFeedUrl = useUpdateFeedUrl()
const updateCredentials = useUpdateFeedCredentials()
const addCredentials = useAddFeedCredentials()
const moveToFolder = useMoveSubscriptionToFolder()
const unsubscribe = useUnsubscribe()
const requestRefresh = useRequestFeedRefresh()
const { data: userProfile } = useUserProfile()
function handleSaveTitle() {
const trimmedTitle = editedTitle.trim()
updateTitle.mutate({
subscriptionIdentifier: subscription.subscriptionIdentifier,
customTitle: trimmedTitle || null,
})
setIsEditingTitle(false)
}
function handleSaveUrl() {
const trimmedUrl = editedUrl.trim()
if (!trimmedUrl || trimmedUrl === subscription.feedUrl) {
setIsEditingUrl(false)
return
}
updateFeedUrl.mutate({
feedIdentifier: subscription.feedIdentifier,
feedUrl: trimmedUrl,
})
setIsEditingUrl(false)
}
function handleSaveCredentials() {
const trimmedCredential = editedCredential.trim()
if (!trimmedCredential) return
if (subscription.feedVisibility === "authenticated") {
updateCredentials.mutate({
feedIdentifier: subscription.feedIdentifier,
authenticationType: editedAuthType,
credential: trimmedCredential,
})
} else {
addCredentials.mutate({
subscriptionIdentifier: subscription.subscriptionIdentifier,
authenticationType: editedAuthType,
credential: trimmedCredential,
})
}
setIsEditingCredentials(false)
setEditedCredential("")
}
function handleFolderChange(folderIdentifier: string) {
const sourceFolder = folderOptions.find(
(folder) => folder.identifier === subscription.folderIdentifier
)
const targetFolder = folderOptions.find(
(folder) => folder.identifier === folderIdentifier
)
moveToFolder.mutate({
subscriptionIdentifier: subscription.subscriptionIdentifier,
folderIdentifier: folderIdentifier || null,
feedTitle: subscription.customTitle ?? subscription.feedTitle ?? undefined,
sourceFolderName: sourceFolder?.name,
folderName: targetFolder?.name,
})
}
return (
{isEditingTitle ? (
setEditedTitle(event.target.value)}
placeholder={subscription.feedTitle}
className="min-w-0 flex-1 border border-border bg-background-primary px-2 py-1 text-text-primary outline-none focus:border-text-dim"
onKeyDown={(event) => {
if (event.key === "Enter") handleSaveTitle()
if (event.key === "Escape") setIsEditingTitle(false)
}}
autoFocus
/>
) : (
{subscription.customTitle ?? subscription.feedTitle}
{subscription.feedVisibility === "authenticated" && (
[auth]
)}
)}
{isEditingUrl ? (
setEditedUrl(event.target.value)}
className="min-w-0 flex-1 border border-border bg-background-primary px-2 py-1 text-text-primary outline-none focus:border-text-dim"
onKeyDown={(event) => {
if (event.key === "Enter") handleSaveUrl()
if (event.key === "Escape") setIsEditingUrl(false)
}}
autoFocus
/>
) : (
{subscription.feedUrl}
)}
last fetched: {formatRelativeTime(subscription.lastFetchedAt)}
interval: {formatRefreshInterval(subscription.fetchIntervalSeconds)}
{subscription.consecutiveFailures > 0 && (
{subscription.consecutiveFailures} consecutive failure{subscription.consecutiveFailures !== 1 && "s"}
)}
{subscription.lastFetchError && subscription.consecutiveFailures > 0 && (
{subscription.lastFetchError}
)}
{isEditingCredentials ? (
setEditedCredential(event.target.value)}
placeholder={
editedAuthType === "query_param"
? "api_key=your_token"
: editedAuthType === "basic"
? "username:password"
: "your token"
}
className="min-w-0 flex-1 border border-border bg-background-primary px-2 py-1 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
onKeyDown={(event) => {
if (event.key === "Enter" && editedCredential.trim()) {
handleSaveCredentials()
}
if (event.key === "Escape") {
setIsEditingCredentials(false)
setEditedCredential("")
}
}}
autoFocus
/>
) : null}
{(userProfile?.tier === "pro" || userProfile?.tier === "developer") && (
)}
{(userProfile?.tier === "pro" || userProfile?.tier === "developer") && !isEditingCredentials && (
)}
{showUnsubscribeConfirm ? (
confirm?
) : (
)}
)
}
export function SubscriptionsSettings() {
const { data: subscriptionsData, isLoading } = useSubscriptions()
const { data: userProfile } = useUserProfile()
const setAddFeedDialogOpen = useUserInterfaceStore(
(state) => state.setAddFeedDialogOpen
)
const [searchQuery, setSearchQuery] = useState("")
const [folderFilter, setFolderFilter] = useState("all")
if (isLoading) {
return loading subscriptions ...
}
const subscriptions = subscriptionsData?.subscriptions ?? []
const folders = subscriptionsData?.folders ?? []
const folderOptions = folders.map((folder) => ({
identifier: folder.folderIdentifier,
name: folder.name,
}))
if (subscriptions.length === 0) {
return (
no subscriptions yet — add a feed to get started
)
}
const normalizedQuery = searchQuery.toLowerCase().trim()
const filteredSubscriptions = subscriptions.filter((subscription) => {
if (folderFilter === "ungrouped" && subscription.folderIdentifier !== null) return false
if (folderFilter !== "all" && folderFilter !== "ungrouped" && subscription.folderIdentifier !== folderFilter) return false
if (normalizedQuery) {
const title = (subscription.customTitle ?? subscription.feedTitle ?? "").toLowerCase()
const url = (subscription.feedUrl ?? "").toLowerCase()
if (!title.includes(normalizedQuery) && !url.includes(normalizedQuery)) return false
}
return true
})
return (
setSearchQuery(event.target.value)}
placeholder="search subscriptions..."
className="min-w-0 flex-1 border border-border bg-background-primary px-3 py-1.5 text-text-primary outline-none placeholder:text-text-dim focus:border-text-dim"
/>
{filteredSubscriptions.length} / {TIER_LIMITS[userProfile?.tier ?? "free"].maximumFeeds}
{filteredSubscriptions.map((subscription) => (
))}
{filteredSubscriptions.length === 0 && (
no subscriptions match your filters
)}
)
}