summaryrefslogtreecommitdiff
path: root/apps/web/lib/stores
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-07 01:42:57 -0800
committerFuwn <[email protected]>2026-02-07 01:42:57 -0800
commit5c5b1993edd890a80870ee05607ac5f088191d4e (patch)
treea721b76bcd49ba10826c53efc87302c7a689512f /apps/web/lib/stores
downloadasa.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/stores')
-rw-r--r--apps/web/lib/stores/notification-store.ts66
-rw-r--r--apps/web/lib/stores/user-interface-store.ts135
2 files changed, 201 insertions, 0 deletions
diff --git a/apps/web/lib/stores/notification-store.ts b/apps/web/lib/stores/notification-store.ts
new file mode 100644
index 0000000..d7eee57
--- /dev/null
+++ b/apps/web/lib/stores/notification-store.ts
@@ -0,0 +1,66 @@
+import { create } from "zustand"
+import { persist } from "zustand/middleware"
+
+const MAXIMUM_NOTIFICATIONS = 50
+
+export interface StoredNotification {
+ identifier: string
+ message: string
+ timestamp: string
+ type: "info" | "success" | "error"
+ actionUrl?: string
+}
+
+interface NotificationState {
+ notifications: StoredNotification[]
+ lastViewedAt: string | null
+
+ addNotification: (
+ message: string,
+ type?: "info" | "success" | "error",
+ actionUrl?: string
+ ) => void
+ dismissNotification: (identifier: string) => void
+ clearAllNotifications: () => void
+ markAllAsViewed: () => void
+}
+
+export const useNotificationStore = create<NotificationState>()(
+ persist(
+ (set) => ({
+ notifications: [],
+ lastViewedAt: null,
+
+ addNotification: (message, type = "info", actionUrl) =>
+ set((state) => {
+ const newNotification: StoredNotification = {
+ identifier: crypto.randomUUID(),
+ message,
+ timestamp: new Date().toISOString(),
+ type,
+ ...(actionUrl ? { actionUrl } : {}),
+ }
+ const updated = [newNotification, ...state.notifications].slice(
+ 0,
+ MAXIMUM_NOTIFICATIONS
+ )
+ return { notifications: updated }
+ }),
+
+ dismissNotification: (identifier) =>
+ set((state) => ({
+ notifications: state.notifications.filter(
+ (notification) => notification.identifier !== identifier
+ ),
+ })),
+
+ clearAllNotifications: () => set({ notifications: [] }),
+
+ markAllAsViewed: () =>
+ set({ lastViewedAt: new Date().toISOString() }),
+ }),
+ {
+ name: "asa-news-notifications",
+ }
+ )
+)
diff --git a/apps/web/lib/stores/user-interface-store.ts b/apps/web/lib/stores/user-interface-store.ts
new file mode 100644
index 0000000..468542d
--- /dev/null
+++ b/apps/web/lib/stores/user-interface-store.ts
@@ -0,0 +1,135 @@
+import { create } from "zustand"
+import { persist } from "zustand/middleware"
+
+type EntryListViewMode = "compact" | "comfortable" | "expanded"
+
+type DisplayDensity = "compact" | "default" | "spacious"
+
+type FocusedPanel = "sidebar" | "entryList" | "detailPanel"
+
+type SettingsTab =
+ | "subscriptions"
+ | "folders"
+ | "muted-keywords"
+ | "custom-feeds"
+ | "import-export"
+ | "appearance"
+ | "account"
+ | "security"
+ | "billing"
+ | "api"
+ | "danger"
+
+interface UserInterfaceState {
+ isSidebarCollapsed: boolean
+ isCommandPaletteOpen: boolean
+ isAddFeedDialogOpen: boolean
+ isSearchOpen: boolean
+ selectedEntryIdentifier: string | null
+ focusedEntryIdentifier: string | null
+ focusedPanel: FocusedPanel
+ focusedSidebarIndex: number
+ entryListViewMode: EntryListViewMode
+ displayDensity: DisplayDensity
+ activeSettingsTab: SettingsTab
+ showFeedFavicons: boolean
+ focusFollowsInteraction: boolean
+ expandedFolderIdentifiers: string[]
+ navigableEntryIdentifiers: string[]
+
+ toggleSidebar: () => void
+ setSidebarCollapsed: (isCollapsed: boolean) => void
+ setCommandPaletteOpen: (isOpen: boolean) => void
+ setAddFeedDialogOpen: (isOpen: boolean) => void
+ setSearchOpen: (isOpen: boolean) => void
+ setSelectedEntryIdentifier: (identifier: string | null) => void
+ setFocusedEntryIdentifier: (identifier: string | null) => void
+ setFocusedPanel: (panel: FocusedPanel) => void
+ setFocusedSidebarIndex: (index: number) => void
+ setEntryListViewMode: (mode: EntryListViewMode) => void
+ setDisplayDensity: (density: DisplayDensity) => void
+ setActiveSettingsTab: (tab: SettingsTab) => void
+ setShowFeedFavicons: (show: boolean) => void
+ setFocusFollowsInteraction: (enabled: boolean) => void
+ toggleFolderExpansion: (folderIdentifier: string) => void
+ setNavigableEntryIdentifiers: (identifiers: string[]) => void
+}
+
+export const useUserInterfaceStore = create<UserInterfaceState>()(
+ persist(
+ (set) => ({
+ isSidebarCollapsed: false,
+ isCommandPaletteOpen: false,
+ isAddFeedDialogOpen: false,
+ isSearchOpen: false,
+ selectedEntryIdentifier: null,
+ focusedEntryIdentifier: null,
+ focusedPanel: "entryList",
+ focusedSidebarIndex: 0,
+ entryListViewMode: "comfortable",
+ displayDensity: "default",
+ activeSettingsTab: "subscriptions",
+ showFeedFavicons: true,
+ focusFollowsInteraction: false,
+ expandedFolderIdentifiers: [],
+ navigableEntryIdentifiers: [],
+
+ toggleSidebar: () =>
+ set((state) => ({ isSidebarCollapsed: !state.isSidebarCollapsed })),
+
+ setSidebarCollapsed: (isCollapsed) =>
+ set({ isSidebarCollapsed: isCollapsed }),
+
+ setCommandPaletteOpen: (isOpen) => set({ isCommandPaletteOpen: isOpen }),
+
+ setAddFeedDialogOpen: (isOpen) => set({ isAddFeedDialogOpen: isOpen }),
+
+ setSearchOpen: (isOpen) => set({ isSearchOpen: isOpen }),
+
+ setSelectedEntryIdentifier: (identifier) =>
+ set({ selectedEntryIdentifier: identifier }),
+
+ setFocusedEntryIdentifier: (identifier) =>
+ set({ focusedEntryIdentifier: identifier }),
+
+ setFocusedPanel: (panel) => set({ focusedPanel: panel }),
+
+ setFocusedSidebarIndex: (index) => set({ focusedSidebarIndex: index }),
+
+ setEntryListViewMode: (mode) => set({ entryListViewMode: mode }),
+
+ setDisplayDensity: (density) => set({ displayDensity: density }),
+
+ setActiveSettingsTab: (tab) => set({ activeSettingsTab: tab }),
+
+ setShowFeedFavicons: (show) => set({ showFeedFavicons: show }),
+
+ setFocusFollowsInteraction: (enabled) =>
+ set({ focusFollowsInteraction: enabled }),
+
+ toggleFolderExpansion: (folderIdentifier) =>
+ set((state) => {
+ const current = state.expandedFolderIdentifiers
+ const isExpanded = current.includes(folderIdentifier)
+ return {
+ expandedFolderIdentifiers: isExpanded
+ ? current.filter((id) => id !== folderIdentifier)
+ : [...current, folderIdentifier],
+ }
+ }),
+
+ setNavigableEntryIdentifiers: (identifiers) =>
+ set({ navigableEntryIdentifiers: identifiers }),
+ }),
+ {
+ name: "asa-news-ui-preferences",
+ partialize: (state) => ({
+ entryListViewMode: state.entryListViewMode,
+ displayDensity: state.displayDensity,
+ showFeedFavicons: state.showFeedFavicons,
+ focusFollowsInteraction: state.focusFollowsInteraction,
+ expandedFolderIdentifiers: state.expandedFolderIdentifiers,
+ }),
+ }
+ )
+)