chore: initial feed adding
4b7b15dd
11 file(s) · +427 −202
| 22 | 22 | *.njsproj |
|
| 23 | 23 | *.sln |
|
| 24 | 24 | *.sw? |
|
| 25 | + | data |
| 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 | ||
| 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", |
| 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 | } |
| 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 | } |
|
| 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 |
|
| 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 | + | } |
| 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 | + | } |
| 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> |
| 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"), |