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 { BrowserRouter, Routes, Route } from "react-router-dom"
|
||||||
|
|
||||||
import HomePage from "./pages/HomePage"
|
import HomePage from "./pages/HomePage"
|
||||||
import DealPage from "./pages/DealPage"
|
import DealPage from "./pages/DealDetailsPage"
|
||||||
import SubmitDealPage from "./pages/SubmitDealPage"
|
|
||||||
|
|
||||||
|
import CreateDealPage from "./pages/CreateDealPage"
|
||||||
import AccountSettingsPage from "./pages/AccountSettingsPage"
|
import AccountSettingsPage from "./pages/AccountSettingsPage"
|
||||||
import ProfilePage from "./pages/ProfilePage"
|
import ProfilePage from "./pages/ProfilePage"
|
||||||
import SearchPage from "./pages/SearchPage"
|
import SearchPage from "./pages/SearchPage"
|
||||||
|
|
@ -33,7 +35,8 @@ export default function App() {
|
||||||
element={<SearchPage onRequireLogin={() => setShowLoginModal(true)} />}
|
element={<SearchPage onRequireLogin={() => setShowLoginModal(true)} />}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/submit-deal" element={<SubmitDealPage />} />
|
|
||||||
|
<Route path="/create-deal" element={<CreateDealPage />} />
|
||||||
<Route path="/account" element={<AccountSettingsPage />} />
|
<Route path="/account" element={<AccountSettingsPage />} />
|
||||||
<Route path="/user/:userName" element={<ProfilePage />} />
|
<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
|
// src/api/deal/dealApi.ts
|
||||||
import instance from "../axiosInstance"
|
import instance from "../axiosInstance"
|
||||||
|
import type { DealCardResponse,DealDetailResponse} from "./types"
|
||||||
|
|
||||||
export async function getDeals(page = 1) {
|
export async function getDeals(
|
||||||
try {
|
page = 1
|
||||||
|
): Promise<DealCardResponse[]> {
|
||||||
const { data } = await instance.get(`/deals?page=${page}`)
|
const { data } = await instance.get(`/deals?page=${page}`)
|
||||||
return data.results
|
return data.results
|
||||||
} catch (error) {
|
|
||||||
console.error("Deal listesi hatası:", error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDeal(id: number) {
|
export async function getDealDetail(
|
||||||
try {
|
id: number
|
||||||
|
): Promise<DealDetailResponse> {
|
||||||
const { data } = await instance.get(`/deals/${id}`)
|
const { data } = await instance.get(`/deals/${id}`)
|
||||||
return data
|
return data
|
||||||
} catch (error) {
|
|
||||||
console.error("Deal alma hatası:", error)
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ type DealData = {
|
||||||
description?: string
|
description?: string
|
||||||
url?: string
|
url?: string
|
||||||
imageUrl?: string
|
imageUrl?: string
|
||||||
|
customCompany?: string
|
||||||
price?: number
|
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 { getComments, postComment } from "../../api/deal/commentDeal"
|
||||||
import { useAuth } from "../../context/AuthContext"
|
import { useAuth } from "../../context/AuthContext"
|
||||||
import { timeAgo } from "../../utils/timeAgo"
|
import { timeAgo } from "../../utils/timeAgo"
|
||||||
import type { Comment } from "../../models/Comment"
|
import type { Comment } from "../../models/deal/DealComment"
|
||||||
|
|
||||||
type DealCommentsProps = {
|
type DealCommentsProps = {
|
||||||
dealId: number
|
dealId: number
|
||||||
|
|
@ -47,7 +47,7 @@ export default function Navbar() {
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<UserInfo />
|
<UserInfo />
|
||||||
<Link
|
<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"
|
className="bg-primary text-white font-semibold px-4 py-2 rounded-md hover:bg-primary/90 transition"
|
||||||
>
|
>
|
||||||
Fırsat Yolla
|
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 { DealImage } from "./deal/DealImage.ts"
|
||||||
import type { Comment } from "./Comment"
|
|
||||||
import type { DealVote } from "./DealVote"
|
import type { DealVote } from "./deal/DealVote.ts"
|
||||||
import type { User } from "./User"
|
import type { User } from "./User"
|
||||||
|
import type { Seller } from "./seller/Seller.ts"
|
||||||
|
|
||||||
export type Deal = {
|
export type Deal = {
|
||||||
id: number
|
id: number
|
||||||
|
|
@ -9,13 +10,21 @@ export type Deal = {
|
||||||
description?: string
|
description?: string
|
||||||
url?: string
|
url?: string
|
||||||
price?: number
|
price?: number
|
||||||
|
|
||||||
|
score: number
|
||||||
|
status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED"
|
||||||
|
saleType: "ONLINE" | "OFFLINE" | "CODE"
|
||||||
|
affiliateType: "AFFILIATE" | "NON_AFFILIATE" | "USER_AFFILIATE"
|
||||||
|
|
||||||
|
|
||||||
|
sellerName: string
|
||||||
|
|
||||||
createdAt: string
|
createdAt: string
|
||||||
updatedAt: string
|
updatedAt: string
|
||||||
score: number
|
|
||||||
userId: number
|
|
||||||
|
|
||||||
// ilişkiler
|
// ilişkiler
|
||||||
user?: Pick<User, "id" | "username" | "avatarUrl">
|
user?: Pick<User, "id" | "username" | "avatarUrl">
|
||||||
|
company?: Pick<Seller, "id" | "name">
|
||||||
images?: DealImage[]
|
images?: DealImage[]
|
||||||
votes?: DealVote[]
|
votes?: DealVote[]
|
||||||
comments?: Comment[]
|
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
|
id: number
|
||||||
username: string
|
username: string
|
||||||
email: string
|
email: string
|
||||||
avatarUrl?: string
|
avatarUrl: string|null
|
||||||
createdAt: string
|
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 "./User"
|
||||||
export * from "./Deal"
|
export * from "./Deal"
|
||||||
export * from "./DealImage"
|
export * from "./deal/DealImage"
|
||||||
export * from "./DealVote"
|
export * from "./deal/DealVote"
|
||||||
export * from "./Comment"
|
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 MainLayout from "../layouts/MainLayout"
|
||||||
import DealCardMain from "../components/Shared/DealCardMain"
|
import DealCardMain from "../components/Shared/DealCardMain"
|
||||||
import { getDeals } from "../api/deal/getDeal"
|
import { getDeals } from "../api/deal/getDeal"
|
||||||
|
import { mapDealCardResponseToDeal } from "../adapters/responses/dealCardAdapter"
|
||||||
import { timeAgo } from "../utils/timeAgo"
|
import { timeAgo } from "../utils/timeAgo"
|
||||||
|
import type { DealCard } from "../models/deal/DealCard"
|
||||||
type Deal = {
|
|
||||||
id: number
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
url: string
|
|
||||||
images: { imageUrl: string }[]
|
|
||||||
price: number
|
|
||||||
score: number
|
|
||||||
createdAt: string
|
|
||||||
user?: { username: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
type HomeProps = {
|
type HomeProps = {
|
||||||
onRequireLogin: () => void
|
onRequireLogin: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HomePage({ onRequireLogin }: HomeProps) {
|
export default function HomePage({ onRequireLogin }: HomeProps) {
|
||||||
const [deals, setDeals] = useState<Deal[]>([])
|
const [deals, setDeals] = useState<DealCard[]>([])
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [hasMore, setHasMore] = useState(true)
|
const [hasMore, setHasMore] = useState(true)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
@ -33,23 +23,29 @@ export default function HomePage({ onRequireLogin }: HomeProps) {
|
||||||
const loadDeals = async () => {
|
const loadDeals = async () => {
|
||||||
if (loading || !hasMore) return
|
if (loading || !hasMore) return
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newDeals = await getDeals(page)
|
const apiDeals = await getDeals(page)
|
||||||
if (newDeals.length === 0) {
|
if (apiDeals.length === 0) {
|
||||||
setHasMore(false)
|
setHasMore(false)
|
||||||
} else {
|
} else {
|
||||||
|
const mappedDeals = apiDeals.map(mapDealCardResponseToDeal)
|
||||||
|
|
||||||
setDeals((prev) => {
|
setDeals((prev) => {
|
||||||
const existingIds = new Set(prev.map((d) => d.id))
|
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]
|
return [...prev, ...filtered]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(err.message)
|
setError(err.message ?? "Bir hata oluştu")
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDeals()
|
loadDeals()
|
||||||
}, [page])
|
}, [page])
|
||||||
|
|
||||||
|
|
@ -62,44 +58,54 @@ export default function HomePage({ onRequireLogin }: HomeProps) {
|
||||||
},
|
},
|
||||||
{ threshold: 1 }
|
{ threshold: 1 }
|
||||||
)
|
)
|
||||||
|
|
||||||
if (observerRef.current) observer.observe(observerRef.current)
|
if (observerRef.current) observer.observe(observerRef.current)
|
||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
}, [hasMore, loading])
|
}, [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 (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
|
<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">
|
<div className="lg:col-span-3 space-y-4">
|
||||||
{deals.map((deal) => (
|
{deals.map((deal) => (
|
||||||
<DealCardMain
|
<DealCardMain
|
||||||
key={deal.id}
|
key={deal.id}
|
||||||
id={deal.id}
|
id={deal.id}
|
||||||
image={deal.images[0]?.imageUrl || "/placeholder.png"}
|
image={deal.imageUrl || "/placeholder.png"}
|
||||||
title={deal.title}
|
title={deal.title}
|
||||||
price={`${deal.price}₺`}
|
price={deal.price ? `${deal.price}₺` : ""}
|
||||||
store={deal.url ? new URL(deal.url).hostname : "Bilinmiyor"}
|
store={deal.seller.name}
|
||||||
postedBy={deal.user?.username || "unknown"}
|
postedBy={deal.user?.username ?? "unknown"}
|
||||||
score={deal.score}
|
score={deal.score}
|
||||||
comments={0}
|
comments={deal.commentsCount}
|
||||||
postedAgo={timeAgo(deal.createdAt)}
|
postedAgo={timeAgo(deal.createdAt)}
|
||||||
onRequireLogin={onRequireLogin}
|
onRequireLogin={onRequireLogin}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{loading && <p className="text-center py-4">Yükleniyor...</p>}
|
|
||||||
|
{loading && (
|
||||||
|
<p className="text-center py-4">Yükleniyor...</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{!hasMore && (
|
{!hasMore && (
|
||||||
<p className="text-center py-4 text-muted-foreground">
|
<p className="text-center py-4 text-muted-foreground">
|
||||||
Tüm fırsatlar yüklendi.
|
Tüm fırsatlar yüklendi.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={observerRef} className="h-8" />
|
<div ref={observerRef} className="h-8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SAĞ: sidebar alanı */}
|
{/* SAĞ: sidebar */}
|
||||||
<aside className="hidden lg:block bg-surface/50 rounded-lg p-4">
|
<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>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import DealCardMain from "../components/Shared/DealCardMain"
|
||||||
import CommentCard from "../components/Profile/CommentCard"
|
import CommentCard from "../components/Profile/CommentCard"
|
||||||
import { fetchUserProfile } from "../services/userService"
|
import { fetchUserProfile } from "../services/userService"
|
||||||
import type { Deal, Comment } from "../models"
|
import type { Deal, Comment } from "../models"
|
||||||
import type { PublicUser } from "../models/User"
|
import type { PublicUserSummary } from "../models/User"
|
||||||
|
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const { userName } = useParams<{ userName: string }>()
|
const { userName } = useParams<{ userName: string }>()
|
||||||
const [activeTab, setActiveTab] = useState<"deals" | "comments">("deals")
|
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 [deals, setDeals] = useState<Deal[]>([])
|
||||||
const [comments, setComments] = useState<Comment[]>([])
|
const [comments, setComments] = useState<Comment[]>([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { getUser } from "../api/user/getUser"
|
import { getUser } from "../api/user/getUser"
|
||||||
import type { PublicUser, Deal, Comment } from "../models"
|
import type { PublicUserSummary, Deal, Comment } from "../models"
|
||||||
|
|
||||||
export type UserProfile = {
|
export type UserProfile = {
|
||||||
user: PublicUser
|
user: PublicUserSummary
|
||||||
deals: Deal[]
|
deals: Deal[]
|
||||||
comments: Comment[]
|
comments: Comment[]
|
||||||
}
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ export type UserProfile = {
|
||||||
export async function fetchUserProfile(userName: string): Promise<UserProfile> {
|
export async function fetchUserProfile(userName: string): Promise<UserProfile> {
|
||||||
const data = await getUser(userName)
|
const data = await getUser(userName)
|
||||||
|
|
||||||
const user: PublicUser = {
|
const user: PublicUserSummary = {
|
||||||
username: data.user.username,
|
username: data.user.username,
|
||||||
avatarUrl: data.user.avatarUrl,
|
avatarUrl: data.user.avatarUrl,
|
||||||
createdAt: data.user.createdAt,
|
createdAt: data.user.createdAt,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user