Compare commits

..
7 Commits
Author SHA1 Message Date
luriasb b862b85fd6 spec 01 implementada 2026-10-19 01:32:32 -06:00
luriasb 4aced1227d paso 06 2026-10-19 01:03:45 -06:00
luriasb edc17fb13c paso 05 2026-10-19 00:58:45 -06:00
luriasb a6b0ea91cd Create Sidebar.tsx 2026-10-19 00:53:21 -06:00
luriasb e9586acbaa Create feed.ts 2026-10-19 00:50:37 -06:00
luriasb 63a6523483 Update globals.css 2026-10-19 00:49:06 -06:00
luriasb ddc74afc2c step 01 2026-10-19 00:44:50 -06:00
12 changed files with 482 additions and 89 deletions
+2
View File
@@ -39,3 +39,5 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
.playwright-mcp/*
+5
View File
@@ -7,3 +7,8 @@ This version has breaking changes — APIs, conventions, and file structure may
This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.
<!-- END:nextjs-agent-rules --> <!-- END:nextjs-agent-rules -->
## MCPs
- Playwright Scheenshots y cualquier cosa relacionada a Playwright tiene que estar en la carpeta .playwright-mcp.
- Context7 Usaremos este MCP para traer la documentación actualizada del framework.
+11
View File
@@ -0,0 +1,11 @@
export default function FeedHeader() {
return (
<div className="mb-6">
<div className="mb-1 text-[12.5px] font-extrabold tracking-[.8px] text-[#D9583C]">
GUARDERÍA · SALA SOLES
</div>
<h1 className="font-display text-[30px] font-semibold text-[#3F362E]">Buenas, Caro</h1>
<p className="mt-[5px] text-[14.5px] text-[#94887B]">12 niños · martes 17 jun</p>
</div>
);
}
+100
View File
@@ -0,0 +1,100 @@
import Link from "next/link";
import type { Post, PostCategory } from "@/app/data/feed";
/** Badge por categoría: colores exactos de la plantilla. */
const BADGES: Record<PostCategory, { label: string; classes: string }> = {
logro: { label: "LOGRO", classes: "bg-[#CFEBD8] text-[#3E9B6C]" },
actividad: { label: "ACTIVIDAD", classes: "bg-[#C7E7F1] text-[#2E89A6]" },
anuncio: { label: "ANUNCIO", classes: "bg-[#CCD8F4] text-[#4E72C8]" },
};
/**
* Colores dinámicos → utilities Tailwind con valores exactos.
* El set de pares del mock es cerrado; nunca se usan atributos style.
*/
const AVATAR_COLORS: Record<string, string> = {
"A9D9E8-1F7A93": "bg-[#A9D9E8] text-[#1F7A93]",
"CCD8F4-4E72C8": "bg-[#CCD8F4] text-[#4E72C8]",
};
function avatarClasses(post: Post): string {
const key = `${post.authorColors.bg.replace("#", "")}-${post.authorColors.text.replace("#", "")}`;
return AVATAR_COLORS[key] ?? "";
}
export default function PostCard({ post }: { post: Post }) {
const badge = BADGES[post.category];
return (
<article className="rounded-[20px] border border-[#ECE0D0] bg-[#FFFDF9] px-[22px] py-5 shadow-[0_4px_16px_-12px_rgba(120,90,60,.5)]">
{/* Encabezado: avatar + autor + badge */}
<div className="mb-[14px] flex items-center gap-3">
<div
className={`flex h-11 w-11 flex-none items-center justify-center rounded-full font-display text-[17px] font-semibold ${avatarClasses(post)}`}
>
{post.authorInitial ? (
post.authorInitial
) : (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m3 11 18-5v12L3 14v-3zM11.6 16.8a3 3 0 1 1-5.8-1.6" />
</svg>
)}
</div>
<div className="flex-1">
<div className="font-display text-[16.5px] font-semibold text-[#3F362E]">{post.authorName}</div>
<div className="text-[12.5px] text-[#A89A8B]">
{post.timeLabel} · {post.publishedBy}
</div>
</div>
<div className={`flex items-center gap-[7px] rounded-full px-3 py-[6px] ${badge.classes}`}>
<span className="h-2 w-2 rounded-full bg-current" />
<span className="text-xs font-extrabold tracking-[.5px]">{badge.label}</span>
</div>
</div>
{/* Destinatario */}
<div className="mb-2.5 text-[12.5px] text-[#A89A8B]">Para: {post.audience}</div>
{/* Contenido */}
<p className="text-[15.5px] leading-[1.55] text-[#4A4038]">{post.body}</p>
{/* Placeholder de foto (solo posts de actividad con foto) */}
{post.photoLabel && (
<Link
href="/foto"
className="mt-[14px] flex h-[200px] flex-col items-center justify-center gap-2 rounded-2xl border-[1.5px] border-dashed border-[#DBCDBA] bg-[#F4ECE1] text-[#B0A290]"
>
<svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.6-3.6a2 2 0 0 0-2.8 0L6 21" />
</svg>
<span className="text-[13.5px]">Foto · {post.photoLabel}</span>
</Link>
)}
{/* Acciones */}
<div className="mt-4 flex items-center gap-[18px] border-t border-[#F0E6D8] pt-[14px]">
<span className="flex items-center gap-[7px] text-sm font-bold text-[#E0654A]">
<svg width="19" height="19" viewBox="0 0 24 24" fill="#E0654A" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1 1L12 21.2l7.8-7.8 1-1a5.5 5.5 0 0 0 0-7.8z" />
</svg>
{post.likes}
</span>
<Link
href="/detalle-publicacion"
className="flex items-center gap-[7px] text-sm font-bold text-[#94887B]"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8z" />
</svg>
{post.comments}
</Link>
<span className="flex-1" />
<Link href="/crear-publicacion" className="text-sm font-extrabold text-[#C5503A]">
Editar
</Link>
</div>
</article>
);
}
+21
View File
@@ -0,0 +1,21 @@
import Link from "next/link";
export default function ShareBox() {
return (
<Link
href="/crear-publicacion"
className="mb-6 flex items-center gap-[14px] rounded-[18px] border border-[#ECE0D0] bg-[#FFFDF9] px-[18px] py-[14px] shadow-[0_4px_14px_-10px_rgba(120,90,60,.4)]"
>
<div className="flex h-10 w-10 flex-none items-center justify-center rounded-full bg-[#F2937A] font-display text-base font-semibold text-white">
C
</div>
<span className="flex-1 text-[15px] text-[#A89A8B]">Compartí un momento…</span>
<span className="flex h-[38px] w-[38px] items-center justify-center rounded-[12px] bg-[#FBE3D8] text-[#E0654A]">
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</svg>
</span>
</Link>
);
}
+123
View File
@@ -0,0 +1,123 @@
import Link from "next/link";
type NavItem = {
href: string;
label: string;
icon: React.ReactNode;
};
const NAV_ITEMS: NavItem[] = [
{
href: "/",
label: "Feed",
icon: (
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 9.5 12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z" />
</svg>
),
},
{
href: "/ninos",
label: "Niños",
icon: (
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="9" cy="7" r="3" />
<circle cx="17" cy="9" r="2.4" />
<path d="M2.5 20a6.5 6.5 0 0 1 13 0M16 20a5 5 0 0 1 5.5-4.9" />
</svg>
),
},
{
href: "/avisos",
label: "Avisos",
icon: (
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0" />
</svg>
),
},
{
href: "/mi-cuenta",
label: "Mi cuenta",
icon: (
<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
),
},
];
export default function Sidebar({ activeHref = "/" }: { activeHref?: string }) {
return (
<aside className="sticky top-0 flex h-screen w-[248px] flex-none flex-col border-r border-[#ECE0D0] bg-[#FFFDF9] px-4 py-6">
{/* Logo */}
<Link href="/" className="flex items-center gap-[11px] px-2 pb-[22px] pt-1">
<div className="flex h-[38px] w-[38px] flex-none items-center justify-center rounded-[12px] bg-[linear-gradient(155deg,#F8C3A8,#F2937A)]">
<svg width="21" height="21" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4" />
</svg>
</div>
<div>
<div className="font-display text-[17px] font-semibold leading-none text-[#3F362E]">OpenDayCare</div>
<div className="mt-[2px] text-[11.5px] text-[#A89A8B]">Sala Soles</div>
</div>
</Link>
{/* Nueva publicación */}
<Link
href="/crear-publicacion"
className="mb-[18px] flex w-full items-center justify-center gap-2 rounded-[14px] bg-[linear-gradient(180deg,#F4977E,#EE8164)] py-3 text-[14.5px] font-extrabold text-white shadow-[0_8px_18px_-8px_rgba(238,129,100,.75)]"
>
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 5v14M5 12h14" />
</svg>
Nueva publicación
</Link>
{/* Navegación */}
<nav className="flex flex-1 flex-col gap-1">
{NAV_ITEMS.map((item) => {
const active = item.href === activeHref;
return (
<Link
key={item.href}
href={item.href}
className={`flex items-center gap-3 rounded-xl px-3 py-[11px] text-[14.5px] ${
active
? "bg-[#FBE3D8] font-extrabold text-[#D9583C]"
: "bg-transparent font-semibold text-[#6E6359]"
}`}
>
{item.icon}
{item.label}
</Link>
);
})}
</nav>
{/* Usuario */}
<div className="mt-[10px] border-t border-[#ECE0D0] pt-[14px]">
<div className="flex items-center gap-[11px] px-2 py-[6px]">
<div className="flex h-[38px] w-[38px] flex-none items-center justify-center rounded-full bg-[#F2937A] font-display text-base font-semibold text-white">
C
</div>
<div className="min-w-0 flex-1">
<div className="text-sm font-extrabold text-[#3F362E]">Caro Giménez</div>
<div className="text-xs text-[#A89A8B]">Maestra · Soles</div>
</div>
<Link
href="/login"
title="Cerrar sesión"
className="flex h-8 w-8 flex-none items-center justify-center rounded-[10px] bg-[#F6ECDF] text-[#94887B]"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" />
</svg>
</Link>
</div>
</div>
</aside>
);
}
+64
View File
@@ -0,0 +1,64 @@
export type PostCategory = "logro" | "actividad" | "anuncio";
export interface Post {
id: string;
authorName: string;
/** Inicial para el avatar circular (solo posts de niño; el anuncio usa ícono). */
authorInitial: string;
/** Colores del avatar, exactos a la plantilla. */
authorColors: { bg: string; text: string };
timeLabel: string;
publishedBy: string;
/** Define el badge y sus colores vía mapa en PostCard. */
category: PostCategory;
/** Texto tras "Para: " (sin el prefijo). */
audience: string;
body: string;
/** Si existe, se muestra el placeholder dashed de foto con este rótulo. */
photoLabel?: string;
likes: number;
comments: number;
}
export const posts: Post[] = [
{
id: "mateo-logro",
authorName: "Mateo",
authorInitial: "M",
authorColors: { bg: "#A9D9E8", text: "#1F7A93" },
timeLabel: "14:20",
publishedBy: "publicado por vos",
category: "logro",
audience: "familia de Mateo",
body: "¡Usó el orinal solito por primera vez! Estaba feliz de contárselo a todos. Un gran paso.",
likes: 3,
comments: 1,
},
{
id: "mateo-actividad",
authorName: "Mateo",
authorInitial: "M",
authorColors: { bg: "#A9D9E8", text: "#1F7A93" },
timeLabel: "09:40",
publishedBy: "publicado por vos",
category: "actividad",
audience: "familia de Mateo",
body: "Pintamos con témperas esta mañana. Mateo eligió el azul para todo y se concentró un montón mezclando colores.",
photoLabel: "pintando con témperas",
likes: 5,
comments: 2,
},
{
id: "anuncio-parque",
authorName: "Anuncio general",
authorInitial: "",
authorColors: { bg: "#CCD8F4", text: "#4E72C8" },
timeLabel: "07:50",
publishedBy: "publicado por vos",
category: "anuncio",
audience: "toda la sala",
body: "El viernes salimos al parque por la mañana. Recuerden mandar gorra y una botellita de agua.",
likes: 8,
comments: 0,
},
];
+19 -19
View File
@@ -1,26 +1,26 @@
@import "tailwindcss"; @import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline { @theme inline {
--color-background: var(--background); --font-sans: var(--font-nunito), system-ui, sans-serif;
--color-foreground: var(--foreground); --font-display: var(--font-fredoka), system-ui, sans-serif;
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
} }
/* Tema claro fijo: la plantilla no define modo oscuro */
body { body {
background: var(--background); background: #F6ECDF;
color: var(--foreground); color: #3F362E;
font-family: Arial, Helvetica, sans-serif; font-family: var(--font-nunito), system-ui, sans-serif;
-webkit-font-smoothing: antialiased;
}
/* Scrollbar estilo plantilla */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-thumb {
background: #E4D6C4;
border-radius: 8px;
border: 3px solid transparent;
background-clip: content-box;
} }
+9 -9
View File
@@ -1,27 +1,27 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google"; import { Fredoka, Nunito } from "next/font/google";
import "./globals.css"; import "./globals.css";
const geistSans = Geist({ const fredoka = Fredoka({
variable: "--font-geist-sans", variable: "--font-fredoka",
subsets: ["latin"], subsets: ["latin"],
}); });
const geistMono = Geist_Mono({ const nunito = Nunito({
variable: "--font-geist-mono", variable: "--font-nunito",
subsets: ["latin"], subsets: ["latin"],
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App", title: "OpenDayCare",
description: "Generated by create next app", description: "Feed de la guardería OpenDayCare · Sala Soles",
}; };
export default function RootLayout({ children }: LayoutProps<"/">) { export default function RootLayout({ children }: LayoutProps<"/">) {
return ( return (
<html <html
lang="en" lang="es"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`} className={`${fredoka.variable} ${nunito.variable} h-full antialiased`}
> >
<body className="min-h-full flex flex-col">{children}</body> <body className="min-h-full flex flex-col">{children}</body>
</html> </html>
+24 -61
View File
@@ -1,67 +1,30 @@
import Image from "next/image"; import FeedHeader from "@/app/components/FeedHeader";
import PostCard from "@/app/components/PostCard";
import ShareBox from "@/app/components/ShareBox";
import Sidebar from "@/app/components/Sidebar";
import { posts } from "@/app/data/feed";
export default function Home() { export default function Home() {
return ( return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black"> <div className="flex min-h-screen bg-[#F6ECDF]">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start"> <Sidebar activeHref="/" />
<Image <main className="h-screen min-w-0 flex-1 overflow-y-auto">
className="dark:invert h-5 w-[100px]" <div className="mx-auto w-full max-w-[760px] px-10 pb-20 pt-[34px]">
src="/next.svg" <FeedHeader />
alt="Next.js logo" <ShareBox />
width={100}
height={20} <div className="mb-[14px] flex items-center gap-[14px]">
priority <span className="text-[12.5px] font-extrabold tracking-[.8px] text-[#8A7C6D]">
/> PUBLICADO HOY
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left"> </span>
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50"> <span className="h-px flex-1 bg-[#E7DAC8]" />
To get started, edit the{" "} </div>
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx <div className="flex flex-col gap-4">
</code>{" "} {posts.map((post) => (
file. <PostCard key={post.id} post={post} />
</h1> ))}
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400"> </div>
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 h-[14px] w-4"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
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> </div>
</main> </main>
</div> </div>
+6
View File
@@ -0,0 +1,6 @@
# spec workflow configuration
#
# AutoCreateBranch — controls whether /spec-impl creates the git branch automatically.
# true (default) → /spec-impl creates and switches to spec-NN-slug without asking
# false → /spec-impl asks for [y/N] confirmation before creating the branch
AutoCreateBranch: true
+98
View File
@@ -0,0 +1,98 @@
# SPEC 01 — Home: feed de la guardería (plantilla feed.dc.html)
> **Estado:** Implementada
> **Depende de:** —
> **Fecha:** 2026-10-19
> **Objetivo:** Implementar la plantilla `references/pantallas/feed.dc.html` como página home (`/`) en Next.js, visualmente idéntica al original, sin autenticación ni base de datos.
## Alcance
**In:**
- Página `/` (`app/page.tsx`) con: sidebar fijo de 248px (logo OpenDayCare · Sala Soles, botón "Nueva publicación", nav con Feed activo + Niños/Avisos/Mi cuenta, tarjeta de usuario "Caro Giménez" con logout), saludo ("GUARDERÍA · SALA SOLES" / "Buenas, Caro" / "12 niños · martes 17 jun"), caja "Compartí un momento…", separador "PUBLICADO HOY" y los 3 posts de la plantilla (logro, actividad con placeholder de foto, anuncio).
- Datos mock tipados en `app/data/feed.ts` con los 3 posts exactos de la plantilla.
- Componentes en `app/components/`: `Sidebar.tsx`, `FeedHeader.tsx`, `ShareBox.tsx`, `PostCard.tsx`.
- Cambio de fuentes en `app/layout.tsx` (Fredoka + Nunito vía `next/font/google`), `lang="es"` y metadata "OpenDayCare".
- Estilos base en `app/globals.css` (fondo #F6ECDF, texto #3F362E, scrollbar custom, tema claro fijo).
- Todos los links como `<Link>` a rutas futuras (`/crear-publicacion`, `/ninos`, `/avisos`, `/mi-cuenta`, `/detalle-publicacion`, `/login`).
**Out of scope (para specs futuras):**
- Las páginas destino del nav y sus funcionalidades (hoy darán 404 y está bien).
- Autenticación y logout funcional.
- Base de datos y persistencia.
- Interactividad real de likes, comentarios y editar (solo visual, contadores fijos).
- Responsive móvil.
- Dark mode.
- Foto real en el post de actividad (se mantiene el placeholder dashed de la plantilla).
## Modelo de datos
```ts
// app/data/feed.ts
export type PostCategory = "logro" | "actividad" | "anuncio";
export interface Post {
id: string;
authorName: string; // "Mateo" | "Anuncio general"
authorInitial: string; // "M" (solo posts de niño)
authorColors: { bg: string; text: string }; // p.ej. { bg: "#A9D9E8", text: "#1F7A93" }
timeLabel: string; // "14:20"
publishedBy: string; // "publicado por vos"
category: PostCategory; // define badge y colores vía mapa en PostCard
audience: string; // "familia de Mateo" | "toda la sala"
body: string;
photoLabel?: string; // "pintando con témperas" → placeholder dashed
likes: number;
comments: number;
}
export const posts: Post[] = [
// Los 3 posts de la plantilla: logro (3/1), actividad con foto (5/2), anuncio (8/0)
];
```
Convenciones: los colores van como hex exactos de la plantilla; el mapa `category → colores del badge` vive en `PostCard.tsx`.
## Plan de implementación
1. **Fuentes y layout** — `app/layout.tsx`: reemplazar Geist por Fredoka y Nunito como fuentes variables con `next/font/google` (variables `--font-fredoka` / `--font-nunito`), `lang="es"`, title "OpenDayCare". Test manual: `npm run dev` sigue compilando.
2. **Estilos globales** — `app/globals.css`: body con `background:#F6ECDF`, `color:#3F362E`, `font-family: var(--font-nunito), system-ui, sans-serif`; scrollbar custom (thumb `#E4D6C4`); eliminar el bloque `prefers-color-scheme` y las variables Geist. Test manual: la app ya se ve crema.
3. **Mock del feed** — crear `app/data/feed.ts` con el tipo `Post` y los 3 posts de la plantilla.
4. **Sidebar** — crear `app/components/Sidebar.tsx` con logo, botón "Nueva publicación" (→ `/crear-publicacion`), nav (Feed activo, Niños, Avisos, Mi cuenta) y usuario + logout (→ `/login`).
5. **Componentes del feed** — crear `FeedHeader.tsx`, `ShareBox.tsx` y `PostCard.tsx` con utilities Tailwind de valores exactos.
6. **Página home** — reemplazar `app/page.tsx`: flex con Sidebar + `<main>` de scroll propio, contenido centrado `max-width:760px`, mapeando `posts`. Test manual: `http://localhost:3000` muestra el feed completo.
7. **Fidelidad visual** — comparación lado a lado con `references/pantallas/feed.dc.html` a ancho ≥1280px; ajustar diferencias. Captura de verificación en `.playwright-mcp/`.
## Criterios de aceptación
- [ ] `npm run dev` levanta sin errores y `/` muestra el feed completo: sidebar + saludo + caja + separador + 3 posts.
- [ ] `npm run build` completa sin errores.
- [ ] A ancho ≥1280px el layout coincide con la plantilla: sidebar 248px sticky, main con scroll propio y contenido centrado de 760px.
- [ ] Fredoka en logo, saludo y nombres de autor; Nunito (pesos 400–800) en el resto.
- [ ] Post 1: badge LOGRO (fondo `#CFEBD8`, texto `#3E9B6C`), ♥ 3, 💬 1, link "Editar".
- [ ] Post 2: badge ACTIVIDAD (fondo `#C7E7F1`, texto `#2E89A6`), placeholder dashed "Foto · pintando con témperas", ♥ 5, 💬 2.
- [ ] Post 3: avatar megáfono (fondo `#CCD8F4`), badge ANUNCIO, "Para: toda la sala", ♥ 8, 💬 0.
- [ ] Todos los links navegan con `<Link>`: `/crear-publicacion` (botón, caja y Editar), `/ninos`, `/avisos`, `/mi-cuenta`, `/detalle-publicacion` (íconos de comentarios), `/login` (logout).
- [ ] Colores base exactos: fondo `#F6ECDF`, tarjetas `#FFFDF9` con borde `#ECE0D0`, sombras y radios según plantilla.
- [ ] Con el sistema en modo oscuro la página se mantiene en tema claro (sin dark mode).
- [ ] Los componentes nuevos no usan atributos `style`; todo está en utilities Tailwind con valores arbitrarios exactos.
## Decisiones
- **Sí:** Tailwind 4 con valores arbitrarios exactos (`bg-[#F6ECDF]`, `shadow-[0_4px_16px_-12px_rgba(120,90,60,.5)]`…) — consistente con el proyecto y pixel-perfect.
- **No:** estilos inline literales ni CSS Modules — divergen de la convención del repo.
- **Sí:** componentes separados en `app/components/` — el sidebar se reutiliza en Niños, Avisos y Mi cuenta.
- **Sí:** mock tipado en `app/data/feed.ts` — deja el terreno listo para la BD futura.
- **Sí:** `<Link>` a rutas futuras aunque hoy den 404 — la navegación queda cableada desde el día uno.
- **Sí:** solo desktop, réplica exacta — el móvil merece decisiones de diseño propias (otra spec).
- **Sí:** `next/font/google` para Fredoka y Nunito como fuentes variables — misma familia y pesos que el `<link>` de la plantilla, self-hosted y sin FOUT.
- **Sí:** tema claro fijo, eliminando el dark mode automático de `globals.css` — la plantilla no define modo oscuro.
- **No:** autenticación, BD, likes/comentarios/editar funcionales ni foto real — excluidos explícitamente por el usuario.
## Qué **no** está en esta spec
- Otras páginas (Niños, Avisos, Mi cuenta, crear/editar publicación, detalle, login) — cada una irá en su propia spec.
- Autenticación y base de datos.
- Likes, comentarios y edición funcionales.
- Responsive móvil y dark mode.