refactor and overhaul
This commit is contained in:
parent
a7a44410fd
commit
c06b4b8211
|
|
@ -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 />} />
|
||||
|
||||
|
|
|
|||
18
src/adapters/requests/dealCreateAdapter.ts
Normal file
18
src/adapters/requests/dealCreateAdapter.ts
Normal 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
|
||||
}
|
||||
}
|
||||
8
src/adapters/requests/sellerFromLookupAdapter.ts
Normal file
8
src/adapters/requests/sellerFromLookupAdapter.ts
Normal 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
|
||||
}
|
||||
}
|
||||
35
src/adapters/responses/dealCardAdapter.ts
Normal file
35
src/adapters/responses/dealCardAdapter.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
41
src/adapters/responses/dealDetailAdapter.ts
Normal file
41
src/adapters/responses/dealDetailAdapter.ts
Normal 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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
12
src/adapters/responses/sellerFromLookupAdapter.ts
Normal file
12
src/adapters/responses/sellerFromLookupAdapter.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ type DealData = {
|
|||
description?: string
|
||||
url?: string
|
||||
imageUrl?: string
|
||||
customCompany?: string
|
||||
price?: number
|
||||
}
|
||||
|
||||
|
|
|
|||
97
src/api/deal/types.ts
Normal file
97
src/api/deal/types.ts
Normal 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
|
||||
}
|
||||
|
||||
13
src/api/seller/from-lookup.ts
Normal file
13
src/api/seller/from-lookup.ts
Normal 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
9
src/api/seller/types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export type SellerFromLookupResponse= {
|
||||
id:number
|
||||
name:string
|
||||
}
|
||||
|
||||
|
||||
export type SellerFromLookupRequest={
|
||||
url:string|null
|
||||
}
|
||||
175
src/components/CreateDeal/DealDetailsStep.tsx
Normal file
175
src/components/CreateDeal/DealDetailsStep.tsx
Normal 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">
|
||||
Açı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>
|
||||
)
|
||||
}
|
||||
108
src/components/CreateDeal/DealLinkStep.tsx
Normal file
108
src/components/CreateDeal/DealLinkStep.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
47
src/components/CreateDeal/DealReviewstep.tsx
Normal file
47
src/components/CreateDeal/DealReviewstep.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
13
src/models/DealDraft.ts
Normal 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
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
export type DealImage = {
|
||||
id: number
|
||||
imageUrl: string
|
||||
order: number
|
||||
createdAt: string
|
||||
}
|
||||
|
|
@ -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" >
|
||||
25
src/models/deal/DealCard.ts
Normal file
25
src/models/deal/DealCard.ts
Normal 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
|
||||
}
|
||||
8
src/models/deal/DealComment.ts
Normal file
8
src/models/deal/DealComment.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import type { PublicUserSummary } from "../User"
|
||||
|
||||
export type DealComment = {
|
||||
id: number
|
||||
text: string
|
||||
createdAt: string
|
||||
user:PublicUserSummary
|
||||
}
|
||||
27
src/models/deal/DealDetail.ts
Normal file
27
src/models/deal/DealDetail.ts
Normal 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[]
|
||||
}
|
||||
4
src/models/deal/DealImage.ts
Normal file
4
src/models/deal/DealImage.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export type DealImage = {
|
||||
url: string
|
||||
order: number
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
7
src/models/seller/Seller.ts
Normal file
7
src/models/seller/Seller.ts
Normal 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
105
src/pages/ContactPage.tsx
Normal 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 iş 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>
|
||||
)
|
||||
}
|
||||
156
src/pages/CreateDealPage.tsx
Normal file
156
src/pages/CreateDealPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
77
src/pages/DealDetailsPage.tsx
Normal file
77
src/pages/DealDetailsPage.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user