Nicholai d30decf723
feat(ui): add mobile support and dashboard improvements (#30)
* feat(schema): add auth, people, and financial tables

Add users, organizations, teams, groups, and project
members tables. Extend customers/vendors with netsuite
fields. Add netsuite schema for invoices, bills,
payments, and credit memos. Include all migrations,
seeds, new UI primitives, and config updates.

* feat(auth): add WorkOS authentication system

Add login, signup, password reset, email verification,
and invitation flows via WorkOS AuthKit. Includes auth
middleware, permission helpers, dev mode fallbacks,
and auth page components.

* feat(people): add people management system

Add user, team, group, and organization management
with CRUD actions, dashboard pages, invite dialog,
user drawer, and role-based filtering. Includes
WorkOS invitation integration.

* feat(netsuite): add NetSuite integration and financials

Add bidirectional NetSuite REST API integration with
OAuth 2.0, rate limiting, sync engine, and conflict
resolution. Includes invoices, vendor bills, payments,
credit memos CRUD, customer/vendor management pages,
and financial dashboard with tabbed views.

* feat(ui): add mobile support and dashboard improvements

Add mobile bottom nav, FAB, filter bar, search, project
switcher, pull-to-refresh, and schedule mobile view.
Update sidebar with new nav items, settings modal with
integrations tab, responsive dialogs, improved schedule
and file components, PWA manifest, and service worker.

* ci: retrigger build

* ci: retrigger build

---------

Co-authored-by: Nicholai <nicholaivogelfilms@gmail.com>
2026-02-04 16:39:39 -07:00

135 lines
4.3 KiB
TypeScript
Executable File

"use client"
import { forwardRef } from "react"
import { IconStar, IconStarFilled, IconUsers, IconDots } from "@tabler/icons-react"
import { useRouter } from "next/navigation"
import type { FileItem as FileItemType } from "@/lib/files-data"
import { formatRelativeDate } from "@/lib/file-utils"
import { FileIcon } from "./file-icon"
import { useFiles } from "@/hooks/use-files"
import { cn } from "@/lib/utils"
const fileTypeColors: Record<string, string> = {
document: "bg-blue-50 dark:bg-blue-950/30",
spreadsheet: "bg-green-50 dark:bg-green-950/30",
image: "bg-purple-50 dark:bg-purple-950/30",
video: "bg-red-50 dark:bg-red-950/30",
pdf: "bg-red-50 dark:bg-red-950/30",
code: "bg-emerald-50 dark:bg-emerald-950/30",
archive: "bg-orange-50 dark:bg-orange-950/30",
audio: "bg-pink-50 dark:bg-pink-950/30",
unknown: "bg-muted",
}
export const FolderCard = forwardRef<
HTMLDivElement,
{
file: FileItemType
selected: boolean
onClick: (e: React.MouseEvent) => void
}
>(function FolderCard({ file, selected, onClick, ...props }, ref) {
const router = useRouter()
const { dispatch } = useFiles()
const handleDoubleClick = () => {
const folderPath = [...file.path, file.name].join("/")
router.push(`/dashboard/files/${folderPath}`)
}
return (
<div
ref={ref}
className={cn(
"group flex items-center gap-3 rounded-xl border bg-card px-3 py-3 cursor-pointer min-h-[60px]",
"hover:shadow-sm hover:border-border/80 transition-all",
selected && "border-primary ring-2 ring-primary/20"
)}
onClick={onClick}
onDoubleClick={handleDoubleClick}
{...props}
>
<FileIcon type="folder" size={22} className="shrink-0" />
<span className="text-sm font-medium line-clamp-2 flex-1 break-words">{file.name}</span>
{file.shared && (
<IconUsers size={14} className="text-muted-foreground shrink-0" />
)}
<button
className={cn(
"shrink-0 opacity-0 group-hover:opacity-100 transition-opacity",
file.starred && "opacity-100"
)}
onClick={(e) => {
e.stopPropagation()
dispatch({ type: "STAR_FILE", payload: file.id })
}}
>
{file.starred ? (
<IconStarFilled size={14} className="text-amber-400" />
) : (
<IconStar size={14} className="text-muted-foreground hover:text-amber-400" />
)}
</button>
</div>
)
})
export const FileCard = forwardRef<
HTMLDivElement,
{
file: FileItemType
selected: boolean
onClick: (e: React.MouseEvent) => void
}
>(function FileCard({ file, selected, onClick, ...props }, ref) {
const { dispatch } = useFiles()
return (
<div
ref={ref}
className={cn(
"group relative flex flex-col rounded-xl border bg-card overflow-hidden cursor-pointer",
"hover:shadow-sm hover:border-border/80 transition-all",
selected && "border-primary ring-2 ring-primary/20"
)}
onClick={onClick}
{...props}
>
<div
className={cn(
"flex items-center justify-center h-20 sm:h-24",
fileTypeColors[file.type] ?? fileTypeColors.unknown
)}
>
<FileIcon type={file.type} size={32} className="opacity-70 sm:size-10" />
</div>
<div className="flex flex-col gap-1 px-2.5 py-2.5 border-t">
<p className="text-sm font-medium line-clamp-2 break-words leading-snug">{file.name}</p>
<div className="flex items-center justify-between gap-2">
<p className="text-xs text-muted-foreground truncate">
{formatRelativeDate(file.modifiedAt)}
{file.shared && " · Shared"}
</p>
<button
className={cn(
"opacity-0 sm:group-hover:opacity-100 transition-opacity shrink-0",
file.starred && "opacity-100"
)}
onClick={(e) => {
e.stopPropagation()
dispatch({ type: "STAR_FILE", payload: file.id })
}}
>
{file.starred ? (
<IconStarFilled size={14} className="text-amber-400" />
) : (
<IconStar size={14} className="text-muted-foreground hover:text-amber-400" />
)}
</button>
</div>
</div>
</div>
)
})