260 lines
10 KiB
TypeScript
260 lines
10 KiB
TypeScript
"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>
|
||
);
|
||
}
|