Adding components for the main dashboard
This commit is contained in:
parent
f7595fc900
commit
5a1489a432
131
app/globals.css
131
app/globals.css
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
64
app/page.tsx
64
app/page.tsx
|
|
@ -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 />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -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 }
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
|
@ -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")],
|
||||
};
|
||||
|
|
@ -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" }
|
||||
};
|
||||
Loading…
Reference in New Issue