Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions docs/superpowers/specs/2026-05-29-gallery-projects-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Student Projects Gallery — Design Spec

**Date:** 2026-05-29
**App:** MakerLab Tools v5 (`v5/`), Next.js 16 App Router, Notion-backed, deployed on Vercel.
**Scope:** One PR. Read side (gallery + bidirectional tool links) is independently reviewable from the submit side.

## Goal

A gallery of student project write-ups (blog-style markdown), backed by a new Notion **Projects** database, with **bidirectional linking** between projects and the tools used to build them. Students submit via a **form** (no AI). Submissions land in Notion unpublished; a staff/admin flips `published` in Notion to make them public.

## Non-goals (explicitly out of scope)

- No AI authoring / assistant involvement. The MakerBot chat is untouched.
- No in-app auth or accounts.
- No automatic translation of project content (separate parked feature). Project body stays in whatever language the student wrote.
- Editing/deleting existing projects from the app (admins manage in Notion).

## Notion schema — new `Projects` database

Operator creates the DB in Notion, shares it with the integration, and sets `NOTION_DB_PROJECTS`.

| Field | Type | Notes |
|-------|------|-------|
| `title` | title | Project name |
| `author` | rich_text | Student name/handle (free text; no account) |
| `body` | rich_text | Markdown write-up |
| `photos` | files | Uploaded images (first = cover) |
| `tools_used` | relation → Tools DB | **Bidirectional link.** Auto-creates a back-relation on Tools. |
| `link` | url | Optional — repo / video / external |
| `materials` | multi_select | Optional |
| `published` | checkbox | Default false. Staff gate. |
| `date` | created_time | Submission timestamp |

Notion relations are inherently two-way, so `tools_used` yields a "Projects" back-relation on each Tool with no extra schema work.

## Data layer (`v5/src/lib/`)

- **`notion.ts`**: add `projects` to the DB env map (`NOTION_DB_PROJECTS`), a `ProjectRecord` type + `pageToProject()` parser, `fetchAllProjects()` (published-only filter option), `fetchProject(id)`, and `createProject(fields)` (writes page with `published=false`, photo `file_upload` attachments reusing the pattern from the maintenance photo flow, and `tools_used` relation IDs). Tolerate lower/Title-cased property names like the existing parsers.
- **`catalog.ts`** (or a new `projects.ts`): `getPublishedProjects()` and `getProject(id)` behind `"use cache"` with a new `cacheTag("projects")` + `cacheLife("minutes")`. Add a helper to map a tool id → its linked published projects for the "Built with this" section (derive from the back-relation, or by scanning projects' `tools_used`).
- **`types.ts` / `catalog-types.ts`**: `MakerLabProject` view type (id, title, author, body, photos[], tools[{id,name,slug}], link, materials[], date).

## Routes & UI

- **`/projects`** — replace the current "coming soon" stub with a gallery grid of published projects: cover photo, title, author, tool chips. Reuse card/grid styling and (optionally) the search pattern from the tool gallery (#17) if cheap; otherwise a simple grid.
- **`/projects/[id]`** — detail: cover + photo gallery, `react-markdown` + `remark-gfm` rendered `body` (already used in ChatFab), author, date, `materials`, optional `link`, and **tool chips** linking to `/tools/[id]`.
- **`/projects/new`** — submission form (client component):
- Fields: title, author, markdown `body` (textarea + small live preview via react-markdown), photo upload (reuse `/api/upload-notion` from PR #12 → collect `file_upload` ids), **tool multiselect** populated from `getCatalogTools()`, optional `link`, optional `materials`.
- Submits to `POST /api/projects`. On success: "Thanks — your project is pending review" confirmation; on error: inline message.
- **`/tools/[id]`** (DetailShell): add a **"Built with this"** section listing published projects that reference the tool → link to `/projects/[id]`. Hidden when none.

## API

- **`POST /api/projects`** — validates payload (title, author, body required; photos/tools/link/materials optional), rate-limited via the existing `rate-limit.ts` (`projects:${ip}`, e.g. 10/min), calls `createProject()` with `published=false`. Returns the new page id. Node default runtime (no `runtime` export — cacheComponents).
- Extend **`POST /api/admin/revalidate`** to also bust the `projects` cache tag (so a freshly-published project appears without waiting for `cacheLife`).

## Cross-cutting

- **Moderation:** `published=false` by default; admin flips in Notion. Gallery + detail + "Built with this" only ever show published projects. `/projects/new` is always reachable.
- **i18n:** all new UI strings (gallery, detail labels, form labels, confirmation/errors, nav) into all 12 `v5/messages/*.json` files; English authoritative, others machine-translated (flag for native QC). Project *content* is not translated.
- **Rate limiting:** the submit endpoint uses the existing limiter.
- **Caching:** `projects` tag; revalidate endpoint busts it.
- **Nav:** the existing `/projects` nav entry stays; add a visible "Submit a project" affordance on `/projects`.

## Build structure (one PR, two logical halves)

1. **Read side:** schema wiring + data layer + `/projects` + `/projects/[id]` + "Built with this" on tool detail. Reviewable against a Notion DB that has a couple of manually-published rows.
2. **Submit side:** `/projects/new` form + `POST /api/projects` + photo/tool wiring + revalidate-tag extension.

## Operator follow-up (after merge)

1. Create the `Projects` Notion DB with the schema above; add the `tools_used` relation to the Tools DB.
2. Share the DB with the integration; set `NOTION_DB_PROJECTS` in Vercel (all environments).
3. Redeploy / purge the `projects` (and `catalog`) cache tag.
4. Submit a test project; flip `published` in Notion; confirm it appears in `/projects` and under "Built with this" on each linked tool.

## Test plan

- [ ] `cd v5 && npm run typecheck && npm run lint && npm run build` pass.
- [ ] With no `NOTION_DB_PROJECTS` set, `/projects` renders an empty state (no crash) and `/projects/new` still loads.
- [ ] Submitting the form creates a Notion page with `published=false`, photos attached, tool relations set.
- [ ] An unpublished project does NOT appear in `/projects`, `/projects/[id]` (404/!published), or "Built with this".
- [ ] After flipping `published=true` + cache bust, the project appears in the gallery, its detail renders markdown + photos, tool chips link to tools, and the tool pages show it under "Built with this".
- [ ] Submit endpoint returns 429 past the rate limit.
53 changes: 48 additions & 5 deletions v5/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,56 @@
"serial": "الرقم التسلسلي",
"acquired": "تاريخ الاقتناء",
"notesTips": "ملاحظات ونصائح",
"backToTools": "‹ العودة إلى جميع الأدوات"
"backToTools": "‹ العودة إلى جميع الأدوات",
"builtWithThis": "صُنع باستخدام هذه الأداة",
"builtWithThisBy": "بواسطة {author}"
},
"projects": {
"eyebrow": "المشاريع",
"title": "قريبًا",
"body": "يجري إعداد معرض لمشاريع MakerLab — أعمال الطلاب ونتائج المقررات ومعروضات منتقاة من الفريق. في الوقت الحالي، تصفّح كتالوج الأدوات لمعرفة ما هو متاح في المختبر.",
"browseTools": "تصفّح الأدوات"
"title": "مشاريع الطلاب",
"lede": "إبداعات وتجارب ونتائج مقررات من مجتمع MakerLab.",
"galleryLabel": "معرض مشاريع الطلاب",
"submit": "إرسال مشروع",
"by": "بواسطة {author}",
"empty": "لم يتم نشر أي مشاريع بعد. كن أول من يشارك عمله."
},
"projectDetail": {
"breadcrumbProjects": "المشاريع",
"by": "بواسطة {author}",
"viewLink": "عرض رابط المشروع",
"photosLabel": "صور المشروع",
"toolsUsed": "الأدوات المستخدمة",
"materials": "المواد",
"back": "‹ العودة إلى جميع المشاريع"
},
"projectForm": {
"eyebrow": "إرسال مشروع",
"title": "شارك مشروعك",
"lede": "أخبر مجتمع MakerLab بما صنعته. تتم مراجعة المشاركات قبل ظهورها في المعرض.",
"breadcrumbProjects": "المشاريع",
"breadcrumbNew": "إرسال",
"titleLabel": "عنوان المشروع",
"authorLabel": "اسمك",
"bodyLabel": "الوصف (يدعم Markdown)",
"bodyPlaceholder": "صِف مشروعك وكيف صنعته وما تعلمته…",
"previewLabel": "معاينة",
"photosLabel": "الصور",
"uploading": "جارٍ رفع الصور…",
"removePhoto": "إزالة",
"toolsLabel": "الأدوات المستخدمة",
"toolsSearch": "ابحث عن الأدوات…",
"materialsLabel": "المواد (اختياري)",
"materialsPlaceholder": "مفصولة بفواصل، مثل الخشب الرقائقي، PLA، الأكريليك",
"linkLabel": "رابط (اختياري)",
"submit": "إرسال المشروع",
"submitting": "جارٍ الإرسال…",
"cancel": "إلغاء",
"onlyImages": "يتم دعم ملفات الصور فقط.",
"uploadFailed": "فشل رفع الصورة.",
"requiredError": "العنوان واسمك والوصف حقول مطلوبة.",
"submitError": "حدث خطأ ما. يرجى المحاولة مرة أخرى.",
"thanksTitle": "شكرًا — مشروعك قيد المراجعة",
"thanksBody": "سيراجع أحد الموظفين مشاركتك وينشرها في المعرض قريبًا.",
"backToGallery": "العودة إلى المعرض"
},
"about": {
"eyebrow": "حول",
Expand Down
53 changes: 48 additions & 5 deletions v5/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,56 @@
"serial": "Serial",
"acquired": "Acquired",
"notesTips": "Notes & Tips",
"backToTools": "‹ Back to all tools"
"backToTools": "‹ Back to all tools",
"builtWithThis": "Built with this",
"builtWithThisBy": "by {author}"
},
"projects": {
"eyebrow": "Projects",
"title": "Coming soon",
"body": "A gallery of MakerLab projects is in the works — student builds, course outcomes, and staff-curated showcases. For now, browse the tool catalog to see what's available in the lab.",
"browseTools": "Browse tools"
"title": "STUDENT PROJECTS",
"lede": "Builds, experiments, and course outcomes from the MakerLab community.",
"galleryLabel": "Student projects gallery",
"submit": "Submit a project",
"by": "by {author}",
"empty": "No projects published yet. Be the first to share your build."
},
"projectDetail": {
"breadcrumbProjects": "Projects",
"by": "by {author}",
"viewLink": "View project link",
"photosLabel": "Project photos",
"toolsUsed": "Tools used",
"materials": "Materials",
"back": "‹ Back to all projects"
},
"projectForm": {
"eyebrow": "Submit a project",
"title": "Share your project",
"lede": "Tell the MakerLab community what you built. Submissions are reviewed before they appear in the gallery.",
"breadcrumbProjects": "Projects",
"breadcrumbNew": "Submit",
"titleLabel": "Project title",
"authorLabel": "Your name",
"bodyLabel": "Write-up (Markdown supported)",
"bodyPlaceholder": "Describe your project, how you made it, and what you learned…",
"previewLabel": "Preview",
"photosLabel": "Photos",
"uploading": "Uploading photos…",
"removePhoto": "Remove",
"toolsLabel": "Tools used",
"toolsSearch": "Search tools…",
"materialsLabel": "Materials (optional)",
"materialsPlaceholder": "Comma-separated, e.g. plywood, PLA, acrylic",
"linkLabel": "Link (optional)",
"submit": "Submit project",
"submitting": "Submitting…",
"cancel": "Cancel",
"onlyImages": "Only image files are supported.",
"uploadFailed": "Photo upload failed.",
"requiredError": "Title, your name, and a write-up are required.",
"submitError": "Something went wrong. Please try again.",
"thanksTitle": "Thanks — your project is pending review",
"thanksBody": "A staff member will review your submission and publish it to the gallery soon.",
"backToGallery": "Back to gallery"
},
"about": {
"eyebrow": "About",
Expand Down
53 changes: 48 additions & 5 deletions v5/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,56 @@
"serial": "Número de serie",
"acquired": "Adquirido",
"notesTips": "Notas y consejos",
"backToTools": "‹ Volver a todas las herramientas"
"backToTools": "‹ Volver a todas las herramientas",
"builtWithThis": "Hecho con esto",
"builtWithThisBy": "por {author}"
},
"projects": {
"eyebrow": "Proyectos",
"title": "Próximamente",
"body": "Una galería de proyectos del MakerLab está en marcha: creaciones de estudiantes, resultados de cursos y muestras seleccionadas por el personal. Por ahora, explora el catálogo de herramientas para ver lo que hay disponible en el laboratorio.",
"browseTools": "Explorar herramientas"
"title": "PROYECTOS DE ESTUDIANTES",
"lede": "Creaciones, experimentos y resultados de cursos de la comunidad del MakerLab.",
"galleryLabel": "Galería de proyectos de estudiantes",
"submit": "Enviar un proyecto",
"by": "por {author}",
"empty": "Aún no hay proyectos publicados. Sé el primero en compartir tu creación."
},
"projectDetail": {
"breadcrumbProjects": "Proyectos",
"by": "por {author}",
"viewLink": "Ver enlace del proyecto",
"photosLabel": "Fotos del proyecto",
"toolsUsed": "Herramientas utilizadas",
"materials": "Materiales",
"back": "‹ Volver a todos los proyectos"
},
"projectForm": {
"eyebrow": "Enviar un proyecto",
"title": "Comparte tu proyecto",
"lede": "Cuéntale a la comunidad del MakerLab qué construiste. Las propuestas se revisan antes de aparecer en la galería.",
"breadcrumbProjects": "Proyectos",
"breadcrumbNew": "Enviar",
"titleLabel": "Título del proyecto",
"authorLabel": "Tu nombre",
"bodyLabel": "Descripción (admite Markdown)",
"bodyPlaceholder": "Describe tu proyecto, cómo lo hiciste y qué aprendiste…",
"previewLabel": "Vista previa",
"photosLabel": "Fotos",
"uploading": "Subiendo fotos…",
"removePhoto": "Quitar",
"toolsLabel": "Herramientas utilizadas",
"toolsSearch": "Buscar herramientas…",
"materialsLabel": "Materiales (opcional)",
"materialsPlaceholder": "Separados por comas, p. ej. contrachapado, PLA, acrílico",
"linkLabel": "Enlace (opcional)",
"submit": "Enviar proyecto",
"submitting": "Enviando…",
"cancel": "Cancelar",
"onlyImages": "Solo se admiten archivos de imagen.",
"uploadFailed": "Error al subir la foto.",
"requiredError": "El título, tu nombre y una descripción son obligatorios.",
"submitError": "Algo salió mal. Inténtalo de nuevo.",
"thanksTitle": "Gracias: tu proyecto está pendiente de revisión",
"thanksBody": "Un miembro del personal revisará tu propuesta y la publicará pronto en la galería.",
"backToGallery": "Volver a la galería"
},
"about": {
"eyebrow": "Acerca de",
Expand Down
53 changes: 48 additions & 5 deletions v5/messages/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,56 @@
"serial": "Numéro de série",
"acquired": "Acquis le",
"notesTips": "Notes et conseils",
"backToTools": "‹ Retour à tous les outils"
"backToTools": "‹ Retour à tous les outils",
"builtWithThis": "Réalisé avec cet outil",
"builtWithThisBy": "par {author}"
},
"projects": {
"eyebrow": "Projets",
"title": "Bientôt disponible",
"body": "Une galerie de projets du MakerLab est en préparation — réalisations d'étudiants, résultats de cours et vitrines sélectionnées par l'équipe. Pour l'instant, parcourez le catalogue d'outils pour voir ce qui est disponible au labo.",
"browseTools": "Parcourir les outils"
"title": "PROJETS ÉTUDIANTS",
"lede": "Réalisations, expériences et résultats de cours de la communauté du MakerLab.",
"galleryLabel": "Galerie de projets étudiants",
"submit": "Soumettre un projet",
"by": "par {author}",
"empty": "Aucun projet publié pour l'instant. Soyez le premier à partager votre réalisation."
},
"projectDetail": {
"breadcrumbProjects": "Projets",
"by": "par {author}",
"viewLink": "Voir le lien du projet",
"photosLabel": "Photos du projet",
"toolsUsed": "Outils utilisés",
"materials": "Matériaux",
"back": "‹ Retour à tous les projets"
},
"projectForm": {
"eyebrow": "Soumettre un projet",
"title": "Partagez votre projet",
"lede": "Racontez à la communauté du MakerLab ce que vous avez créé. Les soumissions sont examinées avant d'apparaître dans la galerie.",
"breadcrumbProjects": "Projets",
"breadcrumbNew": "Soumettre",
"titleLabel": "Titre du projet",
"authorLabel": "Votre nom",
"bodyLabel": "Description (Markdown pris en charge)",
"bodyPlaceholder": "Décrivez votre projet, comment vous l'avez réalisé et ce que vous avez appris…",
"previewLabel": "Aperçu",
"photosLabel": "Photos",
"uploading": "Téléversement des photos…",
"removePhoto": "Retirer",
"toolsLabel": "Outils utilisés",
"toolsSearch": "Rechercher des outils…",
"materialsLabel": "Matériaux (facultatif)",
"materialsPlaceholder": "Séparés par des virgules, p. ex. contreplaqué, PLA, acrylique",
"linkLabel": "Lien (facultatif)",
"submit": "Soumettre le projet",
"submitting": "Envoi…",
"cancel": "Annuler",
"onlyImages": "Seuls les fichiers image sont pris en charge.",
"uploadFailed": "Échec du téléversement de la photo.",
"requiredError": "Le titre, votre nom et une description sont obligatoires.",
"submitError": "Une erreur est survenue. Veuillez réessayer.",
"thanksTitle": "Merci — votre projet est en attente de validation",
"thanksBody": "Un membre de l'équipe examinera votre soumission et la publiera bientôt dans la galerie.",
"backToGallery": "Retour à la galerie"
},
"about": {
"eyebrow": "À propos",
Expand Down
Loading