chore: initial feed adding 4b7b15dd
Steve · 2025-10-27 22:28 11 file(s) · +427 −202
.gitignore +1 −0
22 22
*.njsproj
23 23
*.sln
24 24
*.sw?
25 +
data
bun.lock +5 −0
20 20
        "@tailwindcss/vite": "^4.1.16",
21 21
        "class-variance-authority": "^0.7.1",
22 22
        "clsx": "^2.1.1",
23 +
        "fast-xml-parser": "^5.3.0",
23 24
        "lucide-react": "^0.548.0",
24 25
        "react": "^19.1.1",
25 26
        "react-dom": "^19.1.1",
443 444
444 445
    "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
445 446
447 +
    "fast-xml-parser": ["fast-xml-parser@5.3.0", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-gkWGshjYcQCF+6qtlrqBqELqNqnt4CxruY6UVAWWnqb3DQ6qaNFEIKqzYep1XzHLM/QtrHVCxyPOtTk4LTQ7Aw=="],
448 +
446 449
    "fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
447 450
448 451
    "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
620 623
    "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
621 624
622 625
    "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
626 +
627 +
    "strnum": ["strnum@2.1.1", "", {}, "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw=="],
623 628
624 629
    "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
625 630
package.json +1 −0
26 26
    "@tailwindcss/vite": "^4.1.16",
27 27
    "class-variance-authority": "^0.7.1",
28 28
    "clsx": "^2.1.1",
29 +
    "fast-xml-parser": "^5.3.0",
29 30
    "lucide-react": "^0.548.0",
30 31
    "react": "^19.1.1",
31 32
    "react-dom": "^19.1.1",
src/App.tsx +2 −5
1 -
import { useEvolu } from "./main";
1 +
import Dashboard from "./components/dashboard";
2 2
3 3
function App() {
4 -
	const { insert, update } = useEvolu();
5 -
6 4
	return (
7 5
		<main className="min-h-screen w-full items-center justify-center flex-col flex gap-2">
8 -
			<h1 className="text-6xl font-bold">ALCOVE</h1>
9 -
			<p className="text-lg">Coming soon</p>
6 +
			<Dashboard />
10 7
		</main>
11 8
	);
12 9
}
src/components/app-sidebar.tsx +252 −183
7 7
	Command,
8 8
	File,
9 9
	Inbox,
10 +
	Plus,
11 +
	RotateCw,
10 12
	Send,
11 13
	Star,
12 14
	Trash2,
29 31
} from "@/components/ui/sidebar";
30 32
import { Switch } from "@/components/ui/switch";
31 33
34 +
import { Button } from "@/components/ui/button";
35 +
import {
36 +
	Dialog,
37 +
	DialogClose,
38 +
	DialogContent,
39 +
	DialogDescription,
40 +
	DialogFooter,
41 +
	DialogHeader,
42 +
	DialogTitle,
43 +
	DialogTrigger,
44 +
} from "@/components/ui/dialog";
45 +
import { Input } from "@/components/ui/input";
46 +
import { allFeedsQuery, useEvolu, reset } from "@/lib/evolu";
47 +
import { XMLParser, XMLBuilder, XMLValidator } from "fast-xml-parser";
48 +
import { useQuery } from "@evolu/react";
49 +
const parser = new XMLParser();
50 +
32 51
// This is sample data
33 52
const data = {
34 53
	user: {
56 75
			isActive: false,
57 76
		},
58 77
	],
59 -
	mails: [
60 -
		{
61 -
			name: "William Smith",
62 -
			email: "williamsmith@example.com",
63 -
			subject: "Meeting Tomorrow",
64 -
			date: "09:34 AM",
65 -
			teaser:
66 -
				"Hi team, just a reminder about our meeting tomorrow at 10 AM.\nPlease come prepared with your project updates.",
67 -
		},
68 -
		{
69 -
			name: "Alice Smith",
70 -
			email: "alicesmith@example.com",
71 -
			subject: "Re: Project Update",
72 -
			date: "Yesterday",
73 -
			teaser:
74 -
				"Thanks for the update. The progress looks great so far.\nLet's schedule a call to discuss the next steps.",
75 -
		},
76 -
		{
77 -
			name: "Bob Johnson",
78 -
			email: "bobjohnson@example.com",
79 -
			subject: "Weekend Plans",
80 -
			date: "2 days ago",
81 -
			teaser:
82 -
				"Hey everyone! I'm thinking of organizing a team outing this weekend.\nWould you be interested in a hiking trip or a beach day?",
83 -
		},
84 -
		{
85 -
			name: "Emily Davis",
86 -
			email: "emilydavis@example.com",
87 -
			subject: "Re: Question about Budget",
88 -
			date: "2 days ago",
89 -
			teaser:
90 -
				"I've reviewed the budget numbers you sent over.\nCan we set up a quick call to discuss some potential adjustments?",
91 -
		},
92 -
		{
93 -
			name: "Michael Wilson",
94 -
			email: "michaelwilson@example.com",
95 -
			subject: "Important Announcement",
96 -
			date: "1 week ago",
97 -
			teaser:
98 -
				"Please join us for an all-hands meeting this Friday at 3 PM.\nWe have some exciting news to share about the company's future.",
99 -
		},
100 -
		{
101 -
			name: "Sarah Brown",
102 -
			email: "sarahbrown@example.com",
103 -
			subject: "Re: Feedback on Proposal",
104 -
			date: "1 week ago",
105 -
			teaser:
106 -
				"Thank you for sending over the proposal. I've reviewed it and have some thoughts.\nCould we schedule a meeting to discuss my feedback in detail?",
107 -
		},
108 -
		{
109 -
			name: "David Lee",
110 -
			email: "davidlee@example.com",
111 -
			subject: "New Project Idea",
112 -
			date: "1 week ago",
113 -
			teaser:
114 -
				"I've been brainstorming and came up with an interesting project concept.\nDo you have time this week to discuss its potential impact and feasibility?",
115 -
		},
116 -
		{
117 -
			name: "Olivia Wilson",
118 -
			email: "oliviawilson@example.com",
119 -
			subject: "Vacation Plans",
120 -
			date: "1 week ago",
121 -
			teaser:
122 -
				"Just a heads up that I'll be taking a two-week vacation next month.\nI'll make sure all my projects are up to date before I leave.",
123 -
		},
124 -
		{
125 -
			name: "James Martin",
126 -
			email: "jamesmartin@example.com",
127 -
			subject: "Re: Conference Registration",
128 -
			date: "1 week ago",
129 -
			teaser:
130 -
				"I've completed the registration for the upcoming tech conference.\nLet me know if you need any additional information from my end.",
131 -
		},
132 -
		{
133 -
			name: "Sophia White",
134 -
			email: "sophiawhite@example.com",
135 -
			subject: "Team Dinner",
136 -
			date: "1 week ago",
137 -
			teaser:
138 -
				"To celebrate our recent project success, I'd like to organize a team dinner.\nAre you available next Friday evening? Please let me know your preferences.",
139 -
		},
140 -
	],
141 78
};
142 79
143 80
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
144 81
	// Note: I'm using state to show active item.
145 82
	// IRL you should use the url/router.
146 83
	const [activeItem, setActiveItem] = React.useState(data.navMain[0]);
147 -
	const [mails, setMails] = React.useState(data.mails);
84 +
	const [urlInput, setUrlInput] = React.useState("");
85 +
	const [categoryInput, setCategoryInput] = React.useState("");
148 86
	const { setOpen } = useSidebar();
87 +
	const [dialogOpen, setDialogOpen] = React.useState(false);
88 +
89 +
	const { insert, update } = useEvolu();
90 +
	const allFeeds = useQuery(allFeedsQuery);
91 +
	console.log(allFeeds);
92 +
93 +
	async function addFeed() {
94 +
		try {
95 +
			let xmlData: string;
96 +
			try {
97 +
				// Try to fetch directly first
98 +
				const xmlFetch = await fetch(urlInput);
99 +
				xmlData = await xmlFetch.text();
100 +
			} catch (corsError) {
101 +
				// Fall back to AllOrigins if CORS error occurs
102 +
				console.log(corsError);
103 +
				const xmlFetch = await fetch(
104 +
					`https://api.allorigins.win/raw?url=${urlInput}`,
105 +
				);
106 +
				xmlData = await xmlFetch.text();
107 +
			}
108 +
			const parsedXmlData = await parser.parse(xmlData);
109 +
			console.log(parsedXmlData);
110 +
111 +
			// Determine if it's RSS or Atom feed
112 +
			let feedData: any;
113 +
			let posts: any[];
114 +
			let isAtom = false;
115 +
116 +
			if (parsedXmlData.rss) {
117 +
				// RSS feed
118 +
				feedData = parsedXmlData.rss.channel;
119 +
				posts = feedData.item || [];
120 +
			} else if (parsedXmlData.feed) {
121 +
				// Atom feed
122 +
				feedData = parsedXmlData.feed;
123 +
				posts = feedData.entry || [];
124 +
				isAtom = true;
125 +
			} else {
126 +
				throw new Error("Unsupported feed format");
127 +
			}
128 +
129 +
			const result = insert("rssFeed", {
130 +
				feedUrl: urlInput,
131 +
				title: feedData.title,
132 +
				description: feedData.description || feedData.subtitle || "",
133 +
				category: "tech",
134 +
			});
135 +
			console.log(result);
136 +
137 +
			// Process posts/entries
138 +
			for (const post of posts) {
139 +
				const addPost = insert("rssPost", {
140 +
					title: post.title,
141 +
					author: isAtom
142 +
						? post.author?.name || "Author"
143 +
						: post.author || "Author",
144 +
					link: isAtom
145 +
						? typeof post.link === "string"
146 +
							? post.link || post.id
147 +
							: post.link?.[0] || post.id
148 +
						: post.link || post.id,
149 +
					feedId: result.value.id,
150 +
				});
151 +
				console.log(addPost);
152 +
			}
153 +
			setDialogOpen(false);
154 +
		} catch (error) {
155 +
			console.log(error);
156 +
		}
157 +
	}
149 158
150 159
	return (
151 -
		<Sidebar
152 -
			collapsible="icon"
153 -
			className="overflow-hidden *:data-[sidebar=sidebar]:flex-row"
154 -
			{...props}
155 -
		>
156 -
			{/* This is the first sidebar */}
157 -
			{/* We disable collapsible and adjust width to icon. */}
158 -
			{/* This will make the sidebar appear as icons. */}
160 +
		<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
159 161
			<Sidebar
160 -
				collapsible="none"
161 -
				className="w-[calc(var(--sidebar-width-icon)+1px)]! border-r"
162 +
				collapsible="icon"
163 +
				className="overflow-hidden *:data-[sidebar=sidebar]:flex-row"
164 +
				{...props}
162 165
			>
163 -
				<SidebarHeader>
164 -
					<SidebarMenu>
165 -
						<SidebarMenuItem>
166 -
							<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
167 -
								<a href="#">
168 -
									<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
169 -
										<Command className="size-4" />
170 -
									</div>
171 -
									<div className="grid flex-1 text-left text-sm leading-tight">
172 -
										<span className="truncate font-medium">Acme Inc</span>
173 -
										<span className="truncate text-xs">Enterprise</span>
174 -
									</div>
175 -
								</a>
176 -
							</SidebarMenuButton>
177 -
						</SidebarMenuItem>
178 -
					</SidebarMenu>
179 -
				</SidebarHeader>
180 -
				<SidebarContent>
181 -
					<SidebarGroup>
182 -
						<SidebarGroupContent className="px-1.5 md:px-0">
183 -
							<SidebarMenu>
184 -
								{data.navMain.map((item) => (
185 -
									<SidebarMenuItem key={item.title}>
186 -
										<SidebarMenuButton
187 -
											tooltip={{
188 -
												children: item.title,
189 -
												hidden: false,
190 -
											}}
191 -
											onClick={() => {
192 -
												setActiveItem(item);
193 -
												const mail = data.mails.sort(() => Math.random() - 0.5);
194 -
												setMails(
195 -
													mail.slice(
196 -
														0,
197 -
														Math.max(5, Math.floor(Math.random() * 10) + 1),
198 -
													),
199 -
												);
200 -
												setOpen(true);
201 -
											}}
202 -
											isActive={activeItem?.title === item.title}
203 -
											className="px-2.5 md:px-2"
204 -
										>
205 -
											<item.icon />
206 -
											<span>{item.title}</span>
166 +
				{/* This is the first sidebar */}
167 +
				{/* We disable collapsible and adjust width to icon. */}
168 +
				{/* This will make the sidebar appear as icons. */}
169 +
				<Sidebar
170 +
					collapsible="none"
171 +
					className="w-[calc(var(--sidebar-width-icon)+1px)]! border-r"
172 +
				>
173 +
					<SidebarHeader>
174 +
						<SidebarMenu>
175 +
							<SidebarMenuItem>
176 +
								<SidebarMenuButton size="lg" asChild className="md:h-8 md:p-0">
177 +
									<a href="#">
178 +
										<div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg">
179 +
											<Command className="size-4" />
180 +
										</div>
181 +
										<div className="grid flex-1 text-left text-sm leading-tight">
182 +
											<span className="truncate font-medium">Acme Inc</span>
183 +
											<span className="truncate text-xs">Enterprise</span>
184 +
										</div>
185 +
									</a>
186 +
								</SidebarMenuButton>
187 +
							</SidebarMenuItem>
188 +
						</SidebarMenu>
189 +
					</SidebarHeader>
190 +
					<SidebarContent>
191 +
						<DialogContent className="sm:max-w-[425px]">
192 +
							<DialogHeader>
193 +
								<DialogTitle>Add Feed</DialogTitle>
194 +
								<DialogDescription>
195 +
									Add a new feed with the RSS URL
196 +
								</DialogDescription>
197 +
							</DialogHeader>
198 +
							<div className="grid gap-4">
199 +
								<div className="grid gap-3">
200 +
									<Label htmlFor="url-input">URL</Label>
201 +
									<Input
202 +
										id="url-input"
203 +
										name="url"
204 +
										value={urlInput}
205 +
										onChange={(e) => setUrlInput(e.target.value)}
206 +
									/>
207 +
								</div>
208 +
								<div className="grid gap-3">
209 +
									<Label htmlFor="category-input">Category</Label>
210 +
									<Input
211 +
										id="category-input"
212 +
										name="category"
213 +
										value={categoryInput}
214 +
										onChange={(e) => setCategoryInput(e.target.value)}
215 +
									/>
216 +
								</div>
217 +
							</div>
218 +
							<DialogFooter>
219 +
								<DialogClose asChild>
220 +
									<Button variant="outline">Cancel</Button>
221 +
								</DialogClose>
222 +
								<Button onClick={addFeed} type="submit">
223 +
									Submit
224 +
								</Button>
225 +
							</DialogFooter>
226 +
						</DialogContent>
227 +
						<SidebarGroup>
228 +
							<SidebarGroupContent className="px-1.5 md:px-0">
229 +
								<SidebarMenu>
230 +
									<DialogTrigger>
231 +
										<SidebarMenuItem>
232 +
											<SidebarMenuButton
233 +
												size="lg"
234 +
												asChild
235 +
												className="md:h-8 md:p-0"
236 +
											>
237 +
												<a href="#">
238 +
													<div className="flex aspect-square size-8 items-center justify-center rounded-lg">
239 +
														<Plus className="size-4" />
240 +
													</div>
241 +
													<div className="grid flex-1 text-left text-sm leading-tight">
242 +
														<span className="truncate font-medium">
243 +
															Add Feed
244 +
														</span>
245 +
													</div>
246 +
												</a>
247 +
											</SidebarMenuButton>
248 +
										</SidebarMenuItem>
249 +
									</DialogTrigger>
250 +
									<SidebarMenuItem>
251 +
										<SidebarMenuButton onClick={reset}>
252 +
											<RotateCw className="size-4" />
207 253
										</SidebarMenuButton>
208 254
									</SidebarMenuItem>
209 -
								))}
210 -
							</SidebarMenu>
211 -
						</SidebarGroupContent>
212 -
					</SidebarGroup>
213 -
				</SidebarContent>
214 -
				<SidebarFooter>
215 -
					<NavUser user={data.user} />
216 -
				</SidebarFooter>
217 -
			</Sidebar>
255 +
									{data.navMain.map((item) => (
256 +
										<SidebarMenuItem key={item.title}>
257 +
											<SidebarMenuButton
258 +
												tooltip={{
259 +
													children: item.title,
260 +
													hidden: false,
261 +
												}}
262 +
												onClick={() => {
263 +
													setActiveItem(item);
264 +
													const mail = data.mails.sort(
265 +
														() => Math.random() - 0.5,
266 +
													);
267 +
													setMails(
268 +
														mail.slice(
269 +
															0,
270 +
															Math.max(5, Math.floor(Math.random() * 10) + 1),
271 +
														),
272 +
													);
273 +
													setOpen(true);
274 +
												}}
275 +
												isActive={activeItem?.title === item.title}
276 +
												className="px-2.5 md:px-2"
277 +
											>
278 +
												<item.icon />
279 +
												<span>{item.title}</span>
280 +
											</SidebarMenuButton>
281 +
										</SidebarMenuItem>
282 +
									))}
283 +
								</SidebarMenu>
284 +
							</SidebarGroupContent>
285 +
						</SidebarGroup>
286 +
					</SidebarContent>
287 +
					<SidebarFooter>
288 +
						<NavUser user={data.user} />
289 +
					</SidebarFooter>
290 +
				</Sidebar>
218 291
219 -
			{/* This is the second sidebar */}
220 -
			{/* We disable collapsible and let it fill remaining space */}
221 -
			<Sidebar collapsible="none" className="hidden flex-1 md:flex">
222 -
				<SidebarHeader className="gap-3.5 border-b p-4">
223 -
					<div className="flex w-full items-center justify-between">
224 -
						<div className="text-foreground text-base font-medium">
225 -
							{activeItem?.title}
292 +
				{/* This is the second sidebar */}
293 +
				{/* We disable collapsible and let it fill remaining space */}
294 +
				<Sidebar collapsible="none" className="hidden flex-1 md:flex">
295 +
					<SidebarHeader className="gap-3.5 border-b p-4">
296 +
						<div className="flex w-full items-center justify-between">
297 +
							<div className="text-foreground text-base font-medium">
298 +
								{activeItem?.title}
299 +
							</div>
300 +
							<Label className="flex items-center gap-2 text-sm">
301 +
								<span>Unreads</span>
302 +
								<Switch className="shadow-none" />
303 +
							</Label>
226 304
						</div>
227 -
						<Label className="flex items-center gap-2 text-sm">
228 -
							<span>Unreads</span>
229 -
							<Switch className="shadow-none" />
230 -
						</Label>
231 -
					</div>
232 -
					<SidebarInput placeholder="Type to search..." />
233 -
				</SidebarHeader>
234 -
				<SidebarContent>
235 -
					<SidebarGroup className="px-0">
236 -
						<SidebarGroupContent>
237 -
							{mails.map((mail) => (
238 -
								<a
239 -
									href="#"
240 -
									key={mail.email}
241 -
									className="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex flex-col items-start gap-2 border-b p-4 text-sm leading-tight whitespace-nowrap last:border-b-0"
242 -
								>
243 -
									<div className="flex w-full items-center gap-2">
244 -
										<span>{mail.name}</span>{" "}
245 -
										<span className="ml-auto text-xs">{mail.date}</span>
246 -
									</div>
247 -
									<span className="font-medium">{mail.subject}</span>
248 -
									<span className="line-clamp-2 w-[260px] text-xs whitespace-break-spaces">
249 -
										{mail.teaser}
250 -
									</span>
251 -
								</a>
252 -
							))}
253 -
						</SidebarGroupContent>
254 -
					</SidebarGroup>
255 -
				</SidebarContent>
305 +
						<SidebarInput placeholder="Type to search..." />
306 +
					</SidebarHeader>
307 +
					<SidebarContent>
308 +
						<SidebarGroup className="px-0">
309 +
							<SidebarGroupContent>
310 +
								{allFeeds.map((feed) => (
311 +
									<a
312 +
										href="#"
313 +
										key={feed.id}
314 +
										className="hover:bg-sidebar-accent hover:text-sidebar-accent-foreground flex flex-col items-start gap-2 border-b p-4 text-sm leading-tight whitespace-nowrap last:border-b-0"
315 +
									>
316 +
										<div className="flex w-full items-center gap-2">
317 +
											<span>{feed.title}</span>{" "}
318 +
										</div>
319 +
									</a>
320 +
								))}
321 +
							</SidebarGroupContent>
322 +
						</SidebarGroup>
323 +
					</SidebarContent>
324 +
				</Sidebar>
256 325
			</Sidebar>
257 -
		</Sidebar>
326 +
		</Dialog>
258 327
	);
259 328
}
src/components/dashboard.tsx +0 −3
1 -
import { useEvolu } from "../main";
2 1
import { AppSidebar } from "@/components/app-sidebar";
3 2
import {
4 3
	Breadcrumb,
16 15
} from "@/components/ui/sidebar";
17 16
18 17
function Dashboard() {
19 -
	const { insert, update } = useEvolu();
20 -
21 18
	return (
22 19
		<main className="min-h-screen w-full items-center justify-center flex-col flex gap-2">
23 20
			<SidebarProvider
src/components/ui/dialog.tsx (added) +141 −0
1 +
import * as React from "react"
2 +
import * as DialogPrimitive from "@radix-ui/react-dialog"
3 +
import { XIcon } from "lucide-react"
4 +
5 +
import { cn } from "@/lib/utils"
6 +
7 +
function Dialog({
8 +
  ...props
9 +
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
10 +
  return <DialogPrimitive.Root data-slot="dialog" {...props} />
11 +
}
12 +
13 +
function DialogTrigger({
14 +
  ...props
15 +
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
16 +
  return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
17 +
}
18 +
19 +
function DialogPortal({
20 +
  ...props
21 +
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
22 +
  return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
23 +
}
24 +
25 +
function DialogClose({
26 +
  ...props
27 +
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
28 +
  return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
29 +
}
30 +
31 +
function DialogOverlay({
32 +
  className,
33 +
  ...props
34 +
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
35 +
  return (
36 +
    <DialogPrimitive.Overlay
37 +
      data-slot="dialog-overlay"
38 +
      className={cn(
39 +
        "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40 +
        className
41 +
      )}
42 +
      {...props}
43 +
    />
44 +
  )
45 +
}
46 +
47 +
function DialogContent({
48 +
  className,
49 +
  children,
50 +
  showCloseButton = true,
51 +
  ...props
52 +
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
53 +
  showCloseButton?: boolean
54 +
}) {
55 +
  return (
56 +
    <DialogPortal data-slot="dialog-portal">
57 +
      <DialogOverlay />
58 +
      <DialogPrimitive.Content
59 +
        data-slot="dialog-content"
60 +
        className={cn(
61 +
          "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
62 +
          className
63 +
        )}
64 +
        {...props}
65 +
      >
66 +
        {children}
67 +
        {showCloseButton && (
68 +
          <DialogPrimitive.Close
69 +
            data-slot="dialog-close"
70 +
            className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
71 +
          >
72 +
            <XIcon />
73 +
            <span className="sr-only">Close</span>
74 +
          </DialogPrimitive.Close>
75 +
        )}
76 +
      </DialogPrimitive.Content>
77 +
    </DialogPortal>
78 +
  )
79 +
}
80 +
81 +
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
82 +
  return (
83 +
    <div
84 +
      data-slot="dialog-header"
85 +
      className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
86 +
      {...props}
87 +
    />
88 +
  )
89 +
}
90 +
91 +
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
92 +
  return (
93 +
    <div
94 +
      data-slot="dialog-footer"
95 +
      className={cn(
96 +
        "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
97 +
        className
98 +
      )}
99 +
      {...props}
100 +
    />
101 +
  )
102 +
}
103 +
104 +
function DialogTitle({
105 +
  className,
106 +
  ...props
107 +
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
108 +
  return (
109 +
    <DialogPrimitive.Title
110 +
      data-slot="dialog-title"
111 +
      className={cn("text-lg leading-none font-semibold", className)}
112 +
      {...props}
113 +
    />
114 +
  )
115 +
}
116 +
117 +
function DialogDescription({
118 +
  className,
119 +
  ...props
120 +
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
121 +
  return (
122 +
    <DialogPrimitive.Description
123 +
      data-slot="dialog-description"
124 +
      className={cn("text-muted-foreground text-sm", className)}
125 +
      {...props}
126 +
    />
127 +
  )
128 +
}
129 +
130 +
export {
131 +
  Dialog,
132 +
  DialogClose,
133 +
  DialogContent,
134 +
  DialogDescription,
135 +
  DialogFooter,
136 +
  DialogHeader,
137 +
  DialogOverlay,
138 +
  DialogPortal,
139 +
  DialogTitle,
140 +
  DialogTrigger,
141 +
}
src/lib/evolu.ts (added) +20 −0
1 +
import { createEvolu, getOrThrow, SimpleName } from "@evolu/common";
2 +
import { evoluReactWebDeps } from "@evolu/react-web";
3 +
import { Schema } from "./scheme.ts";
4 +
import { createUseEvolu } from "@evolu/react";
5 +
6 +
export const evolu = createEvolu(evoluReactWebDeps)(Schema, {
7 +
	name: getOrThrow(SimpleName.from("alcove")),
8 +
	syncUrl: "http://localhost:4000", // optional, defaults to wss://free.evoluhq.com
9 +
	reloadUrl: "/",
10 +
});
11 +
12 +
export const useEvolu = createUseEvolu(evolu);
13 +
14 +
export const allFeedsQuery = evolu.createQuery((db) =>
15 +
	db.selectFrom("rssFeed").selectAll(),
16 +
);
17 +
18 +
export function reset() {
19 +
	evolu.resetAppOwner();
20 +
}
src/main.tsx +2 −11
1 1
import { StrictMode } from "react";
2 2
import { ThemeProvider } from "@/components/theme-provider";
3 3
import { createRoot } from "react-dom/client";
4 -
import { createEvolu, getOrThrow, SimpleName } from "@evolu/common";
5 -
import { createUseEvolu, EvoluProvider } from "@evolu/react";
6 -
import { Schema } from "./scheme.ts";
7 -
import { evoluReactWebDeps } from "@evolu/react-web";
4 +
import { EvoluProvider } from "@evolu/react";
8 5
import "./index.css";
9 6
import App from "./App.tsx";
10 -
11 -
const evolu = createEvolu(evoluReactWebDeps)(Schema, {
12 -
	name: getOrThrow(SimpleName.from("your-app-name")),
13 -
	syncUrl: "wss://your-sync-url", // optional, defaults to wss://free.evoluhq.com
14 -
});
15 -
16 -
export const useEvolu = createUseEvolu(evolu);
7 +
import { evolu } from "./lib/evolu.ts";
17 8
18 9
createRoot(document.getElementById("root")!).render(
19 10
	<StrictMode>
src/scheme.ts → src/lib/scheme.ts +0 −0
vite.config.ts +3 −0
6 6
// https://vite.dev/config/
7 7
export default defineConfig({
8 8
	plugins: [react(), tailwindcss()],
9 +
	optimizeDeps: {
10 +
		exclude: ["@sqlite.org/sqlite-wasm", "kysely", "@evolu/react-web"],
11 +
	},
9 12
	resolve: {
10 13
		alias: {
11 14
			"@": path.resolve(__dirname, "./src"),