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() {
return (
// Dış katman: arka plan tam genişlikte
<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ü */}
<div className="flex items-center gap-15">
{/* Logo */}
<div className="text-primary font-bold text-xl">DealHeat</div>
{/* Menü */}
<ul className="flex gap-6 text-text items-center">
<li><a href="#" className="hover:text-primary transition-colors font-semibold">Anasayfa</a></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>
<li>
<Link
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>
</div>
{/* Sağ kısım: kullanıcı bilgisi */}
{/* Sağ kısım: kullanıcı bilgisi + buton */}
<div className="flex items-center gap-4">
<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>
</nav>
);
)
}

View File

@ -1,77 +1,92 @@
import { useState, useEffect } from "react";
import LoginModal from "../../Auth/LoginModal";
import { useState, useEffect } from "react"
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 = {
id: number;
username: string;
email?: string;
avatarUrl?: string;
};
id: number
username: string
email?: string
avatarUrl?: string
}
export default function UserInfo() {
const [user, setUser] = useState<User | null>(null);
const [showModal, setShowModal] = useState(false);
const [user, setUser] = useState<User | null>(null)
const [showModal, setShowModal] = useState(false)
const [menuOpen, setMenuOpen] = useState(false)
const navigate = useNavigate()
useEffect(() => {
const storedUser = localStorage.getItem("user");
if (storedUser) setUser(JSON.parse(storedUser));
}, []);
const storedUser = localStorage.getItem("user")
if (storedUser) setUser(JSON.parse(storedUser))
}, [])
const handleRegister = async (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 }),
});
const data = await res.json();
// ...
};
try {
const data = await register(username, email, password)
localStorage.setItem("token", data.token)
localStorage.setItem("user", JSON.stringify(data.user))
setUser(data.user)
setShowModal(false)
} catch (err: any) {
alert(err.message)
}
}
const handleLogin = async (email: string, password: string) => {
try {
const res = await fetch("http://localhost:3000/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
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");
const data = await login(email, password)
localStorage.setItem("token", data.token)
localStorage.setItem("user", JSON.stringify(data.user))
setUser(data.user)
setShowModal(false)
} catch (err: any) {
alert(err.message)
}
} catch {
alert("Sunucu hatası");
}
};
const handleLogout = () => {
localStorage.removeItem("token");
localStorage.removeItem("user");
setUser(null);
};
localStorage.removeItem("token")
localStorage.removeItem("user")
setUser(null)
setMenuOpen(false)
}
const goToProfile = () => {
setMenuOpen(false)
navigate("/profile")
}
return (
<div className="flex items-center gap-3">
<div className="relative flex items-center gap-3">
{user ? (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<div className="relative">
<img
src={user.avatarUrl || "https://via.placeholder.com/32"}
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
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
</button>
</div>
)}
</div>
) : (
<button
onClick={() => setShowModal(true)}
@ -89,5 +104,5 @@ export default function UserInfo() {
/>
)}
</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 = {
id: number;
image: string;
title: string;
price: string;
store: string;
postedBy: string;
score: number;
comments: number;
postedAgo: string;
};
id: number
image: string
title: string
price: string
store: string
postedBy: string
score: number
comments: number
postedAgo: string
}
export default function DealCardMain({
id,
@ -23,48 +25,43 @@ export default function DealCardMain({
comments,
postedAgo,
}: DealCardProps) {
const [currentScore, setCurrentScore] = useState<number>(score);
const [voting, setVoting] = useState(false);
const [currentScore, setCurrentScore] = useState<number>(score)
const [voting, setVoting] = useState(false)
const navigate = useNavigate()
// dışarıdan gelen score güncellenirse state'i de güncelle
useEffect(() => {
setCurrentScore(score);
}, [score]);
setCurrentScore(score)
}, [score])
const handleVote = async (type: "UP" | "DOWN") => {
const token = localStorage.getItem("token");
const handleVote = async (e: React.MouseEvent, type: "UP" | "DOWN") => {
e.stopPropagation()
const token = localStorage.getItem("token")
if (!token) {
alert("Giriş yapmalısın");
return;
alert("Giriş yapmalısın")
return
}
setVoting(true);
setVoting(true)
try {
const res = await fetch("http://localhost:3000/api/deal-votes", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
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ı");
const data = await voteDeal(token, id, type)
if (typeof data.score === "number") setCurrentScore(data.score)
else alert(data.error || "Oy gönderilemedi")
} catch {
alert("Sunucu hatası")
} finally {
setVoting(false);
setVoting(false)
}
}
const handleCardClick = () => {
navigate(`/deal/${id}`)
}
};
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">
<img
src={image}
@ -79,7 +76,7 @@ export default function DealCardMain({
<span className="flex items-center gap-2">
<button
disabled={voting}
onClick={() => handleVote("UP")}
onClick={(e) => handleVote(e, "UP")}
className="text-green-600 font-bold text-lg"
>
@ -87,16 +84,16 @@ export default function DealCardMain({
<span>{currentScore ?? 0}°</span>
<button
disabled={voting}
onClick={() => handleVote("DOWN")}
onClick={(e) => handleVote(e, "DOWN")}
className="text-red-600 font-bold text-lg"
>
</button>
</span>
<span>Posted {postedAgo}</span>
<span> {postedAgo} </span>
</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}
</h2>
@ -115,11 +112,14 @@ export default function DealCardMain({
<span>💬 {comments}</span>
<span>🔗 Paylaş</span>
</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
</button>
</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 HomePage from "./pages/HomePage";
import "./global.css";
import DealPage from "./pages/DealPage";
import SubmitDealPage from "./pages/SubmitDealPage";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/deal/:id" element={<DealPage />} />
<Route path="/submit-deal" element={<SubmitDealPage />} />
</Routes>
</BrowserRouter>
</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 MainLayout from "../layouts/MainLayout";
import HomeLayout from "../layouts/HomeLayout";
import DealCardMain from "../components/Shared/DealCardMain";
import { useEffect, useState, useRef } from "react"
import MainLayout from "../layouts/MainLayout"
import HomeLayout from "../layouts/HomeLayout"
import DealCardMain from "../components/Shared/DealCardMain"
import { getDeals } from "../api/deal/getDeal"
import { timeAgo } from "../utils/timeAgo"
type Deal = {
id: number;
title: string;
description: string;
url: string;
imageUrl: string;
price: number;
score:number;
createdAt: string;
user?: { username: string };
};
id: number
title: string
description: string
url: string
imageUrl: string
price: number
score: number
createdAt: string
user?: { username: string }
}
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(() => {
async function fetchDeals() {
const loadDeals = async () => {
if (loading || !hasMore) return
setLoading(true)
try {
const res = await fetch("http://localhost:3000/api/deals");
const data = await res.json();
setDeals(data);
} catch (err) {
console.error("Error fetching deals:", err);
const newDeals = await getDeals(page)
if (newDeals.length === 0) {
setHasMore(false)
} else {
// 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 (
<MainLayout>
@ -38,19 +73,25 @@ export default function Home() {
{deals.map((deal) => (
<DealCardMain
key={deal.id}
id={deal.id} // <-- eksik olan bu satır
id={deal.id}
image={deal.imageUrl}
title={deal.title}
price={`${deal.price}`}
store={new URL(deal.url).hostname}
store={deal.url ? new URL(deal.url).hostname : "Bilinmiyor"}
postedBy={deal.user?.username || "unknown"}
score={deal.score}
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>
</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")
}