first commit
This commit is contained in:
+259
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user