Adding components for the main dashboard

This commit is contained in:
Levi Planelles 2025-12-11 11:50:09 +01:00
parent f7595fc900
commit 5a1489a432
19 changed files with 1610 additions and 129 deletions

View File

@ -1,26 +1,121 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
@layer base {
:root {
--background: #0a0a0a;
--foreground: #ededed;
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--radius: 0.5rem
}
.dark {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,65 +1,5 @@
import Image from "next/image";
import ProjectDashboard from "@/components/dashboard/project-dashboard";
export default function Home() {
return (
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
</div>
);
return <ProjectDashboard />;
}

22
components.json Normal file
View File

@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

View File

@ -0,0 +1,56 @@
"use client";
import { Search, LayoutGrid, List } from 'lucide-react';
interface DashboardHeaderProps {
searchTerm: string;
onSearchChange: (value: string) => void;
viewMode: 'grid' | 'list';
onViewModeChange: (mode: 'grid' | 'list') => void;
}
export default function DashboardHeader({
searchTerm,
onSearchChange,
viewMode,
onViewModeChange
}: DashboardHeaderProps) {
return (
<header className="bg-white border-b border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold text-gray-900">Dashboard de Proyectos</h1>
<p className="text-sm text-gray-500 mt-1">Gestiona y visualiza todos tus proyectos</p>
</div>
<div className="flex items-center gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<input
type="text"
placeholder="Buscar proyectos..."
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent w-64 text-sm"
/>
</div>
<div className="flex gap-1 border border-gray-200 rounded-lg p-1">
<button
onClick={() => onViewModeChange('grid')}
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-gray-100' : 'hover:bg-gray-50'}`}
>
<LayoutGrid className="w-4 h-4 text-gray-600" />
</button>
<button
onClick={() => onViewModeChange('list')}
className={`p-2 rounded ${viewMode === 'list' ? 'bg-gray-100' : 'hover:bg-gray-50'}`}
>
<List className="w-4 h-4 text-gray-600" />
</button>
</div>
</div>
</div>
</div>
</header>
);
}

View File

@ -0,0 +1,81 @@
"use client";
import { Calendar, DollarSign, Users, TrendingUp } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Project, STATUS_CONFIG } from '@/types/project';
interface ProjectCardProps {
project: Project;
}
export default function ProjectCard({ project }: ProjectCardProps) {
return (
<Card className="border-gray-200 hover:shadow-lg transition-all duration-300 hover:-translate-y-1">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<Avatar className="w-12 h-12">
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white font-semibold">
{project.avatar}
</AvatarFallback>
</Avatar>
<div>
<CardTitle className="text-lg text-gray-900">{project.name}</CardTitle>
<CardDescription className="text-sm">{project.client}</CardDescription>
</div>
</div>
</div>
<Badge variant="outline" className={`mt-3 ${STATUS_CONFIG[project.status].color} border`}>
{STATUS_CONFIG[project.status].label}
</Badge>
</CardHeader>
<CardContent>
{/* Progress Bar */}
<div className="mb-4">
<div className="flex items-center justify-between mb-2">
<span className="text-xs font-medium text-gray-600">Progreso</span>
<span className="text-xs font-semibold text-gray-900">{project.progress}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-gradient-to-r from-blue-500 to-purple-600 h-2 rounded-full transition-all duration-500"
style={{ width: `${project.progress}%` }}
/>
</div>
</div>
{/* Budget Info */}
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<div className="flex items-center gap-1 text-gray-500 mb-1">
<DollarSign className="w-3 h-3" />
<span className="text-xs">Presupuesto</span>
</div>
<p className="text-sm font-semibold text-gray-900">€{(project.budget / 1000).toFixed(0)}k</p>
</div>
<div>
<div className="flex items-center gap-1 text-gray-500 mb-1">
<TrendingUp className="w-3 h-3" />
<span className="text-xs">Gastado</span>
</div>
<p className="text-sm font-semibold text-gray-900">€{(project.spent / 1000).toFixed(0)}k</p>
</div>
</div>
{/* Footer Info */}
<div className="pt-4 border-t border-gray-100 flex items-center justify-between">
<div className="flex items-center gap-1 text-gray-500">
<Users className="w-4 h-4" />
<span className="text-xs">{project.team} miembros</span>
</div>
<div className="flex items-center gap-1 text-gray-500">
<Calendar className="w-4 h-4" />
<span className="text-xs">{new Date(project.endDate).toLocaleDateString('es-ES')}</span>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,120 @@
"use client";
import { useState } from 'react';
import DashboardHeader from './dashboard-header';
import StatsOverview from './stats-overview';
import ProjectGrid from './project-grid';
import { Project } from '@/types/project';
// Tipos
type ProjectStatus = 'en_progreso' | 'planificacion' | 'completado' | 'revision';
// Datos mock (luego esto vendrá de Dolibarr)
const mockProjects: Project[] = [
{
id: 1,
name: "Rediseño Web Corporativa",
client: "Tech Solutions SA",
status: "en_progreso",
progress: 65,
budget: 45000,
spent: 29250,
startDate: "2024-10-15",
endDate: "2024-12-30",
team: 4,
avatar: "TS"
},
{
id: 2,
name: "App Móvil E-commerce",
client: "Fashion Store",
status: "en_progreso",
progress: 40,
budget: 80000,
spent: 32000,
startDate: "2024-11-01",
endDate: "2025-02-28",
team: 6,
avatar: "FS"
},
{
id: 3,
name: "Sistema CRM Interno",
client: "Industrial Corp",
status: "planificacion",
progress: 10,
budget: 120000,
spent: 12000,
startDate: "2024-12-01",
endDate: "2025-05-30",
team: 8,
avatar: "IC"
},
{
id: 4,
name: "Dashboard Analytics",
client: "Data Insights",
status: "completado",
progress: 100,
budget: 35000,
spent: 33500,
startDate: "2024-08-01",
endDate: "2024-11-15",
team: 3,
avatar: "DI"
},
{
id: 5,
name: "Migración Cloud AWS",
client: "StartUp Innovate",
status: "en_progreso",
progress: 75,
budget: 95000,
spent: 71250,
startDate: "2024-09-15",
endDate: "2024-12-20",
team: 5,
avatar: "SI"
},
{
id: 6,
name: "Portal de Clientes",
client: "Services Group",
status: "revision",
progress: 90,
budget: 52000,
spent: 48880,
startDate: "2024-10-01",
endDate: "2024-12-10",
team: 4,
avatar: "SG"
}
];
export default function ProjectDashboard() {
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid');
const [searchTerm, setSearchTerm] = useState('');
const filteredProjects = mockProjects.filter(project =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.client.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100">
<DashboardHeader
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
viewMode={viewMode}
onViewModeChange={setViewMode}
/>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<StatsOverview projects={mockProjects} />
<ProjectGrid projects={filteredProjects} viewMode={viewMode} />
</main>
</div>
);
}

View File

@ -0,0 +1,27 @@
"use client";
import ProjectCard from './project-card';
import { Project } from '@/types/project';
interface ProjectGridProps {
projects: Project[];
viewMode: 'grid' | 'list';
}
export default function ProjectGrid({ projects, viewMode }: ProjectGridProps) {
if (projects.length === 0) {
return (
<div className="text-center py-12">
<p className="text-gray-500">No se encontraron proyectos</p>
</div>
);
}
return (
<div className={`grid gap-6 ${viewMode === 'grid' ? 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}>
{projects.map((project) => (
<ProjectCard key={project.id} project={project} />
))}
</div>
);
}

View File

@ -0,0 +1,68 @@
"use client";
import { LayoutGrid, TrendingUp, DollarSign, Clock } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Project } from '@/types/project'; // o la ruta donde pongas types.ts
interface StatsOverviewProps {
projects: Project[];
}
export default function StatsOverview({ projects }: StatsOverviewProps) {
const stats = {
total: projects.length,
activos: projects.filter(p => p.status === 'en_progreso').length,
totalBudget: projects.reduce((acc, p) => acc + p.budget, 0),
avgProgress: Math.round(projects.reduce((acc, p) => acc + p.progress, 0) / projects.length)
};
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<Card className="border-gray-200 hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">Total Proyectos</CardTitle>
<LayoutGrid className="w-4 h-4 text-gray-400" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-gray-900">{stats.total}</div>
<p className="text-xs text-gray-500 mt-1">Todos los proyectos</p>
</CardContent>
</Card>
<Card className="border-gray-200 hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">Activos</CardTitle>
<TrendingUp className="w-4 h-4 text-blue-500" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-gray-900">{stats.activos}</div>
<p className="text-xs text-gray-500 mt-1">En progreso ahora</p>
</CardContent>
</Card>
<Card className="border-gray-200 hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">Presupuesto Total</CardTitle>
<DollarSign className="w-4 h-4 text-green-500" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-gray-900">
€{(stats.totalBudget / 1000).toFixed(0)}k
</div>
<p className="text-xs text-gray-500 mt-1">Suma de presupuestos</p>
</CardContent>
</Card>
<Card className="border-gray-200 hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-gray-600">Progreso Medio</CardTitle>
<Clock className="w-4 h-4 text-purple-500" />
</CardHeader>
<CardContent>
<div className="text-3xl font-bold text-gray-900">{stats.avgProgress}%</div>
<p className="text-xs text-gray-500 mt-1">De todos los proyectos</p>
</CardContent>
</Card>
</div>
);
}

50
components/ui/avatar.tsx Normal file
View File

@ -0,0 +1,50 @@
"use client"
import * as React from "react"
import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cn } from "@/lib/utils"
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
className
)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

36
components/ui/badge.tsx Normal file
View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

56
components/ui/button.tsx Normal file
View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

79
components/ui/card.tsx Normal file
View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,15 @@
import { cn } from "@/lib/utils"
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

827
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -9,18 +9,27 @@
"lint": "eslint"
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.556.0",
"next": "16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0"
"react-dom": "19.2.0",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"autoprefixer": "^10.4.22",
"eslint": "^9",
"eslint-config-next": "16.0.7",
"tailwindcss": "^4",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.18",
"typescript": "^5"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

60
tailwind.config.js Normal file
View File

@ -0,0 +1,60 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
],
theme: {
extend: {
borderRadius: {
lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)',
sm: 'calc(var(--radius) - 4px)'
},
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
card: {
DEFAULT: 'hsl(var(--card))',
foreground: 'hsl(var(--card-foreground))'
},
popover: {
DEFAULT: 'hsl(var(--popover))',
foreground: 'hsl(var(--popover-foreground))'
},
primary: {
DEFAULT: 'hsl(var(--primary))',
foreground: 'hsl(var(--primary-foreground))'
},
secondary: {
DEFAULT: 'hsl(var(--secondary))',
foreground: 'hsl(var(--secondary-foreground))'
},
muted: {
DEFAULT: 'hsl(var(--muted))',
foreground: 'hsl(var(--muted-foreground))'
},
accent: {
DEFAULT: 'hsl(var(--accent))',
foreground: 'hsl(var(--accent-foreground))'
},
destructive: {
DEFAULT: 'hsl(var(--destructive))',
foreground: 'hsl(var(--destructive-foreground))'
},
border: 'hsl(var(--border))',
input: 'hsl(var(--input))',
ring: 'hsl(var(--ring))',
chart: {
'1': 'hsl(var(--chart-1))',
'2': 'hsl(var(--chart-2))',
'3': 'hsl(var(--chart-3))',
'4': 'hsl(var(--chart-4))',
'5': 'hsl(var(--chart-5))'
}
}
}
},
plugins: [require("tailwindcss-animate")],
};

22
types/project.ts Normal file
View File

@ -0,0 +1,22 @@
export type ProjectStatus = 'en_progreso' | 'planificacion' | 'completado' | 'revision';
export interface Project {
id: number;
name: string;
client: string;
status: ProjectStatus;
progress: number;
budget: number;
spent: number;
startDate: string;
endDate: string;
team: number;
avatar: string;
}
export const STATUS_CONFIG: Record<ProjectStatus, { label: string; color: string }> = {
en_progreso: { label: "En Progreso", color: "bg-blue-100 text-blue-800 border-blue-200" },
planificacion: { label: "Planificación", color: "bg-purple-100 text-purple-800 border-purple-200" },
completado: { label: "Completado", color: "bg-green-100 text-green-800 border-green-200" },
revision: { label: "En Revisión", color: "bg-amber-100 text-amber-800 border-amber-200" }
};