commit 5ace6898b680d43e9ef48d374886a579e9ed0756 Author: mstfyldz Date: Sun Aug 16 17:18:23 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/.impeccable/config.json b/.impeccable/config.json new file mode 100644 index 0000000..7142df2 --- /dev/null +++ b/.impeccable/config.json @@ -0,0 +1,3 @@ +{ + "buildPath": "comp" +} diff --git a/.impeccable/decision-payload.json b/.impeccable/decision-payload.json new file mode 100644 index 0000000..28de71a --- /dev/null +++ b/.impeccable/decision-payload.json @@ -0,0 +1,136 @@ +{ + "title": "Choose the visual world for mstfyldz", + "question": "The roll assigned Celestial Mood Logbook & Chrono-Atlas (Seed 6a22c7e2). Keep it, choose an alternate, or steer.", + "options": [ + { + "id": "assigned", + "label": "Celestial Mood Logbook & Chrono-Atlas", + "kicker": "THE ROLL", + "lineage": "17th-century astronomical chart & celestial navigation log", + "thesis": "Thoughts, whimsical notes, and moods charted as celestial orbits, atmospheric phases, and glowing coordinate tags.", + "palette": [ + "#0B0F19", + "#1A2238", + "#F4EBD9", + "#FF9E3B", + "#38E1B0" + ], + "materials": [ + "deep ink navy canvas", + "brass coordinate linework", + "starlight glow", + "parchment card surfaces" + ], + "viewport": "A dynamic 'Atmospheric Signal & Frequency' dial with rotating mood constellations, leading into an asymmetric field of celestial note cards.", + "risk": "Can feel too mystical or ornate if typography loses its crisp modern editorial clarity.", + "raised": [ + { + "from": "Pop-Culture Console", + "raise": "High-contrast structural elbow navigation badges with instantaneous responsive state transitions." + }, + { + "from": "Village Woodcut & Torn Paper", + "raise": "Tactile stamped ink press feedback and organic deckled edge dividers on micro-note scraps." + } + ], + "comp": ".impeccable/mocks/decision/assigned.webp" + }, + { + "id": "model-pick", + "label": "Annotated Field Notebook & Ink Marginalia", + "kicker": "IMPECCABLE’S PICK", + "lineage": "Moleskine / Leuchtturm journal with ink wash and washi tape", + "thesis": "A tactile digital sketchbook with ink density, rubber-stamp timestamps, and handwritten marginalia notes.", + "palette": [ + "#18181B", + "#FAF7F2", + "#E4DCD3", + "#E11D48", + "#D97706" + ], + "materials": [ + "tactile cream paper", + "ink bleed", + "washi tape snippets", + "rubber stamp markers" + ], + "viewport": "A desk-like canvas with pinned daily thought notes, live mood stamps, and rich handwritten marginalia.", + "risk": "Also the direction most runs in personal digital journals land on (familiar & comfortable).", + "comp": ".impeccable/mocks/decision/model-pick.webp" + }, + { + "id": "challenger-elbow-panel", + "label": "LCARS Starship Elbow Console", + "verdict": "competitive", + "lineage": "pill-cornered starship wall panel & LCARS console", + "thesis": "An ultra-clean modular console where mood banks and thought clusters are accessed via sweeping color elbows.", + "palette": [ + "#000000", + "#D8B4E2", + "#FBC4AB", + "#F6BD60", + "#90DBF4" + ], + "materials": [ + "black glass", + "flat pastel pill blocks", + "numeric telemetry clusters" + ], + "viewport": "A black glass HUD with sweeping lilac elbow frames and pastel mood buttons that chirp and switch banks.", + "case": "Fuses cleanly with categorical moods and quick micro-notes; slightly colder emotional resonance.", + "risk": "Reads retro-scifi novelty when stripped of warmth.", + "comp": ".impeccable/mocks/decision/challenger-elbow-panel.webp", + "hero": "https://impeccable.style/worlds/cards/pop-culture-shelf-elbow-panel-console-hero.webp" + }, + { + "id": "challenger-woodcut-sun", + "label": "Village Coffeehouse & Woodcut Sun", + "verdict": "declined", + "lineage": "nineties earthy corporate identity & woodcut zine", + "palette": [ + "#D96B43", + "#556B2F", + "#E6A15C", + "#F4EBD9" + ], + "materials": [ + "recycled kraft", + "torn paper edges", + "woodcut stamp marks" + ], + "case": "Fuses poorly with eccentric personal tech-blog dynamic mood shifting.", + "kept": "Tactile stamped press micro-interactions and torn paper dividers.", + "hero": "https://impeccable.style/worlds/cards/brand-identity-canon-village-coffeehouse-identity-hero.webp" + } + ], + "reroll": { + "registers": [ + "safer", + "bolder" + ] + }, + "buildPath": { + "value": "comp", + "toggle": true + }, + "canon": true, + "canonCard": { + "label": "The Minimalist Developer Blog Standard", + "thesis": "Monochrome clean typography, subtle borders, black-and-white cards with tag pills—executed cleanly.", + "palette": [ + "#FFFFFF", + "#09090B", + "#71717A", + "#3B82F6" + ], + "materials": [ + "clean grid", + "subtle border strokes", + "system mono tags" + ], + "viewport": "Standard two-column feed with sidebar tags and clean article cards.", + "risk": "Indistinguishable from thousands of developer blogs on the internet.", + "comp": ".impeccable/mocks/decision/canon.webp" + }, + "steer": true +} diff --git a/.impeccable/design.json b/.impeccable/design.json new file mode 100644 index 0000000..5780044 --- /dev/null +++ b/.impeccable/design.json @@ -0,0 +1,121 @@ +{ + "schemaVersion": 2, + "generatedAt": "2026-08-16T16:38:00Z", + "title": "Design System: mstfyldz Journal", + "extensions": { + "colorMeta": { + "primary": { + "role": "primary", + "displayName": "Sumi Carbon Ink", + "canonical": "#18181B", + "tonalRamp": ["#09090B", "#18181B", "#27272A", "#3F3F46", "#52525B", "#71717A", "#A1A1AA", "#D4D4D8"] + }, + "neutral-bg": { + "role": "neutral", + "displayName": "Natural Laid Paper", + "canonical": "#FAF7F2", + "tonalRamp": ["#E5DFC8", "#EFE7D6", "#F3ECE2", "#F6F1E7", "#FAF7F2", "#FCFAF7", "#FEFDFB", "#FFFFFF"] + }, + "wax-seal": { + "role": "secondary", + "displayName": "Sealing Wax Ruby", + "canonical": "#E11D48", + "tonalRamp": ["#4C0519", "#881337", "#9F1239", "#BE123C", "#E11D48", "#F43F5E", "#FB7185", "#FFE4E6"] + }, + "amber-highlighter": { + "role": "secondary", + "displayName": "Highlighter Amber", + "canonical": "#D97706", + "tonalRamp": ["#451A03", "#78350F", "#92400E", "#B45309", "#D97706", "#F59E0B", "#FBBF24", "#FEF3C7"] + } + }, + "typographyMeta": { + "display": { "displayName": "Bricolage Grotesque", "purpose": "Hero brand display and section titles." }, + "serif-body": { "displayName": "Lora", "purpose": "Literary longform reading and body notes." }, + "handwritten": { "displayName": "Caveat", "purpose": "Spontaneous marginalia, doodles, and author comments." }, + "mono": { "displayName": "JetBrains Mono", "purpose": "Timestamps, code snippets, and telemetry data." } + }, + "shadows": [ + { + "name": "paper-rest", + "value": "0 4px 20px -2px rgba(28, 25, 23, 0.08), 0 1px 4px 0 rgba(28, 25, 23, 0.04)", + "purpose": "Resting paper sheet elevation on desk." + }, + { + "name": "paper-lifted", + "value": "0 14px 32px -4px rgba(28, 25, 23, 0.14), 0 4px 8px 0 rgba(28, 25, 23, 0.06)", + "purpose": "Hover state for interactive cards." + }, + { + "name": "wax-seal", + "value": "0 6px 16px rgba(225, 29, 72, 0.35), inset 0 2px 4px rgba(255, 255, 255, 0.4), inset 0 -3px 6px rgba(0, 0, 0, 0.3)", + "purpose": "3D tactile sealing wax badge." + } + ], + "motion": [ + { + "name": "spring-tactile", + "value": "cubic-bezier(0.34, 1.56, 0.64, 1)", + "purpose": "Physical bounce for wax seals and card lifts." + } + ], + "breakpoints": [ + { "name": "sm", "value": "640px" }, + { "name": "md", "value": "768px" }, + { "name": "lg", "value": "1024px" }, + { "name": "xl", "value": "1280px" } + ] + }, + "components": [ + { + "name": "Primary Action Button", + "kind": "button", + "refersTo": "button-primary", + "description": "Crimson wax button for creating fresh notes.", + "html": "", + "css": ".ds-btn-wax { background: #E11D48; color: #ffffff; padding: 8px 16px; font-weight: 700; border-radius: 8px; border: none; font-family: 'Bricolage Grotesque', sans-serif; cursor: pointer; transition: all 0.2s; } .ds-btn-wax:hover { background: #9F1239; transform: translateY(-1px); }" + }, + { + "name": "Wax Seal Monogram", + "kind": "custom", + "refersTo": "wax-seal", + "description": "Tactile 3D wax seal displaying current mood frequency.", + "html": "
M
", + "css": ".ds-wax-seal { width: 48px; height: 48px; border-radius: 9999px; background: radial-gradient(circle at 35% 30%, #F43F5E, #E11D48 60%, #9F1239 100%); color: white; display: flex; align-items: center; justify-content: center; font-weight: 900; box-shadow: 0 6px 16px rgba(225,29,72,0.35); cursor: pointer; transition: transform 0.2s; } .ds-wax-seal:hover { transform: scale(1.05) rotate(4deg); }" + } + ], + "narrative": { + "northStar": "The Annotated Field Notebook & Living Sketchbook", + "overview": "The visual language of mstfyldz rejects sterile corporate card grids and generic dark-mode developer templates in favor of a warm, tactile personal field notebook. The interface celebrates human presence through ink density, rubber date stamps, washi tape pins, authentic handwritten marginalia, and dynamic atmospheric mood shifting.", + "keyCharacteristics": [ + "Tactile paper surfaces (#FAF7F2) over warm desk grain (#F0ECE1)", + "Bold sumi carbon typography paired with warm literary serifs and organic cursive marginalia", + "Mood-adaptive atmospheric shifts governed by an interactive live frequency wave and tactile crimson wax seal", + "Physical ephemera: washi tape strips, binder clips, rubber stamps, and marker ink highlights" + ], + "rules": [ + { + "name": "The Ink-and-Paper Rule", + "body": "Surfaces are physical paper sheets or index cards resting on a desk surface. White is never pure #FFFFFF; it always carries the warmth of natural paper grain (#FAF7F2).", + "section": "colors" + }, + { + "name": "The Marginalia Doctrine", + "body": "Handwritten notes in Caveat must always serve as spontaneous human commentary or clarification, styled in vibrant ink, never used as longform body paragraphs.", + "section": "typography" + } + ], + "dos": [ + "Do treat each note format according to its physical archetype.", + "Do preserve organic Turkish typography and authentic colloquial voice.", + "Do ensure all text maintains WCAG AA contrast.", + "Do support smooth atmospheric transitions and respect prefers-reduced-motion." + ], + "donts": [ + "Don't use generic corporate card grids or purple-on-dark SaaS gradients.", + "Don't use colored left-border stripes (border-l-2) on cards as decorative shortcuts.", + "Don't use raw unicode emojis as an icon system.", + "Don't use pure cold black (#000000) or pure cold white (#FFFFFF) on primary reading surfaces." + ] + } +} diff --git a/.impeccable/mocks/decision/assigned.webp b/.impeccable/mocks/decision/assigned.webp new file mode 100644 index 0000000..b13ca46 Binary files /dev/null and b/.impeccable/mocks/decision/assigned.webp differ diff --git a/.impeccable/mocks/decision/canon.webp b/.impeccable/mocks/decision/canon.webp new file mode 100644 index 0000000..f62082c Binary files /dev/null and b/.impeccable/mocks/decision/canon.webp differ diff --git a/.impeccable/mocks/decision/challenger-elbow-panel.webp b/.impeccable/mocks/decision/challenger-elbow-panel.webp new file mode 100644 index 0000000..55551c9 Binary files /dev/null and b/.impeccable/mocks/decision/challenger-elbow-panel.webp differ diff --git a/.impeccable/mocks/decision/model-pick.webp b/.impeccable/mocks/decision/model-pick.webp new file mode 100644 index 0000000..3c64bf4 Binary files /dev/null and b/.impeccable/mocks/decision/model-pick.webp differ diff --git a/.impeccable/questions/ad432173.answer.json b/.impeccable/questions/ad432173.answer.json new file mode 100644 index 0000000..5877fed --- /dev/null +++ b/.impeccable/questions/ad432173.answer.json @@ -0,0 +1 @@ +{"optionId":"model-pick","steer":"","comp":".impeccable/mocks/decision/model-pick.webp","buildPath":"comp","buildPathFlipped":false} diff --git a/.impeccable/questions/ad432173.log b/.impeccable/questions/ad432173.log new file mode 100644 index 0000000..e69de29 diff --git a/.impeccable/questions/ad432173.state.json b/.impeccable/questions/ad432173.state.json new file mode 100644 index 0000000..8b4e252 --- /dev/null +++ b/.impeccable/questions/ad432173.state.json @@ -0,0 +1 @@ +{"pid":78230,"port":64499,"url":"http://127.0.0.1:64499/","lastBeat":1786885494556} \ No newline at end of file diff --git a/.impeccable/review/desktop.png b/.impeccable/review/desktop.png new file mode 100644 index 0000000..92e39b7 Binary files /dev/null and b/.impeccable/review/desktop.png differ diff --git a/.impeccable/review/hero-repro.png b/.impeccable/review/hero-repro.png new file mode 100644 index 0000000..48bbfe0 Binary files /dev/null and b/.impeccable/review/hero-repro.png differ diff --git a/.impeccable/review/mobile.png b/.impeccable/review/mobile.png new file mode 100644 index 0000000..1a32816 Binary files /dev/null and b/.impeccable/review/mobile.png differ diff --git a/.impeccable/surface-brief-home.md b/.impeccable/surface-brief-home.md new file mode 100644 index 0000000..499a097 --- /dev/null +++ b/.impeccable/surface-brief-home.md @@ -0,0 +1,25 @@ +# Surface Brief: Home Page & Journal Feed + + + +## Scope & Visitor Mode +- Target: `app/page.tsx` +- Mode: `Experience` + `Read` + +## Audience & Job +- Primary: Mustafa Yıldız (`mstfyldz`) capturing unfiltered thoughts, daily logs, and mood states. +- Secondary: Curious visitors exploring an expressive, authentic personal digital sketchbook. + +## Outcome & Proof +- The visitor immediately encounters an evocative, tactile personal journal workspace. +- Seamless mood filtering (`⚡ Enerjik`, `☕ Gece Melankolisi`, `💡 Fikir Patlaması`, `🌿 Sakin / Karalama`). +- Fluid reading experience across micro-notes, polaroid memories, audio scraps, and longform essays. + +## Selected Direction & Visual System +- Identity: **Annotated Field Notebook & Ink Marginalia** +- Palette: Warm tactile cream `#FAF7F2`, rich deep ink `#18181B`, soft cardboard `#E4DCD3`, wax seal ruby `#E11D48`, warm amber `#D97706`. +- Materials: Fine paper grain, stamped date marks, washi tape snippets, brass binder clips, highlighter ink wash. +- Signature Interaction: Dynamic Mood Log wave with tactile wax stamp and atmosphere shift. + +## Anti-Goals +- No sterile corporate card grids, no purple-on-dark SaaS cliches, no generic developer template look. diff --git a/.impeccable/surfaces/app-page-tsx.md b/.impeccable/surfaces/app-page-tsx.md new file mode 100644 index 0000000..2f49734 --- /dev/null +++ b/.impeccable/surfaces/app-page-tsx.md @@ -0,0 +1,32 @@ +--- +version: 1 +slug: "app-page-tsx" +primary_target: "app/page.tsx" +related_targets: [] +--- + +# Surface Brief: Home Page & Journal Feed + + + +## Scope & Visitor Mode +- Target: `app/page.tsx` +- Mode: `Experience` + `Read` + +## Audience & Job +- Primary: Mustafa Yıldız (`mstfyldz`) capturing unfiltered thoughts, daily logs, and mood states. +- Secondary: Curious visitors exploring an expressive, authentic personal digital sketchbook. + +## Outcome & Proof +- The visitor immediately encounters an evocative, tactile personal journal workspace. +- Seamless mood filtering (`⚡ Enerjik`, `☕ Gece Melankolisi`, `💡 Fikir Patlaması`, `🌿 Sakin / Karalama`). +- Fluid reading experience across micro-notes, polaroid memories, audio scraps, and longform essays. + +## Selected Direction & Visual System +- Identity: **Annotated Field Notebook & Ink Marginalia** +- Palette: Warm tactile cream `#FAF7F2`, rich deep ink `#18181B`, soft cardboard `#E4DCD3`, wax seal ruby `#E11D48`, warm amber `#D97706`. +- Materials: Fine paper grain, stamped date marks, washi tape snippets, brass binder clips, highlighter ink wash. +- Signature Interaction: Dynamic Mood Log wave with tactile wax stamp and atmosphere shift. + +## Anti-Goals +- No sterile corporate card grids, no purple-on-dark SaaS cliches, no generic developer template look. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +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. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..cf5cc96 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,181 @@ +--- +name: mstfyldz Journal +description: Annotated Field Notebook & Ink Marginalia design system +colors: + primary: "#18181B" + neutral-bg: "#FAF7F2" + desk-surface: "#F0ECE1" + card-dark: "#1E232D" + wax-seal: "#E11D48" + amber-highlighter: "#D97706" + sage-botany: "#059669" + cyan-fountain: "#0284C7" + lavender-sketch: "#7C3AED" +typography: + display: + fontFamily: "Bricolage Grotesque, system-ui, sans-serif" + fontSize: "clamp(2rem, 5vw, 3.5rem)" + fontWeight: 800 + lineHeight: 1.1 + headline: + fontFamily: "Bricolage Grotesque, system-ui, sans-serif" + fontSize: "1.5rem" + fontWeight: 700 + lineHeight: 1.25 + serif-body: + fontFamily: "Lora, Georgia, serif" + fontSize: "1rem" + fontWeight: 400 + lineHeight: 1.65 + handwritten: + fontFamily: "Caveat, cursive" + fontSize: "1.25rem" + fontWeight: 700 + lineHeight: 1.2 + mono: + fontFamily: "JetBrains Mono, monospace" + fontSize: "0.75rem" + fontWeight: 600 + lineHeight: 1.4 +rounded: + xs: "2px" + sm: "6px" + md: "12px" + lg: "16px" + full: "9999px" +spacing: + xs: "4px" + sm: "8px" + md: "16px" + lg: "24px" + xl: "32px" +components: + button-primary: + backgroundColor: "{colors.wax-seal}" + textColor: "#ffffff" + rounded: "{rounded.sm}" + padding: "8px 16px" + button-secondary: + backgroundColor: "{colors.neutral-bg}" + textColor: "{colors.primary}" + rounded: "{rounded.sm}" + padding: "8px 16px" + card-notebook: + backgroundColor: "{colors.neutral-bg}" + rounded: "{rounded.md}" + padding: "24px 28px" +--- + +# Design System: mstfyldz Journal + +## Overview + +**Creative North Star: "The Annotated Field Notebook & Living Sketchbook"** + +The visual language of `mstfyldz` rejects sterile corporate card grids and generic dark-mode developer templates in favor of a warm, tactile personal field notebook. The interface celebrates human presence through ink density, rubber date stamps, washi tape pins, authentic handwritten marginalia, and dynamic atmospheric mood shifting. + +The space feels like opening a cherished, dog-eared sketchbook on a wooden worktable in Kadıköy: spontaneous thoughts, long essays, code scraps, polaroid photographs, and voice memos coexist naturally without corporate pretense. + +**Key Characteristics:** +- Tactile paper surfaces (`#FAF7F2`) over warm desk grain (`#F0ECE1`). +- Bold sumi carbon typography paired with warm literary serifs and organic cursive marginalia. +- Mood-adaptive atmospheric shifts governed by an interactive live frequency wave and tactile crimson wax seal. +- Physical ephemera: washi tape strips, binder clips, rubber stamps, and marker ink highlights. + +## Colors + +The palette is anchored on organic stationery materials, carbon ink, and jewel-toned accent inks. + +### Primary +- **Sumi Carbon Ink** (`#18181B`): Primary body text, titles, brand identity, and structural ink lines. +- **Natural Laid Paper** (`#FAF7F2`): Primary notebook sheet canvas with subtle micro-dot grid texture. + +### Secondary +- **Sealing Wax Ruby** (`#E11D48`): Primary action buttons, active wax seal badge, live broadcast status indicator, and hand-drawn marginalia arrows. +- **Highlighter Amber** (`#D97706`): Sticky note post-it cards, focus mood indicators, and warm highlight markers. + +### Tertiary +- **Field Sage Green** (`#059669`): Calm mood tags, approved stamp marks, and botanical notes. +- **Fountain Cyan** (`#0284C7`): Audio scrap accents and polaroid memory margin notes. +- **Night Lavender** (`#7C3AED`): Late-night mood frequency markers and philosophical musings. + +### Neutral +- **Desk Surface** (`#F0ECE1`): Surrounding worktable background with subtle tactile grid dots. +- **Aged Manila Kraft** (`#EFE7D6`): Cassette cards, active scrollbar tracks, and divider surfaces. +- **Obsidian Carbon** (`#1E232D`): Dark terminal code cards and syntax containers. + +### Named Rules +**The Ink-and-Paper Rule.** Surfaces are physical paper sheets or index cards resting on a desk surface. White is never pure `#FFFFFF`; it always carries the warmth of natural paper grain (`#FAF7F2`). + +## Typography + +**Display Font:** `Bricolage Grotesque` (with system sans-serif fallback) +**Body Font:** `Lora` (with Georgia serif fallback) +**Handwritten Font:** `Caveat` (with cursive fallback) +**Label/Mono Font:** `JetBrains Mono` (with monospace fallback) + +**Character:** A literary and eccentric pairing that balances typographic clarity and editorial authority with whimsical, human hand-drawn marginalia. + +### Hierarchy +- **Display** (`800`, `clamp(2rem, 5vw, 3.5rem)`, `1.1`): Hero titles and brand identity. +- **Headline** (`700`, `1.5rem`, `1.25`): Section headings and post card titles. +- **Body** (`400`, `1rem`, `1.65`, `max 68ch`): Longform essay reading and narrative thoughts. +- **Handwritten** (`700`, `1.25rem`, `1.2`): Marginalia notes, arrow annotations, and humorous commentary. +- **Label / Mono** (`600`, `0.75rem`, `0.1em tracking`, `uppercase`): Stamped dates, telemetry timestamps, and tag chips. + +### Named Rules +**The Marginalia Doctrine.** Handwritten notes in Caveat must always serve as spontaneous human commentary or clarification, styled in vibrant ink (`#E11D48`, `#D97706`, `#0284C7`), never used as longform body paragraphs. + +## Layout + +The spatial model simulates an open desk workspace with an expansive notebook spread: +- **Container Width:** Max 1152px (`max-w-6xl`), centered with fluid 16–28px horizontal gutters. +- **Grid Structure:** Asymmetric 1-column on mobile (<768px), 2-column on tablet (768–1024px), 3-column masonry editorial layout on desktop (>1024px). +- **Rhythm & Density:** Generous 24–32px gaps between cards to allow physical washi tape and binder clip details to breathe. + +## Elevation & Depth + +Depth is conveyed through physical paper layering, soft diffuse shadows, and tactile embossed seals rather than glowing colored halos. + +### Shadow Vocabulary +- **Paper Sheet at Rest** (`box-shadow: 0 4px 20px -2px rgba(28, 25, 23, 0.08), 0 1px 4px 0 rgba(28, 25, 23, 0.04)`): Grounded card elevation. +- **Lifted Paper Sheet** (`box-shadow: 0 14px 32px -4px rgba(28, 25, 23, 0.14), 0 4px 8px 0 rgba(28, 25, 23, 0.06)`): Hover state on interactive cards. +- **Embossed Wax Seal** (`box-shadow: 0 6px 16px rgba(225, 29, 72, 0.35), inset 0 2px 4px rgba(255, 255, 255, 0.4), inset 0 -3px 6px rgba(0, 0, 0, 0.3)`): Tactile 3D sealing stamp. + +## Shapes + +- **Notebook Sheets:** Medium 12–16px rounded corners (`rounded-xl`), 1px subtle carbon stroke (`rgba(24, 24, 27, 0.08)`). +- **Washi Tape:** Translucent textured strip with dashed edges (`rgba(200, 185, 160, 0.6)`), tilted slightly (-2° to +2°). +- **Rubber Stamps:** Monospaced uppercase text with 2px solid border, 4px radius, and radial mask texture. +- **Wax Seal:** Perfect 48px circle with metallic radial reflection and 4° interactive rotation on hover. + +## Components + +### Buttons +- **Primary (Sealing Wax CTA):** Crimson background (`#E11D48`), white text, 8px radius, subtle hover lift and color deepening (`#9F1239`). +- **Secondary (Paper Action):** Cream background (`#FAF7F2`), carbon border (`rgba(24, 24, 27, 0.15)`), carbon text, hover background shift to `#F0ECE1`. + +### Chips & Index Tabs +- **Style:** Small pill tabs with subtle colored backgrounds corresponding to mood/tag roles. +- **Side Index Tabs:** Fixed notebook edge tabs with colored border and 4px rightward slide on hover. + +### Cards / Containers +- **Field Notebook:** Natural cream sheet with top binder clip and ruled/dotted paper background. +- **Post-It Sticky:** Warm amber background (`#FEF3C7`), washi tape top center, -1° rotational offset. +- **Polaroid Memory:** White photo frame, 4:3 aspect ratio, handwritten caption area. +- **Obsidian Code Scrap:** Dark carbon container (`#1E232D`), colored window controls, syntax highlighting. +- **Cassette Audio Scrap:** Manila card with top binder clip, black cassette body, animated sound wave equalizer. + +## Do's and Don'ts + +### Do: +- **Do** treat each note format according to its physical archetype (sticky notes tilt slightly, notebook pages carry clips, polaroids have tape). +- **Do** preserve organic Turkish typography and authentic colloquial voice in copy and notes. +- **Do** ensure all text maintains WCAG AA contrast (≥4.5:1 for body copy). +- **Do** support smooth atmospheric transitions and respect `prefers-reduced-motion`. + +### Don't: +- **Don't** use generic corporate card grids, purple-on-dark SaaS gradients, or sterile white dashboard boxes. +- **Don't** use colored left-border stripes (`border-l-2`) on cards as decorative shortcuts. +- **Don't** use raw unicode emojis as an icon system; use crisp SVG iconography from Lucide. +- **Don't** use pure cold black (`#000000`) or pure cold white (`#FFFFFF`) on primary reading surfaces. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..50299bb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +FROM node:22-alpine AS base + +FROM base AS deps +RUN apk add --no-cache libc6-compat +WORKDIR /app +COPY package.json package-lock.json* ./ +RUN npm ci --legacy-peer-deps + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +ENV NEXT_TELEMETRY_DISABLED=1 +# SUPABASE_SERVICE_ROLE_KEY build-time'da gerekmez (server-only, runtime'da okunuyor). +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +RUN mkdir .next && chown nextjs:nodejs .next +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" +CMD ["node", "server.js"] diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 0000000..2af711e --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,45 @@ +# Product + + + +## Platform + +web + +## Stack +Next.js (Full-stack web framework) + +## Users +- **Primary:** Mustafa Yıldız (mstfyldz) — personal creator, daily writer, and curator of ideas and mood states. +- **Secondary:** Curious visitors, peers, and internet wanderers seeking an authentic, playful, and expressive personal space. + +## Product Purpose +A deeply personal, quirky, and expressive digital journal / blog. It exists to capture genuine moods, whimsical daily notes, spontaneous reflections, and creative musings without the rigidity of conventional corporate blogs or sterilized portfolio templates. Success is measured by how freely the creator writes and how memorable, alive, and evocative the space feels to visit. + +## Positioning +An unapologetically authentic personal sanctuary—counteracting sterile, cookie-cutter web minimalism with mood-driven visual atmosphere, playful micro-details, and unfiltered human character. + +## Operating Context +- Fast, low-friction note-taking and thought drafting. +- Browsing by mood, date, tag, or serendipitous discoveries. +- Web-accessible across desktop and mobile devices under the `mstfyldz` domain/identity. + +## Capabilities and Constraints +- Flexible publishing format supporting quick micro-notes, stream-of-consciousness snippets, and structured longform essays. +- Mood-aware categorization / aesthetic shifts reflecting the vibe of entries. +- Next.js full-stack web architecture. +- Web accessibility standards and fluid responsive design. + +## Brand Commitments +- Identity: `mstfyldz` (Mustafa Yıldız). +- Tone of Voice: Playful, candid, witty, introspective, eccentric, warm, and authentic. + +## Evidence on Hand +- Confirmed product brief and user direction during initial setup. +- Greenfield project repository at `/Users/mstfyldz/github/mstfyldz`. + +## Product Principles +1. **Mood-Led Expression:** The visual and typographic atmosphere serves emotional honesty and playful energy. +2. **Zero Pretense:** Prioritize raw charm, quirky humor, and direct thoughts over corporate polish. +3. **Cabinet of Curiosities:** Exploring notes should feel like leafing through a rich, illustrated personal notebook. +4. **Frictionless Capture:** Keep adding and organizing thoughts effortless and intuitive. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/admin/page.tsx b/app/admin/page.tsx new file mode 100644 index 0000000..01e8215 --- /dev/null +++ b/app/admin/page.tsx @@ -0,0 +1,599 @@ +"use client"; + +import React, { useState, useEffect, useMemo } from "react"; +import { JournalPost, MoodType } from "@/components/types"; +import { MOODS } from "@/components/mockData"; +import { EditPostModal } from "@/components/EditPostModal"; +import { NewPostModal } from "@/components/NewPostModal"; +import { PostDetailModal } from "@/components/PostDetailModal"; +import { + Lock, + User, + KeyRound, + LogOut, + Plus, + Edit3, + Trash2, + Eye, + Search, + RefreshCw, + ShieldCheck, + BarChart2, + FileText, + Heart, + Tag, + CheckCircle, + AlertCircle +} from "lucide-react"; + +export default function AdminPage() { + // Auth state + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [checkingAuth, setCheckingAuth] = useState(true); + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + const [loginError, setLoginError] = useState(""); + const [isLoggingIn, setIsLoggingIn] = useState(false); + + // Data state + const [posts, setPosts] = useState([]); + const [loadingPosts, setLoadingPosts] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedMoodFilter, setSelectedMoodFilter] = useState("all"); + + // Modal states + const [editingPost, setEditingPost] = useState(null); + const [isNewPostOpen, setIsNewPostOpen] = useState(false); + const [detailPost, setDetailPost] = useState(null); + const [deletingId, setDeletingId] = useState(null); + + // Check auth status on mount + useEffect(() => { + async function checkAuth() { + try { + const res = await fetch("/api/admin/check"); + if (res.ok) { + const data = await res.json(); + setIsAuthenticated(data.authenticated); + } + } catch (err) { + console.error("Auth check failed:", err); + } finally { + setCheckingAuth(false); + } + } + checkAuth(); + }, []); + + // Fetch posts if authenticated + const fetchPosts = async () => { + setLoadingPosts(true); + try { + const res = await fetch("/api/posts"); + if (res.ok) { + const data = await res.json(); + setPosts(data); + } + } catch (err) { + console.error("Error fetching posts:", err); + } finally { + setLoadingPosts(false); + } + }; + + useEffect(() => { + if (isAuthenticated) { + fetchPosts(); + } + }, [isAuthenticated]); + + // Handle Login + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setLoginError(""); + setIsLoggingIn(true); + + try { + const res = await fetch("/api/admin/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + + const data = await res.json(); + + if (res.ok) { + setIsAuthenticated(true); + setUsername(""); + setPassword(""); + } else { + setLoginError(data.error || "Giriş başarısız oldu"); + } + } catch (err) { + setLoginError("Sunucuya bağlanılamadı"); + } finally { + setIsLoggingIn(false); + } + }; + + // Handle Logout + const handleLogout = async () => { + try { + await fetch("/api/admin/logout", { method: "POST" }); + setIsAuthenticated(false); + } catch (err) { + console.error("Logout failed:", err); + } + }; + + // Handle Add Post + const handleAddPost = async (newPost: JournalPost) => { + try { + const res = await fetch("/api/posts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newPost), + }); + + if (res.ok) { + fetchPosts(); + } + } catch (err) { + console.error("Error adding post:", err); + } + }; + + // Handle Update Post + const handleUpdatePost = async (updatedPost: JournalPost) => { + try { + const res = await fetch(`/api/posts/${updatedPost.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updatedPost), + }); + + if (res.ok) { + setPosts((prev) => + prev.map((p) => (p.id === updatedPost.id ? updatedPost : p)) + ); + } + } catch (err) { + console.error("Error updating post:", err); + } + }; + + // Handle Delete Post + const handleDeletePost = async (id: string) => { + try { + const res = await fetch(`/api/posts/${id}`, { + method: "DELETE", + }); + + if (res.ok) { + setPosts((prev) => prev.filter((p) => p.id !== id)); + setDeletingId(null); + } + } catch (err) { + console.error("Error deleting post:", err); + } + }; + + // Filtered posts for admin table + const filteredPosts = useMemo(() => { + return posts.filter((post) => { + if (selectedMoodFilter !== "all" && post.mood !== selectedMoodFilter) { + return false; + } + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + const matchesContent = post.content.toLowerCase().includes(q); + const matchesTitle = post.title?.toLowerCase().includes(q); + const matchesTags = post.tags.some((t) => t.toLowerCase().includes(q)); + return matchesContent || matchesTitle || matchesTags; + } + return true; + }); + }, [posts, selectedMoodFilter, searchQuery]); + + // Total likes counter + const totalLikes = useMemo(() => { + return posts.reduce((sum, p) => sum + (p.likes || 0), 0); + }, [posts]); + + if (checkingAuth) { + return ( +
+
+ + Admin Yetkisi Kontrol Ediliyor... +
+
+ ); + } + + // --- LOGIN VIEW --- + if (!isAuthenticated) { + return ( +
+
+ {/* Top Washi Tape */} +
+ + {/* Header */} +
+
+ M + ADM +
+

+ Yönetici Girişi +

+

+ mstfyldz journal • Defter Yönetim Paneli +

+
+ + {/* Error Banner */} + {loginError && ( +
+ + {loginError} +
+ )} + + {/* Login Form */} +
+
+ +
+ + setUsername(e.target.value)} + placeholder="admin" + className="w-full pl-9 pr-3 py-2 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-mono" + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + placeholder="••••••••" + className="w-full pl-9 pr-3 py-2 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-mono" + /> +
+
+ + +
+ +
+

+ Giriş bilgileri .env içindeki ADMIN_USERNAME ve ADMIN_PASSWORD değişkenlerinden okunur. +

+
+
+
+ ); + } + + // --- AUTHENTICATED DASHBOARD VIEW --- + return ( +
+ {/* Top Admin Telemetry Header */} +
+
+
+
+ M +
+
+
+ + mstfyldz admin + + + + YETKİLENDİRİLDİ + +
+
+
+ +
+ + + Siteye Git + + + +
+
+
+ + {/* Main Admin Dashboard Content */} +
+ {/* Metric Cards Overview */} +
+
+
+
+ TOPLAM KARALAMA +
+
+ {posts.length} +
+
+
+ +
+
+ +
+
+
+ TOPLAM BEĞENİ +
+
+ {totalLikes} +
+
+
+ +
+
+ +
+
+
+ VERİTABANI DURUMU +
+
+ + PostgreSQL (65.109...) +
+
+
+ +
+
+
+ + {/* Controls & Search Bar */} +
+
+
+

+ Not & Karalama Yönetimi +

+

+ PostgreSQL veritabanındaki tüm notları görüntüleyin, güncelleyin veya yeni içerik girin. +

+
+ +
+ + + +
+
+ + {/* Search & Mood Filter Inputs */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="Başlık, içerik veya etiketlerde ara..." + className="w-full pl-8 pr-3 py-1.5 text-xs bg-white border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-serif" + /> +
+ +
+ +
+
+
+ + {/* Data Table */} +
+
+ + + + + + + + + + + + + + {filteredPosts.length > 0 ? ( + filteredPosts.map((post) => ( + + {/* Format Badge */} + + + {/* Date */} + + + {/* Mood */} + + + {/* Title / Content Preview */} + + + {/* Likes */} + + + {/* Tags */} + + + {/* Actions */} + + + )) + ) : ( + + + + )} + +
Format / TürTarihMoodBaşlık / İçerikBeğeniEtiketlerİşlemler
+ + {post.type} + + + {post.date} + + + + {post.moodLabel} + + +
+ {post.title || "(Başlıksız Not)"} +
+
+ {post.content} +
+
+ ♥ {post.likes} + +
+ {post.tags.map((t) => ( + + #{t} + + ))} +
+
+
+ + + + + {deletingId === post.id ? ( +
+ + +
+ ) : ( + + )} +
+
+ Filtrelerle eşleşen not bulunamadı. +
+
+
+
+ + {/* Modals */} + setEditingPost(null)} + onUpdatePost={handleUpdatePost} + /> + + setIsNewPostOpen(false)} + onAddPost={handleAddPost} + /> + + setDetailPost(null)} + onTagClick={() => {}} + /> +
+ ); +} diff --git a/app/api/admin/check/route.ts b/app/api/admin/check/route.ts new file mode 100644 index 0000000..401958d --- /dev/null +++ b/app/api/admin/check/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from "next/server"; +import { isAdminAuthenticated, ADMIN_USERNAME } from "@/lib/auth"; + +export async function GET() { + const authenticated = await isAdminAuthenticated(); + return NextResponse.json({ + authenticated, + username: authenticated ? ADMIN_USERNAME : null, + }); +} diff --git a/app/api/admin/login/route.ts b/app/api/admin/login/route.ts new file mode 100644 index 0000000..ef73866 --- /dev/null +++ b/app/api/admin/login/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from "next/server"; +import { validateCredentials, createSessionToken, COOKIE_NAME } from "@/lib/auth"; + +export async function POST(request: Request) { + try { + const { username, password } = await request.json(); + + if (!validateCredentials(username, password)) { + return NextResponse.json( + { error: "Kullanıcı adı veya şifre hatalı!" }, + { status: 401 } + ); + } + + const token = createSessionToken(username); + const response = NextResponse.json({ + success: true, + message: "Giriş başarılı", + username, + }); + + response.cookies.set({ + name: COOKIE_NAME, + value: token, + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "lax", + path: "/", + maxAge: 7 * 24 * 60 * 60, // 7 days + }); + + return response; + } catch (error) { + console.error("Login error:", error); + return NextResponse.json( + { error: "Giriş işlemi sırasında sunucu hatası oluştu" }, + { status: 500 } + ); + } +} diff --git a/app/api/admin/logout/route.ts b/app/api/admin/logout/route.ts new file mode 100644 index 0000000..10dc629 --- /dev/null +++ b/app/api/admin/logout/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from "next/server"; +import { COOKIE_NAME } from "@/lib/auth"; + +export async function POST() { + const response = NextResponse.json({ + success: true, + message: "Çıkış yapıldı", + }); + + response.cookies.set({ + name: COOKIE_NAME, + value: "", + httpOnly: true, + expires: new Date(0), + path: "/", + }); + + return response; +} diff --git a/app/api/posts/[id]/like/route.ts b/app/api/posts/[id]/like/route.ts new file mode 100644 index 0000000..9890354 --- /dev/null +++ b/app/api/posts/[id]/like/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const body = await request.json(); + const { increment } = body; + + const updatedPost = await prisma.post.update({ + where: { id }, + data: { + likes: { + increment: increment !== undefined ? (increment ? 1 : -1) : 1, + }, + }, + }); + + return NextResponse.json(updatedPost); + } catch (error) { + console.error("Error updating likes:", error); + return NextResponse.json( + { error: "Failed to update likes" }, + { status: 500 } + ); + } +} diff --git a/app/api/posts/[id]/route.ts b/app/api/posts/[id]/route.ts new file mode 100644 index 0000000..0f2fd81 --- /dev/null +++ b/app/api/posts/[id]/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { isAdminAuthenticated } from "@/lib/auth"; + +export async function PUT( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const isAuth = await isAdminAuthenticated(); + if (!isAuth) { + return NextResponse.json( + { error: "Bu işlem için yetkiniz yok (Admin girişi gerekli)" }, + { status: 401 } + ); + } + + const { id } = await params; + const body = await request.json(); + + const { + type, + date, + timestamp, + mood, + moodLabel, + title, + content, + marginNotes, + tags, + likes, + highlightWords, + authorNote, + imageUrl, + imageCaption, + codeSnippet, + codeLanguage, + audioDuration, + audioTitle, + stampedText, + } = body; + + const updatedPost = await prisma.post.update({ + where: { id }, + data: { + type, + date, + timestamp, + mood, + moodLabel, + title, + content, + marginNotes: marginNotes || [], + tags: tags || [], + likes: likes !== undefined ? likes : undefined, + highlightWords: highlightWords || [], + authorNote, + imageUrl, + imageCaption, + codeSnippet, + codeLanguage, + audioDuration, + audioTitle, + stampedText, + }, + }); + + return NextResponse.json(updatedPost); + } catch (error) { + console.error("Error updating post:", error); + return NextResponse.json( + { error: "Not güncellenirken sunucu hatası oluştu" }, + { status: 500 } + ); + } +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + try { + const isAuth = await isAdminAuthenticated(); + if (!isAuth) { + return NextResponse.json( + { error: "Bu işlem için yetkiniz yok (Admin girişi gerekli)" }, + { status: 401 } + ); + } + + const { id } = await params; + await prisma.post.delete({ + where: { id }, + }); + + return NextResponse.json({ success: true, message: "Not silindi" }); + } catch (error) { + console.error("Error deleting post:", error); + return NextResponse.json( + { error: "Not silinirken sunucu hatası oluştu" }, + { status: 500 } + ); + } +} diff --git a/app/api/posts/route.ts b/app/api/posts/route.ts new file mode 100644 index 0000000..dda164a --- /dev/null +++ b/app/api/posts/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; + +export async function GET() { + try { + const posts = await prisma.post.findMany({ + orderBy: { + createdAt: "desc", + }, + }); + return NextResponse.json(posts); + } catch (error) { + console.error("Error fetching posts:", error); + return NextResponse.json( + { error: "Failed to fetch posts from database" }, + { status: 500 } + ); + } +} + +export async function POST(request: Request) { + try { + const body = await request.json(); + const { + id, + type, + date, + timestamp, + mood, + moodLabel, + title, + content, + marginNotes, + tags, + likes, + highlightWords, + authorNote, + imageUrl, + imageCaption, + codeSnippet, + codeLanguage, + audioDuration, + audioTitle, + stampedText, + } = body; + + const newPost = await prisma.post.create({ + data: { + id: id || `post-${Date.now()}`, + type, + date, + timestamp, + mood, + moodLabel, + title, + content, + marginNotes: marginNotes || [], + tags: tags || [], + likes: likes || 0, + highlightWords: highlightWords || [], + authorNote, + imageUrl, + imageCaption, + codeSnippet, + codeLanguage, + audioDuration, + audioTitle, + stampedText, + }, + }); + + return NextResponse.json(newPost, { status: 201 }); + } catch (error) { + console.error("Error creating post:", error); + return NextResponse.json( + { error: "Failed to create post in database" }, + { status: 500 } + ); + } +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..e0a9103 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,191 @@ +@import "tailwindcss"; + +:root { + /* Core Palette Tokens */ + --bg-desk: #F0ECE1; + --bg-desk-pattern: #E5DFC8; + --bg-paper: #FAF7F2; + --bg-paper-warm: #F6F1E7; + --bg-paper-aged: #EFE7D6; + --bg-card-dark: #1E232D; + + --ink-primary: #18181B; + --ink-secondary: #4A4E57; + --ink-muted: #787D89; + --ink-faint: #A6ABB5; + + --accent-wax: #E11D48; + --accent-wax-dark: #9F1239; + --accent-amber: #D97706; + --accent-amber-light: #FEF3C7; + --accent-sage: #059669; + --accent-sage-light: #D1FAE5; + --accent-cyan: #0284C7; + --accent-cyan-light: #E0F2FE; + --accent-lavender: #7C3AED; + --accent-lavender-light: #EDE9FE; + + /* Paper & Tactile Shadow Tokens */ + --shadow-paper: 0 4px 20px -2px rgba(28, 25, 23, 0.08), 0 1px 4px 0 rgba(28, 25, 23, 0.04); + --shadow-paper-lifted: 0 14px 32px -4px rgba(28, 25, 23, 0.14), 0 4px 8px 0 rgba(28, 25, 23, 0.06); + --shadow-wax: 0 6px 16px rgba(225, 29, 72, 0.35), inset 0 2px 4px rgba(255, 255, 255, 0.4), inset 0 -3px 6px rgba(0, 0, 0, 0.3); + --shadow-clip: 0 4px 8px rgba(0, 0, 0, 0.18), inset 0 1px 1px rgba(255, 255, 255, 0.6); + + /* Fonts */ + --font-heading: var(--font-heading, "Bricolage Grotesque", sans-serif); + --font-serif: var(--font-serif, "Lora", serif); + --font-handwriting: var(--font-handwriting, "Caveat", cursive); + --font-mono: var(--font-mono, "JetBrains Mono", monospace); +} + +/* Desk Surface */ +.desk-background { + background-color: var(--bg-desk); + background-image: + radial-gradient(var(--bg-desk-pattern) 1.2px, transparent 1.2px), + linear-gradient(to bottom, rgba(240, 236, 225, 0.4), rgba(225, 218, 200, 0.7)); + background-size: 24px 24px, 100% 100%; +} + +/* Notebook Page Canvas */ +.notebook-sheet { + background-color: var(--bg-paper); + background-image: radial-gradient(rgba(24, 24, 27, 0.06) 1px, transparent 1px); + background-size: 20px 20px; + box-shadow: var(--shadow-paper); + border: 1px solid rgba(24, 24, 27, 0.08); +} + +.notebook-ruled { + background-color: var(--bg-paper); + background-image: linear-gradient(to bottom, transparent 31px, rgba(24, 24, 27, 0.06) 32px); + background-size: 100% 32px; +} + +/* Washi Tape Strip */ +.washi-tape { + position: absolute; + height: 24px; + background: rgba(244, 235, 215, 0.85); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); + border-left: 2px dashed rgba(200, 185, 160, 0.6); + border-right: 2px dashed rgba(200, 185, 160, 0.6); + backdrop-filter: blur(2px); + z-index: 10; +} + +.washi-tape-amber { + background: rgba(254, 240, 199, 0.88); + border-left: 2px dashed rgba(217, 119, 6, 0.4); + border-right: 2px dashed rgba(217, 119, 6, 0.4); +} + +.washi-tape-rose { + background: rgba(255, 228, 230, 0.88); + border-left: 2px dashed rgba(225, 29, 72, 0.4); + border-right: 2px dashed rgba(225, 29, 72, 0.4); +} + +/* Red Wax Seal */ +.wax-seal { + background: radial-gradient(circle at 35% 30%, #F43F5E, #E11D48 60%, #9F1239 100%); + box-shadow: var(--shadow-wax); + border: 2px solid rgba(255, 255, 255, 0.2); + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.wax-seal:hover { + transform: scale(1.05) rotate(4deg); +} + +.wax-seal:active { + transform: scale(0.96) rotate(-2deg); +} + +/* Rubber Stamp */ +.rubber-stamp { + font-family: var(--font-mono); + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + border: 2px solid currentColor; + padding: 4px 10px; + border-radius: 4px; + mask-image: radial-gradient(circle, black 70%, transparent 100%); + display: inline-flex; + align-items: center; + gap: 6px; + opacity: 0.88; +} + +/* Highlighter Marker */ +.highlighter-amber { + background: linear-gradient(104deg, rgba(254, 240, 138, 0) 0.9%, rgba(254, 240, 138, 0.85) 2.4%, rgba(254, 240, 138, 0.8) 95%, rgba(254, 240, 138, 0) 98%); + padding: 0 4px; + border-radius: 2px; +} + +.highlighter-sage { + background: linear-gradient(104deg, rgba(167, 243, 208, 0) 0.9%, rgba(167, 243, 208, 0.85) 2.4%, rgba(167, 243, 208, 0.8) 95%, rgba(167, 243, 208, 0) 98%); + padding: 0 4px; + border-radius: 2px; +} + +.highlighter-rose { + background: linear-gradient(104deg, rgba(254, 205, 211, 0) 0.9%, rgba(254, 205, 211, 0.85) 2.4%, rgba(254, 205, 211, 0.8) 95%, rgba(254, 205, 211, 0) 98%); + padding: 0 4px; + border-radius: 2px; +} + +/* Handwritten Typography */ +.font-handwritten { + font-family: var(--font-handwriting); +} + +.font-heading { + font-family: var(--font-heading); +} + +.font-serif { + font-family: var(--font-serif); +} + +.font-mono { + font-family: var(--font-mono); +} + +/* Brass Binder Clip */ +.binder-clip { + background: linear-gradient(135deg, #78716C, #44403C 50%, #292524); + box-shadow: var(--shadow-clip); +} + +/* Custom Journal Scrollbar */ +::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +::-webkit-scrollbar-track { + background: #EFE7D6; +} + +::-webkit-scrollbar-thumb { + background: #C4B9A3; + border-radius: 5px; + border: 2px solid #EFE7D6; +} + +::-webkit-scrollbar-thumb:hover { + background: #A89B82; +} + +/* Smooth Reduced Motion */ +@media (prefers-reduced-motion: reduce) { + *, ::before, ::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..adbfa2e --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,61 @@ +import type { Metadata } from "next"; +import { Bricolage_Grotesque, Lora, Caveat, JetBrains_Mono } from "next/font/google"; +import "./globals.css"; + +const bricolage = Bricolage_Grotesque({ + variable: "--font-heading", + subsets: ["latin"], + display: "swap", +}); + +const lora = Lora({ + variable: "--font-serif", + subsets: ["latin"], + display: "swap", +}); + +const caveat = Caveat({ + variable: "--font-handwriting", + subsets: ["latin"], + display: "swap", +}); + +const jetbrains = JetBrains_Mono({ + variable: "--font-mono", + subsets: ["latin"], + display: "swap", +}); + +export const metadata: Metadata = { + title: "mstfyldz — Kişisel Günlük & Mood Logbook", + description: "Mustafa Yıldız'ın filtrelenmemiş düşünceleri, anlık karalamaları, ruh hali dalgalanmaları ve dijital not defteri.", + authors: [{ name: "Mustafa Yıldız", url: "https://mstfyldz.com" }], + keywords: ["mstfyldz", "günlük", "blog", "journal", "karalama defteri", "notlar", "mood"], +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {/* + THESIS: A personal digital journal rejecting sanitized corporate minimalism in favor of a warm, tactile field notebook with ink marginalia, rubber stamps, and mood-adaptive atmosphere. + OWN-WORLD: Palette of warm tactile cream (#FAF7F2), rich sumi ink (#18181B), cardboard tabs (#E4DCD3), crimson wax seal (#E11D48), and amber highlighter (#D97706); paper grain texture, washi tape pins, and brass clip details. + STORY: Visitors enter a living sketchbook where thoughts are mapped across time and emotion, seamlessly filtering moods and exploring micro-notes and deep reflections. + FIRST VIEWPORT: A full-bleed tactile notebook canvas with an interactive 'Mood Tracker & Weather Log' wave, red wax seal mood stamp, and an asymmetric field of pinned notes, polaroid memories, and handwritten doodles. + FORM: Annotated Field Notebook & Ink Marginalia; seed key 6a22c7e2. + FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md + */} +
+ {children} +
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..84d8252 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,259 @@ +"use client"; + +import React, { useState, useMemo, useEffect } from "react"; +import { Header } from "@/components/Header"; +import { MoodTracker } from "@/components/MoodTracker"; +import { JournalCard } from "@/components/JournalCard"; +import { NewPostModal } from "@/components/NewPostModal"; +import { PostDetailModal } from "@/components/PostDetailModal"; +import { INITIAL_POSTS, MOODS } from "@/components/mockData"; +import { JournalPost, MoodType } from "@/components/types"; +import { Dices, Feather, Sparkles, Filter, Bookmark, Coffee, RefreshCw } from "lucide-react"; + +export default function Home() { + const [posts, setPosts] = useState(INITIAL_POSTS); + const [activeMood, setActiveMood] = useState("all"); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedTag, setSelectedTag] = useState(null); + + const [isNewPostOpen, setIsNewPostOpen] = useState(false); + const [detailPost, setDetailPost] = useState(null); + + // Fetch posts from PostgreSQL API on mount + useEffect(() => { + async function loadPosts() { + try { + const res = await fetch("/api/posts"); + if (res.ok) { + const data = await res.json(); + if (Array.isArray(data) && data.length > 0) { + setPosts(data); + } + } + } catch (err) { + console.error("Failed to load posts from API:", err); + } + } + loadPosts(); + }, []); + + // Extract all unique tags + const allTags = useMemo(() => { + const set = new Set(); + posts.forEach((p) => p.tags.forEach((t) => set.add(t))); + return Array.from(set); + }, [posts]); + + // Filtered posts + const filteredPosts = useMemo(() => { + return posts.filter((post) => { + // Mood filter + if (activeMood !== "all" && post.mood !== activeMood) { + return false; + } + // Tag filter + if (selectedTag && !post.tags.includes(selectedTag)) { + return false; + } + // Search query + if (searchQuery.trim()) { + const q = searchQuery.toLowerCase(); + const matchesContent = post.content.toLowerCase().includes(q); + const matchesTitle = post.title?.toLowerCase().includes(q); + const matchesTags = post.tags.some((t) => t.toLowerCase().includes(q)); + const matchesMargin = post.marginNotes?.some((m) => m.toLowerCase().includes(q)); + return matchesContent || matchesTitle || matchesTags || matchesMargin; + } + return true; + }); + }, [posts, activeMood, selectedTag, searchQuery]); + + const handleAddPost = async (newPost: JournalPost) => { + setPosts([newPost, ...posts]); + try { + await fetch("/api/posts", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newPost), + }); + } catch (err) { + console.error("Failed to save post to database:", err); + } + }; + + const handleRandomPost = () => { + if (posts.length === 0) return; + const randomIndex = Math.floor(Math.random() * posts.length); + setDetailPost(posts[randomIndex]); + }; + + const handleTagClick = (tag: string) => { + setSelectedTag(selectedTag === tag ? null : tag); + }; + + const clearFilters = () => { + setActiveMood("all"); + setSelectedTag(null); + setSearchQuery(""); + }; + + return ( +
+ {/* Top Header */} +
setIsNewPostOpen(true)} + onRandomPostClick={handleRandomPost} + postCount={posts.length} + /> + + {/* Main Journal Canvas */} +
+ {/* Floating Side Index Tabs (Notebook Edge Decoration) */} +
+
+ DİZİN SEKMELERİ +
+ {allTags.slice(0, 6).map((tag, idx) => { + const isSelected = selectedTag === tag; + const tabColors = [ + "bg-[#FEF3C7] text-[#92400E] border-[#D97706]/40", + "bg-[#FFE4E6] text-[#9F1239] border-[#E11D48]/40", + "bg-[#D1FAE5] text-[#065F46] border-[#059669]/40", + "bg-[#E0F2FE] text-[#075985] border-[#0284C7]/40", + "bg-[#EDE9FE] text-[#5B21B6] border-[#7C3AED]/40", + "bg-[#F3ECE2] text-[#18181B] border-[#18181B]/20", + ]; + const colorClass = tabColors[idx % tabColors.length]; + + return ( + + ); + })} +
+ + {/* The Signature Hero: Mood Tracker & Weather Log */} + { + setActiveMood(mood); + }} + filteredCount={filteredPosts.length} + /> + + {/* Active Filter Tags Indicator Bar */} + {(selectedTag || activeMood !== "all" || searchQuery) && ( +
+
+ Aktif Filtreler: + + {activeMood !== "all" && ( + + Mood: {MOODS.find((m) => m.id === activeMood)?.label} + + + )} + + {selectedTag && ( + + #{selectedTag} + + + )} + + {searchQuery && ( + + “{searchQuery}” + + + )} +
+ + +
+ )} + + {/* The Mixed-Media Journal Feed */} + {filteredPosts.length > 0 ? ( +
+ {filteredPosts.map((post) => ( + setDetailPost(p)} + onTagClick={handleTagClick} + /> + ))} +
+ ) : ( + /* Empty State */ +
+
+ +
+

+ Bu Filtrede Karalama Bulunamadı +

+

+ Görünüşe göre bu ruh halinde henüz bir şey yazılmamış. Belki de bir fincan çay alıp aklına gelen ilk şeyi karalama vaktidir? +

+
+ + +
+
+ )} + + {/* Footer Scrapbook Note */} +
+
+
+ "En iyi fikirler, en saçma karalamaların arasından çıkar." +
+
+ © 2026 Mustafa Yıldız • mstfyldz journal • Craft with passion +
+
+
+
+ + {/* Modals */} + setIsNewPostOpen(false)} + onAddPost={handleAddPost} + /> + + setDetailPost(null)} + onTagClick={handleTagClick} + /> +
+ ); +} diff --git a/components/EditPostModal.tsx b/components/EditPostModal.tsx new file mode 100644 index 0000000..3fb02aa --- /dev/null +++ b/components/EditPostModal.tsx @@ -0,0 +1,350 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { JournalPost, MoodType, PostType } from "./types"; +import { MOODS } from "./mockData"; +import { X, Feather, Save, StickyNote, BookOpen, Code, Camera, Volume2 } from "lucide-react"; + +interface EditPostModalProps { + post: JournalPost | null; + isOpen: boolean; + onClose: () => void; + onUpdatePost: (updatedPost: JournalPost) => void; +} + +export const EditPostModal: React.FC = ({ + post, + isOpen, + onClose, + onUpdatePost, +}) => { + const [type, setType] = useState("sticky"); + const [mood, setMood] = useState("spark"); + const [title, setTitle] = useState(""); + const [content, setContent] = useState(""); + const [marginNote, setMarginNote] = useState(""); + const [tagInput, setTagInput] = useState(""); + const [codeSnippet, setCodeSnippet] = useState(""); + const [stampedText, setStampedText] = useState(""); + const [imageUrl, setImageUrl] = useState(""); + const [imageCaption, setImageCaption] = useState(""); + const [likes, setLikes] = useState(0); + + useEffect(() => { + if (post) { + setType(post.type); + setMood(post.mood); + setTitle(post.title || ""); + setContent(post.content); + setMarginNote(post.marginNotes ? post.marginNotes.join(", ") : ""); + setTagInput(post.tags ? post.tags.join(", ") : ""); + setCodeSnippet(post.codeSnippet || ""); + setStampedText(post.stampedText || ""); + setImageUrl(post.imageUrl || ""); + setImageCaption(post.imageCaption || ""); + setLikes(post.likes || 0); + } + }, [post]); + + if (!isOpen || !post) return null; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!content.trim()) return; + + const moodObj = MOODS.find((m) => m.id === mood) || MOODS[0]; + const tags = tagInput + .split(",") + .map((t) => t.trim().replace(/^#/, "")) + .filter((t) => t.length > 0); + + const updated: JournalPost = { + ...post, + type, + mood, + moodLabel: moodObj.label, + title: title.trim() ? title : undefined, + content, + marginNotes: marginNote.trim() + ? marginNote.split(",").map((n) => n.trim()) + : undefined, + tags: tags.length > 0 ? tags : ["genel"], + stampedText: stampedText.trim() ? stampedText.trim() : undefined, + codeSnippet: type === "code" && codeSnippet.trim() ? codeSnippet : undefined, + codeLanguage: type === "code" ? "typescript" : undefined, + imageUrl: type === "polaroid" && imageUrl.trim() ? imageUrl : undefined, + imageCaption: type === "polaroid" && imageCaption.trim() ? imageCaption : undefined, + likes, + }; + + onUpdatePost(updated); + onClose(); + }; + + return ( +
+
e.stopPropagation()} + > + {/* Washi Tape */} +
+ + {/* Header */} +
+
+
+ ✏️ +
+
+

+ Notu Düzenle (Admin) +

+

+ ID: {post.id} • {post.date} +

+
+
+ + +
+ +
+ {/* Format Selector */} +
+ +
+ + + + + + + +
+
+ + {/* Mood Selector */} +
+ +
+ {MOODS.filter((m) => m.id !== "all").map((m) => ( + + ))} +
+
+ + {/* Title */} +
+ + setTitle(e.target.value)} + placeholder="Başlık..." + className="w-full px-3 py-2 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-serif" + /> +
+ + {/* Content */} +
+ +