feat: add new UI components including Drawer, InputOTP, Pagination, Sidebar, Sonner, and ToggleGroup
Some checks failed
GBCI / build (push) Failing after 2m18s

- Implemented Drawer component for modal-like functionality.
- Added InputOTP component for handling one-time password inputs.
- Created Pagination component for navigating through paginated content.
- Developed Sidebar component with collapsible and mobile-friendly features.
- Integrated Sonner for toast notifications with theme support.
- Introduced ToggleGroup for grouping toggle buttons with context support.
- Added useIsMobile hook to determine mobile view based on screen width.
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-06-21 20:30:28 -03:00
parent 473fae930a
commit 1d458886dd
2 changed files with 760 additions and 532 deletions

View file

@ -1,33 +1,259 @@
import React from 'react'; "use client";
import { TeamSwitcher } from './components/TeamSwitcher'; import React, { useState } from 'react';
import { MainNav } from './components/MainNav';
import { Search } from './components/Search'; const Dashboard = () => {
import { UserNav } from './components/UserNav'; const [dateRange, setDateRange] = useState({
import { CalendarDateRangePicker } from './components/DateRangePicker'; startDate: new Date(),
import { Overview } from './components/Overview'; endDate: new Date()
import { RecentSales } from './components/RecentSales'; });
const [selectedTeam, setSelectedTeam] = useState({
label: "Alicia Koch",
value: "personal"
});
const [teamSwitcherOpen, setTeamSwitcherOpen] = useState(false);
const [showNewTeamDialog, setShowNewTeamDialog] = useState(false);
const [userNavOpen, setUserNavOpen] = useState(false);
const formatDate = (date) => {
return date.toLocaleDateString('en-US', {
month: 'short',
day: '2-digit',
year: 'numeric'
});
};
const salesData = [
{ name: "Olivia Martin", email: "olivia.martin@email.com", amount: "+$1,999.00" },
{ name: "Jackson Lee", email: "jackson.lee@email.com", amount: "+$39.00" },
{ name: "Isabella Nguyen", email: "isabella.nguyen@email.com", amount: "+$299.00" },
{ name: "William Kim", email: "will@email.com", amount: "+$99.00" },
{ name: "Sofia Davis", email: "sofia.davis@email.com", amount: "+$39.00" },
];
const groups = [
{
label: "Personal Account",
teams: [
{ label: "Alicia Koch", value: "personal" },
],
},
{
label: "Teams",
teams: [
{ label: "Acme Inc.", value: "acme-inc" },
{ label: "Monsters Inc.", value: "monsters" },
],
},
];
const CalendarDateRangePicker = () => (
<div className="flex items-center gap-2">
<button
className="px-3 py-1 border rounded text-foreground border-border hover:bg-accent hover:text-accent-foreground transition-colors"
onClick={() => {
const date = new Date(prompt("Enter start date (YYYY-MM-DD)") || dateRange.startDate);
setDateRange(prev => ({ ...prev, startDate: date }));
}}
>
Start: {formatDate(dateRange.startDate)}
</button>
<span className="text-foreground">to</span>
<button
className="px-3 py-1 border rounded text-foreground border-border hover:bg-accent hover:text-accent-foreground transition-colors"
onClick={() => {
const date = new Date(prompt("Enter end date (YYYY-MM-DD)") || dateRange.endDate);
setDateRange(prev => ({ ...prev, endDate: date }));
}}
>
End: {formatDate(dateRange.endDate)}
</button>
</div>
);
const MainNav = () => (
<nav className="flex space-x-4">
{['Overview', 'Customers', 'Products', 'Settings'].map((item) => (
<button
key={item}
className="px-3 py-2 text-sm font-medium text-foreground hover:text-primary transition-colors"
>
{item}
</button>
))}
</nav>
);
const Search = () => (
<div className="relative max-w-md">
<input
type="text"
placeholder="Search..."
className="w-full pl-8 pr-4 py-2 rounded-full border focus:outline-none focus:ring-2 bg-input border-border focus:ring-ring text-foreground placeholder:text-muted-foreground"
/>
<svg
className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
);
const TeamSwitcher = () => (
<div className="relative">
<button
onClick={() => setTeamSwitcherOpen(true)}
className="flex items-center space-x-2 hover:bg-accent hover:text-accent-foreground p-2 rounded transition-colors"
>
<div className="w-8 h-8 rounded-full bg-secondary flex items-center justify-center text-secondary-foreground">
{selectedTeam.label[0]}
</div>
<span className="text-foreground">{selectedTeam.label}</span>
</button>
{teamSwitcherOpen && (
<div className="absolute z-10 mt-2 w-56 bg-popover rounded-md shadow-lg border border-border">
<div className="p-2">
<input
type="text"
placeholder="Search team..."
className="w-full p-2 border rounded bg-input border-border text-foreground placeholder:text-muted-foreground"
/>
</div>
{groups.map((group) => (
<div key={group.label} className="py-1">
<p className="px-3 py-1 text-sm font-medium text-muted-foreground">{group.label}</p>
{group.teams.map((team) => (
<button
key={team.value}
onClick={() => {
setSelectedTeam(team);
setTeamSwitcherOpen(false);
}}
className="w-full text-left px-3 py-2 hover:bg-accent hover:text-accent-foreground text-popover-foreground transition-colors"
>
{team.label}
</button>
))}
</div>
))}
<div className="border-t border-border p-2">
<button
onClick={() => {
setTeamSwitcherOpen(false);
setShowNewTeamDialog(true);
}}
className="w-full p-2 text-left hover:bg-accent hover:text-accent-foreground text-popover-foreground transition-colors"
>
Create Team
</button>
</div>
</div>
)}
{showNewTeamDialog && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div className="bg-popover p-4 rounded-md border border-border">
<h3 className="text-lg font-medium mb-4 text-popover-foreground">Create team</h3>
<input
type="text"
placeholder="Team name"
className="w-full p-2 border rounded mb-4 bg-input border-border text-foreground placeholder:text-muted-foreground"
/>
<div className="flex justify-end space-x-2">
<button
onClick={() => setShowNewTeamDialog(false)}
className="px-4 py-2 border rounded border-border text-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
>
Cancel
</button>
<button
onClick={() => setShowNewTeamDialog(false)}
className="px-4 py-2 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity"
>
Create
</button>
</div>
</div>
</div>
)}
</div>
);
const UserNav = () => (
<div className="relative">
<button
onClick={() => setUserNavOpen(!userNavOpen)}
className="w-8 h-8 rounded-full bg-secondary flex items-center justify-center text-secondary-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
>
U
</button>
{userNavOpen && (
<div className="absolute right-0 mt-2 w-48 bg-popover rounded-md shadow-lg z-10 border border-border">
<div className="p-2 border-b border-border">
<p className="font-medium text-popover-foreground">shadcn</p>
<p className="text-sm text-muted-foreground">m@example.com</p>
</div>
{['Profile', 'Billing', 'Settings', 'New Team', 'Log out'].map((item) => (
<button
key={item}
className="w-full text-left px-3 py-2 hover:bg-accent hover:text-accent-foreground text-popover-foreground transition-colors"
>
{item}
</button>
))}
</div>
)}
</div>
);
const Overview = () => (
<div className="p-4 border rounded-lg border-border">
<div className="flex justify-between items-end h-40">
{[100, 80, 60, 40, 20].map((height, index) => (
<div
key={index}
className="w-8 opacity-60"
style={{
height: `${height}px`,
backgroundColor: `hsl(var(--chart-${(index % 5) + 1}))`
}}
/>
))}
</div>
</div>
);
const RecentSales = () => (
<div className="space-y-4">
{salesData.map((item, index) => (
<div key={index} className="flex items-center justify-between p-2 border-b border-border">
<div className="flex items-center space-x-3">
<div className="w-8 h-8 rounded-full bg-secondary flex items-center justify-center text-secondary-foreground">
{item.name[0]}
</div>
<div>
<p className="font-medium text-foreground">{item.name}</p>
<p className="text-sm text-muted-foreground">{item.email}</p>
</div>
</div>
<span className="font-medium text-foreground">{item.amount}</span>
</div>
))}
</div>
);
export default function DashboardScreen() {
return ( return (
<div className="min-h-screen bg-gray-50"> <div className="min-h-screen bg-background text-foreground">
<header className="bg-white border-b">
<div className="container flex items-center justify-between h-16 px-4">
<TeamSwitcher />
<MainNav />
<div className="flex items-center space-x-4">
<Search />
<UserNav />
</div>
</div>
</header>
<main className="container p-4 space-y-4"> <main className="container p-4 space-y-4">
s
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">Dashboard</h1> <h1 className="text-2xl font-bold text-foreground">Dashboard</h1>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<CalendarDateRangePicker /> <CalendarDateRangePicker />
<button className="px-4 py-2 bg-blue-500 text-white rounded"> <button className="px-4 py-2 bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity">
Download Download
</button> </button>
</div> </div>
@ -40,26 +266,28 @@ s
{ title: "Sales", value: "+12,234", subtext: "+19% from last month" }, { title: "Sales", value: "+12,234", subtext: "+19% from last month" },
{ title: "Active Now", value: "+573", subtext: "+201 since last hour" }, { title: "Active Now", value: "+573", subtext: "+201 since last hour" },
].map((card, index) => ( ].map((card, index) => (
<div key={index} className="p-6 bg-white border rounded-lg"> <div key={index} className="p-6 bg-card border rounded-lg border-border">
<h3 className="text-sm font-medium text-gray-500">{card.title}</h3> <h3 className="text-sm font-medium text-muted-foreground">{card.title}</h3>
<p className="text-2xl font-bold mt-1">{card.value}</p> <p className="text-2xl font-bold mt-1 text-card-foreground">{card.value}</p>
<p className="text-xs text-gray-500 mt-1">{card.subtext}</p> <p className="text-xs text-muted-foreground mt-1">{card.subtext}</p>
</div> </div>
))} ))}
</div> </div>
<div className="grid gap-4 md:grid-cols-2"> <div className="grid gap-4 md:grid-cols-2">
<div className="p-6 bg-white border rounded-lg"> <div className="p-6 bg-card border rounded-lg border-border">
<h3 className="text-lg font-medium mb-4">Overview</h3> <h3 className="text-lg font-medium mb-4 text-card-foreground">Overview</h3>
<Overview /> <Overview />
</div> </div>
<div className="p-6 bg-white border rounded-lg space-y-4"> <div className="p-6 bg-card border rounded-lg space-y-4 border-border">
<h3 className="text-lg font-medium">Recent Sales</h3> <h3 className="text-lg font-medium text-card-foreground">Recent Sales</h3>
<p>You made 265 sales this month.</p> <p className="text-card-foreground">You made 265 sales this month.</p>
<RecentSales /> <RecentSales />
</div> </div>
</div> </div>
</main> </main>
</div> </div>
); );
} };
export default Dashboard;

View file

@ -1,64 +1,64 @@
"use client" "use client"
import { accounts, mails } from "./data" import { accounts, mails } from "./data"
import * as React from "react" import * as React from "react"
import { import {
AlertCircle, Archive, ArchiveX, Clock, File, Forward, Inbox, Mail as MailIcon, AlertCircle, Archive, ArchiveX, Clock, File, Forward, Inbox, Mail as MailIcon,
MessagesSquare, MoreVertical, Reply, ReplyAll, Search, Send, ShoppingCart, MessagesSquare, MoreVertical, Reply, ReplyAll, Search, Send, ShoppingCart,
Trash2, Users2, LucideIcon Trash2, Users2, LucideIcon
} from "lucide-react" } from "lucide-react"
import { useMail } from "./use-mail" import { useMail } from "./use-mail"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { format, formatDistanceToNow, addDays, addHours, nextSaturday } from "date-fns" import { format, formatDistanceToNow, addDays, addHours, nextSaturday } from "date-fns"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar" import { Calendar } from "@/components/ui/calendar"
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { import {
ResizableHandle, ResizableHandle,
ResizablePanel, ResizablePanel,
ResizablePanelGroup, ResizablePanelGroup,
} from "@/components/ui/resizable" } from "@/components/ui/resizable"
import { ScrollArea } from "@/components/ui/scroll-area" import { ScrollArea } from "@/components/ui/scroll-area"
import { import {
Select, Select,
SelectContent, SelectContent,
SelectItem, SelectItem,
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/components/ui/select" } from "@/components/ui/select"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { import {
Tabs, Tabs,
TabsContent, TabsContent,
TabsList, TabsList,
TabsTrigger, TabsTrigger,
} from "@/components/ui/tabs" } from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea" import { Textarea } from "@/components/ui/textarea"
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipTrigger, TooltipTrigger,
TooltipProvider, TooltipProvider,
} from "@/components/ui/tooltip" } from "@/components/ui/tooltip"
interface Account { interface Account {
label: string label: string
email: string email: string
icon: React.ReactNode icon: React.ReactNode
} }
interface Mail { interface Mail {
id: string id: string
name: string name: string
email: string email: string
@ -67,31 +67,31 @@ interface Mail {
date: string date: string
read: boolean read: boolean
labels: string[] labels: string[]
} }
interface NavLink { interface NavLink {
title: string title: string
label?: string label?: string
icon: LucideIcon icon: LucideIcon
variant: "default" | "ghost" variant: "default" | "ghost"
} }
interface MailProps { interface MailProps {
accounts: Account[] accounts: Account[]
mails: Mail[] mails: Mail[]
defaultLayout?: number[] defaultLayout?: number[]
defaultCollapsed?: boolean defaultCollapsed?: boolean
navCollapsedSize: number navCollapsedSize: number
} }
function MailComponent({ function MailComponent({
accounts, accounts,
mails, mails,
defaultLayout = [20, 32, 48], defaultLayout = [20, 32, 48],
defaultCollapsed = false, defaultCollapsed = false,
navCollapsedSize, navCollapsedSize,
}: MailProps) { }: MailProps) {
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed) const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed)
const [mail, setMail] = useMail() const [mail, setMail] = useMail()
@ -174,9 +174,9 @@ function MailComponent({
</ResizablePanelGroup> </ResizablePanelGroup>
</TooltipProvider> </TooltipProvider>
) )
} }
function AccountSwitcher({ isCollapsed, accounts }: { isCollapsed: boolean, accounts: Account[] }) { function AccountSwitcher({ isCollapsed, accounts }: { isCollapsed: boolean, accounts: Account[] }) {
const [selectedAccount, setSelectedAccount] = React.useState(accounts[0].email) const [selectedAccount, setSelectedAccount] = React.useState(accounts[0].email)
return ( return (
@ -207,9 +207,9 @@ function AccountSwitcher({ isCollapsed, accounts }: { isCollapsed: boolean, acco
</SelectContent> </SelectContent>
</Select> </Select>
) )
} }
function Nav({ links, isCollapsed }: { links: NavLink[], isCollapsed: boolean }) { function Nav({ links, isCollapsed }: { links: NavLink[], isCollapsed: boolean }) {
return ( return (
<div <div
data-collapsed={isCollapsed} data-collapsed={isCollapsed}
@ -265,9 +265,9 @@ function Nav({ links, isCollapsed }: { links: NavLink[], isCollapsed: boolean })
</nav> </nav>
</div> </div>
) )
} }
function MailList({ items }: { items: Mail[] }) { function MailList({ items }: { items: Mail[] }) {
const [mail, setMail] = useMail() const [mail, setMail] = useMail()
return ( return (
@ -311,9 +311,9 @@ function MailList({ items }: { items: Mail[] }) {
</div> </div>
</ScrollArea> </ScrollArea>
) )
} }
function MailDisplay({ mail }: { mail: Mail | null }) { function MailDisplay({ mail }: { mail: Mail | null }) {
const today = new Date() const today = new Date()
return ( return (
@ -490,18 +490,18 @@ function MailDisplay({ mail }: { mail: Mail | null }) {
)} )}
</div> </div>
) )
} }
function getBadgeVariantFromLabel(label: string) { function getBadgeVariantFromLabel(label: string) {
if (["work"].includes(label.toLowerCase())) return "default" if (["work"].includes(label.toLowerCase())) return "default"
if (["personal"].includes(label.toLowerCase())) return "outline" if (["personal"].includes(label.toLowerCase())) return "outline"
return "secondary" return "secondary"
} }
export default function MailPage() { export default function MailPage() {
const layout = [20, 32, 48]; //cookies().get("react-resizable-panels:layout:mail") const layout = [20, 32, 48]; //cookies().get("react-resizable-panels:layout:mail")
const collapsed = false; //cookies().get("react-resizable-panels:collapsed") const collapsed = false; //cookies().get("react-resizable-panels:collapsed")
@ -522,4 +522,4 @@ export default function MailPage() {
</div> </div>
</> </>
) )
} }