first commit

This commit is contained in:
mstfyldz
2026-08-16 17:18:23 +03:00
commit 5ace6898b6
61 changed files with 12065 additions and 0 deletions
+259
View File
@@ -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">&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 */}
<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">
&copy; 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>
);
}