first commit
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"buildPath": "comp"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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": "<button class=\"ds-btn-wax\">+ YENİ KARALAMA</button>",
|
||||
"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": "<div class=\"ds-wax-seal\"><span>M</span></div>",
|
||||
"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."
|
||||
]
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 624 KiB |
|
After Width: | Height: | Size: 463 KiB |
|
After Width: | Height: | Size: 694 KiB |
|
After Width: | Height: | Size: 726 KiB |
@@ -0,0 +1 @@
|
||||
{"optionId":"model-pick","steer":"","comp":".impeccable/mocks/decision/model-pick.webp","buildPath":"comp","buildPathFlipped":false}
|
||||
@@ -0,0 +1 @@
|
||||
{"pid":78230,"port":64499,"url":"http://127.0.0.1:64499/","lastBeat":1786885494556}
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,25 @@
|
||||
# Surface Brief: Home Page & Journal Feed
|
||||
|
||||
<!-- impeccable:surface-brief 1 -->
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
version: 1
|
||||
slug: "app-page-tsx"
|
||||
primary_target: "app/page.tsx"
|
||||
related_targets: []
|
||||
---
|
||||
|
||||
# Surface Brief: Home Page & Journal Feed
|
||||
|
||||
<!-- impeccable:surface-brief 1 -->
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,9 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
|
||||
# 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.
|
||||
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -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.
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,45 @@
|
||||
# Product
|
||||
|
||||
<!-- impeccable:product-schema 1 -->
|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
@@ -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<boolean>(false);
|
||||
const [checkingAuth, setCheckingAuth] = useState<boolean>(true);
|
||||
const [username, setUsername] = useState<string>("");
|
||||
const [password, setPassword] = useState<string>("");
|
||||
const [loginError, setLoginError] = useState<string>("");
|
||||
const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false);
|
||||
|
||||
// Data state
|
||||
const [posts, setPosts] = useState<JournalPost[]>([]);
|
||||
const [loadingPosts, setLoadingPosts] = useState<boolean>(false);
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [selectedMoodFilter, setSelectedMoodFilter] = useState<string>("all");
|
||||
|
||||
// Modal states
|
||||
const [editingPost, setEditingPost] = useState<JournalPost | null>(null);
|
||||
const [isNewPostOpen, setIsNewPostOpen] = useState<boolean>(false);
|
||||
const [detailPost, setDetailPost] = useState<JournalPost | null>(null);
|
||||
const [deletingId, setDeletingId] = useState<string | null>(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 (
|
||||
<div className="min-h-screen flex items-center justify-center desk-background">
|
||||
<div className="flex items-center gap-3 text-xs font-mono text-[#52525B]">
|
||||
<span className="w-2.5 h-2.5 rounded-full bg-[#E11D48] animate-ping" />
|
||||
<span>Admin Yetkisi Kontrol Ediliyor...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- LOGIN VIEW ---
|
||||
if (!isAuthenticated) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center p-4 desk-background">
|
||||
<div className="relative w-full max-w-md notebook-sheet rounded-2xl p-7 sm:p-9 shadow-2xl border border-[#18181B]/15">
|
||||
{/* Top Washi Tape */}
|
||||
<div className="washi-tape washi-tape-rose top-[-12px] left-1/2 -translate-x-1/2 w-44" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center mb-6 pt-2">
|
||||
<div className="w-14 h-14 rounded-full wax-seal mx-auto flex flex-col items-center justify-center text-white mb-3">
|
||||
<span className="font-heading font-black text-xl leading-none">M</span>
|
||||
<span className="text-[7px] font-mono tracking-tighter uppercase">ADM</span>
|
||||
</div>
|
||||
<h1 className="font-heading font-extrabold text-2xl text-[#18181B]">
|
||||
Yönetici Girişi
|
||||
</h1>
|
||||
<p className="text-xs font-handwritten text-[#787D89] text-lg leading-none mt-1">
|
||||
mstfyldz journal • Defter Yönetim Paneli
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Error Banner */}
|
||||
{loginError && (
|
||||
<div className="mb-5 p-3.5 rounded-xl bg-[#FFE4E6] border border-[#E11D48]/30 flex items-center gap-2.5 text-xs text-[#9F1239] font-mono">
|
||||
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||
<span>{loginError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Login Form */}
|
||||
<form onSubmit={handleLogin} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-[#787D89] font-bold mb-1">
|
||||
Kullanıcı Adı (.env):
|
||||
</label>
|
||||
<div className="relative">
|
||||
<User className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-[#787D89]" />
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase tracking-wider text-[#787D89] font-bold mb-1">
|
||||
Şifre (.env):
|
||||
</label>
|
||||
<div className="relative">
|
||||
<KeyRound className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-[#787D89]" />
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoggingIn}
|
||||
className="w-full py-2.5 rounded-lg bg-[#E11D48] hover:bg-[#9F1239] text-white font-heading font-bold text-sm shadow-sm hover:shadow transition-all flex items-center justify-center gap-2 cursor-pointer mt-2"
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4" />
|
||||
<span>{isLoggingIn ? "Giriş Yapılıyor..." : "Deftere Giriş Yap"}</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 pt-4 border-t border-[#18181B]/10 text-center">
|
||||
<p className="text-[11px] font-mono text-[#787D89]">
|
||||
Giriş bilgileri <code className="bg-[#18181B]/5 px-1 py-0.5 rounded">.env</code> içindeki <code className="text-[#E11D48]">ADMIN_USERNAME</code> ve <code className="text-[#E11D48]">ADMIN_PASSWORD</code> değişkenlerinden okunur.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- AUTHENTICATED DASHBOARD VIEW ---
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col w-full desk-background">
|
||||
{/* Top Admin Telemetry Header */}
|
||||
<header className="w-full border-b border-[#18181B]/10 bg-[#FAF7F2]/90 backdrop-blur-md sticky top-0 z-30">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-[#E11D48] text-white flex items-center justify-center font-heading font-black text-sm shadow-xs">
|
||||
M
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-heading font-extrabold text-lg text-[#18181B]">
|
||||
mstfyldz admin
|
||||
</span>
|
||||
<span className="text-[10px] font-mono px-2 py-0.5 rounded bg-[#059669]/10 text-[#059669] border border-[#059669]/20 font-bold flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[#059669] animate-pulse" />
|
||||
YETKİLENDİRİLDİ
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<a
|
||||
href="/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-xs font-mono text-[#52525B] hover:text-[#18181B] flex items-center gap-1"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
<span className="hidden sm:inline">Siteye Git</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#FAF7F2] border border-[#18181B]/15 text-xs font-mono text-[#9F1239] hover:bg-[#FFE4E6] transition-colors cursor-pointer"
|
||||
>
|
||||
<LogOut className="w-3.5 h-3.5" />
|
||||
<span>Çıkış Yap</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Admin Dashboard Content */}
|
||||
<main className="flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 py-8">
|
||||
{/* Metric Cards Overview */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
|
||||
<div className="notebook-sheet p-4 sm:p-5 rounded-xl border border-[#18181B]/10 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-mono text-[#787D89] uppercase tracking-wider font-bold">
|
||||
TOPLAM KARALAMA
|
||||
</div>
|
||||
<div className="text-3xl font-heading font-black text-[#18181B] mt-1">
|
||||
{posts.length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-[#F3ECE2] text-[#18181B] flex items-center justify-center">
|
||||
<FileText className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="notebook-sheet p-4 sm:p-5 rounded-xl border border-[#18181B]/10 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-mono text-[#787D89] uppercase tracking-wider font-bold">
|
||||
TOPLAM BEĞENİ
|
||||
</div>
|
||||
<div className="text-3xl font-heading font-black text-[#E11D48] mt-1">
|
||||
{totalLikes}
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-[#FFE4E6] text-[#E11D48] flex items-center justify-center">
|
||||
<Heart className="w-5 h-5 fill-current" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="notebook-sheet p-4 sm:p-5 rounded-xl border border-[#18181B]/10 flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-xs font-mono text-[#787D89] uppercase tracking-wider font-bold">
|
||||
VERİTABANI DURUMU
|
||||
</div>
|
||||
<div className="text-sm font-mono font-bold text-[#059669] mt-1 flex items-center gap-1.5">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>PostgreSQL (65.109...)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-10 h-10 rounded-full bg-[#D1FAE5] text-[#059669] flex items-center justify-center">
|
||||
<BarChart2 className="w-5 h-5" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls & Search Bar */}
|
||||
<div className="notebook-sheet rounded-2xl p-5 sm:p-6 mb-6">
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="font-heading font-extrabold text-xl text-[#18181B]">
|
||||
Not & Karalama Yönetimi
|
||||
</h2>
|
||||
<p className="text-xs font-serif text-[#52525B] mt-0.5">
|
||||
PostgreSQL veritabanındaki tüm notları görüntüleyin, güncelleyin veya yeni içerik girin.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2.5 w-full md:w-auto">
|
||||
<button
|
||||
onClick={fetchPosts}
|
||||
title="Yenile"
|
||||
className="p-2 rounded-lg bg-[#FAF7F2] border border-[#18181B]/15 text-[#18181B] hover:bg-[#EFE7D6] transition-colors cursor-pointer"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${loadingPosts ? "animate-spin" : ""}`} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setIsNewPostOpen(true)}
|
||||
className="flex-1 md:flex-initial flex items-center justify-center gap-1.5 px-4 py-2 rounded-lg bg-[#E11D48] text-white text-xs font-heading font-bold hover:bg-[#9F1239] shadow-xs transition-colors cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>+ Yeni Not Ekle</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search & Mood Filter Inputs */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-4 pt-4 border-t border-[#18181B]/10">
|
||||
<div className="relative sm:col-span-2">
|
||||
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-[#787D89]" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<select
|
||||
value={selectedMoodFilter}
|
||||
onChange={(e) => setSelectedMoodFilter(e.target.value)}
|
||||
className="w-full px-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-mono text-[#18181B]"
|
||||
>
|
||||
{MOODS.map((m) => (
|
||||
<option key={m.id} value={m.id}>
|
||||
{m.label} ({m.stampText})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<div className="notebook-sheet rounded-2xl overflow-hidden shadow-sm border border-[#18181B]/10">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-[#F3ECE2] border-b border-[#18181B]/10 text-[11px] font-mono uppercase text-[#787D89]">
|
||||
<th className="py-3 px-4 font-bold">Format / Tür</th>
|
||||
<th className="py-3 px-4 font-bold">Tarih</th>
|
||||
<th className="py-3 px-4 font-bold">Mood</th>
|
||||
<th className="py-3 px-4 font-bold">Başlık / İçerik</th>
|
||||
<th className="py-3 px-4 font-bold">Beğeni</th>
|
||||
<th className="py-3 px-4 font-bold">Etiketler</th>
|
||||
<th className="py-3 px-4 font-bold text-right">İşlemler</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-[#18181B]/8 text-xs font-serif text-[#18181B]">
|
||||
{filteredPosts.length > 0 ? (
|
||||
filteredPosts.map((post) => (
|
||||
<tr key={post.id} className="hover:bg-[#FAF7F2]/60 transition-colors">
|
||||
{/* Format Badge */}
|
||||
<td className="py-3 px-4 font-mono text-[11px] whitespace-nowrap">
|
||||
<span className="px-2 py-0.5 rounded uppercase font-bold bg-[#18181B]/5 border border-[#18181B]/10">
|
||||
{post.type}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Date */}
|
||||
<td className="py-3 px-4 font-mono text-[11px] text-[#52525B] whitespace-nowrap">
|
||||
{post.date}
|
||||
</td>
|
||||
|
||||
{/* Mood */}
|
||||
<td className="py-3 px-4 whitespace-nowrap">
|
||||
<span className="inline-flex items-center gap-1 font-mono text-[11px] text-[#52525B]">
|
||||
<span className="w-2 h-2 rounded-full bg-[#E11D48]" />
|
||||
<span>{post.moodLabel}</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
{/* Title / Content Preview */}
|
||||
<td className="py-3 px-4 max-w-xs">
|
||||
<div className="font-heading font-bold text-[#18181B] truncate">
|
||||
{post.title || "(Başlıksız Not)"}
|
||||
</div>
|
||||
<div className="text-[11px] text-[#787D89] truncate">
|
||||
{post.content}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Likes */}
|
||||
<td className="py-3 px-4 font-mono font-bold text-[#E11D48] whitespace-nowrap">
|
||||
♥ {post.likes}
|
||||
</td>
|
||||
|
||||
{/* Tags */}
|
||||
<td className="py-3 px-4">
|
||||
<div className="flex items-center gap-1 flex-wrap">
|
||||
{post.tags.map((t) => (
|
||||
<span key={t} className="text-[10px] font-mono px-1.5 py-0.5 rounded bg-[#F3ECE2] text-[#52525B]">
|
||||
#{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td className="py-3 px-4 text-right whitespace-nowrap">
|
||||
<div className="flex items-center justify-end gap-1.5">
|
||||
<button
|
||||
onClick={() => setDetailPost(post)}
|
||||
title="Önizle"
|
||||
className="p-1.5 rounded bg-[#FAF7F2] border border-[#18181B]/10 text-[#52525B] hover:text-[#18181B] hover:bg-[#EFE7D6] transition-colors"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setEditingPost(post)}
|
||||
title="Düzenle"
|
||||
className="p-1.5 rounded bg-[#FEF3C7] border border-[#D97706]/30 text-[#D97706] hover:bg-[#FDE68A] transition-colors"
|
||||
>
|
||||
<Edit3 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
|
||||
{deletingId === post.id ? (
|
||||
<div className="inline-flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => handleDeletePost(post.id)}
|
||||
className="px-2 py-0.5 rounded bg-[#E11D48] text-white text-[10px] font-mono font-bold hover:bg-[#9F1239]"
|
||||
>
|
||||
Eminim Sil
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDeletingId(null)}
|
||||
className="text-[10px] font-mono text-[#787D89] hover:underline"
|
||||
>
|
||||
İptal
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setDeletingId(post.id)}
|
||||
title="Sil"
|
||||
className="p-1.5 rounded bg-[#FFE4E6] border border-[#E11D48]/30 text-[#E11D48] hover:bg-[#FECDD3] transition-colors"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={7} className="py-8 text-center text-xs font-mono text-[#787D89]">
|
||||
Filtrelerle eşleşen not bulunamadı.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Modals */}
|
||||
<EditPostModal
|
||||
post={editingPost}
|
||||
isOpen={!!editingPost}
|
||||
onClose={() => setEditingPost(null)}
|
||||
onUpdatePost={handleUpdatePost}
|
||||
/>
|
||||
|
||||
<NewPostModal
|
||||
isOpen={isNewPostOpen}
|
||||
onClose={() => setIsNewPostOpen(false)}
|
||||
onAddPost={handleAddPost}
|
||||
/>
|
||||
|
||||
<PostDetailModal
|
||||
post={detailPost}
|
||||
onClose={() => setDetailPost(null)}
|
||||
onTagClick={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 25 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<html
|
||||
lang="tr"
|
||||
className={`${bricolage.variable} ${lora.variable} ${caveat.variable} ${jetbrains.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col selection:bg-[#E11D48]/20 selection:text-[#18181B]">
|
||||
{/*
|
||||
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
|
||||
*/}
|
||||
<div className="desk-background min-h-screen flex flex-col relative overflow-x-hidden">
|
||||
{children}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -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<JournalPost[]>(INITIAL_POSTS);
|
||||
const [activeMood, setActiveMood] = useState<MoodType>("all");
|
||||
const [searchQuery, setSearchQuery] = useState<string>("");
|
||||
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
||||
|
||||
const [isNewPostOpen, setIsNewPostOpen] = useState(false);
|
||||
const [detailPost, setDetailPost] = useState<JournalPost | null>(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<string>();
|
||||
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 (
|
||||
<div className="flex-1 flex flex-col w-full">
|
||||
{/* Top Header */}
|
||||
<Header
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
onNewPostClick={() => setIsNewPostOpen(true)}
|
||||
onRandomPostClick={handleRandomPost}
|
||||
postCount={posts.length}
|
||||
/>
|
||||
|
||||
{/* Main Journal Canvas */}
|
||||
<main className="flex-1 max-w-6xl w-full mx-auto px-4 sm:px-6 py-8 relative">
|
||||
{/* Floating Side Index Tabs (Notebook Edge Decoration) */}
|
||||
<div className="hidden xl:flex flex-col gap-2.5 fixed left-4 top-36 z-20">
|
||||
<div className="text-[10px] font-mono uppercase tracking-widest text-[#787D89] font-bold px-1 mb-1">
|
||||
DİZİN SEKMELERİ
|
||||
</div>
|
||||
{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 (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => handleTagClick(tag)}
|
||||
className={`text-xs font-mono px-3 py-1.5 rounded-r-md border-y border-r shadow-xs text-left transition-all duration-200 cursor-pointer ${colorClass} ${
|
||||
isSelected ? "translate-x-2 font-bold ring-2 ring-black/20" : "hover:translate-x-1"
|
||||
}`}
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* The Signature Hero: Mood Tracker & Weather Log */}
|
||||
<MoodTracker
|
||||
activeMood={activeMood}
|
||||
onSelectMood={(mood) => {
|
||||
setActiveMood(mood);
|
||||
}}
|
||||
filteredCount={filteredPosts.length}
|
||||
/>
|
||||
|
||||
{/* Active Filter Tags Indicator Bar */}
|
||||
{(selectedTag || activeMood !== "all" || searchQuery) && (
|
||||
<div className="flex items-center justify-between gap-3 p-3.5 mb-6 rounded-xl bg-[#FAF7F2] border border-[#18181B]/12">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs font-mono text-[#787D89] font-semibold">Aktif Filtreler:</span>
|
||||
|
||||
{activeMood !== "all" && (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-[#18181B] text-[#FAF7F2] text-xs font-mono">
|
||||
Mood: {MOODS.find((m) => m.id === activeMood)?.label}
|
||||
<button onClick={() => setActiveMood("all")} className="hover:text-red-300 ml-1">×</button>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{selectedTag && (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-[#E11D48] text-white text-xs font-mono">
|
||||
#{selectedTag}
|
||||
<button onClick={() => setSelectedTag(null)} className="hover:text-red-200 ml-1">×</button>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{searchQuery && (
|
||||
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full bg-[#D97706] text-white text-xs font-mono">
|
||||
“{searchQuery}”
|
||||
<button onClick={() => setSearchQuery("")} className="hover:text-amber-200 ml-1">×</button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="flex items-center gap-1 text-xs font-mono text-[#787D89] hover:text-[#18181B] transition-colors shrink-0"
|
||||
>
|
||||
<RefreshCw className="w-3 h-3" />
|
||||
<span>Filtreleri Temizle</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* The Mixed-Media Journal Feed */}
|
||||
{filteredPosts.length > 0 ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 sm:gap-7 items-start">
|
||||
{filteredPosts.map((post) => (
|
||||
<JournalCard
|
||||
key={post.id}
|
||||
post={post}
|
||||
onOpenDetail={(p) => setDetailPost(p)}
|
||||
onTagClick={handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
/* Empty State */
|
||||
<div className="w-full py-16 px-6 notebook-sheet rounded-2xl text-center flex flex-col items-center justify-center">
|
||||
<div className="w-14 h-14 rounded-full bg-[#FEF3C7] text-[#D97706] flex items-center justify-center mb-4">
|
||||
<Coffee className="w-7 h-7" />
|
||||
</div>
|
||||
<h3 className="font-heading font-black text-xl text-[#18181B] mb-1">
|
||||
Bu Filtrede Karalama Bulunamadı
|
||||
</h3>
|
||||
<p className="font-serif text-sm text-[#52525B] max-w-md mb-6">
|
||||
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?
|
||||
</p>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={clearFilters}
|
||||
className="px-4 py-2 rounded-lg bg-[#FAF7F2] border border-[#18181B]/15 text-xs font-heading font-semibold text-[#18181B] hover:bg-[#EFE7D6] transition-colors"
|
||||
>
|
||||
Filtreleri Temizle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsNewPostOpen(true)}
|
||||
className="px-4 py-2 rounded-lg bg-[#E11D48] text-white text-xs font-heading font-bold hover:bg-[#9F1239] shadow-xs transition-colors"
|
||||
>
|
||||
+ İlk Notu Sen Karala
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer Scrapbook Note */}
|
||||
<footer className="mt-16 pt-8 pb-12 border-t border-[#18181B]/10 text-center">
|
||||
<div className="inline-block relative">
|
||||
<div className="font-handwritten text-2xl text-[#18181B] font-bold">
|
||||
"En iyi fikirler, en saçma karalamaların arasından çıkar."
|
||||
</div>
|
||||
<div className="text-xs font-mono text-[#787D89] mt-1">
|
||||
© 2026 Mustafa Yıldız • mstfyldz journal • Craft with passion
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
|
||||
{/* Modals */}
|
||||
<NewPostModal
|
||||
isOpen={isNewPostOpen}
|
||||
onClose={() => setIsNewPostOpen(false)}
|
||||
onAddPost={handleAddPost}
|
||||
/>
|
||||
|
||||
<PostDetailModal
|
||||
post={detailPost}
|
||||
onClose={() => setDetailPost(null)}
|
||||
onTagClick={handleTagClick}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<EditPostModalProps> = ({
|
||||
post,
|
||||
isOpen,
|
||||
onClose,
|
||||
onUpdatePost,
|
||||
}) => {
|
||||
const [type, setType] = useState<PostType>("sticky");
|
||||
const [mood, setMood] = useState<MoodType>("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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs animate-in fade-in duration-200">
|
||||
<div
|
||||
className="relative w-full max-w-xl notebook-sheet rounded-2xl p-6 sm:p-8 shadow-2xl border border-[#18181B]/15 max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Washi Tape */}
|
||||
<div className="washi-tape washi-tape-amber top-[-10px] left-1/2 -translate-x-1/2 w-40" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between pb-4 border-b border-[#18181B]/10 mb-5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-full bg-[#D97706] text-white flex items-center justify-center font-bold text-sm">
|
||||
✏️
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-heading font-extrabold text-lg text-[#18181B]">
|
||||
Notu Düzenle (Admin)
|
||||
</h2>
|
||||
<p className="text-xs font-mono text-[#787D89]">
|
||||
ID: {post.id} • {post.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 rounded-lg text-[#787D89] hover:text-[#18181B] hover:bg-[#18181B]/5 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Format Selector */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1.5">
|
||||
Not Formatı:
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType("sticky")}
|
||||
className={`flex items-center justify-center gap-1 p-2 rounded-lg text-xs font-heading font-semibold border transition-all ${
|
||||
type === "sticky"
|
||||
? "bg-[#FEF3C7] border-[#D97706] text-[#B45309] shadow-xs"
|
||||
: "bg-[#FAF7F2] border-[#18181B]/10 text-[#52525B]"
|
||||
}`}
|
||||
>
|
||||
<StickyNote className="w-3.5 h-3.5" />
|
||||
<span>Post-it</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType("notebook")}
|
||||
className={`flex items-center justify-center gap-1 p-2 rounded-lg text-xs font-heading font-semibold border transition-all ${
|
||||
type === "notebook"
|
||||
? "bg-[#FAF7F2] border-[#18181B] text-[#18181B] shadow-xs font-bold"
|
||||
: "bg-[#FAF7F2] border-[#18181B]/10 text-[#52525B]"
|
||||
}`}
|
||||
>
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
<span>Defter</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType("code")}
|
||||
className={`flex items-center justify-center gap-1 p-2 rounded-lg text-xs font-heading font-semibold border transition-all ${
|
||||
type === "code"
|
||||
? "bg-[#1E232D] border-[#1E232D] text-white shadow-xs"
|
||||
: "bg-[#FAF7F2] border-[#18181B]/10 text-[#52525B]"
|
||||
}`}
|
||||
>
|
||||
<Code className="w-3.5 h-3.5" />
|
||||
<span>Kod</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType("polaroid")}
|
||||
className={`flex items-center justify-center gap-1 p-2 rounded-lg text-xs font-heading font-semibold border transition-all ${
|
||||
type === "polaroid"
|
||||
? "bg-[#E0F2FE] border-[#0284C7] text-[#0369A1] shadow-xs"
|
||||
: "bg-[#FAF7F2] border-[#18181B]/10 text-[#52525B]"
|
||||
}`}
|
||||
>
|
||||
<Camera className="w-3.5 h-3.5" />
|
||||
<span>Polaroid</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mood Selector */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1.5">
|
||||
Ruh Hali (Mood):
|
||||
</label>
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-1 scrollbar-none">
|
||||
{MOODS.filter((m) => m.id !== "all").map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => setMood(m.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-heading font-semibold shrink-0 border transition-all ${
|
||||
mood === m.id
|
||||
? "bg-[#18181B] text-[#FAF7F2] border-[#18181B]"
|
||||
: "bg-[#FAF7F2] text-[#52525B] border-[#18181B]/10 hover:bg-[#EFE7D6]"
|
||||
}`}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Başlık:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
İçerik:
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
rows={4}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
className="w-full px-3.5 py-2.5 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-serif leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Stamped Text */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Damga Metni (Örn: ONAYLANDI, SUÇLU, COMPILED):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={stampedText}
|
||||
onChange={(e) => setStampedText(e.target.value)}
|
||||
placeholder="ONAYLANDI"
|
||||
className="w-full px-3 py-1.5 text-xs font-mono bg-white/80 border border-[#18181B]/15 rounded-lg uppercase"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Code Snippet */}
|
||||
{type === "code" && (
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Kod Parçası:
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={codeSnippet}
|
||||
onChange={(e) => setCodeSnippet(e.target.value)}
|
||||
className="w-full px-3.5 py-2 text-xs font-mono bg-[#1E232D] text-emerald-400 border border-white/10 rounded-lg focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Polaroid Image URL */}
|
||||
{type === "polaroid" && (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Resim URL:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={imageUrl}
|
||||
onChange={(e) => setImageUrl(e.target.value)}
|
||||
placeholder="https://images.unsplash.com/..."
|
||||
className="w-full px-3 py-1.5 text-xs bg-white/80 border border-[#18181B]/15 rounded-lg font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Resim Yazısı (Caption):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={imageCaption}
|
||||
onChange={(e) => setImageCaption(e.target.value)}
|
||||
placeholder="Zirve esintisi..."
|
||||
className="w-full px-3 py-1.5 text-xs bg-white/80 border border-[#18181B]/15 rounded-lg font-handwritten text-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Margin Notes Input */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Kenar Notları (Virgülle ayırın):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={marginNote}
|
||||
onChange={(e) => setMarginNote(e.target.value)}
|
||||
placeholder="← tam olarak bu!, not al"
|
||||
className="w-full px-3 py-1.5 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-handwritten text-lg text-[#E11D48]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Etiketler (Virgülle ayırın):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
placeholder="felsefe, gece, kahve"
|
||||
className="w-full px-3 py-1.5 text-xs bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Likes */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Beğeni Sayısı:
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={likes}
|
||||
onChange={(e) => setLikes(parseInt(e.target.value, 10) || 0)}
|
||||
className="w-32 px-3 py-1.5 text-xs bg-white/80 border border-[#18181B]/15 rounded-lg font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-2.5 pt-3 border-t border-[#18181B]/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg text-xs font-heading font-semibold text-[#52525B] hover:bg-[#18181B]/5 transition-colors"
|
||||
>
|
||||
Vazgeç
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-1.5 px-5 py-2 rounded-lg bg-[#059669] text-white text-xs font-heading font-bold hover:bg-[#047857] shadow-sm transition-all cursor-pointer"
|
||||
>
|
||||
<Save className="w-3.5 h-3.5" />
|
||||
<span>Günü Güncelle</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Search, Sparkles, Plus, Dices, Feather, Clock } from "lucide-react";
|
||||
|
||||
interface HeaderProps {
|
||||
searchQuery: string;
|
||||
onSearchChange: (q: string) => void;
|
||||
onNewPostClick: () => void;
|
||||
onRandomPostClick: () => void;
|
||||
postCount: number;
|
||||
}
|
||||
|
||||
export const Header: React.FC<HeaderProps> = ({
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
onNewPostClick,
|
||||
onRandomPostClick,
|
||||
postCount,
|
||||
}) => {
|
||||
const [timeString, setTimeString] = useState<string>("16:20");
|
||||
|
||||
useEffect(() => {
|
||||
const updateTime = () => {
|
||||
const now = new Date();
|
||||
setTimeString(
|
||||
now.toLocaleTimeString("tr-TR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})
|
||||
);
|
||||
};
|
||||
updateTime();
|
||||
const timer = setInterval(updateTime, 30000);
|
||||
return () => clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<header className="w-full border-b border-[#18181B]/10 bg-[#FAF7F2]/80 backdrop-blur-md sticky top-0 z-30 transition-colors">
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 py-3.5 flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
{/* Brand & Identity */}
|
||||
<div className="flex items-center gap-3.5 w-full md:w-auto justify-between md:justify-start">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-9 h-9 rounded-full bg-[#18181B] text-[#FAF7F2] flex items-center justify-center font-heading font-black text-lg shadow-sm border border-[#18181B]/20">
|
||||
M
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-heading font-extrabold text-xl tracking-tight text-[#18181B]">
|
||||
mstfyldz
|
||||
</span>
|
||||
<span className="text-[11px] font-mono uppercase px-2 py-0.5 rounded bg-[#18181B]/5 text-[#18181B]/70 border border-[#18181B]/10">
|
||||
v1.0 log
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs font-handwritten text-[#787D89] text-base leading-none mt-0.5">
|
||||
Mustafa Yıldız • Kişisel Karalama & Mood Defteri
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile CTAs */}
|
||||
<div className="flex items-center gap-2 md:hidden">
|
||||
<button
|
||||
onClick={onRandomPostClick}
|
||||
title="Rastgele Bir Not Aç"
|
||||
aria-label="Rastgele Bir Not Aç"
|
||||
className="p-2 rounded-lg bg-[#FAF7F2] border border-[#18181B]/15 text-[#18181B] hover:bg-[#F0ECE1] transition-all shadow-xs"
|
||||
>
|
||||
<Dices className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={onNewPostClick}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#E11D48] text-white text-xs font-heading font-bold shadow-xs hover:bg-[#9F1239] transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>Karala</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Telemetry & Search & Actions */}
|
||||
<div className="flex items-center gap-3 w-full md:w-auto justify-end">
|
||||
{/* Live Telemetry Pill */}
|
||||
<div className="hidden lg:flex items-center gap-2 px-3 py-1.5 rounded-full bg-[#F3ECE2] border border-[#18181B]/10 text-[11px] font-mono text-[#52525B]">
|
||||
<span className="w-2 h-2 rounded-full bg-[#059669] animate-pulse" />
|
||||
<span>Muğla, TR</span>
|
||||
<span>•</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="w-3 h-3 text-[#787D89]" />
|
||||
<span>{timeString}</span>
|
||||
</div>
|
||||
<span>•</span>
|
||||
<span>{postCount} Not Kayıtlı</span>
|
||||
</div>
|
||||
|
||||
{/* Search Box */}
|
||||
<div className="relative flex-1 md:w-56">
|
||||
<Search className="w-3.5 h-3.5 absolute left-3 top-1/2 -translate-y-1/2 text-[#787D89]" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
placeholder="Defterde ara..."
|
||||
className="w-full pl-8 pr-3 py-1.5 text-xs bg-white/70 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 focus:border-[#E11D48] transition-all placeholder:text-[#A6ABB5] font-serif"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => onSearchChange("")}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-[#787D89] hover:text-[#18181B]"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Desktop Actions */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<button
|
||||
onClick={onRandomPostClick}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#FAF7F2] border border-[#18181B]/15 text-[#18181B] text-xs font-heading font-medium hover:bg-[#F0ECE1] hover:shadow-xs transition-all cursor-pointer"
|
||||
>
|
||||
<Dices className="w-3.5 h-3.5 text-[#D97706]" />
|
||||
<span>Rastgele Not</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={onNewPostClick}
|
||||
className="flex items-center gap-1.5 px-3.5 py-1.5 rounded-lg bg-[#E11D48] text-white text-xs font-heading font-bold hover:bg-[#9F1239] shadow-xs hover:shadow transition-all cursor-pointer"
|
||||
>
|
||||
<Feather className="w-3.5 h-3.5" />
|
||||
<span>+ Yeni Düşünce</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,450 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { JournalPost } from "./types";
|
||||
import {
|
||||
Heart,
|
||||
MessageSquare,
|
||||
Tag,
|
||||
Play,
|
||||
Pause,
|
||||
Maximize2,
|
||||
Terminal,
|
||||
Volume2,
|
||||
CornerDownRight,
|
||||
Check,
|
||||
Sparkles
|
||||
} from "lucide-react";
|
||||
|
||||
interface JournalCardProps {
|
||||
post: JournalPost;
|
||||
onOpenDetail: (post: JournalPost) => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
}
|
||||
|
||||
export const JournalCard: React.FC<JournalCardProps> = ({
|
||||
post,
|
||||
onOpenDetail,
|
||||
onTagClick,
|
||||
}) => {
|
||||
const [likes, setLikes] = useState(post.likes);
|
||||
const [hasLiked, setHasLiked] = useState(false);
|
||||
const [isPlayingAudio, setIsPlayingAudio] = useState(false);
|
||||
const [stampFeedback, setStampFeedback] = useState(false);
|
||||
|
||||
const handleLike = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!hasLiked) {
|
||||
setLikes((prev) => prev + 1);
|
||||
setHasLiked(true);
|
||||
setStampFeedback(true);
|
||||
setTimeout(() => setStampFeedback(false), 1200);
|
||||
} else {
|
||||
setLikes((prev) => prev - 1);
|
||||
setHasLiked(false);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAudio = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
setIsPlayingAudio(!isPlayingAudio);
|
||||
};
|
||||
|
||||
// Render format based on type
|
||||
if (post.type === "sticky") {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onOpenDetail(post)}
|
||||
className="group relative bg-[#FEF3C7] border border-[#D97706]/20 p-5 sm:p-6 rounded-lg shadow-sm hover:shadow-md transition-all duration-300 transform hover:-translate-y-1 rotate-[-1deg] cursor-pointer flex flex-col justify-between"
|
||||
>
|
||||
{/* Washi Tape across top */}
|
||||
<div className="washi-tape washi-tape-amber top-[-10px] left-1/2 -translate-x-1/2 w-28" />
|
||||
|
||||
<div>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between text-xs font-mono text-[#D97706] mb-3 pt-2">
|
||||
<span className="font-bold">{post.date} • {post.timestamp}</span>
|
||||
{post.stampedText && (
|
||||
<span className="rubber-stamp text-[9px] text-[#B45309] border-[#B45309] rotate-[3deg]">
|
||||
{post.stampedText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<p className="font-serif text-[#18181B] text-base sm:text-lg leading-snug font-medium mb-4">
|
||||
“{post.content}”
|
||||
</p>
|
||||
|
||||
{/* Margin Notes */}
|
||||
{post.marginNotes && post.marginNotes.length > 0 && (
|
||||
<div className="my-2 p-2 rounded bg-[#FDE68A]/40 border border-[#D97706]/20">
|
||||
{post.marginNotes.map((note, idx) => (
|
||||
<div key={idx} className="font-handwritten text-[#92400E] text-base leading-tight">
|
||||
{note}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-[#D97706]/15 mt-3">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{post.tags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTagClick(tag);
|
||||
}}
|
||||
className="text-[11px] font-mono text-[#92400E] hover:underline"
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`flex items-center gap-1 text-xs font-mono px-2 py-1 rounded transition-colors ${
|
||||
hasLiked ? "text-[#E11D48] font-bold" : "text-[#B45309] hover:text-[#18181B]"
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-3.5 h-3.5 ${hasLiked ? "fill-current" : ""}`} />
|
||||
<span>{likes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.type === "polaroid") {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onOpenDetail(post)}
|
||||
className="group relative bg-[#FAF7F2] border border-[#18181B]/12 p-3 sm:p-4 rounded-sm shadow-sm hover:shadow-lg transition-all duration-300 transform hover:rotate-[0.5deg] rotate-[1deg] cursor-pointer"
|
||||
>
|
||||
{/* Washi Tape */}
|
||||
<div className="washi-tape top-[-10px] left-1/2 -translate-x-1/2 w-32" />
|
||||
|
||||
{/* Image Container */}
|
||||
<div className="w-full aspect-[4/3] rounded-xs overflow-hidden bg-[#18181B]/5 relative mb-3">
|
||||
{post.imageUrl && (
|
||||
<img
|
||||
src={post.imageUrl}
|
||||
alt={post.imageCaption || "Polaroid memory"}
|
||||
className="w-full h-full object-cover group-hover:scale-102 transition-transform duration-500"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute bottom-2 right-2 px-2 py-0.5 rounded bg-black/60 backdrop-blur-xs text-white text-[10px] font-mono">
|
||||
{post.date}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Polaroid Handwritten Caption */}
|
||||
<div className="px-1 pb-1">
|
||||
<div className="font-handwritten text-[#18181B] text-xl leading-snug font-bold">
|
||||
{post.imageCaption || post.content}
|
||||
</div>
|
||||
<p className="text-xs font-serif text-[#52525B] mt-1.5 line-clamp-2">
|
||||
{post.content}
|
||||
</p>
|
||||
|
||||
{/* Margin Note */}
|
||||
{post.marginNotes && post.marginNotes.length > 0 && (
|
||||
<div className="font-handwritten text-[#0284C7] text-base mt-2 flex items-center gap-1">
|
||||
<CornerDownRight className="w-3.5 h-3.5 shrink-0" />
|
||||
<span>{post.marginNotes[0]}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-2.5 border-t border-[#18181B]/8 mt-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{post.tags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTagClick(tag);
|
||||
}}
|
||||
className="text-[10px] font-mono text-[#787D89] hover:text-[#18181B]"
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`flex items-center gap-1 text-xs font-mono transition-colors ${
|
||||
hasLiked ? "text-[#E11D48] font-bold" : "text-[#787D89] hover:text-[#18181B]"
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-3.5 h-3.5 ${hasLiked ? "fill-current" : ""}`} />
|
||||
<span>{likes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.type === "code") {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onOpenDetail(post)}
|
||||
className="group relative bg-[#1E232D] text-[#FAF7F2] p-5 sm:p-6 rounded-xl shadow-md hover:shadow-xl transition-all duration-300 border border-white/10 cursor-pointer"
|
||||
>
|
||||
{/* Terminal Header */}
|
||||
<div className="flex items-center justify-between pb-3 border-b border-white/10 mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#EF4444]" />
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#F59E0B]" />
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#10B981]" />
|
||||
</div>
|
||||
<span className="text-xs font-mono text-white/60 ml-2">
|
||||
{post.title || "terminal_scrap.rs"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span className="text-[10px] font-mono uppercase px-2 py-0.5 rounded bg-white/10 text-white/80">
|
||||
{post.codeLanguage || "code"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Snippet */}
|
||||
{post.codeSnippet && (
|
||||
<pre className="p-3.5 rounded-lg bg-black/40 text-emerald-400 font-mono text-xs overflow-x-auto leading-relaxed border border-white/5 my-2">
|
||||
<code>{post.codeSnippet}</code>
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{/* Narrative */}
|
||||
<p className="text-xs sm:text-sm font-serif text-white/80 mt-3 leading-relaxed">
|
||||
{post.content}
|
||||
</p>
|
||||
|
||||
{/* Handwritten Margin Note on Dark Card */}
|
||||
{post.marginNotes && post.marginNotes.length > 0 && (
|
||||
<div className="font-handwritten text-[#FDE047] text-base mt-2 flex items-center gap-1.5">
|
||||
<span>>></span>
|
||||
<span>{post.marginNotes.join(" • ")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-white/10 mt-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{post.tags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTagClick(tag);
|
||||
}}
|
||||
className="text-[10px] font-mono text-white/60 hover:text-white"
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`flex items-center gap-1 text-xs font-mono ${
|
||||
hasLiked ? "text-[#E11D48] font-bold" : "text-white/60 hover:text-white"
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-3.5 h-3.5 ${hasLiked ? "fill-current" : ""}`} />
|
||||
<span>{likes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (post.type === "audio") {
|
||||
return (
|
||||
<div
|
||||
onClick={() => onOpenDetail(post)}
|
||||
className="group relative bg-[#F3ECE2] border border-[#18181B]/12 p-5 sm:p-6 rounded-xl shadow-sm hover:shadow-md transition-all duration-300 cursor-pointer"
|
||||
>
|
||||
{/* Brass clip top right */}
|
||||
<div className="binder-clip absolute top-[-6px] right-6 w-8 h-4 rounded-xs" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between text-xs font-mono text-[#52525B] mb-2">
|
||||
<span className="font-bold flex items-center gap-1.5">
|
||||
<Volume2 className="w-3.5 h-3.5 text-[#0284C7]" />
|
||||
SES KAYDI • {post.date}
|
||||
</span>
|
||||
<span className="text-[11px] font-mono">{post.audioDuration || "01:00"}</span>
|
||||
</div>
|
||||
|
||||
{/* Cassette Tape Mockup */}
|
||||
<div className="my-3 p-3.5 rounded-lg bg-[#18181B] text-[#FAF7F2] flex items-center gap-3">
|
||||
<button
|
||||
onClick={toggleAudio}
|
||||
title={isPlayingAudio ? "Durdur" : "Dinle"}
|
||||
aria-label={isPlayingAudio ? "Durdur" : "Dinle"}
|
||||
className="w-10 h-10 rounded-full bg-[#0284C7] hover:bg-[#0369A1] text-white flex items-center justify-center transition-transform active:scale-95 shrink-0"
|
||||
>
|
||||
{isPlayingAudio ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4 ml-0.5" />}
|
||||
</button>
|
||||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="text-xs font-mono font-bold truncate text-white">
|
||||
{post.audioTitle || "voice_note.m4a"}
|
||||
</div>
|
||||
{/* Animated Audio Bars */}
|
||||
<div className="flex items-end gap-1 h-5 mt-1.5">
|
||||
{[40, 75, 30, 90, 60, 85, 45, 95, 35, 70, 50, 80, 65, 40].map((h, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`w-1 rounded-full transition-all duration-150 ${
|
||||
isPlayingAudio ? "bg-[#38BDF8] animate-pulse" : "bg-white/30"
|
||||
}`}
|
||||
style={{
|
||||
height: isPlayingAudio ? `${Math.max(20, (h * (i % 2 === 0 ? 1.2 : 0.8)) % 100)}%` : `${h * 0.4}%`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs sm:text-sm font-serif text-[#18181B] mt-2 leading-relaxed">
|
||||
{post.content}
|
||||
</p>
|
||||
|
||||
{post.marginNotes && post.marginNotes.length > 0 && (
|
||||
<div className="font-handwritten text-[#0284C7] text-base mt-2">
|
||||
* {post.marginNotes[0]}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-[#18181B]/8 mt-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
{post.tags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTagClick(tag);
|
||||
}}
|
||||
className="text-[10px] font-mono text-[#787D89] hover:text-[#18181B]"
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`flex items-center gap-1 text-xs font-mono ${
|
||||
hasLiked ? "text-[#E11D48] font-bold" : "text-[#787D89] hover:text-[#18181B]"
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-3.5 h-3.5 ${hasLiked ? "fill-current" : ""}`} />
|
||||
<span>{likes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: Field Notebook Page (Longform / Deep Note)
|
||||
return (
|
||||
<div
|
||||
onClick={() => onOpenDetail(post)}
|
||||
className="group relative notebook-sheet rounded-xl p-5 sm:p-7 shadow-sm hover:shadow-lg transition-all duration-300 border border-[#18181B]/10 cursor-pointer"
|
||||
>
|
||||
{/* Top Binder Clip */}
|
||||
<div className="binder-clip absolute top-[-7px] left-8 w-10 h-4.5 rounded-xs" />
|
||||
|
||||
{/* Header with stamped date and mood badge */}
|
||||
<div className="flex items-start justify-between gap-2 pb-3 border-b border-[#18181B]/8 mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs font-bold text-[#18181B]">
|
||||
{post.date} • {post.timestamp}
|
||||
</span>
|
||||
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-[#18181B]/5 text-[#52525B]">
|
||||
{post.moodLabel}
|
||||
</span>
|
||||
</div>
|
||||
{post.title && (
|
||||
<h3 className="font-heading font-extrabold text-lg sm:text-xl text-[#18181B] mt-1.5 group-hover:text-[#E11D48] transition-colors leading-snug">
|
||||
{post.title}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{post.stampedText && (
|
||||
<span className="rubber-stamp text-[10px] text-[#059669] border-[#059669] rotate-[-2deg] shrink-0">
|
||||
{post.stampedText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Body with Highlighter markers */}
|
||||
<div className="font-serif text-[#18181B] text-sm sm:text-base leading-relaxed space-y-2">
|
||||
<p className="line-clamp-4 whitespace-pre-line">
|
||||
{post.content}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Marginalia & Handwritten Comments */}
|
||||
{post.marginNotes && post.marginNotes.length > 0 && (
|
||||
<div className="my-3.5 p-3 rounded-lg bg-[#FAF7F2] border border-[#18181B]/8 space-y-1">
|
||||
<div className="text-[10px] font-mono uppercase text-[#787D89] font-bold">Kenar Notları:</div>
|
||||
{post.marginNotes.map((note, idx) => (
|
||||
<div key={idx} className="font-handwritten text-[#E11D48] text-base leading-tight flex items-center gap-1.5">
|
||||
<span>•</span>
|
||||
<span>{note}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between pt-3 border-t border-[#18181B]/8 mt-4">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
{post.tags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onTagClick(tag);
|
||||
}}
|
||||
className="text-[11px] font-mono px-2 py-0.5 rounded bg-[#F3ECE2] text-[#52525B] hover:bg-[#EFE7D6] hover:text-[#18181B] transition-colors"
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`flex items-center gap-1 text-xs font-mono transition-colors ${
|
||||
hasLiked ? "text-[#E11D48] font-bold" : "text-[#787D89] hover:text-[#18181B]"
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-3.5 h-3.5 ${hasLiked ? "fill-current" : ""}`} />
|
||||
<span>{likes}</span>
|
||||
</button>
|
||||
|
||||
<span className="text-xs font-heading font-semibold text-[#18181B] flex items-center gap-1 group-hover:translate-x-0.5 transition-transform">
|
||||
<span>Oku</span>
|
||||
<Maximize2 className="w-3 h-3 text-[#787D89]" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { MOODS } from "./mockData";
|
||||
import { MoodType } from "./types";
|
||||
import { Zap, Moon, Sparkles, Feather, Camera, BookOpen, Sun, CloudRain, Wind, Radio } from "lucide-react";
|
||||
|
||||
interface MoodTrackerProps {
|
||||
activeMood: MoodType;
|
||||
onSelectMood: (mood: MoodType) => void;
|
||||
filteredCount: number;
|
||||
}
|
||||
|
||||
export const MoodTracker: React.FC<MoodTrackerProps> = ({
|
||||
activeMood,
|
||||
onSelectMood,
|
||||
filteredCount,
|
||||
}) => {
|
||||
const activeMoodObj = MOODS.find((m) => m.id === activeMood) || MOODS[0];
|
||||
|
||||
const getMoodIcon = (iconName: string, className = "w-4 h-4") => {
|
||||
switch (iconName) {
|
||||
case "Zap":
|
||||
return <Zap className={className} />;
|
||||
case "Moon":
|
||||
return <Moon className={className} />;
|
||||
case "Sparkles":
|
||||
return <Sparkles className={className} />;
|
||||
case "Feather":
|
||||
return <Feather className={className} />;
|
||||
case "Camera":
|
||||
return <Camera className={className} />;
|
||||
default:
|
||||
return <BookOpen className={className} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full relative mb-8">
|
||||
{/* Notebook Top Header Card */}
|
||||
<div className="notebook-sheet rounded-2xl p-5 sm:p-7 relative overflow-hidden transition-all duration-300">
|
||||
{/* Top Paper Header Elements */}
|
||||
<div className="flex flex-col md:flex-row md:items-start justify-between gap-4 pb-5 border-b border-[#18181B]/10">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-handwritten text-2xl sm:text-3xl text-[#18181B] font-bold tracking-tight">
|
||||
MOOD TRACKER & WEATHER LOG
|
||||
</span>
|
||||
<span className="rubber-stamp text-[10px] text-[#E11D48] border-[#E11D48] rotate-[-2deg] hidden sm:inline-flex">
|
||||
CANLI SİNYAL
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs sm:text-sm font-serif text-[#52525B] mt-1 max-w-xl">
|
||||
Günün farklı saatlerindeki zihinsel dalgalanmalar, anlık aydınlanmalar ve saçma gözlemler haritası.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stamped Date & Wax Seal Badge */}
|
||||
<div className="flex items-center gap-3.5 self-start md:self-auto">
|
||||
<div className="text-right font-mono">
|
||||
<div className="text-xs uppercase tracking-wider text-[#787D89] font-semibold">GÜNCEL DURUM</div>
|
||||
<div className="text-sm font-bold text-[#18181B] flex items-center justify-end gap-1.5 mt-0.5">
|
||||
<span className="w-2 h-2 rounded-full bg-[#E11D48] animate-ping" />
|
||||
<span>{activeMoodObj.stampText}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Interactive Wax Seal */}
|
||||
<button
|
||||
onClick={() => onSelectMood("all")}
|
||||
title="Tüm Arşive Dön"
|
||||
aria-label="Tüm Arşive Dön"
|
||||
className="wax-seal w-12 h-12 rounded-full flex flex-col items-center justify-center text-white cursor-pointer relative shrink-0"
|
||||
>
|
||||
<span className="font-heading font-black text-lg leading-none">M</span>
|
||||
<span className="text-[7px] font-mono tracking-tighter uppercase opacity-80 leading-none">LOG</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* The Atmospheric Mood Frequency Wave */}
|
||||
<div className="py-4 relative">
|
||||
<div className="flex items-center justify-between text-[11px] font-mono text-[#787D89] mb-1">
|
||||
<span className="flex items-center gap-1">
|
||||
<Sun className="w-3.5 h-3.5 text-[#D97706]" /> 08:00 (Sabah Kahvesi)
|
||||
</span>
|
||||
<span className="flex items-center gap-1 hidden sm:flex">
|
||||
<Wind className="w-3.5 h-3.5 text-[#0284C7]" /> 14:30 (Kaos & Fikirler)
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<CloudRain className="w-3.5 h-3.5 text-[#7C3AED]" /> 23:45 (Gece Melankolisi)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* SVG Wave Graphic */}
|
||||
<div className="w-full h-28 sm:h-36 relative my-3 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-full h-full"
|
||||
viewBox="0 0 800 120"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
{/* Reference Grid lines */}
|
||||
<line x1="0" y1="65" x2="800" y2="65" stroke="rgba(24, 24, 27, 0.08)" strokeDasharray="4 4" />
|
||||
<line x1="0" y1="25" x2="800" y2="25" stroke="rgba(24, 24, 27, 0.04)" strokeDasharray="2 4" />
|
||||
<line x1="0" y1="105" x2="800" y2="105" stroke="rgba(24, 24, 27, 0.04)" strokeDasharray="2 4" />
|
||||
|
||||
{/* Background gradient fill */}
|
||||
<path
|
||||
d="M 0 65 C 60 25, 140 25, 200 65 C 260 105, 340 105, 400 65 C 460 25, 540 25, 600 65 C 660 105, 740 105, 800 65 L 800 120 L 0 120 Z"
|
||||
fill="url(#moodGradient)"
|
||||
opacity="0.22"
|
||||
/>
|
||||
|
||||
{/* Mathematically precise smooth continuous wave curve */}
|
||||
<path
|
||||
d="M 0 65 C 60 25, 140 25, 200 65 C 260 105, 340 105, 400 65 C 460 25, 540 25, 600 65 C 660 105, 740 105, 800 65"
|
||||
stroke="#18181B"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
fill="none"
|
||||
/>
|
||||
|
||||
{/* Perfectly round circles positioned exactly at wave peaks & valleys */}
|
||||
<circle cx="100" cy="45" r="7" fill="#D97706" stroke="#FAF7F2" strokeWidth="2.5" />
|
||||
<circle cx="300" cy="85" r="7" fill="#E11D48" stroke="#FAF7F2" strokeWidth="2.5" />
|
||||
<circle cx="500" cy="45" r="7" fill="#059669" stroke="#FAF7F2" strokeWidth="2.5" />
|
||||
<circle cx="700" cy="85" r="8" fill="#7C3AED" stroke="#FAF7F2" strokeWidth="2.5" className="animate-pulse" />
|
||||
|
||||
<defs>
|
||||
<linearGradient id="moodGradient" x1="0" y1="0" x2="800" y2="0" gradientUnits="userSpaceOnUse">
|
||||
<stop stopColor="#D97706" />
|
||||
<stop offset="0.33" stopColor="#E11D48" />
|
||||
<stop offset="0.66" stopColor="#059669" />
|
||||
<stop offset="1" stopColor="#7C3AED" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
{/* Handwritten Mood Annotations on Wave */}
|
||||
<div className="absolute top-1 left-[8%] text-xs font-handwritten text-[#D97706] rotate-[-4deg] hidden sm:block">
|
||||
"kahve etkisi ⚡"
|
||||
</div>
|
||||
<div className="absolute bottom-1 left-[32%] text-xs font-handwritten text-[#E11D48] rotate-[3deg] hidden sm:block">
|
||||
"ani fikir! 💡"
|
||||
</div>
|
||||
<div className="absolute top-1 left-[58%] text-xs font-handwritten text-[#059669] rotate-[-2deg] hidden sm:block">
|
||||
"sakin anlar 🌿"
|
||||
</div>
|
||||
<div className="absolute bottom-1 right-[8%] text-xs font-handwritten text-[#7C3AED] rotate-[-3deg] hidden sm:block">
|
||||
"gece kodu ☕"
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live Broadcast Status Banner */}
|
||||
<div className="mt-1 mb-5 px-3.5 py-2.5 rounded-xl bg-[#F4EFE6] border border-[#18181B]/10 flex items-center justify-between gap-3 text-xs">
|
||||
<div className="flex items-center gap-2 text-[#18181B]">
|
||||
<Radio className="w-3.5 h-3.5 text-[#E11D48] animate-pulse shrink-0" />
|
||||
<span className="font-serif">
|
||||
<strong>Şu anki Zihinsel Frekans:</strong> {activeMoodObj.label} • <em>{activeMoodObj.sublabel}</em>
|
||||
</span>
|
||||
</div>
|
||||
<span className="font-mono text-[11px] text-[#787D89] shrink-0">
|
||||
{filteredCount} sonuç gösteriliyor
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Mood Filter Pill Tabs */}
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-1 scrollbar-none">
|
||||
{MOODS.map((mood) => {
|
||||
const isSelected = activeMood === mood.id;
|
||||
return (
|
||||
<button
|
||||
key={mood.id}
|
||||
onClick={() => onSelectMood(mood.id)}
|
||||
className={`flex items-center gap-2 px-3.5 py-2 rounded-xl text-xs font-heading font-semibold transition-all duration-200 shrink-0 cursor-pointer ${
|
||||
isSelected
|
||||
? "bg-[#18181B] text-[#FAF7F2] shadow-sm translate-y-[-1px]"
|
||||
: "bg-[#FAF7F2] text-[#52525B] hover:bg-[#EFE7D6] hover:text-[#18181B] border border-[#18181B]/10"
|
||||
}`}
|
||||
>
|
||||
<span style={{ color: isSelected ? "#FAF7F2" : mood.color }}>
|
||||
{getMoodIcon(mood.iconName, "w-3.5 h-3.5")}
|
||||
</span>
|
||||
<span>{mood.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,283 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { JournalPost, MoodType, PostType } from "./types";
|
||||
import { MOODS } from "./mockData";
|
||||
import { X, Feather, Sparkles, Tag, StickyNote, BookOpen, Code, Camera, Volume2, Check } from "lucide-react";
|
||||
|
||||
interface NewPostModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onAddPost: (post: JournalPost) => void;
|
||||
}
|
||||
|
||||
export const NewPostModal: React.FC<NewPostModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onAddPost,
|
||||
}) => {
|
||||
const [type, setType] = useState<PostType>("sticky");
|
||||
const [mood, setMood] = useState<MoodType>("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("KARALAMA");
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!content.trim()) return;
|
||||
|
||||
const now = new Date();
|
||||
const dateStr = now.toLocaleDateString("tr-TR", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
}).toUpperCase();
|
||||
|
||||
const timeStr = now.toLocaleTimeString("tr-TR", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
|
||||
const moodObj = MOODS.find((m) => m.id === mood) || MOODS[0];
|
||||
const tags = tagInput
|
||||
.split(",")
|
||||
.map((t) => t.trim().replace(/^#/, ""))
|
||||
.filter((t) => t.length > 0);
|
||||
|
||||
if (tags.length === 0) tags.push("yeni-not");
|
||||
|
||||
const newPost: JournalPost = {
|
||||
id: `post-${Date.now()}`,
|
||||
type,
|
||||
date: dateStr,
|
||||
timestamp: timeStr,
|
||||
mood,
|
||||
moodLabel: moodObj.label,
|
||||
title: title.trim() ? title : undefined,
|
||||
content,
|
||||
marginNotes: marginNote.trim() ? [marginNote.trim()] : undefined,
|
||||
tags,
|
||||
likes: 1,
|
||||
stampedText: stampedText || moodObj.stampText,
|
||||
codeSnippet: type === "code" && codeSnippet.trim() ? codeSnippet : undefined,
|
||||
codeLanguage: type === "code" ? "typescript" : undefined,
|
||||
};
|
||||
|
||||
onAddPost(newPost);
|
||||
// Reset form
|
||||
setTitle("");
|
||||
setContent("");
|
||||
setMarginNote("");
|
||||
setTagInput("");
|
||||
setCodeSnippet("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs animate-in fade-in duration-200">
|
||||
<div
|
||||
className="relative w-full max-w-xl notebook-sheet rounded-2xl p-6 sm:p-8 shadow-2xl border border-[#18181B]/15 max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Washi Tape across top */}
|
||||
<div className="washi-tape washi-tape-rose top-[-10px] left-1/2 -translate-x-1/2 w-40" />
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between pb-4 border-b border-[#18181B]/10 mb-5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-full bg-[#E11D48] text-white flex items-center justify-center">
|
||||
<Feather className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-heading font-extrabold text-lg text-[#18181B]">
|
||||
Yeni Düşünce Karala
|
||||
</h2>
|
||||
<p className="text-xs font-handwritten text-[#787D89] text-base leading-none">
|
||||
Aklına gelen o saçma veya harika şeyi hemen kaydet
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
title="Kapat"
|
||||
aria-label="Kapat"
|
||||
className="p-1.5 rounded-lg text-[#787D89] hover:text-[#18181B] hover:bg-[#18181B]/5 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{/* Format Selector */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1.5">
|
||||
Not Formatı:
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType("sticky")}
|
||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-heading font-semibold border transition-all cursor-pointer ${
|
||||
type === "sticky"
|
||||
? "bg-[#FEF3C7] border-[#D97706] text-[#B45309] shadow-xs"
|
||||
: "bg-[#FAF7F2] border-[#18181B]/10 text-[#52525B]"
|
||||
}`}
|
||||
>
|
||||
<StickyNote className="w-3.5 h-3.5" />
|
||||
<span>Post-it / Fikir</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType("notebook")}
|
||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-heading font-semibold border transition-all cursor-pointer ${
|
||||
type === "notebook"
|
||||
? "bg-[#FAF7F2] border-[#18181B] text-[#18181B] shadow-xs font-bold"
|
||||
: "bg-[#FAF7F2] border-[#18181B]/10 text-[#52525B]"
|
||||
}`}
|
||||
>
|
||||
<BookOpen className="w-3.5 h-3.5" />
|
||||
<span>Derin Yazı</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setType("code")}
|
||||
className={`flex items-center justify-center gap-1.5 p-2 rounded-lg text-xs font-heading font-semibold border transition-all cursor-pointer ${
|
||||
type === "code"
|
||||
? "bg-[#1E232D] border-[#1E232D] text-white shadow-xs"
|
||||
: "bg-[#FAF7F2] border-[#18181B]/10 text-[#52525B]"
|
||||
}`}
|
||||
>
|
||||
<Code className="w-3.5 h-3.5" />
|
||||
<span>Kod Kırıntısı</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mood Selector */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1.5">
|
||||
Ruh Hali (Mood):
|
||||
</label>
|
||||
<div className="flex items-center gap-2 overflow-x-auto pb-1 scrollbar-none">
|
||||
{MOODS.filter((m) => m.id !== "all").map((m) => (
|
||||
<button
|
||||
key={m.id}
|
||||
type="button"
|
||||
onClick={() => setMood(m.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-heading font-semibold shrink-0 border transition-all cursor-pointer ${
|
||||
mood === m.id
|
||||
? "bg-[#18181B] text-[#FAF7F2] border-[#18181B]"
|
||||
: "bg-[#FAF7F2] text-[#52525B] border-[#18181B]/10 hover:bg-[#EFE7D6]"
|
||||
}`}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title (for notebook or code) */}
|
||||
{type !== "sticky" && (
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Başlık (Opsiyonel):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Yazı veya karalama başlığı..."
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Düşünce / İçerik:
|
||||
</label>
|
||||
<textarea
|
||||
required
|
||||
rows={4}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="Aklındakileri dök... (Filtresiz ve dürüst)"
|
||||
className="w-full px-3.5 py-2.5 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-serif leading-relaxed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Code Snippet (if code type) */}
|
||||
{type === "code" && (
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Kod Parçası:
|
||||
</label>
|
||||
<textarea
|
||||
rows={3}
|
||||
value={codeSnippet}
|
||||
onChange={(e) => setCodeSnippet(e.target.value)}
|
||||
placeholder="// const bug = fixLater();"
|
||||
className="w-full px-3.5 py-2 text-xs font-mono bg-[#1E232D] text-emerald-400 border border-white/10 rounded-lg focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Margin Notes Input */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
El Yazısı Kenar Notu:
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={marginNote}
|
||||
onChange={(e) => setMarginNote(e.target.value)}
|
||||
placeholder="← tam olarak bu! / not al"
|
||||
className="w-full px-3 py-1.5 text-sm bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-handwritten text-lg text-[#E11D48]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<label className="block text-[11px] font-mono uppercase text-[#787D89] font-bold mb-1">
|
||||
Etiketler (Virgülle ayırın):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
placeholder="felsefe, gece, kahve, kod"
|
||||
className="w-full px-3 py-1.5 text-xs bg-white/80 border border-[#18181B]/15 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#E11D48]/30 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-end gap-2.5 pt-3 border-t border-[#18181B]/10">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg text-xs font-heading font-semibold text-[#52525B] hover:bg-[#18181B]/5 transition-colors cursor-pointer"
|
||||
>
|
||||
Vazgeç
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex items-center gap-1.5 px-5 py-2 rounded-lg bg-[#E11D48] text-white text-xs font-heading font-bold hover:bg-[#9F1239] shadow-sm transition-all cursor-pointer"
|
||||
>
|
||||
<Feather className="w-3.5 h-3.5" />
|
||||
<span>Deftere Ekle</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,179 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { JournalPost } from "./types";
|
||||
import { X, Heart, Share2, CornerDownRight, Check, BookOpen, Volume2 } from "lucide-react";
|
||||
|
||||
interface PostDetailModalProps {
|
||||
post: JournalPost | null;
|
||||
onClose: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
}
|
||||
|
||||
export const PostDetailModal: React.FC<PostDetailModalProps> = ({
|
||||
post,
|
||||
onClose,
|
||||
onTagClick,
|
||||
}) => {
|
||||
const [likes, setLikes] = useState(post?.likes || 0);
|
||||
const [hasLiked, setHasLiked] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
if (!post) return null;
|
||||
|
||||
const handleLike = () => {
|
||||
if (!hasLiked) {
|
||||
setLikes((prev) => prev + 1);
|
||||
setHasLiked(true);
|
||||
} else {
|
||||
setLikes((prev) => prev - 1);
|
||||
setHasLiked(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleShare = () => {
|
||||
navigator.clipboard.writeText(window.location.href);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs animate-in fade-in duration-200">
|
||||
<div
|
||||
className="relative w-full max-w-2xl notebook-sheet rounded-2xl p-6 sm:p-9 shadow-2xl border border-[#18181B]/15 max-h-[90vh] overflow-y-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Top binder clip */}
|
||||
<div className="binder-clip absolute top-[-8px] left-10 w-12 h-5 rounded-xs" />
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={onClose}
|
||||
title="Kapat"
|
||||
aria-label="Kapat"
|
||||
className="absolute top-5 right-5 p-1.5 rounded-lg text-[#787D89] hover:text-[#18181B] hover:bg-[#18181B]/5 transition-colors cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
{/* Metadata Header */}
|
||||
<div className="flex items-center gap-2 text-xs font-mono text-[#787D89] mb-3">
|
||||
<span className="font-bold text-[#18181B]">{post.date} • {post.timestamp}</span>
|
||||
<span>•</span>
|
||||
<span className="px-2 py-0.5 rounded-full bg-[#18181B]/5 text-[#52525B]">
|
||||
{post.moodLabel}
|
||||
</span>
|
||||
{post.stampedText && (
|
||||
<span className="rubber-stamp text-[10px] text-[#E11D48] border-[#E11D48] ml-auto">
|
||||
{post.stampedText}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Post Title */}
|
||||
{post.title && (
|
||||
<h1 className="font-heading font-black text-2xl sm:text-3xl text-[#18181B] leading-tight mb-4">
|
||||
{post.title}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
{/* Polaroid Image if any */}
|
||||
{post.imageUrl && (
|
||||
<div className="my-4 p-3 bg-white border border-[#18181B]/10 rounded shadow-sm">
|
||||
<img
|
||||
src={post.imageUrl}
|
||||
alt={post.imageCaption || "Polaroid"}
|
||||
className="w-full max-h-80 object-cover rounded-xs"
|
||||
/>
|
||||
{post.imageCaption && (
|
||||
<div className="font-handwritten text-[#18181B] text-xl font-bold mt-2 px-1">
|
||||
{post.imageCaption}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Code Snippet if any */}
|
||||
{post.codeSnippet && (
|
||||
<div className="my-4">
|
||||
<div className="px-3 py-1.5 bg-[#1E232D] text-white/70 text-xs font-mono rounded-t-lg border border-b-0 border-white/10 flex items-center justify-between">
|
||||
<span>{post.title || "snippet.rs"}</span>
|
||||
<span className="uppercase text-[10px]">{post.codeLanguage || "code"}</span>
|
||||
</div>
|
||||
<pre className="p-4 rounded-b-lg bg-[#141820] text-emerald-400 font-mono text-xs overflow-x-auto leading-relaxed border border-white/10">
|
||||
<code>{post.codeSnippet}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body Content */}
|
||||
<div className="font-serif text-[#18181B] text-base sm:text-lg leading-relaxed whitespace-pre-line my-4 space-y-4">
|
||||
{post.content}
|
||||
</div>
|
||||
|
||||
{/* Margin Notes */}
|
||||
{post.marginNotes && post.marginNotes.length > 0 && (
|
||||
<div className="my-5 p-4 rounded-xl bg-[#FAF7F2] border border-[#18181B]/10 space-y-1.5">
|
||||
<div className="text-[11px] font-mono uppercase text-[#787D89] font-bold">
|
||||
Yazarın El Yazısı Notları:
|
||||
</div>
|
||||
{post.marginNotes.map((note, idx) => (
|
||||
<div key={idx} className="font-handwritten text-[#E11D48] text-xl leading-tight flex items-center gap-2">
|
||||
<CornerDownRight className="w-4 h-4 shrink-0" />
|
||||
<span>{note}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tags */}
|
||||
<div className="flex items-center gap-2 flex-wrap pt-4 border-t border-[#18181B]/10 mt-6">
|
||||
{post.tags.map((tag) => (
|
||||
<button
|
||||
key={tag}
|
||||
onClick={() => {
|
||||
onTagClick(tag);
|
||||
onClose();
|
||||
}}
|
||||
className="text-xs font-mono px-2.5 py-1 rounded bg-[#F3ECE2] text-[#52525B] hover:bg-[#EFE7D6] hover:text-[#18181B] transition-colors"
|
||||
>
|
||||
#{tag}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between pt-4 mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleLike}
|
||||
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs font-mono transition-all ${
|
||||
hasLiked
|
||||
? "bg-[#FFE4E6] border-[#E11D48] text-[#E11D48] font-bold"
|
||||
: "bg-white border-[#18181B]/15 text-[#52525B] hover:text-[#18181B]"
|
||||
}`}
|
||||
>
|
||||
<Heart className={`w-4 h-4 ${hasLiked ? "fill-current" : ""}`} />
|
||||
<span>{likes} Beğeni</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-white border border-[#18181B]/15 text-xs font-mono text-[#52525B] hover:text-[#18181B] transition-all"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-[#059669]" /> : <Share2 className="w-4 h-4" />}
|
||||
<span>{copied ? "Kopyalandı!" : "Paylaş"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-1.5 rounded-lg bg-[#18181B] text-[#FAF7F2] text-xs font-heading font-semibold hover:bg-black transition-colors"
|
||||
>
|
||||
Kapat
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import { JournalPost, MoodConfig } from "./types";
|
||||
|
||||
export const MOODS: MoodConfig[] = [
|
||||
{
|
||||
id: "all",
|
||||
label: "Tüm Notlar",
|
||||
sublabel: "Bütün karalamalar",
|
||||
iconName: "BookOpen",
|
||||
color: "#18181B",
|
||||
bgColor: "#EFE7D6",
|
||||
stampText: "ARŞİV",
|
||||
},
|
||||
{
|
||||
id: "focus",
|
||||
label: "Aşırı Odak & Kaos",
|
||||
sublabel: "Hyper Focus & Coffee",
|
||||
iconName: "Zap",
|
||||
color: "#D97706",
|
||||
bgColor: "#FEF3C7",
|
||||
stampText: "⚡ HYPER",
|
||||
},
|
||||
{
|
||||
id: "night",
|
||||
label: "Gece Melankolisi",
|
||||
sublabel: "Saat 03:00 kafası",
|
||||
iconName: "Moon",
|
||||
color: "#7C3AED",
|
||||
bgColor: "#EDE9FE",
|
||||
stampText: "☕ GECE",
|
||||
},
|
||||
{
|
||||
id: "spark",
|
||||
label: "Fikir Patlaması",
|
||||
sublabel: "Rastgele aydınlanmalar",
|
||||
iconName: "Sparkles",
|
||||
color: "#E11D48",
|
||||
bgColor: "#FFE4E6",
|
||||
stampText: "💡 SPARK",
|
||||
},
|
||||
{
|
||||
id: "calm",
|
||||
label: "Sakin & Karalama",
|
||||
sublabel: "Dingin anlar & çay",
|
||||
iconName: "Feather",
|
||||
color: "#059669",
|
||||
bgColor: "#D1FAE5",
|
||||
stampText: "🌿 SAKİN",
|
||||
},
|
||||
{
|
||||
id: "visual",
|
||||
label: "Polaroid & Kırıntılar",
|
||||
sublabel: "Görsel hafıza & ses",
|
||||
iconName: "Camera",
|
||||
color: "#0284C7",
|
||||
bgColor: "#E0F2FE",
|
||||
stampText: "📷 SCRAP",
|
||||
},
|
||||
];
|
||||
|
||||
export const INITIAL_POSTS: JournalPost[] = [
|
||||
{
|
||||
id: "post-today-1",
|
||||
type: "notebook",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "16:45",
|
||||
mood: "spark",
|
||||
moodLabel: "Fikir Patlaması",
|
||||
title: "Prisma & PostgreSQL ile Mimari Senkronizasyon",
|
||||
content: `Bugün defterin tüm veri katmanını uzaktaki PostgreSQL sunucusuna ve Prisma ORM mimarisine bağladık. Metin kırıntılarını kaybetmeden canlı veritabanına taşımak harika hissettiriyor.
|
||||
|
||||
Bir sistem karmaşıklaştıkça tasarım dilinin doğallığını koruyabilmesi en kritik başarı kriterimizdir.`,
|
||||
marginNotes: [
|
||||
"← veritabanı canlıda! 🚀",
|
||||
"65.109... sunucusu aktif"
|
||||
],
|
||||
tags: ["postgresql", "prisma", "mimari", "backend"],
|
||||
likes: 18,
|
||||
stampedText: "SENKRONİZE",
|
||||
},
|
||||
{
|
||||
id: "post-today-2",
|
||||
type: "sticky",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "16:55",
|
||||
mood: "focus",
|
||||
moodLabel: "Aşırı Odak & Kaos",
|
||||
content: "NextAuth paketini projeye dahil etmeden, HMAC SHA-256 imzalı HTTP-only çerezler ve .env değişkenleri ile kendi hafif admin yetkilendirmemizi yazdık. Bazen en iyi bağımlılık, hiç eklenmemiş olan bağımlılıktır.",
|
||||
tags: ["güvenlik", "admin", "cookies", "sadelik"],
|
||||
likes: 29,
|
||||
stampedText: "DOĞRU TESPİT",
|
||||
marginNotes: ["(0 bağımlılık, %100 hafiflik)"],
|
||||
},
|
||||
{
|
||||
id: "post-today-3",
|
||||
type: "code",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "17:08",
|
||||
mood: "night",
|
||||
moodLabel: "Gece Melankolisi",
|
||||
title: "SVG Bezier Matematiksel Düzeltmesi",
|
||||
content: "SVG grafiklerinde preserveAspectRatio parametresini unuttuğunuzda daireleriniz elipse dönüşebilir. Kubik Bezier simetrik dalga denklemi ve 1:1 dairesel oranlar ile frekans haritası mükemmel geometriye kavuştu.",
|
||||
codeSnippet: `// Kubik Bezier simetrik dalga & dairesel koordinatlar
|
||||
const wavePath = "M 0 65 C 60 25, 140 25, 200 65 C 260 105, 340 105, 400 65";
|
||||
// Point 1 (Kahve etkisi): cx=100, cy=45, r=7 (tam yuvarlak)`,
|
||||
codeLanguage: "typescript",
|
||||
tags: ["svg", "math", "frontend", "ui-ux"],
|
||||
likes: 31,
|
||||
marginNotes: ["r=7 tam yuvarlak", "yamuk dairelere son!"],
|
||||
stampedText: "FIXED",
|
||||
},
|
||||
{
|
||||
id: "post-today-4",
|
||||
type: "polaroid",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "17:15",
|
||||
mood: "visual",
|
||||
moodLabel: "Polaroid & Kırıntılar",
|
||||
content: "Kod derlendi, PostgreSQL veritabanı eşitlendi, harita eğrileri yerini buldu. Akşamüstü Muğla rüzgarında demli bir fincan çay molası.",
|
||||
imageCaption: "Muğla Esintisi & Veritabanı Zaferi ☕🏔️",
|
||||
imageUrl: "https://images.unsplash.com/photo-1501785888041-af3ef285b470?auto=format&fit=crop&w=1000&q=80",
|
||||
tags: ["muğla", "doğa", "kahve", "mola"],
|
||||
likes: 45,
|
||||
marginNotes: ["rakım: Muğla", "derleme süresi: 693ms"],
|
||||
},
|
||||
{
|
||||
id: "post-today-5",
|
||||
type: "audio",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "17:20",
|
||||
mood: "calm",
|
||||
moodLabel: "Sakin & Karalama",
|
||||
content: "Admin panelini tasarlarken kurumsal soğuk paneller yerine günlüğün mum mühürlü, kağıt dokulu estetigini bozmamak harika bir karardı. İçerik yönetim alanı bile defterin bir parçası gibi hissettiriyor.",
|
||||
audioTitle: "Defter_Ruhu_Ve_Yonetim_Notu.m4a",
|
||||
audioDuration: "02:15",
|
||||
tags: ["ses-kaydı", "tasarım", "ux", "düşünceler"],
|
||||
likes: 22,
|
||||
marginNotes: ["* arka planda rüzgar sesi mevcut"],
|
||||
},
|
||||
{
|
||||
id: "post-1",
|
||||
type: "notebook",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "15:42",
|
||||
mood: "spark",
|
||||
moodLabel: "Fikir Patlaması",
|
||||
title: "Neden Her Şeyi Karmaşıklaştırmaya Bayılıyoruz?",
|
||||
content: `Bazen oturup kod yazarken veya hayatı planlarken kendimi 15 katmanlı bir soyutlama tasarlarken buluyorum. Oysa tek ihtiyacım olan bir metin dosyası ve bir fincan iyi demlenmiş çay.
|
||||
|
||||
Basitlik tembellik değil, aksine zihinsel bir lükstür. Bir sistemi basit tutabilmek için önce onun tüm karmaşıklığını çiğneyip sindirmiş olmak gerekir. Tıpkı bu defter gibi: ne kadar az kurumsal filtre, o kadar çok hakikat.`,
|
||||
marginNotes: [
|
||||
"← tam olarak bu!",
|
||||
"not: yarın bu konuyu tekrar oku",
|
||||
"çayı tazelemeyi unutma ☕"
|
||||
],
|
||||
highlightWords: ["Basitlik tembellik değil", "zihinsel bir lükstür"],
|
||||
tags: ["felsefe", "kodlama", "sadelik", "düşünceler"],
|
||||
likes: 12,
|
||||
stampedText: "ONAYLANDI",
|
||||
},
|
||||
{
|
||||
id: "post-2",
|
||||
type: "polaroid",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "14:15",
|
||||
mood: "visual",
|
||||
moodLabel: "Polaroid & Kırıntılar",
|
||||
content: "Zirveye doğru tırmanırken kahve termosunu açtığım o rüzgarlı an. Şehirdeki tüm o Slack bildirimleri ve sonsuz toplantılar burada sadece birer fısıltı.",
|
||||
imageCaption: "The ridge line view! 🏔️ Zirve esintisi ve dökülen kahve.",
|
||||
imageUrl: "https://images.unsplash.com/photo-1464822759023-fed622ff2c3b?auto=format&fit=crop&w=1000&q=80",
|
||||
tags: ["doğa", "kaçış", "fotoğraf", "dağlar"],
|
||||
likes: 24,
|
||||
marginNotes: ["rakım: ~2100m", "kahve soğumadan önce"],
|
||||
},
|
||||
{
|
||||
id: "post-3",
|
||||
type: "sticky",
|
||||
date: "16 AĞU 2026",
|
||||
timestamp: "11:20",
|
||||
mood: "focus",
|
||||
moodLabel: "Aşırı Odak & Kaos",
|
||||
content: "İnsan beyni bazen 100 sayfalık teknik dokümanı 20 saniyede tarar, ama evden çıkarken kapıyı kilitleyip kilitlemediğini 5 dakika boyunca sokak ortasında sorgular.",
|
||||
tags: ["beyin", "saçmalıklar", "gözlem"],
|
||||
likes: 42,
|
||||
stampedText: "DOĞRU TESPİT",
|
||||
marginNotes: ["(anahtarı yine cebimde buldum)"],
|
||||
},
|
||||
{
|
||||
id: "post-4",
|
||||
type: "code",
|
||||
date: "15 AĞU 2026",
|
||||
timestamp: "23:45",
|
||||
mood: "night",
|
||||
moodLabel: "Gece Melankolisi",
|
||||
title: "Rust & Borrow Checker ile Gece Terapisi",
|
||||
content: "Rust'ta borrow checker ile kavga etmek bir nevi Zen meditasyonu gibi. Sizi acelecilikten arındırıyor ve acımasız bir hakikate zorluyor.",
|
||||
codeSnippet: `// Gece yarısı varoluşsal döngü
|
||||
fn main() -> Result<(), MentalState> {
|
||||
let mut coffee = Coffee::brew("Dark Roast")?;
|
||||
let mut energy = 100;
|
||||
|
||||
while energy > 0 {
|
||||
code_with_passion(&mut coffee);
|
||||
energy -= 10;
|
||||
}
|
||||
|
||||
Ok(Sleep::now())
|
||||
}`,
|
||||
codeLanguage: "rust",
|
||||
tags: ["rust", "yazılım", "gece", "kod"],
|
||||
likes: 19,
|
||||
marginNotes: ["derleyici haklıydı...", "03:12'de çalıştı!"],
|
||||
stampedText: "COMPILED",
|
||||
},
|
||||
{
|
||||
id: "post-5",
|
||||
type: "audio",
|
||||
date: "15 AĞU 2026",
|
||||
timestamp: "21:10",
|
||||
mood: "calm",
|
||||
moodLabel: "Sakin & Karalama",
|
||||
content: "Kadıköy sahilinde yürürken rüzgarın ve vapur düdüğünün arkasında aklıma gelen podcast fikri. Neden yapay zeka çağında el yazısı defterlere bu kadar aşığız?",
|
||||
audioTitle: "Gece Vapuru & Defter Romantizmi.m4a",
|
||||
audioDuration: "01:42",
|
||||
tags: ["ses-kaydı", "kadıköy", "nostalji", "vapur"],
|
||||
likes: 15,
|
||||
marginNotes: ["arka plandaki martı sesi gerçek"],
|
||||
},
|
||||
{
|
||||
id: "post-6",
|
||||
type: "sticky",
|
||||
date: "14 AĞU 2026",
|
||||
timestamp: "18:05",
|
||||
mood: "spark",
|
||||
moodLabel: "Fikir Patlaması",
|
||||
content: "Bugün sokaktaki bir tekir kedi bana öyle bir baktı ki, sanki dün gece geç saatte yazdığım CSS dosyasında 14 tane '!important' kullandığımı biliyordu.",
|
||||
tags: ["kediler", "css", "vicdan-azabı"],
|
||||
likes: 38,
|
||||
stampedText: "SUÇLU",
|
||||
marginNotes: ["(temizleyeceğim söz)"],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,38 @@
|
||||
export type MoodType = "all" | "focus" | "night" | "spark" | "calm" | "visual";
|
||||
|
||||
export interface MoodConfig {
|
||||
id: MoodType;
|
||||
label: string;
|
||||
sublabel: string;
|
||||
iconName: string;
|
||||
color: string;
|
||||
bgColor: string;
|
||||
stampText: string;
|
||||
}
|
||||
|
||||
export type PostType = "notebook" | "sticky" | "polaroid" | "code" | "audio";
|
||||
|
||||
export interface JournalPost {
|
||||
id: string;
|
||||
type: PostType;
|
||||
date: string;
|
||||
timestamp: string;
|
||||
mood: MoodType;
|
||||
moodLabel: string;
|
||||
title?: string;
|
||||
content: string;
|
||||
marginNotes?: string[];
|
||||
tags: string[];
|
||||
likes: number;
|
||||
highlightWords?: string[];
|
||||
authorNote?: string;
|
||||
|
||||
// Specific properties
|
||||
imageUrl?: string;
|
||||
imageCaption?: string;
|
||||
codeSnippet?: string;
|
||||
codeLanguage?: string;
|
||||
audioDuration?: string;
|
||||
audioTitle?: string;
|
||||
stampedText?: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 440 KiB |
|
After Width: | Height: | Size: 265 KiB |
|
After Width: | Height: | Size: 733 KiB |
@@ -0,0 +1,63 @@
|
||||
import { cookies } from "next/headers";
|
||||
import crypto from "crypto";
|
||||
|
||||
const COOKIE_NAME = "admin_session";
|
||||
const ADMIN_SECRET = process.env.ADMIN_SECRET || "fallback-admin-secret-2026";
|
||||
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin";
|
||||
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "password123";
|
||||
|
||||
/**
|
||||
* Generate a signed session token for the admin
|
||||
*/
|
||||
export function createSessionToken(username: string): string {
|
||||
const timestamp = Date.now();
|
||||
const data = `${username}.${timestamp}`;
|
||||
const signature = crypto
|
||||
.createHmac("sha256", ADMIN_SECRET)
|
||||
.update(data)
|
||||
.digest("hex");
|
||||
return `${data}.${signature}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if a session token is valid and signed properly
|
||||
*/
|
||||
export function verifySessionToken(token?: string | null): boolean {
|
||||
if (!token) return false;
|
||||
const parts = token.split(".");
|
||||
if (parts.length !== 3) return false;
|
||||
|
||||
const [username, timestamp, signature] = parts;
|
||||
if (username !== ADMIN_USERNAME) return false;
|
||||
|
||||
// Optional: Check if token is older than 7 days
|
||||
const tokenTime = parseInt(timestamp, 10);
|
||||
if (isNaN(tokenTime) || Date.now() - tokenTime > 7 * 24 * 60 * 60 * 1000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const expectedSignature = crypto
|
||||
.createHmac("sha256", ADMIN_SECRET)
|
||||
.update(`${username}.${timestamp}`)
|
||||
.digest("hex");
|
||||
|
||||
return signature === expectedSignature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to validate admin credentials
|
||||
*/
|
||||
export function validateCredentials(username?: string, password?: string): boolean {
|
||||
return username === ADMIN_USERNAME && password === ADMIN_PASSWORD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for Server Routes to check if caller is authenticated
|
||||
*/
|
||||
export async function isAdminAuthenticated(): Promise<boolean> {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(COOKIE_NAME)?.value;
|
||||
return verifySessionToken(token);
|
||||
}
|
||||
|
||||
export { COOKIE_NAME, ADMIN_USERNAME };
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "mstfyldz",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.19.3",
|
||||
"lucide-react": "^1.31.0",
|
||||
"next": "16.3.1",
|
||||
"prisma": "^6.19.3",
|
||||
"react": "19.2.8",
|
||||
"react-dom": "19.2.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.3.1",
|
||||
"tailwindcss": "^4",
|
||||
"tsx": "^4.23.12",
|
||||
"typescript": "^5"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "npx tsx prisma/seed.ts"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,55 @@
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
enum PostType {
|
||||
notebook
|
||||
sticky
|
||||
polaroid
|
||||
code
|
||||
audio
|
||||
|
||||
@@map("post_type")
|
||||
}
|
||||
|
||||
enum MoodType {
|
||||
focus
|
||||
night
|
||||
spark
|
||||
calm
|
||||
visual
|
||||
|
||||
@@map("mood_type")
|
||||
}
|
||||
|
||||
model Post {
|
||||
id String @id @default(cuid())
|
||||
type PostType
|
||||
date String
|
||||
timestamp String
|
||||
mood MoodType
|
||||
moodLabel String @map("mood_label")
|
||||
title String?
|
||||
content String
|
||||
marginNotes String[] @default([]) @map("margin_notes")
|
||||
tags String[] @default([])
|
||||
likes Int @default(0)
|
||||
highlightWords String[] @default([]) @map("highlight_words")
|
||||
authorNote String? @map("author_note")
|
||||
imageUrl String? @map("image_url")
|
||||
imageCaption String? @map("image_caption")
|
||||
codeSnippet String? @map("code_snippet")
|
||||
codeLanguage String? @map("code_language")
|
||||
audioDuration String? @map("audio_duration")
|
||||
audioTitle String? @map("audio_title")
|
||||
stampedText String? @map("stamped_text")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("posts")
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
-- mstfyldz Journal PostgreSQL Schema for psql
|
||||
|
||||
-- Create Custom Enum Types (if not existing)
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'post_type') THEN
|
||||
CREATE TYPE post_type AS ENUM ('notebook', 'sticky', 'polaroid', 'code', 'audio');
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'mood_type') THEN
|
||||
CREATE TYPE mood_type AS ENUM ('focus', 'night', 'spark', 'calm', 'visual');
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Create Posts Table
|
||||
CREATE TABLE IF NOT EXISTS posts (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
type post_type NOT NULL,
|
||||
date VARCHAR(50) NOT NULL,
|
||||
timestamp VARCHAR(50) NOT NULL,
|
||||
mood mood_type NOT NULL,
|
||||
mood_label VARCHAR(100) NOT NULL,
|
||||
title TEXT,
|
||||
content TEXT NOT NULL,
|
||||
margin_notes TEXT[] DEFAULT '{}',
|
||||
tags TEXT[] DEFAULT '{}',
|
||||
likes INT DEFAULT 0 NOT NULL,
|
||||
highlight_words TEXT[] DEFAULT '{}',
|
||||
author_note TEXT,
|
||||
image_url TEXT,
|
||||
image_caption TEXT,
|
||||
code_snippet TEXT,
|
||||
code_language VARCHAR(50),
|
||||
audio_duration VARCHAR(50),
|
||||
audio_title TEXT,
|
||||
stamped_text VARCHAR(100),
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
-- Performance Indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_mood ON posts(mood);
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_type ON posts(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC);
|
||||
@@ -0,0 +1,52 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { INITIAL_POSTS } from "../components/mockData";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding database with initial posts...");
|
||||
|
||||
for (const post of INITIAL_POSTS) {
|
||||
await prisma.post.upsert({
|
||||
where: { id: post.id },
|
||||
update: {
|
||||
title: post.title,
|
||||
content: post.content,
|
||||
likes: post.likes,
|
||||
},
|
||||
create: {
|
||||
id: post.id,
|
||||
type: post.type,
|
||||
date: post.date,
|
||||
timestamp: post.timestamp,
|
||||
mood: post.mood as any,
|
||||
moodLabel: post.moodLabel,
|
||||
title: post.title,
|
||||
content: post.content,
|
||||
marginNotes: post.marginNotes || [],
|
||||
tags: post.tags || [],
|
||||
likes: post.likes || 0,
|
||||
highlightWords: post.highlightWords || [],
|
||||
authorNote: post.authorNote,
|
||||
imageUrl: post.imageUrl,
|
||||
imageCaption: post.imageCaption,
|
||||
codeSnippet: post.codeSnippet,
|
||||
codeLanguage: post.codeLanguage,
|
||||
audioDuration: post.audioDuration,
|
||||
audioTitle: post.audioTitle,
|
||||
stampedText: post.stampedText,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Seeding completed successfully!");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error("Error during seeding:", e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||