+
);
}
diff --git a/packages/admin/src/hooks/useDarkMode.ts b/packages/admin/src/hooks/useDarkMode.ts
new file mode 100644
index 0000000..c510052
--- /dev/null
+++ b/packages/admin/src/hooks/useDarkMode.ts
@@ -0,0 +1,28 @@
+import { useEffect, useState } from "react";
+
+const STORAGE_KEY = "planner_admin_dark";
+
+export function useDarkMode() {
+ const [dark, setDark] = useState(() => {
+ if (typeof window === "undefined") return false;
+ const stored = window.localStorage.getItem(STORAGE_KEY);
+ if (stored !== null) return stored === "true";
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
+ });
+
+ useEffect(() => {
+ const root = document.documentElement;
+ if (dark) {
+ root.classList.add("dark");
+ } else {
+ root.classList.remove("dark");
+ }
+ window.localStorage.setItem(STORAGE_KEY, dark ? "true" : "false");
+ }, [dark]);
+
+ function toggle() {
+ setDark((prev) => !prev);
+ }
+
+ return { dark, toggle };
+}
diff --git a/packages/admin/src/index.css b/packages/admin/src/index.css
index 51ca5af..8ed1f5b 100644
--- a/packages/admin/src/index.css
+++ b/packages/admin/src/index.css
@@ -55,11 +55,11 @@
}
#root {
- width: 1126px;
+ width: 100%;
max-width: 100%;
- margin: 0 auto;
- text-align: center;
- border-inline: 1px solid var(--border);
+ margin: 0;
+ padding: 0;
+ text-align: left;
min-height: 100svh;
display: flex;
flex-direction: column;
diff --git a/packages/admin/src/lib/auth.ts b/packages/admin/src/lib/auth.ts
index 81337fe..86ff280 100644
--- a/packages/admin/src/lib/auth.ts
+++ b/packages/admin/src/lib/auth.ts
@@ -12,6 +12,11 @@ export function clearToken() {
localStorage.removeItem(TOKEN_KEY);
}
+export function logout() {
+ clearToken();
+ window.location.href = "/login";
+}
+
export function isLoggedIn() {
return Boolean(getToken());
}
diff --git a/packages/admin/src/modules/auth/api.ts b/packages/admin/src/modules/auth/api.ts
new file mode 100644
index 0000000..b091497
--- /dev/null
+++ b/packages/admin/src/modules/auth/api.ts
@@ -0,0 +1,12 @@
+import { fetchJson } from "../../lib/api";
+
+export type AuthTokenResponse = {
+ token: string;
+};
+
+export async function login(email: string, password: string) {
+ return fetchJson
("/api/organizador/auth/login", {
+ method: "POST",
+ body: JSON.stringify({ email, password }),
+ });
+}
diff --git a/packages/admin/src/modules/gifts/api.ts b/packages/admin/src/modules/gifts/api.ts
new file mode 100644
index 0000000..a283976
--- /dev/null
+++ b/packages/admin/src/modules/gifts/api.ts
@@ -0,0 +1,19 @@
+import { fetchJson } from "../../lib/api";
+
+export type Gift = {
+ id: string;
+ name: string;
+ status?: string;
+ ownerId?: string;
+};
+
+export async function listGifts() {
+ return fetchJson("/api/organizador/gift");
+}
+
+export async function createGift(payload: { name: string }) {
+ return fetchJson("/api/organizador/gift", {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+}
diff --git a/packages/admin/src/modules/guests/api.ts b/packages/admin/src/modules/guests/api.ts
new file mode 100644
index 0000000..ea83d63
--- /dev/null
+++ b/packages/admin/src/modules/guests/api.ts
@@ -0,0 +1,19 @@
+import { fetchJson } from "../../lib/api";
+
+export type Guest = {
+ id: string;
+ name: string;
+ email?: string;
+ rsvp?: boolean;
+};
+
+export async function listGuests() {
+ return fetchJson("/api/organizador/guest");
+}
+
+export async function createGuest(payload: { name: string; email?: string }) {
+ return fetchJson("/api/organizador/guest", {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+}
diff --git a/packages/admin/src/modules/todos/api.ts b/packages/admin/src/modules/todos/api.ts
new file mode 100644
index 0000000..157a924
--- /dev/null
+++ b/packages/admin/src/modules/todos/api.ts
@@ -0,0 +1,24 @@
+import { fetchJson } from "../../lib/api";
+
+export type Todo = {
+ id: string;
+ title: string;
+ completed: boolean;
+};
+
+export async function listTodos() {
+ return fetchJson("/api/organizador/todo");
+}
+
+export async function createTodo(payload: { title: string }) {
+ return fetchJson("/api/organizador/todo", {
+ method: "POST",
+ body: JSON.stringify(payload),
+ });
+}
+
+export async function completeTodo(id: string) {
+ return fetchJson(`/api/organizador/todo/${id}/complete`, {
+ method: "PATCH",
+ });
+}
diff --git a/packages/admin/src/pages/Dashboard.tsx b/packages/admin/src/pages/Dashboard.tsx
index 74f03fb..67f6b5d 100644
--- a/packages/admin/src/pages/Dashboard.tsx
+++ b/packages/admin/src/pages/Dashboard.tsx
@@ -1,8 +1,25 @@
+import { Card } from "../ui/Card";
+
export function DashboardPage() {
return (
-
-
Dashboard
-
Select a section from the sidebar to manage guests, gifts, or todos.
+
+
+
+ —
+
+
+ —
+
+
+ —
+
+
+
+
+
+ Use the sidebar to navigate, or start by creating a new guest, gift, or todo.
+
+
);
}
diff --git a/packages/admin/src/pages/Gifts.tsx b/packages/admin/src/pages/Gifts.tsx
index b79b5d6..6cdc602 100644
--- a/packages/admin/src/pages/Gifts.tsx
+++ b/packages/admin/src/pages/Gifts.tsx
@@ -1,14 +1,17 @@
import { useEffect, useState } from "react";
-import { fetchJson } from "../lib/api";
+import { createGift, listGifts, type Gift } from "../modules/gifts/api";
+import { Button } from "../ui/Button";
+import { Card } from "../ui/Card";
+import { Input } from "../ui/Input";
export function GiftsPage() {
- const [gifts, setGifts] = useState
([]);
+ const [gifts, setGifts] = useState([]);
const [name, setName] = useState("");
const [error, setError] = useState(null);
const load = async () => {
try {
- const data = await fetchJson("/api/gift");
+ const data = await listGifts();
setGifts(data);
} catch (err: any) {
setError(err.message);
@@ -23,10 +26,7 @@ export function GiftsPage() {
e.preventDefault();
setError(null);
try {
- await fetchJson("/api/gift", {
- method: "POST",
- body: JSON.stringify({ name }),
- });
+ await createGift({ name });
setName("");
load();
} catch (err: any) {
@@ -35,41 +35,40 @@ export function GiftsPage() {
};
return (
-
-
Gifts
- {error ?
{error}
: null}
-
-
-
-
-
- | Name |
- Status |
- Owner |
-
-
-
- {gifts.map((gift) => (
-
- | {gift.name} |
- {gift.status} |
- {gift.ownerId || "—"} |
+
+ {error ?
{error}
: null}
+
+
+
+
+
+
+
+
+ | Name |
+ Status |
+ Owner |
- ))}
-
-
-
+
+
+ {gifts.map((gift) => (
+
+ | {gift.name} |
+ {gift.status} |
+ {gift.ownerId || "—"} |
+
+ ))}
+
+
+
+
);
}
diff --git a/packages/admin/src/pages/Guests.tsx b/packages/admin/src/pages/Guests.tsx
index 9ff1d9e..8ee7a12 100644
--- a/packages/admin/src/pages/Guests.tsx
+++ b/packages/admin/src/pages/Guests.tsx
@@ -1,15 +1,18 @@
import { useEffect, useState } from "react";
-import { fetchJson } from "../lib/api";
+import { createGuest, listGuests, type Guest } from "../modules/guests/api";
+import { Button } from "../ui/Button";
+import { Card } from "../ui/Card";
+import { Input } from "../ui/Input";
export function GuestsPage() {
- const [guests, setGuests] = useState([]);
+ const [guests, setGuests] = useState([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [error, setError] = useState(null);
const load = async () => {
try {
- const data = await fetchJson('/api/guest');
+ const data = await listGuests();
setGuests(data);
} catch (err: any) {
setError(err.message);
@@ -24,12 +27,9 @@ export function GuestsPage() {
e.preventDefault();
setError(null);
try {
- await fetchJson('/api/guest', {
- method: 'POST',
- body: JSON.stringify({ name, email }),
- });
- setName('');
- setEmail('');
+ await createGuest({ name, email });
+ setName("");
+ setEmail("");
load();
} catch (err: any) {
setError(err.message);
@@ -37,47 +37,39 @@ export function GuestsPage() {
};
return (
-
-
Guests
- {error ?
{error}
: null}
-
-
-
-
-
- | Name |
- Email |
- RSVP |
-
-
-
- {guests.map((guest) => (
-
- | {guest.name} |
- {guest.email ?? "—"} |
- {guest.rsvp ? "Yes" : "No"} |
+
+ {error ?
{error}
: null}
+
+
+
+
+
+
+
+
+ | Name |
+ Email |
+ RSVP |
- ))}
-
-
-
+
+
+ {guests.map((guest) => (
+
+ | {guest.name} |
+ {guest.email ?? "—"} |
+ {guest.rsvp ? "Yes" : "No"} |
+
+ ))}
+
+
+
+
);
}
diff --git a/packages/admin/src/pages/Login.tsx b/packages/admin/src/pages/Login.tsx
index cc01220..99a260c 100644
--- a/packages/admin/src/pages/Login.tsx
+++ b/packages/admin/src/pages/Login.tsx
@@ -1,7 +1,10 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
-import { fetchJson } from "../lib/api";
+import { login } from "../modules/auth/api";
import { setToken } from "../lib/auth";
+import { Button } from "../ui/Button";
+import { Card } from "../ui/Card";
+import { Input } from "../ui/Input";
export function LoginPage() {
const navigate = useNavigate();
@@ -13,10 +16,7 @@ export function LoginPage() {
event.preventDefault();
setError(null);
try {
- const result = await fetchJson<{ token: string }>("/api/auth/login", {
- method: "POST",
- body: JSON.stringify({ email, password }),
- });
+ const result = await login(email, password);
setToken(result.token);
navigate("/");
} catch (err: any) {
@@ -25,41 +25,26 @@ export function LoginPage() {
};
return (
-
-
-
Login
+
+
+ Sign in
{error ? {error}
: null}
-
+
);
}
diff --git a/packages/admin/src/pages/Todos.tsx b/packages/admin/src/pages/Todos.tsx
index 429a0d5..5ca33cf 100644
--- a/packages/admin/src/pages/Todos.tsx
+++ b/packages/admin/src/pages/Todos.tsx
@@ -1,14 +1,17 @@
import { useEffect, useState } from "react";
-import { fetchJson } from "../lib/api";
+import { createTodo, completeTodo, listTodos, type Todo } from "../modules/todos/api";
+import { Button } from "../ui/Button";
+import { Card } from "../ui/Card";
+import { Input } from "../ui/Input";
export function TodosPage() {
- const [todos, setTodos] = useState
([]);
+ const [todos, setTodos] = useState([]);
const [title, setTitle] = useState("");
const [error, setError] = useState(null);
const load = async () => {
try {
- const data = await fetchJson("/api/todo");
+ const data = await listTodos();
setTodos(data);
} catch (err: any) {
setError(err.message);
@@ -23,10 +26,7 @@ export function TodosPage() {
e.preventDefault();
setError(null);
try {
- await fetchJson("/api/todo", {
- method: "POST",
- body: JSON.stringify({ title }),
- });
+ await createTodo({ title });
setTitle("");
load();
} catch (err: any) {
@@ -36,7 +36,7 @@ export function TodosPage() {
const markComplete = async (id: string) => {
try {
- await fetchJson(`/api/todo/${id}/complete`, { method: "PATCH" });
+ await completeTodo(id);
load();
} catch (err: any) {
setError(err.message);
@@ -44,52 +44,48 @@ export function TodosPage() {
};
return (
-
-
Todos
- {error ?
{error}
: null}
-
-
-
-
-
- | Title |
- Completed |
- Actions |
-
-
-
- {todos.map((todo) => (
-
- | {todo.title} |
- {todo.completed ? "Yes" : "No"} |
-
- {!todo.completed ? (
-
- ) : (
- "—"
- )}
- |
+
+ {error ?
{error}
: null}
+
+
+
+
+
+
+
+
+ | Title |
+ Completed |
+ Actions |
- ))}
-
-
-
+
+
+ {todos.map((todo) => (
+
+ | {todo.title} |
+ {todo.completed ? "Yes" : "No"} |
+
+ {!todo.completed ? (
+
+ ) : (
+ "—"
+ )}
+ |
+
+ ))}
+
+
+
+
);
}
diff --git a/packages/admin/src/ui/Button.tsx b/packages/admin/src/ui/Button.tsx
new file mode 100644
index 0000000..9a82948
--- /dev/null
+++ b/packages/admin/src/ui/Button.tsx
@@ -0,0 +1,40 @@
+import type { ButtonHTMLAttributes } from "react";
+
+type ButtonVariant = "primary" | "secondary" | "ghost";
+
+type ButtonSize = "sm" | "md" | "lg";
+
+const variantClasses: Record = {
+ primary:
+ "bg-indigo-600 text-white hover:bg-indigo-500 focus-visible:ring-indigo-500",
+ secondary:
+ "bg-slate-200 text-slate-900 hover:bg-slate-300 focus-visible:ring-slate-400 dark:bg-slate-800 dark:text-slate-100 dark:hover:bg-slate-700",
+ ghost:
+ "bg-transparent text-slate-700 hover:bg-slate-100 focus-visible:ring-slate-400 dark:text-slate-200 dark:hover:bg-slate-800",
+};
+
+const sizeClasses: Record = {
+ sm: "px-3 py-1.5 text-sm",
+ md: "px-4 py-2 text-sm",
+ lg: "px-5 py-3 text-base",
+};
+
+export type ButtonProps = ButtonHTMLAttributes & {
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+};
+
+export function Button({
+ variant = "primary",
+ size = "md",
+ className = "",
+ ...props
+}: ButtonProps) {
+ return (
+
+ );
+}
diff --git a/packages/admin/src/ui/Card.tsx b/packages/admin/src/ui/Card.tsx
new file mode 100644
index 0000000..ac85521
--- /dev/null
+++ b/packages/admin/src/ui/Card.tsx
@@ -0,0 +1,25 @@
+import type { ReactNode } from "react";
+
+export function Card({
+ title,
+ subtitle,
+ children,
+ className = "",
+}: {
+ title?: string;
+ subtitle?: string;
+ children: ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {title ? (
+
+ {title}
+ {subtitle ? {subtitle}
: null}
+
+ ) : null}
+ {children}
+
+ );
+}
diff --git a/packages/admin/src/ui/Input.tsx b/packages/admin/src/ui/Input.tsx
new file mode 100644
index 0000000..c1d9663
--- /dev/null
+++ b/packages/admin/src/ui/Input.tsx
@@ -0,0 +1,10 @@
+import type { InputHTMLAttributes } from "react";
+
+export function Input({ className = "", ...props }: InputHTMLAttributes) {
+ return (
+
+ );
+}
diff --git a/packages/admin/src/ui/PageShell.tsx b/packages/admin/src/ui/PageShell.tsx
new file mode 100644
index 0000000..8568dd0
--- /dev/null
+++ b/packages/admin/src/ui/PageShell.tsx
@@ -0,0 +1,23 @@
+import type { ReactNode } from "react";
+import { Sidebar } from "../components/Sidebar";
+import { Topbar } from "./Topbar";
+
+export function PageShell({
+ children,
+ title,
+}: {
+ children: ReactNode;
+ title?: string;
+}) {
+ return (
+
+ );
+}
diff --git a/packages/admin/src/ui/Topbar.tsx b/packages/admin/src/ui/Topbar.tsx
new file mode 100644
index 0000000..d079ab4
--- /dev/null
+++ b/packages/admin/src/ui/Topbar.tsx
@@ -0,0 +1,51 @@
+import { useMemo } from "react";
+import { Moon, Sun, LogOut } from "lucide-react";
+import { useDarkMode } from "../hooks/useDarkMode";
+import { logout } from "../lib/auth";
+import { Button } from "./Button";
+
+const menuItems = [
+ { label: "Dashboard", to: "/" },
+ { label: "Guests", to: "/guests" },
+ { label: "Gifts", to: "/gifts" },
+ { label: "Todos", to: "/todos" },
+];
+
+export function Topbar({ title = "Dashboard" }: { title?: string }) {
+ const { dark, toggle } = useDarkMode();
+
+ const currentLabel = useMemo(() => {
+ const match = menuItems.find((item) => item.label === title);
+ return match ? match.label : title;
+ }, [title]);
+
+ return (
+
+ );
+}
diff --git a/packages/legacy/mikro-orm.config.js b/packages/legacy/mikro-orm.config.js
deleted file mode 100644
index 4a7e542..0000000
--- a/packages/legacy/mikro-orm.config.js
+++ /dev/null
@@ -1,20 +0,0 @@
-const { PostgreSqlDriver } = require('@mikro-orm/postgresql');
-const { Migrator } = require('@mikro-orm/migrations');
-
-/** @type {import('@mikro-orm/core').MikroORMOptions} */
-module.exports = {
- driver: PostgreSqlDriver,
- host: process.env.DB_HOST || 'localhost',
- port: Number(process.env.DB_PORT) || 5432,
- user: process.env.DB_USER || 'postgres',
- password: process.env.DB_PASSWORD || 'postgres',
- dbName: process.env.DB_NAME || 'planner',
- autoLoadEntities: true,
- entities: ['./dist/**/*.entity.js'],
- extensions: [Migrator],
- migrations: {
- path: './dist/migrations',
- transactional: true,
- emit: 'js',
- },
-};
diff --git a/packages/legacy/package.json b/packages/legacy/package.json
deleted file mode 100644
index 467b4a1..0000000
--- a/packages/legacy/package.json
+++ /dev/null
@@ -1,44 +0,0 @@
-{
- "name": "@planner/legacy",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "start": "ts-node -r tsconfig-paths/register src/main.ts",
- "start:dev": "npx nodemon --watch src --ext ts --exec \"npm run build && node dist/main.js\"",
- "build": "tsc -p tsconfig.build.json",
- "migration:generate": "mikro-orm migration:create",
- "migration:run": "mikro-orm migration:up",
- "migration:down": "mikro-orm migration:down"
- },
- "dependencies": {
- "@mikro-orm/core": "^7.0.3",
- "@mikro-orm/decorators": "^7.0.3",
- "@mikro-orm/migrations": "^7.0.3",
- "@mikro-orm/nestjs": "^7.0.1",
- "@mikro-orm/postgresql": "^7.0.3",
- "@nestjs/common": "^11.1.17",
- "@nestjs/core": "^11.1.17",
- "@nestjs/event-emitter": "^3.0.1",
- "@nestjs/platform-express": "^11.1.17",
- "@nestjs/serve-static": "^5.0.4",
- "bcryptjs": "^3.0.3",
- "express": "^5.2.1",
- "jsonwebtoken": "^9.0.3",
- "pg": "^8.20.0",
- "reflect-metadata": "^0.1.13",
- "rxjs": "^7.8.0",
- "uuid": "^13.0.0"
- },
- "devDependencies": {
- "@mikro-orm/cli": "^7.0.3",
- "@swc-node/register": "^1.11.1",
- "@swc/core": "^1.15.18",
- "@types/express": "^4.17.0",
- "@types/jsonwebtoken": "^9.0.0",
- "@types/node": "^20.0.0",
- "nodemon": "^2.0.0",
- "ts-node": "^10.0.0",
- "tsconfig-paths": "^4.0.0",
- "typescript": "^5.0.0"
- }
-}
diff --git a/packages/legacy/public/admin.html b/packages/legacy/public/admin.html
deleted file mode 100644
index 982123e..0000000
--- a/packages/legacy/public/admin.html
+++ /dev/null
@@ -1,448 +0,0 @@
-
-
-
-
-
- Admin Panel
-
-
-
- Admin Panel
- Interfaz de administración (Vue 3 + JWT).
-
-
-
-
Iniciar sesión
-
-
-
-
-
-
-
-
-
-
{{ loginMessage }}
-
-
-
-
Registrar usuario
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ registerMessage }}
-
-
-
-
-
-
-
-
{{ user?.name || user?.email }}
-
Tenant: unificado
-
-
-
-
-
-
-
Módulos
-
-
-
-
-
-
-
Acciones
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ status }}
-
-
-
-
-
Administrar: {{ selectedModule }}
-
-
-
-
-
-
-
-
-
-
-
-