refactor and overhaul

This commit is contained in:
Burak Cüre 2026-01-20 12:19:02 +00:00
parent a7a44410fd
commit c06b4b8211
38 changed files with 1062 additions and 141 deletions

View File

@ -2,8 +2,10 @@ import { useState } from "react"
import { BrowserRouter, Routes, Route } from "react-router-dom"
import HomePage from "./pages/HomePage"
import DealPage from "./pages/DealPage"
import SubmitDealPage from "./pages/SubmitDealPage"
import DealPage from "./pages/DealDetailsPage"
import CreateDealPage from "./pages/CreateDealPage"
import AccountSettingsPage from "./pages/AccountSettingsPage"
import ProfilePage from "./pages/ProfilePage"
import SearchPage from "./pages/SearchPage"
@ -33,7 +35,8 @@ export default function App() {
element={<SearchPage onRequireLogin={() => setShowLoginModal(true)} />}
/>
<Route path="/submit-deal" element={<SubmitDealPage />} />
<Route path="/create-deal" element={<CreateDealPage />} />
<Route path="/account" element={<AccountSettingsPage />} />
<Route path="/user/:userName" element={<ProfilePage />} />

View File

@ -0,0 +1,18 @@
import type { DealDraft } from "../../models/DealDraft"
import type { CreateDealRequest } from "../../api/deal/types"
export function mapDealDraftToCreateRequest(
draft: DealDraft
): CreateDealRequest {
return {
title: draft.title,
description: draft.description,
price: draft.price,
imageUrl: draft.imageUrl,
url: draft.url || undefined,
sellerName:draft.seller.name
}
}

View File

@ -0,0 +1,8 @@
import type { Seller } from "../../models/seller/Seller";
import type { SellerFromLookupRequest } from "../../api/seller/types";
export function mapSellerFromLookupRequest(seller:Seller):SellerFromLookupRequest{
return{
url:seller.url
}
}

View File

@ -0,0 +1,35 @@
// src/adapters/dealCardAdapter.ts
import type { DealCard } from "../../models/deal/DealCard"
import type { DealCardResponse } from "../../api/deal/types"
export function mapDealCardResponseToDeal(
api: DealCardResponse
): DealCard{
return {
id: api.id,
title: api.title,
description: api.description,
price: api.price ?? undefined,
score: api.score,
commentsCount:api.commentsCount,
status: api.status,
saleType: api.saleType,
affiliateType: api.affiliateType,
createdAt: api.createdAt,
updatedAt:api.updatedAt,
user: {
id: api.user.id,
username: api.user.username,
avatarUrl: api.user.avatarUrl,
},
seller:{
name:api.seller.name,
url:api.seller.url
},
imageUrl:api.imageUrl,
}
}

View File

@ -0,0 +1,41 @@
import type { DealDetailResponse } from "../../api/deal/types"
import type { DealDetail } from "../../models/deal/DealDetail"
export function mapDealDetailResponseToDealDetail(
api: DealDetailResponse
): DealDetail {
return {
id: api.id,
title: api.title,
description: api.description,
url: api.url ?? undefined,
price: api.price ?? undefined,
score: api.score,
commentsCount: api.commentsCount,
status: api.status,
saleType: api.saleType,
affiliateType: api.affiliateType,
createdAt: api.createdAt,
updatedAt: api.updatedAt,
user: api.user,
seller:{
name:api.seller.name,
url:api.seller.url
},
images: api.images.map((img) => ({
url: img.imageUrl,
order: img.order,
})),
comments: api.comments.map((c) => ({
id: c.id,
text: c.text,
createdAt: c.createdAt,
user: c.user,
})),
}
}

View File

@ -0,0 +1,12 @@
import type { SellerFromLookupResponse } from "../../api/seller/types"
import type { Seller } from "../../models/seller/Seller"
export function mapSellerFromLookupResponse(
api: SellerFromLookupResponse
): Seller {
return{
id:api.id,
name:api.name,
url:null
}
}

View File

@ -1,22 +1,17 @@
// src/api/deal/dealApi.ts
import instance from "../axiosInstance"
import type { DealCardResponse,DealDetailResponse} from "./types"
export async function getDeals(page = 1) {
try {
export async function getDeals(
page = 1
): Promise<DealCardResponse[]> {
const { data } = await instance.get(`/deals?page=${page}`)
return data.results
} catch (error) {
console.error("Deal listesi hatası:", error)
throw error
}
}
export async function getDeal(id: number) {
try {
export async function getDealDetail(
id: number
): Promise<DealDetailResponse> {
const { data } = await instance.get(`/deals/${id}`)
return data
} catch (error) {
console.error("Deal alma hatası:", error)
throw error
}
}

View File

@ -6,6 +6,7 @@ type DealData = {
description?: string
url?: string
imageUrl?: string
customCompany?: string
price?: number
}

97
src/api/deal/types.ts Normal file
View File

@ -0,0 +1,97 @@
// src/api/deal/types.ts
export type DealCardResponse = {
id: number
title: string
description: string // DB null → backend "" yapar
price: number | null // fiyat yoksa bilinçli null
score: number
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED"
saleType: "ONLINE" | "OFFLINE" | "CODE"
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE"
createdAt: string // ISO string
updatedAt: string
/* ---------- YAYINLAYAN KULLANICI (profil için yeterli) ---------- */
user: {
id: number // 🔑 profil sayfası için
username: string
avatarUrl: string | null
}
/* ---------- SATICI (company + customCompany sadeleştirilmiş) ---------- */
seller: {
name: string
url:string|null
}
/* ---------- GÖRSELLER ---------- */
imageUrl:string
/* ---------- UX İÇİN TÜRETİLMİŞ ALANLAR ---------- */
commentsCount: number
userVote?: "UP" | "DOWN" | null
}
export type DealDetailResponse = {
id: number
title: string
description: string
url: string | null
price: number | null
score: number
commentsCount: number
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED"
saleType: "ONLINE" | "OFFLINE" | "CODE"
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE"
createdAt: string
updatedAt: string
user: {
id: number
username: string
avatarUrl: string | null
}
seller: {
name: string
url:string|null
}
images: {
id: number
imageUrl: string
order: number
}[]
comments: {
id: number
text: string
createdAt: string
user: {
id: number
username: string
avatarUrl: string | null
}
}[]
}
export type CreateDealRequest = {
title: string
description?: string
price?: number
imageUrl: string
// online deal
url?: string
// seller (known or custom)
sellerName: string
}

View File

@ -0,0 +1,13 @@
import type { SellerFromLookupResponse,SellerFromLookupRequest } from "./types"
import instance from "../axiosInstance"
export async function lookupSellerFromLink(seller:SellerFromLookupRequest) :Promise<SellerFromLookupResponse>{
const { data } = await instance.post(
"/seller/from-link",
{ seller }
)
return data
}

9
src/api/seller/types.ts Normal file
View File

@ -0,0 +1,9 @@
export type SellerFromLookupResponse= {
id:number
name:string
}
export type SellerFromLookupRequest={
url:string|null
}

View File

@ -0,0 +1,175 @@
import type { DealDraft } from "../../models/DealDraft"
type Props = {
data: DealDraft
onChange: (data: DealDraft) => void
onBack: () => void
onSubmit: () => void
}
export default function DealDetailsStep({
data,
onChange,
onBack,
onSubmit,
}: Props) {
const hasDetectedCompany = Boolean(data.sellerId)
return (
<form
onSubmit={(e) => {
e.preventDefault()
onSubmit()
}}
className="max-w-2xl mx-auto flex flex-col gap-6"
>
{/* BAŞLIK */}
<div>
<h2 className="text-lg font-medium mb-1">
Fırsat Detayları
</h2>
<p className="text-sm text-text-muted">
Fırsata ait temel bilgileri aşağıdaki alanlara giriniz.
</p>
</div>
{/* BAŞLIK */}
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">
Fırsat başlığı
</label>
<input
type="text"
placeholder="Örn: %40 indirimli spor ayakkabı"
value={data.title}
onChange={(e) =>
onChange({ ...data, title: e.target.value })
}
className="border rounded-md px-3 py-2"
required
/>
<p className="text-xs text-text-muted">
Kullanıcının ilk göreceği kısa ve net başlık.
</p>
</div>
{/* AÇIKLAMA */}
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">
ıklama
</label>
<textarea
placeholder="İndirim koşulları, geçerlilik süresi veya ek bilgiler..."
value={data.description ?? ""}
onChange={(e) =>
onChange({ ...data, description: e.target.value })
}
className="border rounded-md px-3 py-2 h-28 resize-none"
/>
<p className="text-xs text-text-muted">
İsteğe bağlıdır. Fırsat hakkında ek bilgi verebilirsiniz.
</p>
</div>
{/* SATICI */}
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">
Satıcı bilgisi
</label>
{hasDetectedCompany ? (
<>
<input
type="text"
value={data.sellerName ?? ""}
disabled
className="border rounded-md px-3 py-2 bg-gray-100 text-gray-700 cursor-not-allowed"
/>
<p className="text-xs text-text-muted">
Satıcı bilgisi ürün bağlantısından otomatik olarak algılanmıştır.
</p>
</>
) : (
<>
<input
type="text"
placeholder="Satıcı adını giriniz"
value={data.customCompany ?? ""}
onChange={(e) =>
onChange({
...data,
customCompany: e.target.value,
})
}
className="border rounded-md px-3 py-2"
/>
<p className="text-xs text-text-muted">
Satıcı bilgisi otomatik olarak algılanamazsa manuel olarak girilebilir.
</p>
</>
)}
</div>
{/* GÖRSEL */}
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">
Görsel bağlantısı
</label>
<input
type="url"
placeholder="https://www.siteadi.com/gorsel.jpg"
value={data.imageUrl ?? ""}
onChange={(e) =>
onChange({ ...data, imageUrl: e.target.value })
}
className="border rounded-md px-3 py-2"
/>
<p className="text-xs text-text-muted">
Ürünü temsil eden bir görsel bağlantısı ekleyebilirsiniz.
</p>
</div>
{/* FİYAT */}
<div className="flex flex-col gap-1">
<label className="text-sm font-medium">
Fiyat
</label>
<input
type="number"
placeholder="Örn: 999"
value={data.price ?? ""}
onChange={(e) =>
onChange({
...data,
price: e.target.value
? Number(e.target.value)
: undefined,
})
}
className="border rounded-md px-3 py-2"
/>
<p className="text-xs text-text-muted">
KDV dahil satış fiyatını giriniz.
</p>
</div>
{/* BUTONLAR */}
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={onBack}
className="border px-4 py-2 rounded-md"
>
Geri
</button>
<button
type="submit"
className="bg-primary text-white px-6 py-2 rounded-md"
>
Gönder
</button>
</div>
</form>
)
}

View File

@ -0,0 +1,108 @@
import React, { useEffect, useState } from "react"
type Props = {
url: string
loading: boolean
onChange: (url: string) => void
onSubmit: (e: React.FormEvent<HTMLFormElement>) => void
}
export default function DealLinkStep({
url,
loading,
onChange,
onSubmit,
}: Props) {
// 👉 Varsayılan: ONLINE
const [isOffline, setIsOffline] = useState(false)
// Online seçiliyse ve url boşsa, otomatik https:// koy
useEffect(() => {
if (!isOffline && url === "") {
onChange("https://")
}
}, [isOffline, url, onChange])
return (
<form
onSubmit={onSubmit}
className="space-y-6"
>
{/* Başlık */}
<div>
<h2 className="text-lg font-medium mb-1">
Fırsat Türü
</h2>
<p className="text-sm text-text-muted">
</p>
</div>
{/* Online / Offline seçimi */}
<div className="flex gap-3">
<button
type="button"
onClick={() => {
setIsOffline(false)
onChange("https://")
}}
className={`flex-1 border rounded-md px-4 py-3 text-sm transition
${
!isOffline
? "border-primary bg-primary/10 text-primary"
: "border-white/10 hover:border-white/30"
}
`}
>
Online satış (link mevcut)
</button>
<button
type="button"
onClick={() => {
setIsOffline(true)
onChange("")
}}
className={`flex-1 border rounded-md px-4 py-3 text-sm transition
${
isOffline
? "border-primary bg-primary/10 text-primary"
: "border-white/10 hover:border-white/30"
}
`}
>
Mağaza içi satış
</button>
</div>
{/* Link input (sadece online) */}
{!isOffline && (
<div className="space-y-1">
<label className="block text-sm text-text-muted">
Ürün bağlantısı
</label>
<input
type="url"
value={url}
onChange={(e) => onChange(e.target.value)}
placeholder="https://www.siteadi.com/urun"
required
className="w-full rounded-md border border-white/10 bg-background px-3 py-2 text-sm
focus:outline-none focus:ring-2 focus:ring-primary/40"
/>
</div>
)}
{/* Submit */}
<div className="pt-2">
<button
type="submit"
disabled={loading}
className="w-full bg-primary hover:bg-primary-hover text-black font-medium
px-4 py-2 rounded-md transition disabled:opacity-60"
>
{loading ? "Kontrol ediliyor..." : "Devam et"}
</button>
</div>
</form>
)
}

View File

@ -0,0 +1,47 @@
import type { DealDraft } from "../../models/DealDraft"
type Props = {
data: DealDraft
onBack: () => void
onSubmit: () => void
}
export default function DealReviewStep({
data,
onBack,
onSubmit,
}: Props) {
return (
<div className="flex flex-col gap-4">
<div className="text-sm bg-gray-100 p-3 rounded">
<p><b>Link:</b> {data.url}</p>
<p><b>Başlık:</b> {data.title}</p>
<p><b>Fiyat:</b> {data.price ?? "-"}</p>
</div>
{data.imageUrl && (
<img
src={data.imageUrl}
alt=""
className="w-32 h-32 object-cover rounded"
/>
)}
<div className="flex gap-2">
<button
onClick={onBack}
className="border px-4 py-2 rounded-md"
>
Geri
</button>
<button
onClick={onSubmit}
className="bg-primary text-white px-4 py-2 rounded-md"
>
Gönder
</button>
</div>
</div>
)
}

View File

@ -3,7 +3,7 @@ import { Link } from "react-router-dom"
import { getComments, postComment } from "../../api/deal/commentDeal"
import { useAuth } from "../../context/AuthContext"
import { timeAgo } from "../../utils/timeAgo"
import type { Comment } from "../../models/Comment"
import type { Comment } from "../../models/deal/DealComment"
type DealCommentsProps = {
dealId: number

View File

@ -47,7 +47,7 @@ export default function Navbar() {
<div className="flex items-center gap-4">
<UserInfo />
<Link
to="/submit-deal"
to="/create-deal"
className="bg-primary text-white font-semibold px-4 py-2 rounded-md hover:bg-primary/90 transition"
>
Fırsat Yolla

View File

@ -1,10 +0,0 @@
import type { Deal } from "./Deal"
import type { User } from "./User"
export type Comment = {
id: number
text: string
createdAt: string
user:Pick<User,"username" | "avatarUrl">
deal: Pick<Deal, "id" | "title"> // sadece id ve title yeterli
}

View File

@ -1,7 +1,8 @@
import type { DealImage } from "./DealImage"
import type { Comment } from "./Comment"
import type { DealVote } from "./DealVote"
import type { DealImage } from "./deal/DealImage.ts"
import type { DealVote } from "./deal/DealVote.ts"
import type { User } from "./User"
import type { Seller } from "./seller/Seller.ts"
export type Deal = {
id: number
@ -9,13 +10,21 @@ export type Deal = {
description?: string
url?: string
price?: number
score: number
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED"
saleType: "ONLINE" | "OFFLINE" | "CODE"
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE"
sellerName: string
createdAt: string
updatedAt: string
score: number
userId: number
// ilişkiler
user?: Pick<User, "id" | "username" | "avatarUrl">
company?: Pick<Seller, "id" | "name">
images?: DealImage[]
votes?: DealVote[]
comments?: Comment[]

13
src/models/DealDraft.ts Normal file
View File

@ -0,0 +1,13 @@
import type { Seller } from "./seller/Seller"
export type DealDraft = {
title: string
description?: string
url: string
price?: number
imageUrl: string
seller: Seller
customCompany?: string
}

View File

@ -1,6 +0,0 @@
export type DealImage = {
id: number
imageUrl: string
order: number
createdAt: string
}

View File

@ -3,9 +3,8 @@ export type User = {
id: number
username: string
email: string
avatarUrl?: string
avatarUrl: string|null
createdAt: string
updatedAt: string
}
export type PublicUser = Pick<User, "username" | "avatarUrl" | "createdAt">
export type PublicUserSummary = Pick<User, "id"|"username" | "avatarUrl" >

View File

@ -0,0 +1,25 @@
import type { PublicUserSummary } from "..//User"
import type { SellerSummary } from "..//seller/Seller"
export type DealCard = {
id: number
title: string
description: string
url?: string
price?: number
score: number
commentsCount: number
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED"
saleType: "ONLINE" | "OFFLINE" | "CODE"
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE"
createdAt: string
updatedAt: string
// ilişkiler
user: PublicUserSummary
seller: SellerSummary
imageUrl: string
}

View File

@ -0,0 +1,8 @@
import type { PublicUserSummary } from "../User"
export type DealComment = {
id: number
text: string
createdAt: string
user:PublicUserSummary
}

View File

@ -0,0 +1,27 @@
import type { PublicUserSummary } from "..//User"
import type { SellerSummary } from "..//seller/Seller"
import type { DealImage } from "./DealImage"
import type { DealComment } from "./DealComment"
export type DealDetail = {
id: number
title: string
description: string
url?: string
price?: number
score: number
commentsCount: number
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED"
saleType: "ONLINE" | "OFFLINE" | "CODE"
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE"
createdAt: string
updatedAt: string
// ilişkiler
user: PublicUserSummary
seller: SellerSummary
images: DealImage[]
comments: DealComment[]
}

View File

@ -0,0 +1,4 @@
export type DealImage = {
url: string
order: number
}

View File

@ -1,5 +1,5 @@
export * from "./User"
export * from "./Deal"
export * from "./DealImage"
export * from "./DealVote"
export * from "./Comment"
export * from "./deal/DealImage"
export * from "./deal/DealVote"
export * from "./deal/DealComment"

View File

@ -0,0 +1,7 @@
export type Seller = {
id: number
name: string
url:string | null
}
export type SellerSummary= Pick<Seller,"name"|"url">

105
src/pages/ContactPage.tsx Normal file
View File

@ -0,0 +1,105 @@
import MainLayout from "../layouts/MainLayout"
export default function ContactPage() {
return (
<MainLayout>
<div className="max-w-6xl mx-auto px-4 py-16">
{/* Header */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900">
Bizimle İletişime Geç
</h1>
<p className="mt-4 text-gray-600 max-w-xl mx-auto">
Görüşlerin, önerilerin veya birliği taleplerin için bize yazabilirsin.
</p>
</div>
{/* Content */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
{/* Left - Info */}
<div className="space-y-6">
<div>
<h2 className="text-xl font-semibold text-gray-900">
İletişim Bilgileri
</h2>
<p className="mt-2 text-gray-600">
Sana en kısa sürede dönüş yapabilmemiz için doğru bilgileri doldurduğundan emin ol.
</p>
</div>
<div className="space-y-4">
<div className="flex items-start gap-3">
<div className="text-primary text-xl">📧</div>
<div>
<p className="font-medium text-gray-900">E-posta</p>
<p className="text-gray-600">support@hotdeals.com</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-primary text-xl">🌐</div>
<div>
<p className="font-medium text-gray-900">Website</p>
<p className="text-gray-600">www.hotdeals.com</p>
</div>
</div>
<div className="flex items-start gap-3">
<div className="text-primary text-xl">📍</div>
<div>
<p className="font-medium text-gray-900">Konum</p>
<p className="text-gray-600">Türkiye</p>
</div>
</div>
</div>
</div>
{/* Right - Form */}
<div className="bg-white rounded-xl shadow-sm border p-8">
<form className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">
Ad Soyad
</label>
<input
type="text"
placeholder="Adınızı girin"
className="mt-1 w-full rounded-md border-gray-300 focus:border-primary focus:ring-primary"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
E-posta
</label>
<input
type="email"
placeholder="email@example.com"
className="mt-1 w-full rounded-md border-gray-300 focus:border-primary focus:ring-primary"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
Mesaj
</label>
<textarea
rows={4}
placeholder="Mesajını buraya yaz..."
className="mt-1 w-full rounded-md border-gray-300 focus:border-primary focus:ring-primary"
/>
</div>
<button
type="submit"
className="w-full bg-primary text-white py-3 rounded-md font-medium hover:opacity-90 transition"
>
Mesajı Gönder
</button>
</form>
</div>
</div>
</div>
</MainLayout>
)
}

View File

@ -0,0 +1,156 @@
import { useState } from "react"
import MainLayout from "../layouts/MainLayout"
import { createDeal } from "../api/deal/newDeal"
import { lookupSellerFromLink } from "../api/seller/from-lookup"
import { mapSellerFromLookupRequest } from "../adapters/requests/sellerFromLookupAdapter"
import { mapSellerFromLookupResponse } from "../adapters/responses/sellerFromLookupAdapter"
import { mapDealDraftToCreateRequest } from "../adapters/requests/dealCreateAdapter.ts"
import DealLinkStep from "../components/CreateDeal/DealLinkStep"
import DealDetailsStep from "../components/CreateDeal/DealDetailsStep"
import type { DealDraft } from "../models/DealDraft"
import type { Seller } from "../models/seller/Seller"
type Step = "link" | "details"
export default function CreateDealPage() {
const [step, setStep] = useState<Step>("link")
const [loading, setLoading] = useState(false)
const [dealDraft, setDealDraft] = useState<DealDraft>({
title: "",
description: "",
url: "",
price: undefined,
imageUrl: "",
seller: {
id: -1,
name: "",
url: null,
},
customCompany: undefined,
})
/* -------- STEP 1 — LINK -------- */
const handleLinkSubmit = async (
e: React.FormEvent<HTMLFormElement>
) => {
e.preventDefault()
// OFFLINE DEAL
if (!dealDraft.url) {
setStep("details")
return
}
setLoading(true)
try {
// 🔥 1. URL → Seller (temporary)
const tempSeller: Seller = {
id: -1,
name: "",
url: dealDraft.url,
}
// 🔥 2. Seller → Lookup Request (ADAPTER)
const lookupRequest =
mapSellerFromLookupRequest(tempSeller)
// 🔥 3. API CALL
const lookupResponse =
await lookupSellerFromLink(lookupRequest)
// 🔥 4. Response → Seller (ADAPTER)
const seller =
mapSellerFromLookupResponse(lookupResponse)
setDealDraft(d => ({
...d,
seller,
}))
setStep("details")
} catch (err) {
console.error("Seller lookup failed:", err)
setDealDraft(d => ({
...d,
seller: {
id: -1,
name: "",
url: null,
},
}))
setStep("details")
} finally {
setLoading(false)
}
}
/* -------- FINAL SUBMIT -------- */
const handleFinalSubmit = async () => {
try {
await createDeal(
mapDealDraftToCreateRequest(dealDraft)
)
alert("Fırsat başarıyla gönderildi.")
resetForm()
} catch (err: unknown) {
alert(err instanceof Error ? err.message : "Sunucu hatası")
}
}
function resetForm() {
setDealDraft({
title: "",
description: "",
url: "",
price: undefined,
imageUrl: "",
seller: {
id: -1,
name: "",
url: null,
},
customCompany: undefined,
})
setStep("link")
}
return (
<MainLayout>
<div className="max-w-xl mx-auto bg-surface p-6 mt-8 rounded-lg">
{step === "link" && (
<DealLinkStep
url={dealDraft.url}
loading={loading}
onChange={(url) =>
setDealDraft(d => ({
...d,
url,
}))
}
onSubmit={handleLinkSubmit}
/>
)}
{step === "details" && (
<DealDetailsStep
data={dealDraft}
onChange={setDealDraft}
onBack={() => setStep("link")}
onSubmit={handleFinalSubmit}
/>
)}
</div>
</MainLayout>
)
}

View File

@ -0,0 +1,77 @@
// src/pages/DealPage.tsx
import { useEffect, useState } from "react"
import { useParams } from "react-router-dom"
import MainLayout from "../layouts/MainLayout"
import DealImages from "../components/DealDetails/DealImages"
import DealDetails from "../components/DealDetails/DealDetails"
import DealDescription from "../components/DealDetails/DealDescription"
import DealComments from "../components/DealDetails/DealComments"
import { getDealDetail } from "../api/deal/getDeal"
import { mapDealDetailResponseToDealDetail } from "../adapters/responses/dealDetailAdapter"
import type { DealDetail } from "../models/deal/DealDetail"
type DealPageProps = {
onRequireLogin: () => void
}
export default function DealPage({ onRequireLogin }: DealPageProps) {
const { id } = useParams()
const [deal, setDeal] = useState<DealDetail | null>(null)
useEffect(() => {
if (!id) return
const loadDeal = async () => {
try {
const apiDeal = await getDealDetail(Number(id))
const mapped = mapDealDetailResponseToDealDetail(apiDeal)
setDeal(mapped)
} catch (err) {
console.error("Deal yüklenemedi:", err)
}
}
loadDeal()
}, [id])
if (!deal) return <p className="p-4">Yükleniyor...</p>
return (
<MainLayout>
<div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sol: görseller */}
<div className="lg:col-span-1 flex justify-center items-start">
<DealImages imageUrl={deal.images[0].url} />
</div>
{/* Sağ: temel bilgiler */}
<div className="lg:col-span-3 flex flex-col gap-6">
<DealDetails
title={deal.title}
price={deal.price?.toString() ?? "-"}
store={deal.seller.name}
link={deal.url ?? ""}
postedBy={deal.user.username}
postedAgo={deal.createdAt}
/>
</div>
{/* Alt: açıklama + yorumlar */}
<div className="lg:col-span-4 flex flex-col gap-6">
<section>
<DealDescription description={deal.description} />
</section>
<section>
<DealComments
dealId={deal.id}
onRequireLogin={onRequireLogin}
/>
</section>
</div>
</div>
</MainLayout>
)
}

View File

@ -1,61 +0,0 @@
// src/pages/DealPage.tsx
import { useEffect, useState } from "react"
import { useParams } from "react-router-dom"
import { getDeal } from "../api/deal/getDeal"
import MainLayout from "../layouts/MainLayout"
import DealImages from "../components/DealScreen/DealImages"
import DealDetails from "../components/DealScreen/DealDetails"
import DealDescription from "../components/DealScreen/DealDescription"
import DealComments from "../components/DealScreen/DealComments"
import type { Deal } from "../models/Deal"
type DealPageProps = {
onRequireLogin: () => void
}
export default function DealPage({ onRequireLogin }: DealPageProps) {
const { id } = useParams()
const [deal, setDeal] = useState<Deal | null>(null)
useEffect(() => {
if (!id) return
getDeal(Number(id))
.then(setDeal)
.catch((err) => console.error("Deal yüklenemedi:", err))
}, [id])
if (!deal) return <p className="p-4">Yükleniyor...</p>
return (
<MainLayout>
<div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Sol: görsel */}
<div className="lg:col-span-1 flex justify-center items-start">
<DealImages imageUrl={deal.images?.[0]?.imageUrl || "/placeholder.png"} />
</div>
{/* Sağ: detaylar */}
<div className="lg:col-span-3 flex flex-col gap-6">
<DealDetails
title={deal.title}
price={deal.price?.toString() || "-"}
store={deal.url || ""}
link={deal.url || ""}
postedBy={deal.user?.username || "Anonim"}
postedAgo={deal.createdAt}
/>
</div>
{/* Alt: açıklama + yorumlar */}
<div className="lg:col-span-4 flex flex-col gap-6">
<section>
<DealDescription description={deal.description || ""} />
</section>
<section>
<DealComments dealId={deal.id} onRequireLogin={onRequireLogin} />
</section>
</div>
</div>
</MainLayout>
)
}

View File

@ -3,26 +3,16 @@ import { useEffect, useState, useRef } from "react"
import MainLayout from "../layouts/MainLayout"
import DealCardMain from "../components/Shared/DealCardMain"
import { getDeals } from "../api/deal/getDeal"
import { mapDealCardResponseToDeal } from "../adapters/responses/dealCardAdapter"
import { timeAgo } from "../utils/timeAgo"
type Deal = {
id: number
title: string
description: string
url: string
images: { imageUrl: string }[]
price: number
score: number
createdAt: string
user?: { username: string }
}
import type { DealCard } from "../models/deal/DealCard"
type HomeProps = {
onRequireLogin: () => void
}
export default function HomePage({ onRequireLogin }: HomeProps) {
const [deals, setDeals] = useState<Deal[]>([])
const [deals, setDeals] = useState<DealCard[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
@ -33,23 +23,29 @@ export default function HomePage({ onRequireLogin }: HomeProps) {
const loadDeals = async () => {
if (loading || !hasMore) return
setLoading(true)
try {
const newDeals = await getDeals(page)
if (newDeals.length === 0) {
const apiDeals = await getDeals(page)
if (apiDeals.length === 0) {
setHasMore(false)
} else {
const mappedDeals = apiDeals.map(mapDealCardResponseToDeal)
setDeals((prev) => {
const existingIds = new Set(prev.map((d) => d.id))
const filtered = newDeals.filter((d: Deal) => !existingIds.has(d.id))
const filtered = mappedDeals.filter(
(d) => !existingIds.has(d.id)
)
return [...prev, ...filtered]
})
}
} catch (err: any) {
setError(err.message)
setError(err.message ?? "Bir hata oluştu")
} finally {
setLoading(false)
}
}
loadDeals()
}, [page])
@ -62,44 +58,54 @@ export default function HomePage({ onRequireLogin }: HomeProps) {
},
{ 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>
if (error) {
return <p className="p-4 text-red-600">{error}</p>
}
return (
<MainLayout>
<div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* SOL: 3/4 - Deal listesi */}
{/* SOL: Deal listesi */}
<div className="lg:col-span-3 space-y-4">
{deals.map((deal) => (
<DealCardMain
key={deal.id}
id={deal.id}
image={deal.images[0]?.imageUrl || "/placeholder.png"}
image={deal.imageUrl || "/placeholder.png"}
title={deal.title}
price={`${deal.price}`}
store={deal.url ? new URL(deal.url).hostname : "Bilinmiyor"}
postedBy={deal.user?.username || "unknown"}
price={deal.price ? `${deal.price}` : ""}
store={deal.seller.name}
postedBy={deal.user?.username ?? "unknown"}
score={deal.score}
comments={0}
comments={deal.commentsCount}
postedAgo={timeAgo(deal.createdAt)}
onRequireLogin={onRequireLogin}
/>
))}
{loading && <p className="text-center py-4">Yükleniyor...</p>}
{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" />
</div>
{/* SAĞ: sidebar alanı */}
{/* SAĞ: sidebar */}
<aside className="hidden lg:block bg-surface/50 rounded-lg p-4">
<p className="text-sm text-muted-foreground">Yan içerik / filtre / reklam alanı</p>
<p className="text-sm text-muted-foreground">
Yan içerik / filtre / reklam alanı
</p>
</aside>
</div>
</MainLayout>

View File

@ -5,13 +5,13 @@ import DealCardMain from "../components/Shared/DealCardMain"
import CommentCard from "../components/Profile/CommentCard"
import { fetchUserProfile } from "../services/userService"
import type { Deal, Comment } from "../models"
import type { PublicUser } from "../models/User"
import type { PublicUserSummary } from "../models/User"
export default function ProfilePage() {
const { userName } = useParams<{ userName: string }>()
const [activeTab, setActiveTab] = useState<"deals" | "comments">("deals")
const [user, setUser] = useState<PublicUser | null>(null)
const [user, setUser] = useState<PublicUserSummary | null>(null)
const [deals, setDeals] = useState<Deal[]>([])
const [comments, setComments] = useState<Comment[]>([])
const [loading, setLoading] = useState(true)

View File

@ -1,8 +1,8 @@
import { getUser } from "../api/user/getUser"
import type { PublicUser, Deal, Comment } from "../models"
import type { PublicUserSummary, Deal, Comment } from "../models"
export type UserProfile = {
user: PublicUser
user: PublicUserSummary
deals: Deal[]
comments: Comment[]
}
@ -10,7 +10,7 @@ export type UserProfile = {
export async function fetchUserProfile(userName: string): Promise<UserProfile> {
const data = await getUser(userName)
const user: PublicUser = {
const user: PublicUserSummary = {
username: data.user.username,
avatarUrl: data.user.avatarUrl,
createdAt: data.user.createdAt,