chore: added toasts and loading states 6850ad03
Steve · 2025-11-01 22:18 6 file(s) · +91 −17
TODO.md +2 −2
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
bun.lock +6 −0
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
package.json +2 −0
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
  },
src/components/app-sidebar.tsx +41 −15
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>
src/components/ui/sonner.tsx (added) +38 −0
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 }
src/main.tsx +2 −0
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>,