chore: added toasts and loading states
6850ad03
6 file(s) · +91 −17
| 8 | 8 | - [x] Add auto-find RSS feed when adding a feed |
|
| 9 | 9 | - [x] When scrolling through one feed and then selecting another, make the content scroll back to the top |
|
| 10 | 10 | - [ ] Can't open add feed dialog when on mobile |
|
| 11 | - | - [ ] Add error handling and checks for adding a feed |
|
| 12 | - | - [ ] Add loading states for new feed |
|
| 11 | + | - [x] Add error handling and checks for adding a feed |
|
| 12 | + | - [x] Add loading states for new feed |
|
| 13 | 13 | - [ ] Find a way to paginate through feeds and get more posts if they only return a few |
|
| 14 | 14 | - [ ] Collapse both side bars if desired? |
|
| 15 | 15 | - [x] Update logo in top left |
| 23 | 23 | "clsx": "^2.1.1", |
|
| 24 | 24 | "fast-xml-parser": "^5.3.0", |
|
| 25 | 25 | "lucide-react": "^0.548.0", |
|
| 26 | + | "next-themes": "^0.4.6", |
|
| 26 | 27 | "react": "^19.1.1", |
|
| 27 | 28 | "react-dom": "^19.1.1", |
|
| 28 | 29 | "react-markdown": "^10.1.0", |
|
| 29 | 30 | "rehype-raw": "^7.0.0", |
|
| 30 | 31 | "rehype-sanitize": "^6.0.0", |
|
| 31 | 32 | "remark-gfm": "^4.0.1", |
|
| 33 | + | "sonner": "^2.0.7", |
|
| 32 | 34 | "tailwind-merge": "^3.3.1", |
|
| 33 | 35 | "tailwindcss": "^4.1.16", |
|
| 34 | 36 | }, |
|
| 733 | 735 | ||
| 734 | 736 | "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], |
|
| 735 | 737 | ||
| 738 | + | "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], |
|
| 739 | + | ||
| 736 | 740 | "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], |
|
| 737 | 741 | ||
| 738 | 742 | "node-releases": ["node-releases@2.0.26", "", {}, "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA=="], |
|
| 812 | 816 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], |
|
| 813 | 817 | ||
| 814 | 818 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], |
|
| 819 | + | ||
| 820 | + | "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], |
|
| 815 | 821 | ||
| 816 | 822 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], |
|
| 817 | 823 | ||
| 29 | 29 | "clsx": "^2.1.1", |
|
| 30 | 30 | "fast-xml-parser": "^5.3.0", |
|
| 31 | 31 | "lucide-react": "^0.548.0", |
|
| 32 | + | "next-themes": "^0.4.6", |
|
| 32 | 33 | "react": "^19.1.1", |
|
| 33 | 34 | "react-dom": "^19.1.1", |
|
| 34 | 35 | "react-markdown": "^10.1.0", |
|
| 35 | 36 | "rehype-raw": "^7.0.0", |
|
| 36 | 37 | "rehype-sanitize": "^6.0.0", |
|
| 37 | 38 | "remark-gfm": "^4.0.1", |
|
| 39 | + | "sonner": "^2.0.7", |
|
| 38 | 40 | "tailwind-merge": "^3.3.1", |
|
| 39 | 41 | "tailwindcss": "^4.1.16" |
|
| 40 | 42 | }, |
| 1 | 1 | "use client"; |
|
| 2 | 2 | ||
| 3 | 3 | import * as React from "react"; |
|
| 4 | - | import { |
|
| 5 | - | ChartNoAxesColumnIcon, |
|
| 6 | - | ChartNoAxesGanttIcon, |
|
| 7 | - | CircleSlash2, |
|
| 8 | - | Command, |
|
| 9 | - | Plus, |
|
| 10 | - | RotateCw, |
|
| 11 | - | } from "lucide-react"; |
|
| 4 | + | import { Plus, RotateCw } from "lucide-react"; |
|
| 12 | 5 | ||
| 13 | 6 | import { NavUser } from "@/components/nav-user"; |
|
| 14 | 7 | import { NavFeeds } from "@/components/nav-feeds"; |
|
| 20 | 13 | SidebarGroupContent, |
|
| 21 | 14 | SidebarGroupLabel, |
|
| 22 | 15 | SidebarHeader, |
|
| 23 | - | SidebarInput, |
|
| 24 | 16 | SidebarMenu, |
|
| 25 | 17 | SidebarMenuButton, |
|
| 26 | 18 | SidebarMenuItem, |
|
| 40 | 32 | } from "@/components/ui/dialog"; |
|
| 41 | 33 | import { Input } from "@/components/ui/input"; |
|
| 42 | 34 | import { Label } from "@/components/ui/label"; |
|
| 35 | + | import { toast } from "sonner"; |
|
| 43 | 36 | import { |
|
| 44 | 37 | allFeedsQuery, |
|
| 45 | 38 | allPostsQuery, |
|
| 77 | 70 | }: AppSidebarProps) { |
|
| 78 | 71 | const [urlInput, setUrlInput] = React.useState(""); |
|
| 79 | 72 | const [categoryInput, setCategoryInput] = React.useState(""); |
|
| 80 | - | const { setOpen } = useSidebar(); |
|
| 81 | 73 | const [dialogOpen, setDialogOpen] = React.useState(false); |
|
| 82 | 74 | const [searchQuery, setSearchQuery] = React.useState(""); |
|
| 75 | + | const [isAddingFeed, setIsAddingFeed] = React.useState(false); |
|
| 76 | + | const [statusMessage, setStatusMessage] = React.useState(""); |
|
| 77 | + | ||
| 78 | + | const { setOpen } = useSidebar(); |
|
| 83 | 79 | ||
| 84 | 80 | const { insert, update } = useEvolu(); |
|
| 85 | 81 | const allFeeds = useQuery(allFeedsQuery); |
|
| 108 | 104 | }, [feedPosts, searchQuery]); |
|
| 109 | 105 | ||
| 110 | 106 | async function addFeed() { |
|
| 107 | + | if (!urlInput.trim()) { |
|
| 108 | + | setStatusMessage("Please enter a URL"); |
|
| 109 | + | return; |
|
| 110 | + | } |
|
| 111 | + | ||
| 112 | + | setIsAddingFeed(true); |
|
| 113 | + | setStatusMessage(""); |
|
| 114 | + | ||
| 111 | 115 | try { |
|
| 112 | 116 | // Try to discover feeds if the URL doesn't look like a direct feed URL |
|
| 113 | 117 | let feedUrl = urlInput; |
|
| 157 | 161 | } |
|
| 158 | 162 | ||
| 159 | 163 | if (!xmlData) { |
|
| 160 | - | alert( |
|
| 164 | + | setStatusMessage( |
|
| 161 | 165 | "Could not find an RSS feed at this URL. Please enter a direct feed URL.", |
|
| 162 | 166 | ); |
|
| 167 | + | setIsAddingFeed(false); |
|
| 163 | 168 | return; |
|
| 164 | 169 | } |
|
| 165 | 170 | } else { |
|
| 179 | 184 | xmlData = await xmlFetch.text(); |
|
| 180 | 185 | } |
|
| 181 | 186 | } |
|
| 187 | + | ||
| 182 | 188 | const parsedXmlData = await parser.parse(xmlData); |
|
| 183 | 189 | console.log(parsedXmlData); |
|
| 184 | 190 | ||
| 226 | 232 | post["content:encoded"] || post.content || "Please open on the web", |
|
| 227 | 233 | }); |
|
| 228 | 234 | } |
|
| 235 | + | ||
| 236 | + | toast.success( |
|
| 237 | + | `Successfully added "${feedData.title}" with ${posts.length} post${posts.length !== 1 ? "s" : ""}`, |
|
| 238 | + | ); |
|
| 239 | + | ||
| 229 | 240 | setUrlInput(""); |
|
| 230 | 241 | setCategoryInput(""); |
|
| 242 | + | setStatusMessage(""); |
|
| 231 | 243 | setDialogOpen(false); |
|
| 232 | 244 | } catch (error) { |
|
| 233 | - | console.log(error); |
|
| 245 | + | console.error("Error adding feed:", error); |
|
| 246 | + | setStatusMessage( |
|
| 247 | + | error instanceof Error |
|
| 248 | + | ? error.message |
|
| 249 | + | : "Failed to add feed. Please check the URL and try again.", |
|
| 250 | + | ); |
|
| 251 | + | } finally { |
|
| 252 | + | setIsAddingFeed(false); |
|
| 234 | 253 | } |
|
| 235 | 254 | } |
|
| 236 | 255 | ||
| 297 | 316 | value={urlInput} |
|
| 298 | 317 | onChange={(e) => setUrlInput(e.target.value)} |
|
| 299 | 318 | placeholder="https://example.com" |
|
| 319 | + | disabled={isAddingFeed} |
|
| 300 | 320 | /> |
|
| 301 | 321 | <p className="text-xs text-muted-foreground"> |
|
| 302 | 322 | We'll automatically discover the RSS feed for you |
|
| 310 | 330 | value={categoryInput} |
|
| 311 | 331 | onChange={(e) => setCategoryInput(e.target.value)} |
|
| 312 | 332 | placeholder="e.g., Tech, News, Blogs" |
|
| 333 | + | disabled={isAddingFeed} |
|
| 313 | 334 | /> |
|
| 314 | 335 | </div> |
|
| 336 | + | {statusMessage && ( |
|
| 337 | + | <div className="text-sm text-primary">{statusMessage}</div> |
|
| 338 | + | )} |
|
| 315 | 339 | </div> |
|
| 316 | 340 | <DialogFooter> |
|
| 317 | 341 | <DialogClose asChild> |
|
| 318 | - | <Button variant="outline">Cancel</Button> |
|
| 342 | + | <Button variant="outline" disabled={isAddingFeed}> |
|
| 343 | + | Cancel |
|
| 344 | + | </Button> |
|
| 319 | 345 | </DialogClose> |
|
| 320 | - | <Button onClick={addFeed} type="submit"> |
|
| 321 | - | Submit |
|
| 346 | + | <Button onClick={addFeed} type="submit" disabled={isAddingFeed}> |
|
| 347 | + | {isAddingFeed ? "Adding..." : "Submit"} |
|
| 322 | 348 | </Button> |
|
| 323 | 349 | </DialogFooter> |
|
| 324 | 350 | </DialogContent> |
|
| 1 | + | import { |
|
| 2 | + | CircleCheckIcon, |
|
| 3 | + | InfoIcon, |
|
| 4 | + | Loader2Icon, |
|
| 5 | + | OctagonXIcon, |
|
| 6 | + | TriangleAlertIcon, |
|
| 7 | + | } from "lucide-react" |
|
| 8 | + | import { useTheme } from "next-themes" |
|
| 9 | + | import { Toaster as Sonner, type ToasterProps } from "sonner" |
|
| 10 | + | ||
| 11 | + | const Toaster = ({ ...props }: ToasterProps) => { |
|
| 12 | + | const { theme = "system" } = useTheme() |
|
| 13 | + | ||
| 14 | + | return ( |
|
| 15 | + | <Sonner |
|
| 16 | + | theme={theme as ToasterProps["theme"]} |
|
| 17 | + | className="toaster group" |
|
| 18 | + | icons={{ |
|
| 19 | + | success: <CircleCheckIcon className="size-4" />, |
|
| 20 | + | info: <InfoIcon className="size-4" />, |
|
| 21 | + | warning: <TriangleAlertIcon className="size-4" />, |
|
| 22 | + | error: <OctagonXIcon className="size-4" />, |
|
| 23 | + | loading: <Loader2Icon className="size-4 animate-spin" />, |
|
| 24 | + | }} |
|
| 25 | + | style={ |
|
| 26 | + | { |
|
| 27 | + | "--normal-bg": "var(--popover)", |
|
| 28 | + | "--normal-text": "var(--popover-foreground)", |
|
| 29 | + | "--normal-border": "var(--border)", |
|
| 30 | + | "--border-radius": "var(--radius)", |
|
| 31 | + | } as React.CSSProperties |
|
| 32 | + | } |
|
| 33 | + | {...props} |
|
| 34 | + | /> |
|
| 35 | + | ) |
|
| 36 | + | } |
|
| 37 | + | ||
| 38 | + | export { Toaster } |
| 5 | 5 | import "./index.css"; |
|
| 6 | 6 | import App from "./App.tsx"; |
|
| 7 | 7 | import { evolu } from "./lib/evolu.ts"; |
|
| 8 | + | import { Toaster } from "./components/ui/sonner.tsx"; |
|
| 8 | 9 | ||
| 9 | 10 | createRoot(document.getElementById("root")!).render( |
|
| 10 | 11 | <StrictMode> |
|
| 11 | 12 | <EvoluProvider value={evolu}> |
|
| 12 | 13 | <ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme"> |
|
| 13 | 14 | <App /> |
|
| 15 | + | <Toaster /> |
|
| 14 | 16 | </ThemeProvider> |
|
| 15 | 17 | </EvoluProvider> |
|
| 16 | 18 | </StrictMode>, |