Add avatarUrl field to User schema and Supabase setup

This commit is contained in:
cureb 2025-11-03 03:07:38 +00:00
parent ac7a47c911
commit c577e89218
19 changed files with 736 additions and 159 deletions

14
src/api/auth/login.ts Normal file
View File

@ -0,0 +1,14 @@
export async function login(email: string, password: string) {
const res = await fetch("http://localhost:3000/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.message || "Giriş başarısız")
}
return res.json() // { token, user }
}

14
src/api/auth/register.ts Normal file
View File

@ -0,0 +1,14 @@
export async function register(username: string, email: string, password: string) {
const res = await fetch("http://localhost:3000/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, email, password }),
})
if (!res.ok) {
const data = await res.json()
throw new Error(data.message || "Kayıt başarısız")
}
return res.json() // { token, user }
}

View File

@ -0,0 +1,24 @@
const API_URL = "http://localhost:3000/api/comments"
export async function getComments(dealId: number) {
const res = await fetch(`${API_URL}/${dealId}`)
const data = await res.json()
if (!res.ok) throw new Error(data.error || "Yorumlar alınamadı")
return data
}
export async function postComment(token: string, dealId: number, text: string) {
const res = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ dealId, text }),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || "Yorum gönderilemedi")
return data
}

22
src/api/deal/getDeal.ts Normal file
View File

@ -0,0 +1,22 @@
const API_URL = "http://localhost:3000/api"
export async function getDeals(page = 1) {
try {
const res = await fetch(`http://localhost:3000/api/deals?page=${page}`)
if (!res.ok) throw new Error("Deal listesi alınamadı")
const data = await res.json()
// Sadece results dizisini döndür
return data.results
} catch (err) {
console.error("Deal listesi hatası:", err)
throw err
}
}
export async function getDeal(id: number) {
const res = await fetch(`${API_URL}/deals/${id}`)
if (!res.ok) throw new Error("Deal alınamadı")
return res.json()
}

22
src/api/deal/newDeal.ts Normal file
View File

@ -0,0 +1,22 @@
const API_URL = "http://localhost:3000/api"
export async function createDeal(token: string, dealData: {
title: string
description?: string
url?: string
imageUrl?: string
price?: number
}) {
const res = await fetch(`${API_URL}/deals`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(dealData),
})
const data = await res.json()
if (!res.ok) throw new Error(data.error || "Fırsat eklenemedi")
return data
}

14
src/api/deal/voteDeal.ts Normal file
View File

@ -0,0 +1,14 @@
const API_URL = "http://localhost:3000/api"
export async function voteDeal(token: string, dealId: number, type: "UP" | "DOWN") {
const res = await fetch(`${API_URL}/deal-votes`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ dealId, voteType: type }),
})
if (!res.ok) throw new Error("Vote hatası")
return res.json()
}

View File

@ -0,0 +1,109 @@
import React, { useEffect, useState } from "react"
import { getComments,postComment } from "../../api/deal/commentDeal"
type Comment = {
id: number
user: { username: string }
text: string
createdAt: string
}
type DealCommentsProps = {
dealId: number
}
export default function DealComments({ dealId }: DealCommentsProps) {
const [comments, setComments] = useState<Comment[]>([])
const [newComment, setNewComment] = useState("")
const [loading, setLoading] = useState(false)
const token = localStorage.getItem("token")
// Yorumları yükle
useEffect(() => {
async function loadComments() {
try {
const data = await getComments(dealId)
setComments(data)
} catch (err) {
console.error("Yorumlar alınamadı:", err)
}
}
loadComments()
}, [dealId])
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!newComment.trim()) return
if (!token) {
alert("Giriş yapmalısın.")
return
}
setLoading(true)
try {
const added = await postComment(token, dealId, newComment)
setComments((prev) => [added, ...prev]) // yeni yorumu ekle
setNewComment("")
} catch (err: any) {
alert(err.message || "Sunucu hatası")
} finally {
setLoading(false)
}
}
return (
<div className="bg-surface/50 rounded-lg p-4 shadow-sm">
<h2 className="text-lg font-semibold mb-4">Yorumlar</h2>
<ul className="space-y-3 mb-4">
{comments.length > 0 ? (
comments.map((c) => (
<li key={c.id} className="border-b border-border pb-2">
<p className="text-sm">
<span className="font-medium">{c.user.username}</span>: {c.text}
</p>
<p className="text-xs text-muted-foreground">
{new Date(c.createdAt).toLocaleString("tr-TR")}
</p>
</li>
))
) : (
<p className="text-sm text-muted-foreground">Henüz yorum yok.</p>
)}
</ul>
{token ? (
<form onSubmit={handleSubmit} className="flex gap-2">
<input
type="text"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="Yorum ekle..."
className="flex-1 border rounded-md px-3 py-2 text-sm"
disabled={loading}
/>
<button
type="submit"
disabled={loading}
className="bg-primary text-white text-sm px-4 py-2 rounded-md hover:bg-primary/90 disabled:opacity-60"
>
{loading ? "Gönderiliyor..." : "Gönder"}
</button>
</form>
) : (
<p className="text-sm text-muted-foreground text-center">
Yorum yazmak için{" "}
<a href="/login" className="text-primary font-medium hover:underline">
giriş yap
</a>{" "}
veya{" "}
<a href="/register" className="text-primary font-medium hover:underline">
kayıt ol
</a>
.
</p>
)}
</div>
)
}

View File

@ -0,0 +1,16 @@
import React from "react"
type DealDescriptionProps = {
description: string
}
export default function DealDescription({ description }: DealDescriptionProps) {
return (
<div className="bg-surface/50 rounded-lg p-4 shadow-sm">
<h2 className="text-lg font-semibold mb-2">ıklama</h2>
<p className="text-sm text-muted-foreground leading-relaxed whitespace-pre-line">
{description}
</p>
</div>
)
}

View File

@ -0,0 +1,44 @@
import React from "react"
type DealDetailsProps = {
title: string
price: string
store: string
link: string
postedBy: string
postedAgo: string
}
export default function DealDetails({
title,
price,
store,
link,
postedBy,
postedAgo,
}: DealDetailsProps) {
return (
<div className="bg-surface/50 rounded-lg p-4 shadow-sm">
<h1 className="text-2xl font-semibold mb-2">{title}</h1>
<div className="text-xl font-bold text-primary mb-2">{price}</div>
<div className="text-sm text-muted-foreground mb-4">
Mağaza: {store}
</div>
<a
href={link}
target="_blank"
rel="noopener noreferrer"
className="inline-block text-sm text-blue-600 hover:underline mb-4"
>
Anlaşmayı Gör
</a>
<div className="text-xs text-muted-foreground">
Paylaşan: {postedBy} {postedAgo} önce
</div>
</div>
)
}

View File

@ -0,0 +1,17 @@
type DealImagesProps = {
imageUrl: string
alt?: string
}
export default function DealImages({ imageUrl, alt }: DealImagesProps) {
return (
<div className="w-full bg-surface rounded-lg overflow-hidden shadow-sm">
<img
src={imageUrl}
alt={alt ?? "deal image"}
className="w-full h-auto object-cover"
/>
</div>
)
}

View File

@ -1,28 +1,53 @@
import UserInfo from "./UserInfo"; import UserInfo from "./UserInfo"
import { Link } from "react-router-dom"
export default function Navbar() { export default function Navbar() {
return ( return (
// Dış katman: arka plan tam genişlikte
<nav className="bg-surface"> <nav className="bg-surface">
{/* İç katman: içeriği ortala ve sınırlı genişlikte tut */}
<div className="mx-auto flex justify-between items-center px-6 py-3"> <div className="mx-auto flex justify-between items-center px-6 py-3">
{/* Sol kısım: logo + menü */} {/* Sol kısım: logo + menü */}
<div className="flex items-center gap-15"> <div className="flex items-center gap-15">
{/* Logo */}
<div className="text-primary font-bold text-xl">DealHeat</div> <div className="text-primary font-bold text-xl">DealHeat</div>
{/* Menü */}
<ul className="flex gap-6 text-text items-center"> <ul className="flex gap-6 text-text items-center">
<li><a href="#" className="hover:text-primary transition-colors font-semibold">Anasayfa</a></li> <li>
<li><a href="#" className="hover:text-primary transition-colors font-semibold">Fırsatlar</a></li> <Link
<li><a href="#" className="hover:text-primary transition-colors font-semibold">İletişim</a></li> to="/"
className="hover:text-primary transition-colors font-semibold"
>
Anasayfa
</Link>
</li>
<li>
<a
href="#"
className="hover:text-primary transition-colors font-semibold"
>
Fırsatlar
</a>
</li>
<li>
<a
href="#"
className="hover:text-primary transition-colors font-semibold"
>
İletişim
</a>
</li>
</ul> </ul>
</div> </div>
{/* Sağ kısım: kullanıcı bilgisi */} {/* Sağ kısım: kullanıcı bilgisi + buton */}
<div className="flex items-center gap-4">
<UserInfo /> <UserInfo />
<Link
to="/submit-deal"
className="bg-primary text-white font-semibold px-4 py-2 rounded-md hover:bg-primary/90 transition"
>
Fırsat Yolla
</Link>
</div>
</div> </div>
</nav> </nav>
); )
} }

View File

@ -1,77 +1,92 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react"
import LoginModal from "../../Auth/LoginModal"; import { useNavigate } from "react-router-dom"
import LoginModal from "../../Auth/LoginModal"
import { login } from "../../../api/auth/login"
import { register } from "../../../api/auth/register"
type User = { type User = {
id: number; id: number
username: string; username: string
email?: string; email?: string
avatarUrl?: string; avatarUrl?: string
}; }
export default function UserInfo() { export default function UserInfo() {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null)
const [showModal, setShowModal] = useState(false); const [showModal, setShowModal] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const navigate = useNavigate()
useEffect(() => { useEffect(() => {
const storedUser = localStorage.getItem("user"); const storedUser = localStorage.getItem("user")
if (storedUser) setUser(JSON.parse(storedUser)); if (storedUser) setUser(JSON.parse(storedUser))
}, []); }, [])
const handleRegister = async (username: string, email: string, password: string) => { const handleRegister = async (username: string, email: string, password: string) => {
const res = await fetch("http://localhost:3000/api/auth/register", { try {
method: "POST", const data = await register(username, email, password)
headers: { "Content-Type": "application/json" }, localStorage.setItem("token", data.token)
body: JSON.stringify({ username, email, password }), localStorage.setItem("user", JSON.stringify(data.user))
}); setUser(data.user)
const data = await res.json(); setShowModal(false)
// ... } catch (err: any) {
}; alert(err.message)
}
}
const handleLogin = async (email: string, password: string) => { const handleLogin = async (email: string, password: string) => {
try { try {
const res = await fetch("http://localhost:3000/api/auth/login", { const data = await login(email, password)
method: "POST", localStorage.setItem("token", data.token)
headers: { "Content-Type": "application/json" }, localStorage.setItem("user", JSON.stringify(data.user))
body: JSON.stringify({ email, password }), setUser(data.user)
}); setShowModal(false)
const data = await res.json(); } catch (err: any) {
alert(err.message)
if (res.ok) {
localStorage.setItem("token", data.token);
localStorage.setItem("user", JSON.stringify(data.user));
setUser(data.user);
setShowModal(false);
} else {
alert(data.message || "Giriş başarısız");
} }
} catch {
alert("Sunucu hatası");
} }
};
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem("token"); localStorage.removeItem("token")
localStorage.removeItem("user"); localStorage.removeItem("user")
setUser(null); setUser(null)
}; setMenuOpen(false)
}
const goToProfile = () => {
setMenuOpen(false)
navigate("/profile")
}
return ( return (
<div className="flex items-center gap-3"> <div className="relative flex items-center gap-3">
{user ? ( {user ? (
<div className="flex items-center gap-3"> <div className="relative">
<div className="flex items-center gap-2">
<img <img
src={user.avatarUrl || "https://via.placeholder.com/32"} src={user.avatarUrl || "https://via.placeholder.com/32"}
alt={user.username} alt={user.username}
className="w-8 h-8 rounded-full" className="w-8 h-8 rounded-full cursor-pointer border border-gray-400 hover:border-orange-500 transition"
onClick={() => setMenuOpen((prev) => !prev)}
/> />
<span className="font-medium text-white">{user.username}</span>
</div> {menuOpen && (
<div className="absolute right-0 mt-2 w-44 bg-surface border border-border rounded-lg shadow-md py-2 z-50">
<p className="px-4 py-2 text-sm text-text font-semibold">{user.username}</p>
<button
onClick={goToProfile}
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Profilim
</button>
<button <button
onClick={handleLogout} onClick={handleLogout}
className="bg-gray-700 text-white px-3 py-1 rounded hover:bg-gray-600 text-sm" className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100"
> >
Çıkış yap Çıkış yap
</button> </button>
</div> </div>
)}
</div>
) : ( ) : (
<button <button
onClick={() => setShowModal(true)} onClick={() => setShowModal(true)}
@ -89,5 +104,5 @@ export default function UserInfo() {
/> />
)} )}
</div> </div>
); )
} }

View File

@ -1,16 +1,18 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { voteDeal } from "../../api/deal/voteDeal" // yeni api dosyan
type DealCardProps = { type DealCardProps = {
id: number; id: number
image: string; image: string
title: string; title: string
price: string; price: string
store: string; store: string
postedBy: string; postedBy: string
score: number; score: number
comments: number; comments: number
postedAgo: string; postedAgo: string
}; }
export default function DealCardMain({ export default function DealCardMain({
id, id,
@ -23,48 +25,43 @@ export default function DealCardMain({
comments, comments,
postedAgo, postedAgo,
}: DealCardProps) { }: DealCardProps) {
const [currentScore, setCurrentScore] = useState<number>(score); const [currentScore, setCurrentScore] = useState<number>(score)
const [voting, setVoting] = useState(false); const [voting, setVoting] = useState(false)
const navigate = useNavigate()
// dışarıdan gelen score güncellenirse state'i de güncelle
useEffect(() => { useEffect(() => {
setCurrentScore(score); setCurrentScore(score)
}, [score]); }, [score])
const handleVote = async (type: "UP" | "DOWN") => { const handleVote = async (e: React.MouseEvent, type: "UP" | "DOWN") => {
const token = localStorage.getItem("token"); e.stopPropagation()
const token = localStorage.getItem("token")
if (!token) { if (!token) {
alert("Giriş yapmalısın"); alert("Giriş yapmalısın")
return; return
} }
setVoting(true); setVoting(true)
try { try {
const res = await fetch("http://localhost:3000/api/deal-votes", { const data = await voteDeal(token, id, type)
method: "POST", if (typeof data.score === "number") setCurrentScore(data.score)
headers: { else alert(data.error || "Oy gönderilemedi")
"Content-Type": "application/json", } catch {
Authorization: `Bearer ${token}`, alert("Sunucu hatası")
},
body: JSON.stringify({ dealId: id, voteType: type }),
});
const data = await res.json();
if (res.ok && typeof data.score === "number") {
setCurrentScore(data.score);
} else {
alert(data.error || "Oy gönderilemedi");
}
} catch (err) {
console.error("Vote error:", err);
alert("Sunucu hatası");
} finally { } finally {
setVoting(false); setVoting(false)
}
}
const handleCardClick = () => {
navigate(`/deal/${id}`)
} }
};
return ( return (
<div className="flex bg-surface p-4 rounded-xl hover:shadow-md transition mb-5"> <div
onClick={handleCardClick}
className="flex bg-surface p-4 rounded-xl hover:shadow-md transition mb-5 cursor-pointer"
>
<div className="w-42 h-42 flex-shrink-0"> <div className="w-42 h-42 flex-shrink-0">
<img <img
src={image} src={image}
@ -79,7 +76,7 @@ export default function DealCardMain({
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<button <button
disabled={voting} disabled={voting}
onClick={() => handleVote("UP")} onClick={(e) => handleVote(e, "UP")}
className="text-green-600 font-bold text-lg" className="text-green-600 font-bold text-lg"
> >
@ -87,16 +84,16 @@ export default function DealCardMain({
<span>{currentScore ?? 0}°</span> <span>{currentScore ?? 0}°</span>
<button <button
disabled={voting} disabled={voting}
onClick={() => handleVote("DOWN")} onClick={(e) => handleVote(e, "DOWN")}
className="text-red-600 font-bold text-lg" className="text-red-600 font-bold text-lg"
> >
</button> </button>
</span> </span>
<span>Posted {postedAgo}</span> <span> {postedAgo} </span>
</div> </div>
<h2 className="text-xl font-semibold text-text mt-1 hover:text-primary cursor-pointer"> <h2 className="text-xl font-semibold text-text mt-1 hover:text-primary">
{title} {title}
</h2> </h2>
@ -115,11 +112,14 @@ export default function DealCardMain({
<span>💬 {comments}</span> <span>💬 {comments}</span>
<span>🔗 Paylaş</span> <span>🔗 Paylaş</span>
</div> </div>
<button className="bg-primary text-white px-4 py-2 rounded-md hover:bg-primary/90"> <button
onClick={(e) => e.stopPropagation()}
className="bg-primary text-white px-4 py-2 rounded-md hover:bg-primary/90"
>
Fırsatı kap Fırsatı kap
</button> </button>
</div> </div>
</div> </div>
</div> </div>
); )
} }

View File

@ -0,0 +1,31 @@
export default function DealDetailsLayout({
Image,
Details,
Description,
Comments,
}: {
Image: React.ReactNode
Details: React.ReactNode
Description: React.ReactNode
Comments: React.ReactNode
}) {
return (
<div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Üst kısım: sol görsel + sağ detay */}
<div className="lg:col-span-1 flex justify-center items-start">
{Image}
</div>
<div className="lg:col-span-3 flex flex-col gap-6">
{Details}
</div>
{/* Alt kısım: açıklama ve yorumlar tam genişlikte */}
<div className="lg:col-span-4 flex flex-col gap-6">
<section>{Description}</section>
<section>{Comments}</section>
</div>
</div>
)
}

View File

@ -3,12 +3,16 @@ import ReactDOM from "react-dom/client";
import { BrowserRouter, Routes, Route } from "react-router-dom"; // <-- Burası önemli import { BrowserRouter, Routes, Route } from "react-router-dom"; // <-- Burası önemli
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import "./global.css"; import "./global.css";
import DealPage from "./pages/DealPage";
import SubmitDealPage from "./pages/SubmitDealPage";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/deal/:id" element={<DealPage />} />
<Route path="/submit-deal" element={<SubmitDealPage />} />
</Routes> </Routes>
</BrowserRouter> </BrowserRouter>
</React.StrictMode> </React.StrictMode>

62
src/pages/DealPage.tsx Normal file
View File

@ -0,0 +1,62 @@
import { useEffect, useState } from "react"
import { useParams } from "react-router-dom"
import { getDeal } from "../api/deal/getDeal"
import MainLayout from "../layouts/MainLayout"
import DealDetailsLayout from "../layouts/DealDetailsLayout"
import DealImages from "../components/DealScreen/DealImages"
import DealDetails from "../components/DealScreen/DealDetails"
import DealDescription from "../components/DealScreen/DealDescription"
import DealComments from "../components/DealScreen/DealComments"
type Deal = {
id: number
title: string
price: string
store: string
link: string
image: string
description: string
postedBy: string
postedAgo: string
comments: {
id: number
author: string
text: string
postedAgo: string
}[]
}
export default function DealPage() {
const { id } = useParams()
const [deal, setDeal] = useState<Deal | null>(null)
useEffect(() => {
if (!id) return
getDeal(Number(id))
.then(setDeal)
.catch((err) => console.error(err))
}, [id])
if (!deal) return <p className="p-4">Yükleniyor...</p>
return (
<MainLayout>
<DealDetailsLayout
Image={<DealImages imageUrl={deal.image} />}
Details={
<DealDetails
title={deal.title}
price={deal.price}
store={deal.store}
link={deal.link}
postedBy={deal.postedBy}
postedAgo={deal.postedAgo}
/>
}
Description={<DealDescription description={deal.description} />}
Comments={<DealComments dealId={deal.id} />}
/>
</MainLayout>
)
}

View File

@ -1,36 +1,71 @@
import { useEffect, useState } from "react"; import { useEffect, useState, useRef } from "react"
import MainLayout from "../layouts/MainLayout"; import MainLayout from "../layouts/MainLayout"
import HomeLayout from "../layouts/HomeLayout"; import HomeLayout from "../layouts/HomeLayout"
import DealCardMain from "../components/Shared/DealCardMain"; import DealCardMain from "../components/Shared/DealCardMain"
import { getDeals } from "../api/deal/getDeal"
import { timeAgo } from "../utils/timeAgo"
type Deal = { type Deal = {
id: number; id: number
title: string; title: string
description: string; description: string
url: string; url: string
imageUrl: string; imageUrl: string
price: number; price: number
score:number; score: number
createdAt: string; createdAt: string
user?: { username: string }; user?: { username: string }
}; }
export default function Home() { export default function Home() {
const [deals, setDeals] = useState<Deal[]>([]); const [deals, setDeals] = useState<Deal[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const observerRef = useRef<HTMLDivElement | null>(null)
// Sayfa değiştiğinde yeni verileri çek
useEffect(() => { useEffect(() => {
async function fetchDeals() { const loadDeals = async () => {
if (loading || !hasMore) return
setLoading(true)
try { try {
const res = await fetch("http://localhost:3000/api/deals"); const newDeals = await getDeals(page)
const data = await res.json(); if (newDeals.length === 0) {
setDeals(data); setHasMore(false)
} catch (err) { } else {
console.error("Error fetching deals:", err); // yinelenen veriyi engelle
setDeals((prev) => {
const existingIds = new Set(prev.map((d) => d.id))
const filtered = newDeals.filter((d: Deal) => !existingIds.has(d.id))
return [...prev, ...filtered]
})
}
} catch (err: any) {
setError(err.message)
} finally {
setLoading(false)
} }
} }
fetchDeals(); loadDeals()
}, []); }, [page]) // page değişince tekrar çalışır
// Scroll gözlemleyici
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !loading) {
setPage((prev) => prev + 1)
}
},
{ threshold: 1 }
)
if (observerRef.current) observer.observe(observerRef.current)
return () => observer.disconnect()
}, [hasMore, loading])
if (error) return <p className="p-4 text-red-600">{error}</p>
return ( return (
<MainLayout> <MainLayout>
@ -38,19 +73,25 @@ export default function Home() {
{deals.map((deal) => ( {deals.map((deal) => (
<DealCardMain <DealCardMain
key={deal.id} key={deal.id}
id={deal.id} // <-- eksik olan bu satır id={deal.id}
image={deal.imageUrl} image={deal.imageUrl}
title={deal.title} title={deal.title}
price={`${deal.price}`} price={`${deal.price}`}
store={new URL(deal.url).hostname} store={deal.url ? new URL(deal.url).hostname : "Bilinmiyor"}
postedBy={deal.user?.username || "unknown"} postedBy={deal.user?.username || "unknown"}
score={deal.score} score={deal.score}
comments={0} comments={0}
postedAgo={new Date(deal.createdAt).toLocaleDateString("tr-TR")} postedAgo={timeAgo(deal.createdAt)}
/> />
))} ))}
{loading && <p className="text-center py-4">Yükleniyor...</p>}
{!hasMore && (
<p className="text-center py-4 text-muted-foreground">
Tüm fırsatlar yüklendi.
</p>
)}
<div ref={observerRef} className="h-8" />
</HomeLayout> </HomeLayout>
</MainLayout> </MainLayout>
); )
} }

View File

@ -0,0 +1,90 @@
import { useState } from "react"
import MainLayout from "../layouts/MainLayout"
import { createDeal } from "../api/deal/newDeal"
export default function SubmitDealPage() {
const [title, setTitle] = useState("")
const [description, setDescription] = useState("")
const [url, setUrl] = useState("")
const [imageUrl, setImageUrl] = useState("")
const [price, setPrice] = useState("")
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const token = localStorage.getItem("token")
if (!token) {
alert("Fırsat eklemek için giriş yapmalısın.")
return
}
try {
await createDeal(token, {
title,
description,
url,
imageUrl,
price: parseFloat(price),
})
alert("Fırsat başarıyla gönderildi.")
setTitle("")
setDescription("")
setUrl("")
setImageUrl("")
setPrice("")
} catch (err: any) {
alert(err.message || "Sunucu hatası.")
}
}
return (
<MainLayout>
<div className="max-w-xl mx-auto bg-surface/50 p-6 rounded-lg shadow-sm mt-8">
<h1 className="text-2xl font-semibold mb-4 text-primary">Fırsat Yolla</h1>
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
<input
type="text"
placeholder="Başlık"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="border rounded-md px-3 py-2"
required
/>
<textarea
placeholder="Açıklama"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="border rounded-md px-3 py-2 h-24"
/>
<input
type="url"
placeholder="Ürün Linki"
value={url}
onChange={(e) => setUrl(e.target.value)}
className="border rounded-md px-3 py-2"
/>
<input
type="url"
placeholder="Görsel URL"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
className="border rounded-md px-3 py-2"
/>
<input
type="number"
placeholder="Fiyat"
value={price}
onChange={(e) => setPrice(e.target.value)}
className="border rounded-md px-3 py-2"
/>
<button
type="submit"
className="bg-primary text-white py-2 rounded-md hover:bg-primary/90 transition"
>
Gönder
</button>
</form>
</div>
</MainLayout>
)
}

13
src/utils/timeAgo.ts Normal file
View File

@ -0,0 +1,13 @@
// src/utils/timeAgo.ts
export function timeAgo(dateString: string) {
const rtf = new Intl.RelativeTimeFormat("tr", { numeric: "auto" })
const diffMs = new Date(dateString).getTime() - Date.now()
const diffSec = Math.floor(diffMs / 1000)
const absSec = Math.abs(diffSec)
if (absSec < 60) return rtf.format(Math.round(diffSec / 1), "second")
if (absSec < 3600) return rtf.format(Math.round(diffSec / 60), "minute")
if (absSec < 86400) return rtf.format(Math.round(diffSec / 3600), "hour")
return rtf.format(Math.round(diffSec / 86400), "day")
}