gbclient/app/mail/page.tsx
Rodrigo Rodriguez (Pragmatismo) 2a7aa20c4d
Some checks failed
GBCI / build (push) Failing after 10m45s
Add new themes: Orange and XTree Gold
- Introduced a new CSS theme for Orange, featuring a modern color palette with distinct foreground and background colors.
- Added an XTree Gold theme that emulates the classic 1980s DOS interface, complete with authentic colors and styles for file management elements.
- Both themes include variables for customization and specific styles for various UI components such as cards, popovers, and menus.
2025-06-28 19:30:35 -03:00

565 lines
22 KiB
TypeScript

"use client"
import { accounts, mails } from "./data"
import * as React from "react"
import {
AlertCircle, Archive, ArchiveX, Clock, File, Forward, Inbox, Mail as MailIcon,
MessagesSquare, MoreVertical, Reply, ReplyAll, Search, Send, ShoppingCart,
Trash2, Users2, LucideIcon
} from "lucide-react"
import { useMail } from "./use-mail"
import { cn } from "@/lib/utils"
import { format, formatDistanceToNow, addDays, addHours, nextSaturday } from "date-fns"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import Footer from '../footer';
import { ScrollArea } from "@/components/ui/scroll-area"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Separator } from "@/components/ui/separator"
import { Switch } from "@/components/ui/switch"
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs"
import { Textarea } from "@/components/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
TooltipProvider,
} from "@/components/ui/tooltip"
interface Account {
label: string
email: string
icon: React.ReactNode
}
interface Mail {
id: string
name: string
email: string
subject: string
text: string
date: string
read: boolean
labels: string[]
}
interface NavLink {
title: string
label?: string
icon: LucideIcon
variant: "default" | "ghost"
}
interface MailProps {
accounts: Account[]
mails: Mail[]
defaultLayout?: number[]
defaultCollapsed?: boolean
navCollapsedSize: number
}
function MailComponent({
accounts,
mails,
defaultLayout = [20, 32, 48],
defaultCollapsed = false,
navCollapsedSize,
}: MailProps) {
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed)
const [mail, setMail] = useMail()
const navLinks: NavLink[] = [
{ title: "Inbox", label: "128", icon: Inbox, variant: "default" },
{ title: "Drafts", label: "9", icon: File, variant: "ghost" },
{ title: "Sent", label: "", icon: Send, variant: "ghost" },
{ title: "Junk", label: "23", icon: ArchiveX, variant: "ghost" },
{ title: "Trash", label: "", icon: Trash2, variant: "ghost" },
{ title: "Archive", label: "", icon: Archive, variant: "ghost" },
{ title: "Social", label: "972", icon: Users2, variant: "ghost" },
{ title: "Updates", label: "342", icon: AlertCircle, variant: "ghost" },
{ title: "Forums", label: "128", icon: MessagesSquare, variant: "ghost" },
{ title: "Shopping", label: "8", icon: ShoppingCart, variant: "ghost" },
{ title: "Promotions", label: "21", icon: Archive, variant: "ghost" }
]
return (
<TooltipProvider delayDuration={0}>
<ResizablePanelGroup
direction="horizontal"
className="h-full max-h-[800px] items-stretch"
>
{/* Left Sidebar */}
<ResizablePanel
defaultSize={defaultLayout[0]}
collapsedSize={navCollapsedSize}
collapsible={true}
minSize={15}
maxSize={20}
onCollapse={() => setIsCollapsed(true)}
onResize={() => setIsCollapsed(false)}
className={cn(isCollapsed && "min-w-[50px] transition-all duration-300 ease-in-out")}
>
<div className={cn("flex h-[52px] items-center justify-center", isCollapsed ? "h-[52px]" : "px-2")}>
<AccountSwitcher isCollapsed={isCollapsed} accounts={accounts} />
</div>
<Separator />
<Nav links={navLinks.slice(0, 6)} isCollapsed={isCollapsed} />
<Separator />
<Nav links={navLinks.slice(6)} isCollapsed={isCollapsed} />
</ResizablePanel>
<ResizableHandle withHandle />
{/* Middle Mail List */}
<ResizablePanel defaultSize={defaultLayout[1]} minSize={30}>
<Tabs defaultValue="all">
<div className="flex items-center px-4 py-2">
<h1 className="text-xl font-bold">Inbox</h1>
<TabsList className="ml-auto">
<TabsTrigger value="all" className="text-foreground/80">All mail</TabsTrigger>
<TabsTrigger value="unread" className="text-foreground/80">Unread</TabsTrigger>
</TabsList>
</div>
<Separator />
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<form>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input placeholder="Search" className="pl-8" />
</div>
</form>
</div>
<TabsContent value="all" className="m-0">
<MailList items={mails} />
</TabsContent>
<TabsContent value="unread" className="m-0">
<MailList items={mails.filter((item) => !item.read)} />
</TabsContent>
</Tabs>
</ResizablePanel>
<ResizableHandle withHandle />
{/* Right Mail Display */}
<ResizablePanel defaultSize={defaultLayout[2]} minSize={30}>
<MailDisplay mail={mails.find((item) => item.id === mail.selected) || null} />
</ResizablePanel>
</ResizablePanelGroup>
</TooltipProvider>
)
}
function AccountSwitcher({ isCollapsed, accounts }: { isCollapsed: boolean, accounts: Account[] }) {
const [selectedAccount, setSelectedAccount] = React.useState(accounts[0].email)
return (
<Select defaultValue={selectedAccount} onValueChange={setSelectedAccount}>
<SelectTrigger
className={cn(
"flex items-center gap-2 [&>span]:line-clamp-1 [&>span]:flex [&>span]:w-full [&>span]:items-center [&>span]:gap-1 [&>span]:truncate [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0",
isCollapsed && "flex h-9 w-9 shrink-0 items-center justify-center p-0 [&>span]:w-auto [&>svg]:hidden"
)}
aria-label="Select account"
>
<SelectValue placeholder="Select an account">
{accounts.find((account) => account.email === selectedAccount)?.icon}
<span className={cn("ml-2", isCollapsed && "hidden")}>
{accounts.find((account) => account.email === selectedAccount)?.label}
</span>
</SelectValue>
</SelectTrigger>
<SelectContent>
{accounts.map((account) => (
<SelectItem key={account.email} value={account.email}>
<div className="flex items-center gap-3 [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0 [&_svg]:text-foreground">
{account.icon}
{account.email}
</div>
</SelectItem>
))}
</SelectContent>
</Select>
)
}
function Nav({ links, isCollapsed }: { links: NavLink[], isCollapsed: boolean }) {
return (
<div
data-collapsed={isCollapsed}
className="group flex flex-col gap-4 py-2 data-[collapsed=true]:py-2"
>
<nav className="grid gap-1 px-2 group-[[data-collapsed=true]]:justify-center group-[[data-collapsed=true]]:px-2">
{links.map((link, index) =>
isCollapsed ? (
<Tooltip key={index} delayDuration={0}>
<TooltipTrigger asChild>
<a
href="#"
className={cn(
"h-9 w-9 inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
link.variant === "default"
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "hover:bg-secondary hover:text-secondary-foreground",
link.variant === "default" && "dark:bg-primary/10 dark:text-primary-foreground"
)}
>
<link.icon className="h-4 w-4" />
<span className="sr-only">{link.title}</span>
</a>
</TooltipTrigger>
<TooltipContent side="right" className="flex items-center gap-4">
{link.title}
{link.label && <span className="ml-auto text-muted-foreground">{link.label}</span>}
</TooltipContent>
</Tooltip>
) : (
<a
key={index}
href="#"
className={cn(
"inline-flex items-center h-9 px-3 rounded-md text-sm font-medium transition-colors",
link.variant === "default"
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "hover:bg-secondary hover:text-secondary-foreground",
link.variant === "default" && "dark:bg-primary/10 dark:text-primary-foreground",
"justify-start"
)}
>
<link.icon className="mr-2 h-4 w-4" />
{link.title}
{link.label && (
<span className={cn("ml-auto", link.variant === "default" && "text-primary-foreground")}>
{link.label}
</span>
)}
</a>
)
)}
</nav>
</div>
)
}
function MailList({ items }: { items: Mail[] }) {
const [mail, setMail] = useMail()
return (
<ScrollArea className="h-screen">
<div className="flex flex-col gap-2 p-4 pt-0">
{items.map((item) => (
<button
key={item.id}
className={cn(
"flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent",
mail.selected === item.id && "bg-muted"
)}
onClick={() => setMail({ ...mail, selected: item.id })}
>
<div className="flex w-full flex-col gap-1">
<div className="flex items-center">
<div className="flex items-center gap-2">
<div className="font-semibold">{item.name}</div>
{!item.read && <span className="flex h-2 w-2 rounded-full bg-primary" />}
</div>
<div className={cn("ml-auto text-xs", mail.selected === item.id ? "text-foreground" : "text-muted-foreground")}>
{formatDistanceToNow(new Date(item.date), { addSuffix: true })}
</div>
</div>
<div className="text-xs font-medium">{item.subject}</div>
</div>
<div className="line-clamp-2 text-xs text-muted-foreground">
{item.text.substring(0, 300)}
</div>
{item.labels.length ? (
<div className="flex items-center gap-2">
{item.labels.map((label) => (
<Badge key={label} variant={getBadgeVariantFromLabel(label)}>
{label}
</Badge>
))}
</div>
) : null}
</button>
))}
</div>
</ScrollArea>
)
}
function MailDisplay({ mail }: { mail: Mail | null }) {
const today = new Date()
return (
<div className="flex h-full flex-col">
<div className="flex items-center p-2">
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<Archive className="h-4 w-4" />
<span className="sr-only">Archive</span>
</Button>
</TooltipTrigger>
<TooltipContent>Archive</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<ArchiveX className="h-4 w-4" />
<span className="sr-only">Move to junk</span>
</Button>
</TooltipTrigger>
<TooltipContent>Move to junk</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Move to trash</span>
</Button>
</TooltipTrigger>
<TooltipContent>Move to trash</TooltipContent>
</Tooltip>
<Separator orientation="vertical" className="mx-1 h-6" />
<Tooltip>
<Popover>
<PopoverTrigger asChild>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<Clock className="h-4 w-4" />
<span className="sr-only">Snooze</span>
</Button>
</TooltipTrigger>
</PopoverTrigger>
<PopoverContent className="flex w-[535px] p-0">
<div className="flex flex-col gap-2 border-r px-2 py-4">
<div className="px-4 text-sm font-medium">Snooze until</div>
<div className="grid min-w-[250px] gap-1">
<Button variant="ghost" className="justify-start font-normal">
Later today <span className="ml-auto text-muted-foreground">
{format(addHours(today, 4), "E, h:m b")}
</span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
Tomorrow<span className="ml-auto text-muted-foreground">
{format(addDays(today, 1), "E, h:m b")}
</span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
This weekend<span className="ml-auto text-muted-foreground">
{format(nextSaturday(today), "E, h:m b")}
</span>
</Button>
<Button variant="ghost" className="justify-start font-normal">
Next week<span className="ml-auto text-muted-foreground">
{format(addDays(today, 7), "E, h:m b")}
</span>
</Button>
</div>
</div>
<div className="p-2">
<Calendar />
</div>
</PopoverContent>
</Popover>
<TooltipContent>Snooze</TooltipContent>
</Tooltip>
</div>
<div className="ml-auto flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<Reply className="h-4 w-4" />
<span className="sr-only">Reply</span>
</Button>
</TooltipTrigger>
<TooltipContent>Reply</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<ReplyAll className="h-4 w-4" />
<span className="sr-only">Reply all</span>
</Button>
</TooltipTrigger>
<TooltipContent>Reply all</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<Forward className="h-4 w-4" />
<span className="sr-only">Forward</span>
</Button>
</TooltipTrigger>
<TooltipContent>Forward</TooltipContent>
</Tooltip>
</div>
<Separator orientation="vertical" className="mx-2 h-6" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" disabled={!mail}>
<MoreVertical className="h-4 w-4" />
<span className="sr-only">More</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Mark as unread</DropdownMenuItem>
<DropdownMenuItem>Star thread</DropdownMenuItem>
<DropdownMenuItem>Add label</DropdownMenuItem>
<DropdownMenuItem>Mute thread</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<Separator />
{mail ? (
<div className="flex flex-1 flex-col">
<div className="flex items-start p-4">
<div className="flex items-start gap-4 text-sm">
<Avatar>
<AvatarImage alt={mail.name} />
<AvatarFallback>
{mail.name.split(" ").map((chunk) => chunk[0]).join("")}
</AvatarFallback>
</Avatar>
<div className="grid gap-1">
<div className="font-semibold">{mail.name}</div>
<div className="line-clamp-1 text-xs">{mail.subject}</div>
<div className="line-clamp-1 text-xs">
<span className="font-medium">Reply-To:</span> {mail.email}
</div>
</div>
</div>
{mail.date && (
<div className="ml-auto text-xs text-muted-foreground">
{format(new Date(mail.date), "PPpp")}
</div>
)}
</div>
<Separator />
<div className="flex-1 whitespace-pre-wrap p-4 text-sm">
{mail.text}
</div>
<Separator className="mt-auto" />
<div className="p-4">
<form>
<div className="grid gap-4">
<Textarea className="p-4" placeholder={`Reply ${mail.name}...`} />
<div className="flex items-center">
<Label htmlFor="mute" className="flex items-center gap-2 text-xs font-normal">
<Switch id="mute" aria-label="Mute thread" /> Mute this thread
</Label>
<Button onClick={(e) => e.preventDefault()} size="sm" className="ml-auto">
Send
</Button>
</div>
</div>
</form>
</div>
</div>
) : (
<div className="p-8 text-center text-muted-foreground">
No message selected
</div>
)}
</div>
)
}
function getBadgeVariantFromLabel(label: string) {
if (["work"].includes(label.toLowerCase())) return "default"
if (["personal"].includes(label.toLowerCase())) return "outline"
return "secondary"
}
export default function MailPage() {
const layout = [20, 32, 48]; //cookies().get("react-resizable-panels:layout:mail")
const collapsed = false; //cookies().get("react-resizable-panels:collapsed")
const defaultLayout = layout// ? JSON.parse(layout.value) : undefined
const defaultCollapsed = collapsed //? JSON.parse(collapsed.value) : undefined
// Drive-specific keyboard shortcuts
// XTreeGold classic shortcut layout - two rows
// XTree/NC-style: only unique, non-browser, non-reserved keys for file ops
// No F1-F12, Ctrl+F, Ctrl+T, Ctrl+W, Ctrl+N, Ctrl+R, Ctrl+P, etc.
// Use Q, W, E, R, T, Y, U, I, O, P, A, S, D, G, H, J, K, L, Z, X, C, V, B, M with Ctrl/Shift if needed
const shortcuts = [
// File operations row (unique qkeys)
[
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
{ key: 'W', label: 'View', action: () => console.log('View') },
{ key: 'E', label: 'Edit', action: () => console.log('Edit') },
{ key: 'R', label: 'Move', action: () => console.log('Move') },
{ key: 'T', label: 'MkDir', action: () => console.log('Make Directory') },
{ key: 'Y', label: 'Delete', action: () => console.log('Delete') },
{ key: 'U', label: 'Copy', action: () => console.log('Copy') },
{ key: 'I', label: 'Cut', action: () => console.log('Cut') },
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
],
// Navigation/info row (unique qkeys)
[
{ key: 'A', label: 'Select', action: () => console.log('Select') },
{ key: 'S', label: 'Select All', action: () => console.log('Select All') },
{ key: 'D', label: 'Deselect', action: () => console.log('Deselect') },
{ key: 'G', label: 'Details', action: () => console.log('Details') },
{ key: 'H', label: 'History', action: () => console.log('History') },
{ key: 'J', label: 'Share', action: () => console.log('Share') },
{ key: 'K', label: 'Star', action: () => console.log('Star') },
{ key: 'L', label: 'Download', action: () => console.log('Download') },
{ key: 'Z', label: 'Upload', action: () => console.log('Upload') },
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') },
]
];
return (
<>
<div className="flex-col md:flex">
<MailComponent
accounts={accounts}
mails={mails}
defaultLayout={defaultLayout}
defaultCollapsed={defaultCollapsed}
navCollapsedSize={4}
/>
</div>
{/* Footer with Status Bar */}
<Footer shortcuts={shortcuts} />
</>
)
}