ui changes
This commit is contained in:
@@ -89,12 +89,12 @@ export function DashboardMap({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className={`overflow-hidden ${className}`}>
|
||||
<Card className={`overflow-hidden pb-0 bg-white/60 rounded-b-none shadow-none border-none ${className}`}>
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center shadow-lg border-2 border-[#1e40af]">
|
||||
<MapPin className="h-5 w-5 text-[#2563eb]" />
|
||||
<div className="w-10 h-10 rounded-md bg-linear-to-b from-[#225999] to-[#56A5FF] flex items-center justify-center ">
|
||||
<MapPin className="h-5 w-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-bold text-[#2563eb]">
|
||||
@@ -132,7 +132,7 @@ export function DashboardMap({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<div className="h-[400px] w-full relative">
|
||||
<div className="h-[500px] p-2 w-full relative">
|
||||
{isLoading ? (
|
||||
<div className="h-full w-full flex items-center justify-center bg-gray-50 dark:bg-gray-900/50">
|
||||
<Loader2 className="h-8 w-8 text-indigo-500" />
|
||||
|
||||
59
components/dashboard/dashboard-skeleton.tsx
Normal file
59
components/dashboard/dashboard-skeleton.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client"
|
||||
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||
|
||||
export function DashboardSkeleton() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats Cards Skeleton */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Card key={i} className="bg-white/60 backdrop-blur-lg border-none shadow-none overflow-hidden py-8 px-6">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-10 w-20" />
|
||||
<Skeleton className="h-3 w-40" />
|
||||
</div>
|
||||
<Skeleton className="w-16 h-16 rounded-lg shrink-0" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Charts Row Skeleton */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-4">
|
||||
{[...Array(2)].map((_, i) => (
|
||||
<Card key={i} className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||
<CardHeader className="pb-2 border-none">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-10 h-10 rounded-md" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 h-[350px] flex items-center justify-center">
|
||||
<Skeleton className="h-[300px] w-full max-w-[300px] rounded-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Map Skeleton */}
|
||||
<Card className="rounded-md bg-white/60 backdrop-blur-lg overflow-hidden border-none shadow-none">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="w-10 h-10 rounded-md" />
|
||||
<Skeleton className="h-6 w-48" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-32 rounded-full" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-2">
|
||||
<Skeleton className="h-[500px] w-full rounded-md" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ interface DetectionDonutChartProps {
|
||||
roadCrack: number
|
||||
damagedRoadMarking: number
|
||||
goodSignboard: number
|
||||
isLoading: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
@@ -26,7 +26,7 @@ export function DetectionDonutChart({
|
||||
roadCrack,
|
||||
damagedRoadMarking,
|
||||
goodSignboard,
|
||||
isLoading
|
||||
isLoading = false
|
||||
}: DetectionDonutChartProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
@@ -36,23 +36,26 @@ export function FilterSelector({
|
||||
isLoading = false
|
||||
}: FilterSelectorProps) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-3 p-3 rounded-xl bg-card border border-[var(--border)] shadow-sm">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
{/* Project Dropdown */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-blue-100 dark:bg-blue-900/50">
|
||||
<FolderOpen className="h-4 w-4 text-indigo-500" />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-blue-100 dark:bg-blue-900/50">
|
||||
<FolderOpen className="h-3.5 w-3.5 text-blue-600" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Project</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedProjectId || ""}
|
||||
onValueChange={onProjectChange}
|
||||
disabled={isLoading || projects.length === 0}
|
||||
>
|
||||
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectValue placeholder="Select project" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
{projects.map((project) => (
|
||||
<SelectItem key={project.id} value={project.id}>
|
||||
<SelectItem key={project.id} value={project.id} className="rounded-lg">
|
||||
{project.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -60,29 +63,27 @@ export function FilterSelector({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
{selectedProjectId && packages.length > 0 && (
|
||||
<div className="h-5 w-px bg-gray-300 dark:bg-gray-600" />
|
||||
)}
|
||||
|
||||
{/* Package Dropdown */}
|
||||
{selectedProjectId && packages.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-emerald-100 dark:bg-emerald-900/50">
|
||||
<Package className="h-4 w-4 text-emerald-500" />
|
||||
{selectedProjectId && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-emerald-100 dark:bg-emerald-900/50">
|
||||
<Package className="h-3.5 w-3.5 text-emerald-600" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Package</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedPackageId || "all"}
|
||||
onValueChange={onPackageChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectValue placeholder="All packages" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Packages</SelectItem>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
<SelectItem value="all" className="rounded-lg">All Packages</SelectItem>
|
||||
{packages.map((pkg) => (
|
||||
<SelectItem key={pkg.id} value={pkg.id}>
|
||||
<SelectItem key={pkg.id} value={pkg.id} className="rounded-lg">
|
||||
{pkg.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -91,29 +92,27 @@ export function FilterSelector({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
{selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && (
|
||||
<div className="h-5 w-px bg-gray-300 dark:bg-gray-600" />
|
||||
)}
|
||||
|
||||
{/* Location Dropdown */}
|
||||
{selectedPackageId && selectedPackageId !== "all" && locations.length > 0 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-purple-100 dark:bg-purple-900/50">
|
||||
<MapPin className="h-4 w-4 text-purple-500" />
|
||||
{selectedPackageId && selectedPackageId !== "all" && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<div className="p-1 rounded-md bg-purple-100 dark:bg-purple-900/50">
|
||||
<MapPin className="h-3.5 w-3.5 text-purple-600" />
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-gray-400 uppercase tracking-wider">Location</span>
|
||||
</div>
|
||||
<Select
|
||||
value={selectedLocationId || "all"}
|
||||
onValueChange={onLocationChange}
|
||||
disabled={isLoading}
|
||||
>
|
||||
<SelectTrigger className="w-[200px] h-8 text-sm bg-transparent border-0 shadow-none focus:ring-0 px-1">
|
||||
<SelectTrigger className="h-9 text-sm bg-slate-50 border border-slate-200 shadow-none ring-0! focus:ring-0 px-3 w-full rounded-lg">
|
||||
<SelectValue placeholder="All locations" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Locations</SelectItem>
|
||||
<SelectContent className="rounded-xl border-slate-200">
|
||||
<SelectItem value="all" className="rounded-lg">All Locations</SelectItem>
|
||||
{locations.map((loc) => (
|
||||
<SelectItem key={loc.id} value={loc.id}>
|
||||
<SelectItem key={loc.id} value={loc.id} className="rounded-lg">
|
||||
{loc.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
@@ -122,5 +121,5 @@ export function FilterSelector({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,43 +17,43 @@ const gradientStyles = {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||
|
||||
},
|
||||
coral: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||
|
||||
},
|
||||
blue: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||
|
||||
},
|
||||
purple: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||
|
||||
},
|
||||
orange: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||
|
||||
},
|
||||
indigo: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||
|
||||
},
|
||||
emerald: {
|
||||
background: "bg-card",
|
||||
border: "border-l-4 border-l-[var(--border)]",
|
||||
iconBg: "bg-gradient-to-br from-blue-400 to-blue-600",
|
||||
iconShadow: "shadow-lg shadow-blue-500/20"
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ export function GradientStatsCard({
|
||||
const styles = gradientStyles[gradient]
|
||||
|
||||
return (
|
||||
<Card className="bg-white dark:bg-gray-900 border-none shadow-xl shadow-blue-500/5 overflow-hidden py-8 px-6">
|
||||
<Card className="bg-white/60 backdrop-blur-lg border-none shadow-none hover:shadow-none overflow-hidden py-8 px-6">
|
||||
<CardContent className="p-0 flex items-center justify-between gap-6">
|
||||
<div className="flex flex-col gap-1 min-w-0">
|
||||
<h3 className="text-sm font-bold text-blue-600/70 dark:text-blue-400/70 uppercase tracking-widest">
|
||||
@@ -79,11 +79,11 @@ export function GradientStatsCard({
|
||||
<div className="h-14 w-24 bg-gray-100 dark:bg-gray-800 rounded-xl mt-2" />
|
||||
) : (
|
||||
<>
|
||||
<p className="text-3xl md:text-4xl font-black tracking-tighter leading-tight text-[#1e3a8a]">
|
||||
<p className="text-4xl font-bold heading-blue-gradient">
|
||||
{value}
|
||||
</p>
|
||||
{subtitle && (
|
||||
<p className="text-xs font-medium text-gray-500 dark:text-gray-400 mt-1 opacity-80 italic">
|
||||
<p className="text-xs font-medium text-gray-500 mt-1">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
@@ -92,9 +92,12 @@ export function GradientStatsCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`
|
||||
w-20 h-20 rounded-3xl flex items-center justify-center flex-shrink-0
|
||||
${styles.iconBg} shadow-[0_10px_30px_rgba(37,99,235,0.2)]
|
||||
<div
|
||||
style={{
|
||||
background: "linear-gradient(180deg, #225999 0%, #56A5FF 100%)",
|
||||
}}
|
||||
className={`
|
||||
w-16 h-16 self-start rounded-lg flex items-center justify-center
|
||||
`}>
|
||||
<Icon className="h-10 w-10 text-white" />
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ interface LocationData {
|
||||
|
||||
interface LocationBarChartProps {
|
||||
data: LocationData[]
|
||||
isLoading: boolean
|
||||
isLoading?: boolean
|
||||
}
|
||||
|
||||
const COLORS = {
|
||||
@@ -26,7 +26,7 @@ const COLORS = {
|
||||
good_sign_board: "#34d399" // Lighter Emerald
|
||||
}
|
||||
|
||||
export function LocationBarChart({ data, isLoading }: LocationBarChartProps) {
|
||||
export function LocationBarChart({ data, isLoading = false }: LocationBarChartProps) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="h-[300px] flex items-center justify-center">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Eye, AlertCircle } from "lucide-react"
|
||||
import { type Video } from "@/lib/api"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
interface RecentAnalysesTableProps {
|
||||
videos: Video[]
|
||||
@@ -56,9 +57,19 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{isLoading ? (
|
||||
<div className="p-6 space-y-3">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<div key={i} className="h-14 bg-primary/5 rounded-lg" />
|
||||
<div className="p-6 space-y-4">
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
<div key={i} className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-[60%]" />
|
||||
<Skeleton className="h-3 w-[40%]" />
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Skeleton className="h-6 w-16 rounded-full" />
|
||||
<Skeleton className="h-6 w-16 rounded-full" />
|
||||
</div>
|
||||
<Skeleton className="h-8 w-16" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : videos.length === 0 ? (
|
||||
@@ -83,11 +94,11 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent
|
||||
{formatDate(video.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{getDetectionTypeBadge(video.detection_type)}
|
||||
{getStatusBadge(video.status)}
|
||||
</div>
|
||||
<div className="text-right flex-shrink-0 w-20">
|
||||
<div className="text-right shrink-0 w-20">
|
||||
<p className="text-sm font-bold text-foreground">
|
||||
{video.detection_type === "pothole-detection"
|
||||
? video.unique_pothole ?? 0
|
||||
@@ -103,7 +114,7 @@ export function RecentAnalysesTable({ videos, isLoading, onViewResults }: Recent
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewResults(video.id, video.detection_type)}
|
||||
className="ml-4 flex-shrink-0"
|
||||
className="ml-4 shrink-0"
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-1" />
|
||||
View
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
|
||||
interface Column<T> {
|
||||
key: string
|
||||
@@ -42,51 +43,62 @@ export function DataTable<T extends Record<string, any>>({
|
||||
return (
|
||||
<div>
|
||||
{/* Header with Title and Add Button */}
|
||||
<div className="mb-4 flex justify-between items-center bg-card p-3 rounded-xl border border-[var(--border)] shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold tracking-tight text-blue-600">{title}</h2>
|
||||
<p className="text-[10px] text-gray-500 dark:text-gray-400 uppercase font-medium">Total items: {data.length}</p>
|
||||
<div className="flex justify-between items-center p-3 ">
|
||||
<div className="flex items-center gap-3">
|
||||
<h2 className="text-2xl font-bold tracking-tight heading-blue-gradient">{title}</h2>
|
||||
<span className="badge-blue min-w-[28px] h-[28px] px-2">
|
||||
{data.length}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onAddNew}
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white shadow-lg shadow-blue-500/25 transition-all duration-300 hover:scale-105 active:scale-95 border-none h-8 px-4 text-xs font-semibold"
|
||||
className="btn-blue-gradient rounded-full text-sm px-4 py-2.5 h-auto flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5 mr-1.5 stroke-[3px]" />
|
||||
<Plus className="h-4 w-4" />
|
||||
{addButtonText}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-2xl border border-[var(--border)] bg-card shadow-2xl shadow-black/5 overflow-hidden">
|
||||
<div className="overflow-auto max-h-[400px]">
|
||||
<table className="min-w-full divide-y divide-gray-200/50 dark:divide-gray-700/50 border-collapse">
|
||||
<thead className="bg-[#f8fafc] dark:bg-gray-900/50 sticky top-0 z-10">
|
||||
<div className="rounded-lg overflow-hidden">
|
||||
<div className="overflow-auto max-h-[450px]">
|
||||
<table className="min-w-full rounded-lg!">
|
||||
<thead className="bg-white/40 backdrop-blur-md rounded-lg! sticky top-0 z-10">
|
||||
<tr>
|
||||
{columns.map((col) => (
|
||||
<th
|
||||
key={col.key}
|
||||
className="px-4 py-3 text-left text-[10px] font-black text-gray-500 dark:text-gray-400 uppercase tracking-[0.1em] border-b border-[var(--border)] bg-[#f8fafc] dark:bg-gray-900 shadow-[0_1px_0_0_rgba(0,0,0,0.05)]"
|
||||
className="px-4 py-3 text-slate-500 text-left font-bold"
|
||||
>
|
||||
{col.header}
|
||||
</th>
|
||||
))}
|
||||
{showActions && (
|
||||
<th className="px-4 py-3 text-right text-[10px] font-black text-gray-500 dark:text-gray-400 uppercase tracking-[0.1em] border-b border-[var(--border)] bg-[#f8fafc] dark:bg-gray-900 shadow-[0_1px_0_0_rgba(0,0,0,0.05)]">
|
||||
<th className="px-4 py-3 text-slate-500 text-left font-bold">
|
||||
Actions
|
||||
</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100/30 dark:divide-gray-800/30">
|
||||
<tbody className="divide-y divide-gray-300 backdrop-blur-lg bg-white/70">
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td colSpan={totalCols} className="px-4 py-12 text-center">
|
||||
<div className="flex flex-col items-center justify-center gap-3">
|
||||
<div className="h-8 w-8 border-3 border-gray-200 border-t-blue-500 rounded-full animate-spin" />
|
||||
<span className="text-sm font-medium text-gray-500 dark:text-gray-400">Loading records...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
Array.from({ length: 5 }).map((_, idx) => (
|
||||
<tr key={idx} className="border-b border-gray-100 dark:border-gray-800">
|
||||
{columns.map((col) => (
|
||||
<td key={col.key} className="px-4 py-4">
|
||||
<Skeleton className="h-4 w-full max-w-[120px]" />
|
||||
</td>
|
||||
))}
|
||||
{showActions && (
|
||||
<td className="px-4 py-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Skeleton className="h-8 w-8 rounded-sm" />
|
||||
<Skeleton className="h-8 w-8 rounded-sm" />
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))
|
||||
) : data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={totalCols} className="px-4 py-12 text-center">
|
||||
@@ -121,7 +133,7 @@ export function DataTable<T extends Record<string, any>>({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onEdit(item)}
|
||||
className="h-8 w-8 rounded-full bg-amber-100 text-amber-600 hover:bg-amber-600 hover:text-white dark:bg-amber-900/40 dark:text-amber-400 dark:hover:bg-amber-700"
|
||||
className="h-8 w-8 rounded-sm bg-white text-blue-500 border-slate-200! border hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -138,7 +150,7 @@ export function DataTable<T extends Record<string, any>>({
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onDelete(item)}
|
||||
className="h-8 w-8 rounded-full bg-rose-100 text-rose-600 hover:bg-rose-600 hover:text-white dark:bg-rose-900/40 dark:text-rose-400 dark:hover:bg-rose-700"
|
||||
className="h-8 w-8 rounded-sm border border-slate-200! bg-white text-red-400 hover:bg-red-400 hover:text-white"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -114,12 +114,12 @@ export function NavigationMenu() {
|
||||
: "hover:bg-slate-50 border border-transparent hover:border-slate-100"
|
||||
}`}
|
||||
>
|
||||
<div className={`p-2.5 rounded-lg transition-all duration-300 border shadow-sm ${isActive
|
||||
? "bg-[#2563eb] text-white border-[#2563eb]"
|
||||
: "bg-[#f0fafd] text-slate-900 group-hover:bg-[#2563eb] group-hover:text-white group-hover:border-[#2563eb] border-slate-100"
|
||||
<div className={`p-2.5 rounded-lg transition-all duration-300 ${isActive
|
||||
? "bg-linear-to-b from-[#3895FF] to-[#86B8F1] text-white"
|
||||
: "bg-[#f0fafd] text-slate-900 group-hover:bg-linear-to-b group-hover:from-[#3895FF] group-hover:to-[#86B8F1] group-hover:text-white border border-slate-100 shadow-sm"
|
||||
}`}>
|
||||
<Icon
|
||||
className={`h-5 w-5 ${isActive ? 'stroke-[2.5]' : 'stroke-[2]'}`}
|
||||
className={`h-5 w-5 ${isActive ? 'stroke-[2.5]' : 'stroke-2'}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
|
||||
30
components/page-header.tsx
Normal file
30
components/page-header.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client"
|
||||
|
||||
import { LucideIcon } from "lucide-react"
|
||||
|
||||
interface PageHeaderProps {
|
||||
title: string
|
||||
description: string
|
||||
icon?: LucideIcon
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
export function PageHeader({ title, description, icon: Icon, children }: PageHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="p-3 rounded-lg bg-blue-gradient flex items-center justify-center relative overflow-hidden">
|
||||
|
||||
{Icon && <Icon className="h-8 w-8 text-white relative z-10" />}
|
||||
{children}
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<h1 className="text-3xl font-bold heading-blue-gradient">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400 mt-1 text-sm font-medium">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useRouter, usePathname } from "next/navigation"
|
||||
import { LayoutDashboard, Plus, User, FolderPlus, Package, MapPin } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from "@/components/ui/tooltip"
|
||||
|
||||
interface NavItem {
|
||||
title: string
|
||||
@@ -19,19 +20,19 @@ const navItems: NavItem[] = [
|
||||
gradient: "from-blue-400 to-indigo-500"
|
||||
},
|
||||
{
|
||||
title: "Create Project",
|
||||
title: "Project",
|
||||
href: "/create-project",
|
||||
icon: FolderPlus,
|
||||
gradient: "from-blue-400 to-blue-600"
|
||||
},
|
||||
{
|
||||
title: "Create Package",
|
||||
title: "Package",
|
||||
href: "/create-package",
|
||||
icon: Package,
|
||||
gradient: "from-violet-400 to-purple-500"
|
||||
},
|
||||
{
|
||||
title: "Create Location",
|
||||
title: "Location",
|
||||
href: "/create-location",
|
||||
icon: MapPin,
|
||||
gradient: "from-amber-400 to-orange-500"
|
||||
@@ -57,7 +58,7 @@ export function SidebarNavigation() {
|
||||
const isNewAnalysisActive = pathname === "/new-analysis"
|
||||
|
||||
return (
|
||||
<aside className="fixed left-0 top-0 h-screen w-20 bg-white border-r border-slate-200 z-50 flex flex-col items-center py-8 shadow-sm">
|
||||
<aside className="fixed left-0 top-0 h-screen w-20 bg-white/40 backdrop-blur-xl border-r border-slate-200/50 z-50 flex flex-col items-center py-8">
|
||||
{/* Navigation Items */}
|
||||
<nav className="flex-1 flex flex-col items-center gap-4">
|
||||
{/* Dashboard - first item */}
|
||||
@@ -66,58 +67,52 @@ export function SidebarNavigation() {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={item.href} className="relative group">
|
||||
<button
|
||||
onClick={() => handleNavigation(item)}
|
||||
className={`
|
||||
relative w-12 h-12 rounded-xl flex items-center justify-center
|
||||
${isActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'}
|
||||
hover:bg-[#2563eb] hover:border-[#2563eb]
|
||||
border
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-900 group-hover:text-white'} stroke-[2.5]`}
|
||||
/>
|
||||
</button>
|
||||
<div className="
|
||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
||||
px-3 py-1.5 rounded-lg
|
||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
||||
whitespace-nowrap shadow-lg pointer-events-none
|
||||
">
|
||||
{item.title}
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => handleNavigation(item)}
|
||||
className={`
|
||||
relative w-12 h-12 rounded-md flex items-center justify-center
|
||||
${isActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
|
||||
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
|
||||
transition-all ease-in-out duration-200
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={12}>
|
||||
<p>{item.title}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* New Analysis - Special eye-catchy button */}
|
||||
<div className="relative group">
|
||||
<button
|
||||
onClick={() => router.push("/new-analysis")}
|
||||
className={`
|
||||
relative w-12 h-12 rounded-xl flex items-center justify-center
|
||||
${isNewAnalysisActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'}
|
||||
hover:bg-[#2563eb] hover:border-[#2563eb]
|
||||
border
|
||||
`}
|
||||
>
|
||||
<Plus
|
||||
className={`h-6 w-6 ${isNewAnalysisActive ? 'text-white' : 'text-slate-900 group-hover:text-white'} stroke-[2.5]`}
|
||||
/>
|
||||
</button>
|
||||
<div className="
|
||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
||||
px-3 py-1.5 rounded-lg
|
||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
||||
whitespace-nowrap shadow-lg pointer-events-none
|
||||
">
|
||||
Start New Analysis
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => router.push("/new-analysis")}
|
||||
className={`
|
||||
relative w-12 h-12 rounded-md flex items-center justify-center
|
||||
${isNewAnalysisActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
|
||||
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
|
||||
transition-all ease-in-out duration-200
|
||||
`}
|
||||
>
|
||||
<Plus
|
||||
className={`h-6 w-6 ${isNewAnalysisActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={12}>
|
||||
<p>New Analysis</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
@@ -129,29 +124,26 @@ export function SidebarNavigation() {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={item.href} className="relative group">
|
||||
<button
|
||||
onClick={() => handleNavigation(item)}
|
||||
className={`
|
||||
relative w-12 h-12 rounded-xl flex items-center justify-center
|
||||
${isActive ? 'bg-[#2563eb] border-[#2563eb] shadow-md' : 'bg-[#f0fafd] border-slate-100'}
|
||||
hover:bg-[#2563eb] hover:border-[#2563eb]
|
||||
border
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-900 group-hover:text-white'} stroke-[2.5]`}
|
||||
/>
|
||||
</button>
|
||||
<div className="
|
||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
||||
px-3 py-1.5 rounded-lg
|
||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
||||
whitespace-nowrap shadow-lg pointer-events-none
|
||||
">
|
||||
{item.title}
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => handleNavigation(item)}
|
||||
className={`
|
||||
relative w-12 h-12 rounded-md flex items-center justify-center
|
||||
${isActive ? 'bg-linear-to-b from-[#3895FF] to-[#86B8F1]' : 'bg-[#f0fafd]/60 border border-slate-100/50'}
|
||||
hover:bg-linear-to-b hover:from-[#3895FF] hover:to-[#86B8F1] hover:border-none
|
||||
transition-all duration-200
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
className={`h-6 w-6 ${isActive ? 'text-white' : 'text-slate-500 group-hover:text-white'} stroke-[2.5]`}
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={12}>
|
||||
<p>{item.title}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@@ -160,23 +152,20 @@ export function SidebarNavigation() {
|
||||
{/* Bottom section */}
|
||||
<div className="mt-auto">
|
||||
<div className="relative group">
|
||||
<button
|
||||
onClick={() => { }}
|
||||
className="w-12 h-12 rounded-full bg-[#f0fafd] flex items-center justify-center cursor-not-allowed opacity-80 border border-slate-100 hover:bg-[#2563eb] hover:border-[#2563eb]"
|
||||
disabled
|
||||
>
|
||||
<User className="h-6 w-6 text-slate-900 group-hover:text-white" />
|
||||
</button>
|
||||
<div className="
|
||||
absolute left-full ml-3 top-1/2 -translate-y-1/2
|
||||
px-3 py-1.5 rounded-lg
|
||||
bg-gray-900 dark:bg-gray-700 text-white text-sm font-medium
|
||||
opacity-0 invisible group-hover:opacity-100 group-hover:visible
|
||||
whitespace-nowrap shadow-lg pointer-events-none
|
||||
">
|
||||
Account (Coming Soon)
|
||||
<div className="absolute right-full top-1/2 -translate-y-1/2 border-4 border-transparent border-r-gray-900 dark:border-r-gray-700" />
|
||||
</div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
onClick={() => { }}
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center bg-linear-to-b from-[#225999] to-[#56A5FF] transition-all duration-200"
|
||||
disabled
|
||||
>
|
||||
<User className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" sideOffset={12}>
|
||||
<p>Account</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -7,7 +7,7 @@ function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
<div
|
||||
data-slot="card"
|
||||
className={cn(
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm hover:shadow-md hover:border-[var(--card-hover-border)] hover:shadow-[var(--card-glow)]',
|
||||
'bg-card text-card-foreground flex flex-col gap-6 rounded-md border-none py-6 shadow-none hover:shadow-none',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -60,7 +60,7 @@ function DialogContent({
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
'bg-background 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 sm:max-w-lg',
|
||||
'bg-dialog 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 sm:max-w-lg',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn('bg-accent animate-pulse rounded-md', className)}
|
||||
className={cn('bg-muted animate-pulse rounded-md', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ function TooltipContent({
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
'bg-foreground text-background z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||
'bg-foreground text-white z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user