Files

262 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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 { AyrisFooterBadge } from "@/components/AyrisFooterBadge";
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">&times;</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">&times;</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">
&ldquo;{searchQuery}&rdquo;
<button onClick={() => setSearchQuery("")} className="hover:text-amber-200 ml-1">&times;</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 & Ayris Tech Signature */}
<footer className="mt-16 pt-8 border-t border-[#18181B]/10 text-center">
<div className="inline-block relative mb-8 px-4">
<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">
&copy; 2026 Mustafa Yıldız mstfyldz journal Craft with passion
</div>
</div>
<AyrisFooterBadge />
</footer>
</main>
{/* Modals */}
<NewPostModal
isOpen={isNewPostOpen}
onClose={() => setIsNewPostOpen(false)}
onAddPost={handleAddPost}
/>
<PostDetailModal
post={detailPost}
onClose={() => setDetailPost(null)}
onTagClick={handleTagClick}
/>
</div>
);
}