feat: add complete frontend with React, Tailwind 4K, and Docker setup

- Vite + React 18 + TypeScript scaffolding
- Tailwind CSS configured for 4K dark theme (24px base)
- Three full-screen rotating views: Network Topology (D3.js),
  Kanban Board (Odoo tasks), Calendar (Odoo events)
- Hooks for data fetching, WebSocket, and view rotation
- Header with live clock and connection status
- Framer Motion fade transitions between views
- Docker Compose with backend (host network for nmap) and
  frontend (nginx proxy to backend API/WS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 09:34:01 +00:00
parent e6f6dbbab6
commit a7967ecb4a
27 changed files with 3913 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
import { EventCard } from "./EventCard";
import type { CalendarEvent } from "../../types";
interface CalendarViewProps {
events: CalendarEvent[];
}
export function CalendarView({ events }: CalendarViewProps) {
const now = new Date();
const today = now.toISOString().split("T")[0];
const tomorrow = new Date(now.getTime() + 86400000).toISOString().split("T")[0];
const todayEvents = events.filter((e) => e.start.startsWith(today));
const tomorrowEvents = events.filter((e) => e.start.startsWith(tomorrow));
const laterEvents = events.filter(
(e) => !e.start.startsWith(today) && !e.start.startsWith(tomorrow)
);
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("es-MX", {
weekday: "long",
month: "long",
day: "numeric",
});
};
return (
<div className="flex flex-col h-full p-12 overflow-hidden">
<section className="mb-10">
<h2 className="text-3xl font-bold text-text-primary mb-6">
Hoy &mdash; {formatDate(today)}
</h2>
{todayEvents.length === 0 ? (
<p className="text-xl text-text-secondary">Sin eventos programados</p>
) : (
<div className="space-y-4">
{todayEvents.map((e) => (
<EventCard key={e.id} event={e} />
))}
</div>
)}
</section>
<section className="mb-10">
<h2 className="text-2xl font-bold text-text-primary mb-4">
Ma&ntilde;ana &mdash; {formatDate(tomorrow)}
</h2>
{tomorrowEvents.length === 0 ? (
<p className="text-lg text-text-secondary">Sin eventos programados</p>
) : (
<div className="space-y-4">
{tomorrowEvents.map((e) => (
<EventCard key={e.id} event={e} />
))}
</div>
)}
</section>
{laterEvents.length > 0 && (
<section>
<h2 className="text-2xl font-bold text-text-primary mb-4">Esta semana</h2>
<div className="space-y-3">
{laterEvents.map((e) => (
<div key={e.id} className="flex gap-4 text-lg text-text-secondary">
<span className="font-mono min-w-[200px] capitalize">
{formatDate(e.start)}
</span>
<span className="text-text-primary">{e.name}</span>
</div>
))}
</div>
</section>
)}
</div>
);
}