chore: added home page
6e6a91c4
9 file(s) · +290 −207
| 15 | 15 | - [x] Marking Read and Unread |
|
| 16 | 16 | - [x] Change Reset to "Log out" or something along those lines |
|
| 17 | 17 | - [x] Add About to settings menu? |
|
| 18 | - | - [ ] Add Home screen with start button + acknowledgements |
|
| 18 | + | - [x] Add Home screen with start button + acknowledgements |
|
| 19 | 19 | - [ ] Import + Export OPML in settings |
| 1 | 1 | import Dashboard from "./components/dashboard"; |
|
| 2 | + | import { useQuery } from "@evolu/react"; |
|
| 3 | + | import { allFeedsQuery, useEvolu } from "@/lib/evolu"; |
|
| 4 | + | import { Button } from "@/components/ui/button"; |
|
| 5 | + | import { Input } from "@/components/ui/input"; |
|
| 6 | + | import * as React from "react"; |
|
| 7 | + | import { toast } from "sonner"; |
|
| 8 | + | import { |
|
| 9 | + | fetchFeedWithFallback, |
|
| 10 | + | parseFeedXml, |
|
| 11 | + | discoverFeed, |
|
| 12 | + | looksLikeFeedUrl, |
|
| 13 | + | extractPostLink, |
|
| 14 | + | extractPostAuthor, |
|
| 15 | + | extractPostContent, |
|
| 16 | + | extractPostDate, |
|
| 17 | + | } from "@/lib/feed-operations"; |
|
| 2 | 18 | ||
| 3 | 19 | function App() { |
|
| 20 | + | const allFeeds = useQuery(allFeedsQuery); |
|
| 21 | + | const hasFeeds = allFeeds.length > 0; |
|
| 22 | + | const [urlInput, setUrlInput] = React.useState(""); |
|
| 23 | + | const [isAddingFeed, setIsAddingFeed] = React.useState(false); |
|
| 24 | + | const [errorMessage, setErrorMessage] = React.useState(""); |
|
| 25 | + | ||
| 26 | + | const evolu = useEvolu(); |
|
| 27 | + | ||
| 28 | + | async function addFeed() { |
|
| 29 | + | if (!urlInput.trim()) { |
|
| 30 | + | setErrorMessage("Please enter a URL"); |
|
| 31 | + | return; |
|
| 32 | + | } |
|
| 33 | + | ||
| 34 | + | setIsAddingFeed(true); |
|
| 35 | + | setErrorMessage(""); |
|
| 36 | + | ||
| 37 | + | try { |
|
| 38 | + | let feedUrl = urlInput; |
|
| 39 | + | let xmlData: string | null = null; |
|
| 40 | + | ||
| 41 | + | if (!looksLikeFeedUrl(urlInput)) { |
|
| 42 | + | const discovered = await discoverFeed(urlInput); |
|
| 43 | + | ||
| 44 | + | if (!discovered) { |
|
| 45 | + | setErrorMessage( |
|
| 46 | + | "Could not find an RSS feed at this URL. Please enter a direct feed URL.", |
|
| 47 | + | ); |
|
| 48 | + | setIsAddingFeed(false); |
|
| 49 | + | return; |
|
| 50 | + | } |
|
| 51 | + | ||
| 52 | + | feedUrl = discovered.feedUrl; |
|
| 53 | + | xmlData = discovered.xmlData; |
|
| 54 | + | } else { |
|
| 55 | + | xmlData = await fetchFeedWithFallback(feedUrl); |
|
| 56 | + | } |
|
| 57 | + | ||
| 58 | + | const { feedData, posts, isAtom } = parseFeedXml(xmlData); |
|
| 59 | + | ||
| 60 | + | const result = evolu.insert("rssFeed", { |
|
| 61 | + | feedUrl: feedUrl, |
|
| 62 | + | title: feedData.title, |
|
| 63 | + | description: feedData.description || feedData.subtitle || "", |
|
| 64 | + | category: "Uncategorized", |
|
| 65 | + | dateUpdated: new Date().toISOString(), |
|
| 66 | + | }); |
|
| 67 | + | ||
| 68 | + | if (!result.ok) { |
|
| 69 | + | throw new Error("Failed to insert feed"); |
|
| 70 | + | } |
|
| 71 | + | ||
| 72 | + | for (const post of posts) { |
|
| 73 | + | evolu.insert("rssPost", { |
|
| 74 | + | title: post.title, |
|
| 75 | + | author: extractPostAuthor(post, isAtom, feedData.title), |
|
| 76 | + | publishedDate: extractPostDate(post), |
|
| 77 | + | link: extractPostLink(post, isAtom), |
|
| 78 | + | feedId: result.value.id, |
|
| 79 | + | content: extractPostContent(post), |
|
| 80 | + | }); |
|
| 81 | + | } |
|
| 82 | + | ||
| 83 | + | toast.success( |
|
| 84 | + | `Successfully added "${feedData.title}" with ${posts.length} post${posts.length !== 1 ? "s" : ""}`, |
|
| 85 | + | ); |
|
| 86 | + | ||
| 87 | + | setUrlInput(""); |
|
| 88 | + | setErrorMessage(""); |
|
| 89 | + | } catch (error) { |
|
| 90 | + | console.error("Error adding feed:", error); |
|
| 91 | + | setErrorMessage( |
|
| 92 | + | error instanceof Error |
|
| 93 | + | ? error.message |
|
| 94 | + | : "Failed to add feed. Please check the URL and try again.", |
|
| 95 | + | ); |
|
| 96 | + | } finally { |
|
| 97 | + | setIsAddingFeed(false); |
|
| 98 | + | } |
|
| 99 | + | } |
|
| 100 | + | ||
| 4 | 101 | return ( |
|
| 5 | 102 | <main className="min-h-screen w-full items-center justify-center flex-col flex gap-2"> |
|
| 6 | - | <Dashboard /> |
|
| 103 | + | {hasFeeds ? ( |
|
| 104 | + | <Dashboard /> |
|
| 105 | + | ) : ( |
|
| 106 | + | <div className="flex flex-col items-start justify-center gap-6 max-w-md w-full px-4"> |
|
| 107 | + | <div className="flex flex-col gap-2"> |
|
| 108 | + | <h1 className="text-4xl font-bold">Alcove</h1> |
|
| 109 | + | <h4 className="sm:text-sm text-xs"> |
|
| 110 | + | A privacy focused RSS reader for the open web |
|
| 111 | + | </h4> |
|
| 112 | + | </div> |
|
| 113 | + | <div className="flex flex-col gap-3 w-full"> |
|
| 114 | + | <div className="flex flex-row gap-3 w-full"> |
|
| 115 | + | <Input |
|
| 116 | + | value={urlInput} |
|
| 117 | + | onChange={(e) => setUrlInput(e.target.value)} |
|
| 118 | + | placeholder="https://example.com" |
|
| 119 | + | disabled={isAddingFeed} |
|
| 120 | + | onKeyDown={(e) => { |
|
| 121 | + | if (e.key === "Enter") { |
|
| 122 | + | addFeed(); |
|
| 123 | + | } |
|
| 124 | + | }} |
|
| 125 | + | /> |
|
| 126 | + | <Button onClick={addFeed} disabled={isAddingFeed} size="lg"> |
|
| 127 | + | {isAddingFeed ? "Adding..." : "Add Feed"} |
|
| 128 | + | </Button> |
|
| 129 | + | </div> |
|
| 130 | + | {errorMessage && ( |
|
| 131 | + | <div className="text-sm text-center text-destructive"> |
|
| 132 | + | {errorMessage} |
|
| 133 | + | </div> |
|
| 134 | + | )} |
|
| 135 | + | </div> |
|
| 136 | + | </div> |
|
| 137 | + | )} |
|
| 7 | 138 | </main> |
|
| 8 | 139 | ); |
|
| 9 | 140 | } |
| 1 | - | "use client"; |
|
| 2 | - | ||
| 3 | 1 | import * as React from "react"; |
|
| 4 | 2 | import { NavUser } from "@/components/nav-user"; |
|
| 5 | 3 | import { NavFeeds } from "@/components/nav-feeds"; |
|
| 24 | 22 | allReadStatusesQuery, |
|
| 25 | 23 | allReadStatusesWithUnreadQuery, |
|
| 26 | 24 | useEvolu, |
|
| 27 | - | reset, |
|
| 28 | 25 | evolu as evoluInstance, |
|
| 29 | 26 | } from "@/lib/evolu"; |
|
| 30 | 27 | import * as Evolu from "@evolu/common"; |
|
| 37 | 34 | extractPostContent, |
|
| 38 | 35 | extractPostDate, |
|
| 39 | 36 | } from "@/lib/feed-operations"; |
|
| 40 | - | ||
| 41 | - | // This is sample data |
|
| 42 | - | const data = { |
|
| 43 | - | user: { |
|
| 44 | - | name: "shadcn", |
|
| 45 | - | email: "m@example.com", |
|
| 46 | - | avatar: "/avatars/shadcn.jpg", |
|
| 47 | - | }, |
|
| 48 | - | }; |
|
| 49 | 37 | ||
| 50 | 38 | interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> { |
|
| 51 | 39 | selectedFeedId?: string | null; |
|
| 419 | 407 | <FeedActions |
|
| 420 | 408 | onAddFeed={() => setDialogOpen(true)} |
|
| 421 | 409 | onRefresh={refreshFeeds} |
|
| 422 | - | onReset={reset} |
|
| 423 | 410 | /> |
|
| 424 | 411 | <NavFeeds |
|
| 425 | 412 | feeds={allFeeds} |
|
| 430 | 417 | )} |
|
| 431 | 418 | </SidebarContent> |
|
| 432 | 419 | <SidebarFooter> |
|
| 433 | - | {!isMobile || mobileView === "feeds" ? ( |
|
| 434 | - | <NavUser user={data.user} /> |
|
| 435 | - | ) : null} |
|
| 420 | + | {!isMobile || mobileView === "feeds" ? <NavUser /> : null} |
|
| 436 | 421 | </SidebarFooter> |
|
| 437 | 422 | </Sidebar> |
|
| 438 | 423 | ||
| 1 | - | "use client"; |
|
| 2 | - | ||
| 3 | 1 | import * as React from "react"; |
|
| 4 | 2 | import { AppSidebar } from "@/components/app-sidebar"; |
|
| 5 | 3 | import { |
| 11 | 11 | interface FeedActionsProps { |
|
| 12 | 12 | onAddFeed: () => void; |
|
| 13 | 13 | onRefresh: () => void; |
|
| 14 | - | onReset: () => void; |
|
| 15 | 14 | } |
|
| 16 | 15 | ||
| 17 | - | export function FeedActions({ |
|
| 18 | - | onAddFeed, |
|
| 19 | - | onRefresh, |
|
| 20 | - | onReset, |
|
| 21 | - | }: FeedActionsProps) { |
|
| 16 | + | export function FeedActions({ onAddFeed, onRefresh }: FeedActionsProps) { |
|
| 22 | 17 | return ( |
|
| 23 | 18 | <SidebarGroup> |
|
| 24 | 19 | <SidebarGroupLabel>Actions</SidebarGroupLabel> |
|
| 28 | 23 | <SidebarMenuButton onClick={onAddFeed}> |
|
| 29 | 24 | <Plus className="size-4" /> |
|
| 30 | 25 | <span>Add Feed</span> |
|
| 31 | - | </SidebarMenuButton> |
|
| 32 | - | </SidebarMenuItem> |
|
| 33 | - | <SidebarMenuItem> |
|
| 34 | - | <SidebarMenuButton onClick={onReset}> |
|
| 35 | - | <RotateCw className="size-4" /> |
|
| 36 | - | <span>Reset</span> |
|
| 37 | 26 | </SidebarMenuButton> |
|
| 38 | 27 | </SidebarMenuItem> |
|
| 39 | 28 | <SidebarMenuItem> |
|
| 1 | - | "use client" |
|
| 2 | - | ||
| 3 | 1 | import { |
|
| 4 | - | ArrowUpRight, |
|
| 5 | - | Link, |
|
| 6 | - | MoreHorizontal, |
|
| 7 | - | StarOff, |
|
| 8 | - | Trash2, |
|
| 9 | - | } from "lucide-react" |
|
| 2 | + | ArrowUpRight, |
|
| 3 | + | Link, |
|
| 4 | + | MoreHorizontal, |
|
| 5 | + | StarOff, |
|
| 6 | + | Trash2, |
|
| 7 | + | } from "lucide-react"; |
|
| 10 | 8 | ||
| 11 | 9 | import { |
|
| 12 | - | DropdownMenu, |
|
| 13 | - | DropdownMenuContent, |
|
| 14 | - | DropdownMenuItem, |
|
| 15 | - | DropdownMenuSeparator, |
|
| 16 | - | DropdownMenuTrigger, |
|
| 17 | - | } from "@/components/ui/dropdown-menu" |
|
| 10 | + | DropdownMenu, |
|
| 11 | + | DropdownMenuContent, |
|
| 12 | + | DropdownMenuItem, |
|
| 13 | + | DropdownMenuSeparator, |
|
| 14 | + | DropdownMenuTrigger, |
|
| 15 | + | } from "@/components/ui/dropdown-menu"; |
|
| 18 | 16 | import { |
|
| 19 | - | SidebarGroup, |
|
| 20 | - | SidebarGroupLabel, |
|
| 21 | - | SidebarMenu, |
|
| 22 | - | SidebarMenuAction, |
|
| 23 | - | SidebarMenuButton, |
|
| 24 | - | SidebarMenuItem, |
|
| 25 | - | useSidebar, |
|
| 26 | - | } from "@/components/ui/sidebar" |
|
| 17 | + | SidebarGroup, |
|
| 18 | + | SidebarGroupLabel, |
|
| 19 | + | SidebarMenu, |
|
| 20 | + | SidebarMenuAction, |
|
| 21 | + | SidebarMenuButton, |
|
| 22 | + | SidebarMenuItem, |
|
| 23 | + | useSidebar, |
|
| 24 | + | } from "@/components/ui/sidebar"; |
|
| 27 | 25 | ||
| 28 | 26 | export function NavFavorites({ |
|
| 29 | - | favorites, |
|
| 27 | + | favorites, |
|
| 30 | 28 | }: { |
|
| 31 | - | favorites: { |
|
| 32 | - | name: string |
|
| 33 | - | url: string |
|
| 34 | - | emoji: string |
|
| 35 | - | }[] |
|
| 29 | + | favorites: { |
|
| 30 | + | name: string; |
|
| 31 | + | url: string; |
|
| 32 | + | emoji: string; |
|
| 33 | + | }[]; |
|
| 36 | 34 | }) { |
|
| 37 | - | const { isMobile } = useSidebar() |
|
| 35 | + | const { isMobile } = useSidebar(); |
|
| 38 | 36 | ||
| 39 | - | return ( |
|
| 40 | - | <SidebarGroup className="group-data-[collapsible=icon]:hidden"> |
|
| 41 | - | <SidebarGroupLabel>Favorites</SidebarGroupLabel> |
|
| 42 | - | <SidebarMenu> |
|
| 43 | - | {favorites.map((item) => ( |
|
| 44 | - | <SidebarMenuItem key={item.name}> |
|
| 45 | - | <SidebarMenuButton asChild> |
|
| 46 | - | <a href={item.url} title={item.name}> |
|
| 47 | - | <span>{item.emoji}</span> |
|
| 48 | - | <span>{item.name}</span> |
|
| 49 | - | </a> |
|
| 50 | - | </SidebarMenuButton> |
|
| 51 | - | <DropdownMenu> |
|
| 52 | - | <DropdownMenuTrigger asChild> |
|
| 53 | - | <SidebarMenuAction showOnHover> |
|
| 54 | - | <MoreHorizontal /> |
|
| 55 | - | <span className="sr-only">More</span> |
|
| 56 | - | </SidebarMenuAction> |
|
| 57 | - | </DropdownMenuTrigger> |
|
| 58 | - | <DropdownMenuContent |
|
| 59 | - | className="w-56 rounded-lg" |
|
| 60 | - | side={isMobile ? "bottom" : "right"} |
|
| 61 | - | align={isMobile ? "end" : "start"} |
|
| 62 | - | > |
|
| 63 | - | <DropdownMenuItem> |
|
| 64 | - | <StarOff className="text-muted-foreground" /> |
|
| 65 | - | <span>Remove from Favorites</span> |
|
| 66 | - | </DropdownMenuItem> |
|
| 67 | - | <DropdownMenuSeparator /> |
|
| 68 | - | <DropdownMenuItem> |
|
| 69 | - | <Link className="text-muted-foreground" /> |
|
| 70 | - | <span>Copy Link</span> |
|
| 71 | - | </DropdownMenuItem> |
|
| 72 | - | <DropdownMenuItem> |
|
| 73 | - | <ArrowUpRight className="text-muted-foreground" /> |
|
| 74 | - | <span>Open in New Tab</span> |
|
| 75 | - | </DropdownMenuItem> |
|
| 76 | - | <DropdownMenuSeparator /> |
|
| 77 | - | <DropdownMenuItem> |
|
| 78 | - | <Trash2 className="text-muted-foreground" /> |
|
| 79 | - | <span>Delete</span> |
|
| 80 | - | </DropdownMenuItem> |
|
| 81 | - | </DropdownMenuContent> |
|
| 82 | - | </DropdownMenu> |
|
| 83 | - | </SidebarMenuItem> |
|
| 84 | - | ))} |
|
| 85 | - | <SidebarMenuItem> |
|
| 86 | - | <SidebarMenuButton className="text-sidebar-foreground/70"> |
|
| 87 | - | <MoreHorizontal /> |
|
| 88 | - | <span>More</span> |
|
| 89 | - | </SidebarMenuButton> |
|
| 90 | - | </SidebarMenuItem> |
|
| 91 | - | </SidebarMenu> |
|
| 92 | - | </SidebarGroup> |
|
| 93 | - | ) |
|
| 37 | + | return ( |
|
| 38 | + | <SidebarGroup className="group-data-[collapsible=icon]:hidden"> |
|
| 39 | + | <SidebarGroupLabel>Favorites</SidebarGroupLabel> |
|
| 40 | + | <SidebarMenu> |
|
| 41 | + | {favorites.map((item) => ( |
|
| 42 | + | <SidebarMenuItem key={item.name}> |
|
| 43 | + | <SidebarMenuButton asChild> |
|
| 44 | + | <a href={item.url} title={item.name}> |
|
| 45 | + | <span>{item.emoji}</span> |
|
| 46 | + | <span>{item.name}</span> |
|
| 47 | + | </a> |
|
| 48 | + | </SidebarMenuButton> |
|
| 49 | + | <DropdownMenu> |
|
| 50 | + | <DropdownMenuTrigger asChild> |
|
| 51 | + | <SidebarMenuAction showOnHover> |
|
| 52 | + | <MoreHorizontal /> |
|
| 53 | + | <span className="sr-only">More</span> |
|
| 54 | + | </SidebarMenuAction> |
|
| 55 | + | </DropdownMenuTrigger> |
|
| 56 | + | <DropdownMenuContent |
|
| 57 | + | className="w-56 rounded-lg" |
|
| 58 | + | side={isMobile ? "bottom" : "right"} |
|
| 59 | + | align={isMobile ? "end" : "start"} |
|
| 60 | + | > |
|
| 61 | + | <DropdownMenuItem> |
|
| 62 | + | <StarOff className="text-muted-foreground" /> |
|
| 63 | + | <span>Remove from Favorites</span> |
|
| 64 | + | </DropdownMenuItem> |
|
| 65 | + | <DropdownMenuSeparator /> |
|
| 66 | + | <DropdownMenuItem> |
|
| 67 | + | <Link className="text-muted-foreground" /> |
|
| 68 | + | <span>Copy Link</span> |
|
| 69 | + | </DropdownMenuItem> |
|
| 70 | + | <DropdownMenuItem> |
|
| 71 | + | <ArrowUpRight className="text-muted-foreground" /> |
|
| 72 | + | <span>Open in New Tab</span> |
|
| 73 | + | </DropdownMenuItem> |
|
| 74 | + | <DropdownMenuSeparator /> |
|
| 75 | + | <DropdownMenuItem> |
|
| 76 | + | <Trash2 className="text-muted-foreground" /> |
|
| 77 | + | <span>Delete</span> |
|
| 78 | + | </DropdownMenuItem> |
|
| 79 | + | </DropdownMenuContent> |
|
| 80 | + | </DropdownMenu> |
|
| 81 | + | </SidebarMenuItem> |
|
| 82 | + | ))} |
|
| 83 | + | <SidebarMenuItem> |
|
| 84 | + | <SidebarMenuButton className="text-sidebar-foreground/70"> |
|
| 85 | + | <MoreHorizontal /> |
|
| 86 | + | <span>More</span> |
|
| 87 | + | </SidebarMenuButton> |
|
| 88 | + | </SidebarMenuItem> |
|
| 89 | + | </SidebarMenu> |
|
| 90 | + | </SidebarGroup> |
|
| 91 | + | ); |
|
| 94 | 92 | } |
| 1 | - | "use client"; |
|
| 2 | - | ||
| 3 | - | import { ChevronRight, Rss } from "lucide-react"; |
|
| 1 | + | import { ChevronRight } from "lucide-react"; |
|
| 4 | 2 | ||
| 5 | 3 | import { |
|
| 6 | 4 | Collapsible, |
| 1 | - | import { |
|
| 2 | - | BadgeCheck, |
|
| 3 | - | Bell, |
|
| 4 | - | BookKey, |
|
| 5 | - | Copy, |
|
| 6 | - | CreditCard, |
|
| 7 | - | Eye, |
|
| 8 | - | EyeOff, |
|
| 9 | - | Info, |
|
| 10 | - | LogOut, |
|
| 11 | - | Sparkles, |
|
| 12 | - | Trash2, |
|
| 13 | - | Upload, |
|
| 14 | - | } from "lucide-react"; |
|
| 1 | + | import { BookKey, Copy, Eye, EyeOff, Info, Trash2, Upload } from "lucide-react"; |
|
| 15 | 2 | import { |
|
| 16 | 3 | Dialog, |
|
| 17 | 4 | DialogContent, |
|
| 29 | 16 | DropdownMenuContent, |
|
| 30 | 17 | DropdownMenuGroup, |
|
| 31 | 18 | DropdownMenuItem, |
|
| 32 | - | DropdownMenuLabel, |
|
| 33 | 19 | DropdownMenuSeparator, |
|
| 34 | 20 | DropdownMenuTrigger, |
|
| 35 | 21 | } from "@/components/ui/dropdown-menu"; |
|
| 1 | - | "use client" |
|
| 2 | - | ||
| 3 | - | import * as React from "react" |
|
| 4 | - | import { ChevronDown, Plus } from "lucide-react" |
|
| 1 | + | import * as React from "react"; |
|
| 2 | + | import { ChevronDown, Plus } from "lucide-react"; |
|
| 5 | 3 | ||
| 6 | 4 | import { |
|
| 7 | - | DropdownMenu, |
|
| 8 | - | DropdownMenuContent, |
|
| 9 | - | DropdownMenuItem, |
|
| 10 | - | DropdownMenuLabel, |
|
| 11 | - | DropdownMenuSeparator, |
|
| 12 | - | DropdownMenuShortcut, |
|
| 13 | - | DropdownMenuTrigger, |
|
| 14 | - | } from "@/components/ui/dropdown-menu" |
|
| 5 | + | DropdownMenu, |
|
| 6 | + | DropdownMenuContent, |
|
| 7 | + | DropdownMenuItem, |
|
| 8 | + | DropdownMenuLabel, |
|
| 9 | + | DropdownMenuSeparator, |
|
| 10 | + | DropdownMenuShortcut, |
|
| 11 | + | DropdownMenuTrigger, |
|
| 12 | + | } from "@/components/ui/dropdown-menu"; |
|
| 15 | 13 | import { |
|
| 16 | - | SidebarMenu, |
|
| 17 | - | SidebarMenuButton, |
|
| 18 | - | SidebarMenuItem, |
|
| 19 | - | } from "@/components/ui/sidebar" |
|
| 14 | + | SidebarMenu, |
|
| 15 | + | SidebarMenuButton, |
|
| 16 | + | SidebarMenuItem, |
|
| 17 | + | } from "@/components/ui/sidebar"; |
|
| 20 | 18 | ||
| 21 | 19 | export function TeamSwitcher({ |
|
| 22 | - | teams, |
|
| 20 | + | teams, |
|
| 23 | 21 | }: { |
|
| 24 | - | teams: { |
|
| 25 | - | name: string |
|
| 26 | - | logo: React.ElementType |
|
| 27 | - | plan: string |
|
| 28 | - | }[] |
|
| 22 | + | teams: { |
|
| 23 | + | name: string; |
|
| 24 | + | logo: React.ElementType; |
|
| 25 | + | plan: string; |
|
| 26 | + | }[]; |
|
| 29 | 27 | }) { |
|
| 30 | - | const [activeTeam, setActiveTeam] = React.useState(teams[0]) |
|
| 28 | + | const [activeTeam, setActiveTeam] = React.useState(teams[0]); |
|
| 31 | 29 | ||
| 32 | - | if (!activeTeam) { |
|
| 33 | - | return null |
|
| 34 | - | } |
|
| 30 | + | if (!activeTeam) { |
|
| 31 | + | return null; |
|
| 32 | + | } |
|
| 35 | 33 | ||
| 36 | - | return ( |
|
| 37 | - | <SidebarMenu> |
|
| 38 | - | <SidebarMenuItem> |
|
| 39 | - | <DropdownMenu> |
|
| 40 | - | <DropdownMenuTrigger asChild> |
|
| 41 | - | <SidebarMenuButton className="w-fit px-1.5"> |
|
| 42 | - | <div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-5 items-center justify-center rounded-md"> |
|
| 43 | - | <activeTeam.logo className="size-3" /> |
|
| 44 | - | </div> |
|
| 45 | - | <span className="truncate font-medium">{activeTeam.name}</span> |
|
| 46 | - | <ChevronDown className="opacity-50" /> |
|
| 47 | - | </SidebarMenuButton> |
|
| 48 | - | </DropdownMenuTrigger> |
|
| 49 | - | <DropdownMenuContent |
|
| 50 | - | className="w-64 rounded-lg" |
|
| 51 | - | align="start" |
|
| 52 | - | side="bottom" |
|
| 53 | - | sideOffset={4} |
|
| 54 | - | > |
|
| 55 | - | <DropdownMenuLabel className="text-muted-foreground text-xs"> |
|
| 56 | - | Teams |
|
| 57 | - | </DropdownMenuLabel> |
|
| 58 | - | {teams.map((team, index) => ( |
|
| 59 | - | <DropdownMenuItem |
|
| 60 | - | key={team.name} |
|
| 61 | - | onClick={() => setActiveTeam(team)} |
|
| 62 | - | className="gap-2 p-2" |
|
| 63 | - | > |
|
| 64 | - | <div className="flex size-6 items-center justify-center rounded-xs border"> |
|
| 65 | - | <team.logo className="size-4 shrink-0" /> |
|
| 66 | - | </div> |
|
| 67 | - | {team.name} |
|
| 68 | - | <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut> |
|
| 69 | - | </DropdownMenuItem> |
|
| 70 | - | ))} |
|
| 71 | - | <DropdownMenuSeparator /> |
|
| 72 | - | <DropdownMenuItem className="gap-2 p-2"> |
|
| 73 | - | <div className="bg-background flex size-6 items-center justify-center rounded-md border"> |
|
| 74 | - | <Plus className="size-4" /> |
|
| 75 | - | </div> |
|
| 76 | - | <div className="text-muted-foreground font-medium">Add team</div> |
|
| 77 | - | </DropdownMenuItem> |
|
| 78 | - | </DropdownMenuContent> |
|
| 79 | - | </DropdownMenu> |
|
| 80 | - | </SidebarMenuItem> |
|
| 81 | - | </SidebarMenu> |
|
| 82 | - | ) |
|
| 34 | + | return ( |
|
| 35 | + | <SidebarMenu> |
|
| 36 | + | <SidebarMenuItem> |
|
| 37 | + | <DropdownMenu> |
|
| 38 | + | <DropdownMenuTrigger asChild> |
|
| 39 | + | <SidebarMenuButton className="w-fit px-1.5"> |
|
| 40 | + | <div className="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-5 items-center justify-center rounded-md"> |
|
| 41 | + | <activeTeam.logo className="size-3" /> |
|
| 42 | + | </div> |
|
| 43 | + | <span className="truncate font-medium">{activeTeam.name}</span> |
|
| 44 | + | <ChevronDown className="opacity-50" /> |
|
| 45 | + | </SidebarMenuButton> |
|
| 46 | + | </DropdownMenuTrigger> |
|
| 47 | + | <DropdownMenuContent |
|
| 48 | + | className="w-64 rounded-lg" |
|
| 49 | + | align="start" |
|
| 50 | + | side="bottom" |
|
| 51 | + | sideOffset={4} |
|
| 52 | + | > |
|
| 53 | + | <DropdownMenuLabel className="text-muted-foreground text-xs"> |
|
| 54 | + | Teams |
|
| 55 | + | </DropdownMenuLabel> |
|
| 56 | + | {teams.map((team, index) => ( |
|
| 57 | + | <DropdownMenuItem |
|
| 58 | + | key={team.name} |
|
| 59 | + | onClick={() => setActiveTeam(team)} |
|
| 60 | + | className="gap-2 p-2" |
|
| 61 | + | > |
|
| 62 | + | <div className="flex size-6 items-center justify-center rounded-xs border"> |
|
| 63 | + | <team.logo className="size-4 shrink-0" /> |
|
| 64 | + | </div> |
|
| 65 | + | {team.name} |
|
| 66 | + | <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut> |
|
| 67 | + | </DropdownMenuItem> |
|
| 68 | + | ))} |
|
| 69 | + | <DropdownMenuSeparator /> |
|
| 70 | + | <DropdownMenuItem className="gap-2 p-2"> |
|
| 71 | + | <div className="bg-background flex size-6 items-center justify-center rounded-md border"> |
|
| 72 | + | <Plus className="size-4" /> |
|
| 73 | + | </div> |
|
| 74 | + | <div className="text-muted-foreground font-medium">Add team</div> |
|
| 75 | + | </DropdownMenuItem> |
|
| 76 | + | </DropdownMenuContent> |
|
| 77 | + | </DropdownMenu> |
|
| 78 | + | </SidebarMenuItem> |
|
| 79 | + | </SidebarMenu> |
|
| 80 | + | ); |
|
| 83 | 81 | } |