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/stores | |
| 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/stores')
| -rw-r--r-- | apps/web/lib/stores/notification-store.ts | 66 | ||||
| -rw-r--r-- | apps/web/lib/stores/user-interface-store.ts | 135 |
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, + }), + } + ) +) |