diff --git a/index.html b/index.html index 072a57e..c34366c 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,5 @@ + diff --git a/package-lock.json b/package-lock.json index 4c9e5b2..7362c5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.1", + "lucide-react": "^0.562.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.4" @@ -3703,6 +3704,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", diff --git a/package.json b/package.json index 2d2104c..e5fbd63 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dependencies": { "@tailwindcss/vite": "^4.1.16", "axios": "^1.13.1", + "lucide-react": "^0.562.0", "react": "^19.1.1", "react-dom": "^19.1.1", "react-router-dom": "^7.9.4" diff --git a/src/App.tsx b/src/App.tsx index 72529c3..9085cf9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { useState } from "react" import { BrowserRouter, Routes, Route } from "react-router-dom" - +import { ErrorBoundary } from "./components/ErrorBoundary" import HomePage from "./pages/HomePage" import DealPage from "./pages/DealDetailsPage" @@ -16,6 +16,7 @@ export default function App() { const [showLoginModal, setShowLoginModal] = useState(false) return ( + {showLoginModal && ( + ) } diff --git a/src/adapters/requests/dealCreateAdapter.ts b/src/adapters/requests/dealCreateAdapter.ts index aadf95b..7424b10 100644 --- a/src/adapters/requests/dealCreateAdapter.ts +++ b/src/adapters/requests/dealCreateAdapter.ts @@ -1,18 +1,22 @@ - +// adapters/requests/dealCreateAdapter.ts 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, +export function mapDealDraftToCreateRequest(draft: DealDraft): FormData { + const fd = new FormData() - url: draft.url || undefined, + fd.append("title", draft.title) + if (draft.description) fd.append("description", draft.description) + if (draft.url) fd.append("url", draft.url) - sellerName:draft.seller.name - } + if (draft.price != null) fd.append("price", String(draft.price)) + + +if (draft.customCompany) fd.append("sellerName", draft.customCompany) + + // files + ;(draft.images ?? []).slice(0, 5).forEach((f) => { + fd.append("images", f) // field adı backend'deki upload.array("images", 5) ile aynı + }) + + return fd } diff --git a/src/adapters/requests/sellerFromLookupAdapter.ts b/src/adapters/requests/sellerFromLookupAdapter.ts deleted file mode 100644 index dee7319..0000000 --- a/src/adapters/requests/sellerFromLookupAdapter.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Seller } from "../../models/seller/Seller"; -import type { SellerFromLookupRequest } from "../../api/seller/types"; - -export function mapSellerFromLookupRequest(seller:Seller):SellerFromLookupRequest{ - return{ - url:seller.url - } -} \ No newline at end of file diff --git a/src/adapters/responses/dealCardAdapter.ts b/src/adapters/responses/dealCardAdapter.ts index c41a67f..c46847f 100644 --- a/src/adapters/responses/dealCardAdapter.ts +++ b/src/adapters/responses/dealCardAdapter.ts @@ -16,6 +16,8 @@ export function mapDealCardResponseToDeal( status: api.status, saleType: api.saleType, affiliateType: api.affiliateType, + + myVote:api.myVote, createdAt: api.createdAt, updatedAt:api.updatedAt, diff --git a/src/api/auth/login.ts b/src/api/auth/login.ts index e58402e..e84f6f8 100644 --- a/src/api/auth/login.ts +++ b/src/api/auth/login.ts @@ -1,13 +1,10 @@ // src/api/auth/login.ts import instance from "../axiosInstance" +import type { LoginInput } from "./types" -export async function login(email: string, password: string) { - try { - const { data } = await instance.post("/auth/login", { email, password }) - return data // { token, user } - } catch (error: any) { - const message = error.response?.data?.message || "Giriş başarısız" - console.error("Login hatası:", message) - throw new Error(message) - } + + +export async function login(input: LoginInput) { + const { data } = await instance.post("/auth/login", input) + return data } diff --git a/src/api/auth/types.ts b/src/api/auth/types.ts new file mode 100644 index 0000000..552b7c9 --- /dev/null +++ b/src/api/auth/types.ts @@ -0,0 +1,4 @@ +export type LoginInput = { + email: string + password: string +} diff --git a/src/api/deal/commentDeal.ts b/src/api/deal/commentDeal.ts index d877ce9..9590882 100644 --- a/src/api/deal/commentDeal.ts +++ b/src/api/deal/commentDeal.ts @@ -1,9 +1,11 @@ // src/api/deal/commentApi.ts import instance from "../axiosInstance" -export async function getComments(dealId: number) { +import type { Comment } from "../../models" + +export async function getComments(dealId: number): Promise { try { - const { data } = await instance.get(`/comments/${dealId}`) + const { data } = await instance.get(`/comments/${dealId}`) return data } catch (error: any) { const message = error.response?.data?.error || "Yorumlar alınamadı" diff --git a/src/api/deal/getDeal.ts b/src/api/deal/getDeal.ts index f050ec6..a287a9a 100644 --- a/src/api/deal/getDeal.ts +++ b/src/api/deal/getDeal.ts @@ -1,17 +1,16 @@ // src/api/deal/dealApi.ts import instance from "../axiosInstance" -import type { DealCardResponse,DealDetailResponse} from "./types" +import type { DealCard } from "../../models/deal/DealCard" +import type { DealDetail } from "../../models/deal/DealDetail" -export async function getDeals( - page = 1 -): Promise { - const { data } = await instance.get(`/deals?page=${page}`) +export async function getDeals(page = 1): Promise { + const { data } = await instance.get<{ results: DealCard[] }>( + `/deals?page=${page}` + ) + console.log(data.results) return data.results } - -export async function getDealDetail( - id: number -): Promise { - const { data } = await instance.get(`/deals/${id}`) +export async function getDealDetail(id: number): Promise { + const { data } = await instance.get(`/deals/${id}`) return data } diff --git a/src/api/deal/newDeal.ts b/src/api/deal/newDeal.ts index b8addc8..d22aaed 100644 --- a/src/api/deal/newDeal.ts +++ b/src/api/deal/newDeal.ts @@ -1,21 +1,18 @@ // src/api/deal/createDeal.ts import instance from "../axiosInstance" -type DealData = { - title: string - description?: string - url?: string - imageUrl?: string - customCompany?: string - price?: number -} - -export async function createDeal(dealData: DealData) { +export async function createDeal(formData: FormData) { try { - const { data } = await instance.post("/deals", dealData) + + const { data } = await instance.post("/deals", formData, { + // Axios FormData gönderirken header'ı genelde kendi ayarlar. + // Ama bazı kurulumlarda gerekebilir: + headers: { "Content-Type": "multipart/form-data" }, + }) return data } catch (error: any) { - const message = error.response?.data?.error || "Fırsat eklenemedi" + const message = + error.response?.data?.error || "Fırsat eklenemedi" console.error("Fırsat oluşturma hatası:", message) throw new Error(message) } diff --git a/src/api/deal/searchDeal.ts b/src/api/deal/searchDeal.ts index 4d0f1cc..46bfe79 100644 --- a/src/api/deal/searchDeal.ts +++ b/src/api/deal/searchDeal.ts @@ -1,14 +1,17 @@ import instance from "../axiosInstance" +import type { DealCard } from "../../models/deal/DealCard" -export async function searchDeals(query: string, page = 1) { - try { - const { data } = await instance.get(`/deals/search`, { + +export async function searchDeals( + query: string, + page = 1 +): Promise { + const { data } = await instance.get<{ results: DealCard[] }>( + `/deals/`, + { params: { q: query, page }, - }) - // backend response { results, total, totalPages } formatındaysa: - return data.results - } catch (error) { - console.error("Deal arama hatası:", error) - throw error - } -} + } + ) + + return data.results +} \ No newline at end of file diff --git a/src/api/deal/types.ts b/src/api/deal/types.ts index 73c2906..425cc5e 100644 --- a/src/api/deal/types.ts +++ b/src/api/deal/types.ts @@ -6,6 +6,7 @@ export type DealCardResponse = { description: string // DB null → backend "" yapar price: number | null // fiyat yoksa bilinçli null + myVote: -1 | 0 | 1 score: number status: "PENDING" | "ACTIVE" | "EXPIRED" | "REJECTED" saleType: "ONLINE" | "OFFLINE" | "CODE" @@ -87,7 +88,7 @@ export type CreateDealRequest = { description?: string price?: number imageUrl: string - + images?:File[] // online deal url?: string diff --git a/src/api/deal/voteDeal.ts b/src/api/deal/voteDeal.ts index 167ee5f..d2574bc 100644 --- a/src/api/deal/voteDeal.ts +++ b/src/api/deal/voteDeal.ts @@ -1,9 +1,9 @@ // src/api/deal/voteDeal.ts import instance from "../axiosInstance" -export async function voteDeal(dealId: number, type: "UP" | "DOWN") { +export async function voteDeal(dealId: number, type: 1 | 0 | -1) { try { - const { data } = await instance.post("/deal-votes", { + const { data } = await instance.post("/vote", { dealId, voteType: type, }) diff --git a/src/api/seller/from-lookup.ts b/src/api/seller/from-lookup.ts index bcfb377..3058e54 100644 --- a/src/api/seller/from-lookup.ts +++ b/src/api/seller/from-lookup.ts @@ -1,13 +1,12 @@ -import type { SellerFromLookupResponse,SellerFromLookupRequest } from "./types" +import type { SellerLookupInput } from "./types" +import type { Seller } from "../../models/seller/Seller" + import instance from "../axiosInstance" -export async function lookupSellerFromLink(seller:SellerFromLookupRequest) :Promise{ - const { data } = await instance.post( - "/seller/from-link", - { seller } - ) - +export async function lookupSellerFromLink(input: SellerLookupInput): Promise { + const { data } = await instance.post("/seller/from-link", input) return data } + diff --git a/src/api/seller/types.ts b/src/api/seller/types.ts index 3b992e7..23fa645 100644 --- a/src/api/seller/types.ts +++ b/src/api/seller/types.ts @@ -4,6 +4,6 @@ export type SellerFromLookupResponse= { } -export type SellerFromLookupRequest={ +export type SellerLookupInput={ url:string|null } \ No newline at end of file diff --git a/src/api/user/getUser.ts b/src/api/user/getUser.ts index 11231aa..dec6c2b 100644 --- a/src/api/user/getUser.ts +++ b/src/api/user/getUser.ts @@ -1,12 +1,15 @@ // src/api/user/getUser.ts import instance from "../axiosInstance" +import type { UserProfile } from "../../models/user/UserProfile" -export async function getUser(userName: string) { +export async function getUser(userName: string): Promise { try { - const res = await instance.get(`/user/${userName}`) - return res.data // { user, deals, comments } + const { data } = await instance.get(`/user/${userName}`) + return data } catch (err: any) { console.error("Kullanıcı bilgileri alınamadı:", err) - throw new Error(err.response?.data?.message || "Kullanıcı bilgileri alınamadı") + throw new Error( + err.response?.data?.message || "Kullanıcı bilgileri alınamadı" + ) } -} +} \ No newline at end of file diff --git a/src/components/Auth/LoginModal.tsx b/src/components/Auth/LoginModal.tsx index 62b296a..f6ac41e 100644 --- a/src/components/Auth/LoginModal.tsx +++ b/src/components/Auth/LoginModal.tsx @@ -2,7 +2,7 @@ import { useState } from "react" import { useAuth } from "../../context/AuthContext" import { login as loginApi } from "../../api/auth/login" import { register as registerApi } from "../../api/auth/register" - +import type { LoginInput } from "../../api/auth/types" type LoginModalProps = { onClose: () => void } @@ -20,7 +20,12 @@ export default function LoginModal({ onClose }: LoginModalProps) { const data = await registerApi(username, email, password) login(data.user, data.token) } else { - const data = await loginApi(email, password) + + const input: LoginInput = { + email, + password, + } + const data = await loginApi(input) login(data.user, data.token) } onClose() diff --git a/src/components/CreateDeal/DealDetailsStep.tsx b/src/components/CreateDeal/DealDetailsStep.tsx index e60d6e6..b45541e 100644 --- a/src/components/CreateDeal/DealDetailsStep.tsx +++ b/src/components/CreateDeal/DealDetailsStep.tsx @@ -1,3 +1,4 @@ +import { useMemo, useRef } from "react" import type { DealDraft } from "../../models/DealDraft" type Props = { @@ -7,13 +8,47 @@ type Props = { onSubmit: () => void } -export default function DealDetailsStep({ - data, - onChange, - onBack, - onSubmit, -}: Props) { - const hasDetectedCompany = Boolean(data.sellerId) +const MAX_IMAGES = 5 + +export default function DealDetailsStep({ data, onChange, onBack, onSubmit }: Props) { +const hasDetectedCompany = data?.seller?.id !== -1 + const fileInputRef = useRef(null) + + const images = data.images ?? [] + const remaining = MAX_IMAGES - images.length + + const previews = useMemo(() => { + return images.map((f) => ({ file: f, url: URL.createObjectURL(f) })) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [images]) + + function openFileDialog() { + fileInputRef.current?.click() + } + + function onPickFiles(fileList: FileList | null) { + if (!fileList) return + const picked = Array.from(fileList).filter((f) => f.type.startsWith("image/")) + if (picked.length === 0) return + + const next = [...images, ...picked].slice(0, MAX_IMAGES) + onChange({ ...data, images: next }) + + if (fileInputRef.current) fileInputRef.current.value = "" + } + + function removeImage(index: number) { + const next = images.filter((_, i) => i !== index) + onChange({ ...data, images: next }) + } + + function moveImage(from: number, to: number) { + if (to < 0 || to >= images.length) return + const next = [...images] + const [item] = next.splice(from, 1) + next.splice(to, 0, item) + onChange({ ...data, images: next }) + } return (
- {/* BAŞLIK */} -
-

- Fırsat Detayları -

-

- Fırsata ait temel bilgileri aşağıdaki alanlara giriniz. -

-
+ {/* Outer card */} +
+
+ {/* LEFT: Photos */} +
+
+
+
+

Fotoğraflar

+

+ En fazla {MAX_IMAGES} fotoğraf ekleyebilirsiniz. +

+
- {/* BAŞLIK */} -
- - - onChange({ ...data, title: e.target.value }) - } - className="border rounded-md px-3 py-2" - required - /> -

- Kullanıcının ilk göreceği kısa ve net başlık. -

-
+ - {/* AÇIKLAMA */} -
- -