chore: basic abilities added

This commit is contained in:
cureb 2025-11-05 14:55:26 +00:00
parent 90e325d79d
commit a7a44410fd
44 changed files with 1214 additions and 404 deletions

261
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.1",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.4" "react-router-dom": "^7.9.4"
@ -2048,6 +2049,12 @@
"dev": true, "dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.21", "version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@ -2086,6 +2093,17 @@
"postcss": "^8.1.0" "postcss": "^8.1.0"
} }
}, },
"node_modules/axios": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz",
"integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -2161,6 +2179,19 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -2229,6 +2260,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/commander": { "node_modules/commander": {
"version": "14.0.2", "version": "14.0.2",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.2.tgz",
@ -2309,6 +2352,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dependency-cruiser": { "node_modules/dependency-cruiser": {
"version": "17.2.0", "version": "17.2.0",
"resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.2.0.tgz", "resolved": "https://registry.npmjs.org/dependency-cruiser/-/dependency-cruiser-17.2.0.tgz",
@ -2418,6 +2470,20 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.241", "version": "1.5.241",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.241.tgz",
@ -2438,6 +2504,51 @@
"node": ">=10.13.0" "node": ">=10.13.0"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.11", "version": "0.25.11",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz",
@ -2821,6 +2932,42 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@ -2853,7 +3000,6 @@
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
@ -2869,6 +3015,43 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": { "node_modules/glob-parent": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -2911,6 +3094,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graceful-fs": { "node_modules/graceful-fs": {
"version": "4.2.11", "version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@ -2934,11 +3129,37 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
@ -3491,6 +3712,15 @@
"@jridgewell/sourcemap-codec": "^1.5.5" "@jridgewell/sourcemap-codec": "^1.5.5"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/memoize": { "node_modules/memoize": {
"version": "10.2.0", "version": "10.2.0",
"resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz", "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.2.0.tgz",
@ -3531,6 +3761,27 @@
"node": ">=8.6" "node": ">=8.6"
} }
}, },
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mimic-function": { "node_modules/mimic-function": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
@ -3784,6 +4035,12 @@
"node": ">= 6" "node": ">= 6"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",

View File

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.16",
"axios": "^1.13.1",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-router-dom": "^7.9.4" "react-router-dom": "^7.9.4"

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 KiB

View File

@ -1,19 +1,43 @@
import { useState } from 'react' import { useState } from "react"
import './App.css' import { BrowserRouter, Routes, Route } from "react-router-dom"
import Navbar from './components/Layout/Navbar/Navbar'
import HomePage from "./pages/HomePage"
import DealPage from "./pages/DealPage"
import SubmitDealPage from "./pages/SubmitDealPage"
import AccountSettingsPage from "./pages/AccountSettingsPage"
import ProfilePage from "./pages/ProfilePage"
import SearchPage from "./pages/SearchPage"
import LoginModal from "./components/Auth/LoginModal"
export default function App() {
const [showLoginModal, setShowLoginModal] = useState(false)
function App() {
const [count, setCount] = useState(0)
return ( return (
<> <BrowserRouter>
{showLoginModal && (
<LoginModal
onClose={() => setShowLoginModal(false)}
/>
)}
<Routes>
<Route path="/" element={<HomePage onRequireLogin={() => setShowLoginModal(true)} />} />
<Route
path="/deal/:id"
element={<DealPage onRequireLogin={() => setShowLoginModal(true)} />}
/>
<Route
path="/search"
element={<SearchPage onRequireLogin={() => setShowLoginModal(true)} />}
/>
<Route path="/submit-deal" element={<SubmitDealPage />} />
<Route path="/account" element={<AccountSettingsPage />} />
<Route path="/user/:userName" element={<ProfilePage />} />
</Routes>
</> </BrowserRouter>
) )
} }
export default App

View File

@ -1,17 +1,21 @@
const API_URL = "http://localhost:3000/api" // src/api/account/uploadAvatar.ts
import instance from "../axiosInstance"
export async function uploadAvatar(token: string, file: File) { export async function uploadAvatar(file: File) {
const formData = new FormData() try {
formData.append("file", file) const formData = new FormData()
formData.append("file", file)
const res = await fetch(`${API_URL}/account/avatar`, { const { data } = await instance.post("/account/avatar", formData, {
method: "POST", headers: {
headers: { "Content-Type": "multipart/form-data",
Authorization: `Bearer ${token}`, },
}, })
body: formData,
})
if (!res.ok) throw new Error("Avatar yükleme hatası") return data
return res.json() } catch (error: any) {
const message = error.response?.data?.error || "Avatar yükleme hatası"
console.error("Avatar yükleme hatası:", message)
throw new Error(message)
}
} }

View File

@ -1,14 +1,13 @@
// src/api/auth/login.ts
import instance from "../axiosInstance"
export async function login(email: string, password: string) { export async function login(email: string, password: string) {
const res = await fetch("http://localhost:3000/api/auth/login", { try {
method: "POST", const { data } = await instance.post("/auth/login", { email, password })
headers: { "Content-Type": "application/json" }, return data // { token, user }
body: JSON.stringify({ email, password }), } catch (error: any) {
}) const message = error.response?.data?.message || "Giriş başarısız"
console.error("Login hatası:", message)
if (!res.ok) { throw new Error(message)
const data = await res.json()
throw new Error(data.message || "Giriş başarısız")
} }
return res.json() // { token, user }
} }

13
src/api/auth/me.ts Normal file
View File

@ -0,0 +1,13 @@
// src/api/auth/me.ts
import instance from "../axiosInstance"
export async function me() {
try {
const { data } = await instance.get("/auth/me")
return data
} catch (error: any) {
const message = error.response?.data?.error || "Kullanıcı bilgisi alınamadı"
console.error("Kullanıcı bilgisi alma hatası:", message)
throw new Error(message)
}
}

View File

@ -1,14 +1,17 @@
// src/api/auth/register.ts
import instance from "../axiosInstance"
export async function register(username: string, email: string, password: string) { export async function register(username: string, email: string, password: string) {
const res = await fetch("http://localhost:3000/api/auth/register", { try {
method: "POST", const { data } = await instance.post("/auth/register", {
headers: { "Content-Type": "application/json" }, username,
body: JSON.stringify({ username, email, password }), email,
}) password,
})
if (!res.ok) { return data // { token, user }
const data = await res.json() } catch (error: any) {
throw new Error(data.message || "Kayıt başarısız") const message = error.response?.data?.message || "Kayıt başarısız"
console.error("Register hatası:", message)
throw new Error(message)
} }
return res.json() // { token, user }
} }

27
src/api/axiosInstance.ts Normal file
View File

@ -0,0 +1,27 @@
import axios from "axios";
const instance = axios.create({
baseURL: "http://localhost:3000/api",
headers: {
"Content-Type": "application/json",
},
});
instance.interceptors.request.use((config) => {
const token = localStorage.getItem("token");
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
instance.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.clear();
window.location.href = "/" // anasayfaya yönlendir
}
return Promise.reject(error);
}
);
export default instance;

View File

@ -1,24 +1,24 @@
const API_URL = "http://localhost:3000/api/comments" // src/api/deal/commentApi.ts
import instance from "../axiosInstance"
export async function getComments(dealId: number) { export async function getComments(dealId: number) {
const res = await fetch(`${API_URL}/${dealId}`) try {
const data = await res.json() const { data } = await instance.get(`/comments/${dealId}`)
if (!res.ok) throw new Error(data.error || "Yorumlar alınamadı") return data
return data } catch (error: any) {
const message = error.response?.data?.error || "Yorumlar alınamadı"
console.error("Yorumları alma hatası:", message)
throw new Error(message)
}
} }
export async function postComment(token: string, dealId: number, text: string) { export async function postComment(dealId: number, text: string) {
const res = await fetch(API_URL, { try {
method: "POST", const { data } = await instance.post("/comments", { dealId, text })
headers: { return data
"Content-Type": "application/json", } catch (error: any) {
Authorization: `Bearer ${token}`, const message = error.response?.data?.error || "Yorum gönderilemedi"
}, console.error("Yorum gönderme hatası:", message)
body: JSON.stringify({ dealId, text }), throw new Error(message)
}) }
const data = await res.json()
if (!res.ok) throw new Error(data.error || "Yorum gönderilemedi")
return data
} }

View File

@ -1,22 +1,22 @@
const API_URL = "http://localhost:3000/api" // src/api/deal/dealApi.ts
import instance from "../axiosInstance"
export async function getDeals(page = 1) { export async function getDeals(page = 1) {
try { try {
const res = await fetch(`http://localhost:3000/api/deals?page=${page}`) const { data } = await instance.get(`/deals?page=${page}`)
if (!res.ok) throw new Error("Deal listesi alınamadı")
const data = await res.json()
// Sadece results dizisini döndür
return data.results return data.results
} catch (err) { } catch (error) {
console.error("Deal listesi hatası:", err) console.error("Deal listesi hatası:", error)
throw err throw error
} }
} }
export async function getDeal(id: number) { export async function getDeal(id: number) {
const res = await fetch(`${API_URL}/deals/${id}`) try {
if (!res.ok) throw new Error("Deal alınamadı") const { data } = await instance.get(`/deals/${id}`)
return res.json() return data
} } catch (error) {
console.error("Deal alma hatası:", error)
throw error
}
}

View File

@ -1,22 +1,21 @@
const API_URL = "http://localhost:3000/api" // src/api/deal/createDeal.ts
import instance from "../axiosInstance"
export async function createDeal(token: string, dealData: { type DealData = {
title: string title: string
description?: string description?: string
url?: string url?: string
imageUrl?: string imageUrl?: string
price?: number price?: number
}) { }
const res = await fetch(`${API_URL}/deals`, {
method: "POST", export async function createDeal(dealData: DealData) {
headers: { try {
"Content-Type": "application/json", const { data } = await instance.post("/deals", dealData)
Authorization: `Bearer ${token}`, return data
}, } catch (error: any) {
body: JSON.stringify(dealData), const message = error.response?.data?.error || "Fırsat eklenemedi"
}) console.error("Fırsat oluşturma hatası:", message)
throw new Error(message)
const data = await res.json() }
if (!res.ok) throw new Error(data.error || "Fırsat eklenemedi")
return data
} }

View File

@ -0,0 +1,14 @@
import instance from "../axiosInstance"
export async function searchDeals(query: string, page = 1) {
try {
const { data } = await instance.get(`/deals/search`, {
params: { q: query, page },
})
// backend response { results, total, totalPages } formatındaysa:
return data.results
} catch (error) {
console.error("Deal arama hatası:", error)
throw error
}
}

View File

@ -1,14 +1,15 @@
const API_URL = "http://localhost:3000/api" // src/api/deal/voteDeal.ts
import instance from "../axiosInstance"
export async function voteDeal(token: string, dealId: number, type: "UP" | "DOWN") { export async function voteDeal(dealId: number, type: "UP" | "DOWN") {
const res = await fetch(`${API_URL}/deal-votes`, { try {
method: "POST", const { data } = await instance.post("/deal-votes", {
headers: { dealId,
"Content-Type": "application/json", voteType: type,
Authorization: `Bearer ${token}`, })
}, return data
body: JSON.stringify({ dealId, voteType: type }), } catch (error) {
}) console.error("Vote hatası:", error)
if (!res.ok) throw new Error("Vote hatası") throw error
return res.json() }
} }

12
src/api/user/getUser.ts Normal file
View File

@ -0,0 +1,12 @@
// src/api/user/getUser.ts
import instance from "../axiosInstance"
export async function getUser(userName: string) {
try {
const res = await instance.get(`/user/${userName}`)
return res.data // { user, deals, comments }
} catch (err: any) {
console.error("Kullanıcı bilgileri alınamadı:", err)
throw new Error(err.response?.data?.message || "Kullanıcı bilgileri alınamadı")
}
}

View File

@ -1,31 +1,38 @@
import { useState } from "react"; 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"
type LoginModalProps = { type LoginModalProps = {
onClose: () => void; onClose: () => void
onLogin: (email: string, password: string) => void; }
onRegister: (username: string, email: string, password: string) => void;
};
export default function LoginModal({ onClose, onLogin, onRegister }: LoginModalProps) { export default function LoginModal({ onClose }: LoginModalProps) {
const [isRegister, setIsRegister] = useState(false); const [isRegister, setIsRegister] = useState(false)
const [username, setUsername] =useState(""); const [username, setUsername] = useState("")
const [email, setEmail] = useState(""); const [email, setEmail] = useState("")
const [password, setPassword] = useState(""); const [password, setPassword] = useState("")
const { login } = useAuth() // global auth fonksiyonu
const handleSubmit = () => { const handleSubmit = async () => {
if (isRegister) { try {
if (!username || !email || !password) return; if (isRegister) {
onRegister(username, email, password); const data = await registerApi(username, email, password)
} else { login(data.user, data.token)
if (!email || !password) return; } else {
onLogin(email, password); const data = await loginApi(email, password)
login(data.user, data.token)
}
onClose()
} catch (err) {
alert("Giriş/Kayıt başarısız")
} }
}; }
return ( return (
<div className="fixed inset-0 flex items-center justify-center bg-black/60"> <div className="fixed inset-0 flex items-center justify-center bg-black/60">
<div className="bg-background text-text p-8 rounded-2xl w-96 shadow-xl relative"> <div className="bg-background p-8 rounded-2xl w-96 shadow-xl relative">
<button onClick={onClose} className="absolute top-3 right-3 text-text-muted hover:text-text">×</button> <button onClick={onClose} className="absolute top-3 right-3">×</button>
<h2 className="text-xl font-semibold mb-6 text-center"> <h2 className="text-xl font-semibold mb-6 text-center">
{isRegister ? "Kayıt Ol" : "Giriş Yap"} {isRegister ? "Kayıt Ol" : "Giriş Yap"}
</h2> </h2>
@ -36,7 +43,7 @@ export default function LoginModal({ onClose, onLogin, onRegister }: LoginModalP
placeholder="Kullanıcı adı" placeholder="Kullanıcı adı"
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
className="w-full mb-3 p-3 rounded bg-surface border border-neutral-700 text-text placeholder-text-muted" className="w-full mb-3 p-3 rounded bg-surface border"
/> />
)} )}
@ -45,27 +52,23 @@ export default function LoginModal({ onClose, onLogin, onRegister }: LoginModalP
placeholder="E-posta" placeholder="E-posta"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full mb-3 p-3 rounded bg-surface border border-neutral-700 text-text placeholder-text-muted" className="w-full mb-3 p-3 rounded bg-surface border"
/> />
<input <input
type="password" type="password"
placeholder="Şifre" placeholder="Şifre"
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full mb-5 p-3 rounded bg-surface border border-neutral-700 text-text placeholder-text-muted" className="w-full mb-5 p-3 rounded bg-surface border"
/> />
<div className="flex justify-center"> <div className="flex justify-center">
<button <button onClick={handleSubmit} className="bg-primary text-white px-6 py-2 rounded">
onClick={handleSubmit}
className="bg-primary hover:bg-primary-hover text-white font-medium px-6 py-2 rounded"
>
{isRegister ? "Kayıt Ol" : "Giriş Yap"} {isRegister ? "Kayıt Ol" : "Giriş Yap"}
</button> </button>
</div> </div>
<p className="text-sm text-center text-text-muted mt-5"> <p className="text-sm text-center mt-5">
{isRegister ? "Zaten hesabın var mı?" : "Hesabın yok mu?"} {isRegister ? "Zaten hesabın var mı?" : "Hesabın yok mu?"}
<button <button
onClick={() => setIsRegister(!isRegister)} onClick={() => setIsRegister(!isRegister)}
@ -76,5 +79,5 @@ export default function LoginModal({ onClose, onLogin, onRegister }: LoginModalP
</p> </p>
</div> </div>
</div> </div>
); )
} }

View File

@ -1,25 +1,21 @@
import React, { useEffect, useState } from "react" import React, { useEffect, useState } from "react"
import { getComments,postComment } from "../../api/deal/commentDeal" import { Link } from "react-router-dom"
import { getComments, postComment } from "../../api/deal/commentDeal"
import { useAuth } from "../../context/AuthContext"
type Comment = { import { timeAgo } from "../../utils/timeAgo"
id: number import type { Comment } from "../../models/Comment"
user: { username: string }
text: string
createdAt: string
}
type DealCommentsProps = { type DealCommentsProps = {
dealId: number dealId: number
onRequireLogin: () => void
} }
export default function DealComments({ dealId }: DealCommentsProps) { export default function DealComments({ dealId, onRequireLogin }: DealCommentsProps) {
const [comments, setComments] = useState<Comment[]>([]) const [comments, setComments] = useState<Comment[]>([])
const [newComment, setNewComment] = useState("") const [newComment, setNewComment] = useState("")
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const token = localStorage.getItem("token") const { isAuthenticated } = useAuth()
// Yorumları yükle
useEffect(() => { useEffect(() => {
async function loadComments() { async function loadComments() {
try { try {
@ -35,17 +31,15 @@ export default function DealComments({ dealId }: DealCommentsProps) {
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!newComment.trim()) return if (!newComment.trim()) return
if (!token) { if (!isAuthenticated) return onRequireLogin()
alert("Giriş yapmalısın.")
return
}
setLoading(true) setLoading(true)
try { try {
const added = await postComment(token, dealId, newComment) const added = await postComment(dealId, newComment)
setComments((prev) => [added, ...prev]) // yeni yorumu ekle setComments((prev) => [added, ...prev])
setNewComment("") setNewComment("")
} catch (err: any) { } catch (err: any) {
console.error(err)
alert(err.message || "Sunucu hatası") alert(err.message || "Sunucu hatası")
} finally { } finally {
setLoading(false) setLoading(false)
@ -53,56 +47,85 @@ export default function DealComments({ dealId }: DealCommentsProps) {
} }
return ( return (
<div className="bg-surface/50 rounded-lg p-4 shadow-sm"> <div className="bg-surface/50 rounded-xl p-6 border border-border/40">
<h2 className="text-lg font-semibold mb-4">Yorumlar</h2> <h2 className="text-lg font-semibold mb-6">Yorumlar</h2>
<ul className="space-y-3 mb-4"> <div className="space-y-6 mb-6">
{comments.length > 0 ? ( {comments.length > 0 ? (
comments.map((c) => ( comments.map((c) => (
<li key={c.id} className="border-b border-border pb-2"> <div
<p className="text-sm"> key={c.id}
<span className="font-medium">{c.user.username}</span>: {c.text} className="flex gap-3 border-b border-border/30 pb-5 last:border-none"
</p> >
<p className="text-xs text-muted-foreground"> <Link to={`/user/${c.user.username}`}>
{new Date(c.createdAt).toLocaleString("tr-TR")} <img
</p> src={c.user.avatarUrl || `${import.meta.env.BASE_URL}placeholders/placeholder-profile.png`}
</li> alt={c.user.username}
className="w-10 h-10 rounded-full object-cover"
/>
</Link>
<div className="flex-1">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<Link
to={`/user/${c.user.username}`}
className="font-medium text-sm hover:underline"
>
{c.user.username}
</Link>
<span className="text-xs text-muted-foreground">
{timeAgo(c.createdAt)}
</span>
</div>
</div>
<p className="mt-1 text-sm leading-relaxed">{c.text}</p>
<div className="flex items-center gap-5 mt-2 text-xs text-muted-foreground">
<button className="flex items-center gap-1 hover:text-primary transition">
<span>👍</span> <span>25</span>
</button>
<button className="flex items-center gap-1 hover:text-primary transition">
<span>💬</span> <span>Yanıtla</span>
</button>
<button className="hover:text-primary transition"></button>
</div>
</div>
</div>
)) ))
) : ( ) : (
<p className="text-sm text-muted-foreground">Henüz yorum yok.</p> <p className="text-sm text-muted-foreground text-center py-4">
Henüz yorum yok.
</p>
)} )}
</ul> </div>
{token ? ( {isAuthenticated ? (
<form onSubmit={handleSubmit} className="flex gap-2"> <form onSubmit={handleSubmit} className="flex items-center gap-3 pt-4 border-t border-border/40">
<input <input
type="text" type="text"
value={newComment} value={newComment}
onChange={(e) => setNewComment(e.target.value)} onChange={(e) => setNewComment(e.target.value)}
placeholder="Yorum ekle..." placeholder="Yorum ekle..."
className="flex-1 border rounded-md px-3 py-2 text-sm" className="flex-1 border border-border/40 rounded-full px-4 py-2 text-sm bg-background focus:ring-2 focus:ring-primary/40 focus:outline-none"
disabled={loading} disabled={loading}
/> />
<button <button
type="submit" type="submit"
disabled={loading} disabled={loading}
className="bg-primary text-white text-sm px-4 py-2 rounded-md hover:bg-primary/90 disabled:opacity-60" className="px-5 py-2 rounded-full text-sm font-medium bg-primary text-white hover:bg-primary/90 transition disabled:opacity-60"
> >
{loading ? "Gönderiliyor..." : "Gönder"} {loading ? "Gönderiliyor..." : "Gönder"}
</button> </button>
</form> </form>
) : ( ) : (
<p className="text-sm text-muted-foreground text-center"> <button
Yorum yazmak için{" "} onClick={onRequireLogin}
<a href="/login" className="text-primary font-medium hover:underline"> className="w-full text-primary font-medium hover:underline text-sm"
giriş yap >
</a>{" "} Yorum yazmak için giriş yap veya kayıt ol
veya{" "} </button>
<a href="/register" className="text-primary font-medium hover:underline">
kayıt ol
</a>
.
</p>
)} )}
</div> </div>
) )

View File

@ -1,5 +1,3 @@
import React from "react"
type DealDescriptionProps = { type DealDescriptionProps = {
description: string description: string
} }

View File

@ -1,12 +1,13 @@
import UserInfo from "./UserInfo" import UserInfo from "./UserInfo"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import SearchBar from "../Navbar/SearchBar"
export default function Navbar() { export default function Navbar() {
return ( return (
<nav className="bg-surface"> <nav className="bg-surface">
<div className="mx-auto flex justify-between items-center px-6 py-3"> <div className="mx-auto flex justify-between items-center px-6 py-3">
{/* Sol kısım: logo + menü */} {/* Sol kısım: logo + menü */}
<div className="flex items-center gap-15"> <div className="flex items-center gap-10">
<div className="text-primary font-bold text-xl">DealHeat</div> <div className="text-primary font-bold text-xl">DealHeat</div>
<ul className="flex gap-6 text-text items-center"> <ul className="flex gap-6 text-text items-center">
@ -37,6 +38,11 @@ export default function Navbar() {
</ul> </ul>
</div> </div>
{/* Orta kısım: arama kutusu */}
<div className="flex-1 flex justify-center">
<SearchBar />
</div>
{/* Sağ kısım: kullanıcı bilgisi + buton */} {/* Sağ kısım: kullanıcı bilgisi + buton */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<UserInfo /> <UserInfo />

View File

@ -0,0 +1,34 @@
// src/components/Search/SearchBar.tsx
import { useState } from "react"
import { useNavigate } from "react-router-dom"
export default function SearchBar() {
const [query, setQuery] = useState("")
const navigate = useNavigate()
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (query.trim()) navigate(`/search?query=${encodeURIComponent(query)}`)
}
return (
<form
onSubmit={handleSubmit}
className="flex items-center bg-[#2A2A2A] rounded-md overflow-hidden w-full max-w-md"
>
<input
type="text"
placeholder="Fırsat ara..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="flex-1 px-4 py-2 bg-transparent text-white placeholder-gray-400 focus:outline-none"
/>
<button
type="submit"
className="bg-primary hover:bg-primary/90 text-white font-semibold px-4 py-2 transition"
>
Ara
</button>
</form>
)
}

View File

@ -1,70 +1,30 @@
import { useState, useEffect } from "react" import { useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { useAuth } from "../../../context/AuthContext"
import LoginModal from "../../Auth/LoginModal" import LoginModal from "../../Auth/LoginModal"
import { login } from "../../../api/auth/login"
import { register } from "../../../api/auth/register"
type User = {
id: number
username: string
email?: string
avatarUrl?: string
}
export default function UserInfo() { export default function UserInfo() {
const [user, setUser] = useState<User | null>(null) const { user, isAuthenticated, logout } = useAuth()
const [showModal, setShowModal] = useState(false)
const [menuOpen, setMenuOpen] = useState(false) const [menuOpen, setMenuOpen] = useState(false)
const [showModal, setShowModal] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
useEffect(() => {
const storedUser = localStorage.getItem("user")
if (storedUser) setUser(JSON.parse(storedUser))
}, [])
const handleRegister = async (username: string, email: string, password: string) => {
try {
const data = await register(username, email, password)
localStorage.setItem("token", data.token)
localStorage.setItem("user", JSON.stringify(data.user))
setUser(data.user)
setShowModal(false)
} catch (err: any) {
alert(err.message)
}
}
const handleLogin = async (email: string, password: string) => {
try {
const data = await login(email, password)
localStorage.setItem("token", data.token)
localStorage.setItem("user", JSON.stringify(data.user))
console.log(JSON.stringify(data.user))
setUser(data.user)
setShowModal(false)
} catch (err: any) {
alert(err.message)
}
}
const handleLogout = () => {
localStorage.removeItem("token")
localStorage.removeItem("user")
setUser(null)
setMenuOpen(false)
}
const goToAccount = () => { const goToAccount = () => {
setMenuOpen(false) setMenuOpen(false)
navigate("/account") navigate("/account")
} }
const handleLogout = () => {
logout()
setMenuOpen(false)
}
return ( return (
<div className="relative flex items-center gap-3"> <div className="relative flex items-center gap-3">
{user ? ( {isAuthenticated && user ? (
<div className="relative"> <div className="relative">
<img <img
src={user.avatarUrl || "https://via.placeholder.com/32"} src={user.avatarUrl || `${import.meta.env.BASE_URL}placeholders/placeholder-profile.png`}
alt={user.username} alt={user.username}
className="w-8 h-8 rounded-full cursor-pointer border border-gray-400 hover:border-orange-500 transition" className="w-8 h-8 rounded-full cursor-pointer border border-gray-400 hover:border-orange-500 transition"
onClick={() => setMenuOpen((prev) => !prev)} onClick={() => setMenuOpen((prev) => !prev)}
@ -98,11 +58,7 @@ export default function UserInfo() {
)} )}
{showModal && ( {showModal && (
<LoginModal <LoginModal onClose={() => setShowModal(false)} />
onClose={() => setShowModal(false)}
onLogin={handleLogin}
onRegister={handleRegister}
/>
)} )}
</div> </div>
) )

View File

@ -0,0 +1,17 @@
import type { Comment } from "../../models"
type CommentCardProps = {
comment: Comment
}
export default function CommentCard({ comment }: CommentCardProps) {
return (
<div className="border rounded-lg p-3 text-sm">
<p className="font-medium mb-1">{comment.deal.title}</p>
<p>{comment.text}</p>
<p className="text-xs text-muted-foreground mt-1">
{new Date(comment.createdAt).toLocaleString("tr-TR")}
</p>
</div>
)
}

View File

@ -0,0 +1,20 @@
// src/components/Profile/ProfileHeader.tsx
type Props = {
username: string
avatarUrl?: string
}
export default function ProfileHeader({ username, avatarUrl }: Props) {
return (
<div className="w-full bg-surface/50 rounded-lg flex flex-col items-center justify-center py-12">
<div className="relative w-32 h-32 mb-4">
<img
src={avatarUrl || `${import.meta.env.BASE_URL}placeholders/placeholder-profile.png`}
alt={username}
className="w-32 h-32 rounded-full object-cover border-4 border-background"
/>
</div>
<h2 className="text-2xl font-semibold text-center">{username}</h2>
</div>
)
}

View File

@ -1,6 +1,7 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { voteDeal } from "../../api/deal/voteDeal" // yeni api dosyan import { voteDeal } from "../../api/deal/voteDeal"
import { useAuth } from "../../hooks/useAuth"
type DealCardProps = { type DealCardProps = {
id: number id: number
@ -12,6 +13,7 @@ type DealCardProps = {
score: number score: number
comments: number comments: number
postedAgo: string postedAgo: string
onRequireLogin: () => void // yeni prop
} }
export default function DealCardMain({ export default function DealCardMain({
@ -24,26 +26,31 @@ export default function DealCardMain({
score, score,
comments, comments,
postedAgo, postedAgo,
onRequireLogin,
}: DealCardProps) { }: DealCardProps) {
const [currentScore, setCurrentScore] = useState<number>(score) const [currentScore, setCurrentScore] = useState<number>(score)
const [voting, setVoting] = useState(false) const [voting, setVoting] = useState(false)
const navigate = useNavigate() const navigate = useNavigate()
const auth = useAuth() // her render'da güncel context
useEffect(() => { useEffect(() => {
setCurrentScore(score) setCurrentScore(score)
}, [score]) }, [score])
const handleVote = async (e: React.MouseEvent, type: "UP" | "DOWN") => { const handleVote = async (e: React.MouseEvent, type: "UP" | "DOWN") => {
e.stopPropagation() e.stopPropagation()
const token = localStorage.getItem("token")
if (!token) { // Contextten anlık token kontrolü
alert("Giriş yapmalısın") const { token, isAuthenticated } = auth
if (!isAuthenticated || !token) {
onRequireLogin()
return return
} }
setVoting(true) setVoting(true)
try { try {
const data = await voteDeal(token, id, type) const data = await voteDeal( id, type)
if (typeof data.score === "number") setCurrentScore(data.score) if (typeof data.score === "number") setCurrentScore(data.score)
else alert(data.error || "Oy gönderilemedi") else alert(data.error || "Oy gönderilemedi")
} catch { } catch {
@ -52,7 +59,6 @@ export default function DealCardMain({
setVoting(false) setVoting(false)
} }
} }
const handleCardClick = () => { const handleCardClick = () => {
navigate(`/deal/${id}`) navigate(`/deal/${id}`)
} }
@ -90,7 +96,7 @@ export default function DealCardMain({
</button> </button>
</span> </span>
<span> {postedAgo} </span> <span>{postedAgo}</span>
</div> </div>
<h2 className="text-xl font-semibold text-text mt-1 hover:text-primary"> <h2 className="text-xl font-semibold text-text mt-1 hover:text-primary">

View File

@ -0,0 +1,61 @@
import { createContext, useContext, useState, useEffect } from "react"
type AuthContextType = {
user: any
token: string | null
isAuthenticated: boolean
login: (userData: any, tokenData: string) => void
logout: () => void
}
const AuthContext = createContext<AuthContextType | null>(null)
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<any>(null)
const [token, setToken] = useState<string | null>(null)
useEffect(() => {
const storedUser = localStorage.getItem("user")
const storedToken = localStorage.getItem("token")
if (storedUser && storedToken) {
setUser(JSON.parse(storedUser))
setToken(storedToken)
}
}, [])
const login = (userData: any, tokenData: string) => {
localStorage.setItem("user", JSON.stringify(userData))
localStorage.setItem("token", tokenData)
setUser(userData)
setToken(tokenData)
window.location.reload()
}
const logout = () => {
localStorage.removeItem("user")
localStorage.removeItem("token")
setUser(null)
setToken(null)
window.location.reload()
}
return (
<AuthContext.Provider
value={{
user,
token,
isAuthenticated: Boolean(token),
login,
logout,
}}
>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const ctx = useContext(AuthContext)
if (!ctx) throw new Error("useAuth must be used within AuthProvider")
return ctx
}

36
src/hooks/useAuth.ts Normal file
View File

@ -0,0 +1,36 @@
import { useState, useEffect } from "react"
export function useAuth() {
const [user, setUser] = useState<any>(null)
const [token, setToken] = useState<string | null>(null)
// Uygulama açıldığında localStorage kontrolü
useEffect(() => {
const storedUser = localStorage.getItem("user")
const storedToken = localStorage.getItem("token")
if (storedUser && storedToken) {
setUser(JSON.parse(storedUser))
setToken(storedToken)
}
}, [])
// Giriş başarılı olduğunda çağrılır
const login = (userData: any, tokenData: string) => {
localStorage.setItem("user", JSON.stringify(userData))
localStorage.setItem("token", tokenData)
setUser(userData)
setToken(tokenData)
}
// Çıkış yapıldığında çağrılır
const logout = () => {
localStorage.removeItem("user")
localStorage.removeItem("token")
setUser(null)
setToken(null)
}
const isAuthenticated = Boolean(token)
return { user, token, isAuthenticated, login, logout }
}

25
src/hooks/useAuthCheck.ts Normal file
View File

@ -0,0 +1,25 @@
// src/hooks/useAuthCheck.ts
import { useEffect } from "react"
import { useNavigate, useLocation } from "react-router-dom"
import { useAuth } from "../context/AuthContext"
import instance from "../api/axiosInstance"
export function useAuthCheck() {
const { token, logout } = useAuth()
const navigate = useNavigate()
const location = useLocation()
useEffect(() => {
if (!token) {
navigate(`/login?next=${location.pathname}`, { replace: true })
return
}
instance
.get("/auth/me")
.catch(() => {
logout()
navigate(`/login?next=${location.pathname}`, { replace: true })
})
}, [token, location.pathname, logout, navigate])
}

View File

@ -1,31 +0,0 @@
export default function DealDetailsLayout({
Image,
Details,
Description,
Comments,
}: {
Image: React.ReactNode
Details: React.ReactNode
Description: React.ReactNode
Comments: React.ReactNode
}) {
return (
<div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Üst kısım: sol görsel + sağ detay */}
<div className="lg:col-span-1 flex justify-center items-start">
{Image}
</div>
<div className="lg:col-span-3 flex flex-col gap-6">
{Details}
</div>
{/* Alt kısım: açıklama ve yorumlar tam genişlikte */}
<div className="lg:col-span-4 flex flex-col gap-6">
<section>{Description}</section>
<section>{Comments}</section>
</div>
</div>
)
}

View File

@ -1,19 +0,0 @@
export default function PageLayout({
children,
sidebar,
}: {
children: React.ReactNode
sidebar?: React.ReactNode
}) {
return (
<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 */}
<div className="lg:col-span-3">{children}</div>
{/* SAĞ: 1/4 - isteğe bağlı alan */}
<aside className="hidden lg:block bg-surface/50 rounded-lg p-4">
{sidebar}
</aside>
</div>
)
}

View File

@ -1,25 +1,14 @@
import React from "react"; import React from "react"
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client"
import { BrowserRouter, Routes, Route } from "react-router-dom"; // <-- Burası önemli import App from "./App"
import HomePage from "./pages/HomePage"; import "./global.css"
import "./global.css"; import { AuthProvider } from "./context/AuthContext"
import DealPage from "./pages/DealPage";
import SubmitDealPage from "./pages/SubmitDealPage";
import AccountSettingsPage from "./pages/AccountSettingsPage";
ReactDOM.createRoot(document.getElementById("root")!).render( ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <AuthProvider>
<Routes> <App />
<Route path="/" element={<HomePage />} />
<Route path="/deal/:id" element={<DealPage />} /> </AuthProvider>
<Route path="/submit-deal" element={<SubmitDealPage />} />
<Route path="/account" element={<AccountSettingsPage />} />
</Routes>
</BrowserRouter>
</React.StrictMode> </React.StrictMode>
); )

10
src/models/Comment.ts Normal file
View File

@ -0,0 +1,10 @@
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
}

22
src/models/Deal.ts Normal file
View File

@ -0,0 +1,22 @@
import type { DealImage } from "./DealImage"
import type { Comment } from "./Comment"
import type { DealVote } from "./DealVote"
import type { User } from "./User"
export type Deal = {
id: number
title: string
description?: string
url?: string
price?: number
createdAt: string
updatedAt: string
score: number
userId: number
// ilişkiler
user?: Pick<User, "id" | "username" | "avatarUrl">
images?: DealImage[]
votes?: DealVote[]
comments?: Comment[]
}

6
src/models/DealImage.ts Normal file
View File

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

7
src/models/DealVote.ts Normal file
View File

@ -0,0 +1,7 @@
export type DealVote = {
id: number
dealId: number
userId: number
voteType: string
createdAt: string
}

11
src/models/User.ts Normal file
View File

@ -0,0 +1,11 @@
// src/models/User.ts
export type User = {
id: number
username: string
email: string
avatarUrl?: string
createdAt: string
updatedAt: string
}
export type PublicUser = Pick<User, "username" | "avatarUrl" | "createdAt">

5
src/models/index.ts Normal file
View File

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

View File

@ -1,11 +1,17 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import MainLayout from "../layouts/MainLayout"; import MainLayout from "../layouts/MainLayout";
import { uploadAvatar } from "../api/account/uploadAvatar"; import { uploadAvatar } from "../api/account/uploadAvatar";
import { me } from "../api/auth/me"; // bu fonksiyon token ile kullanıcıyı döndürecek
export default function AccountSettingsPage() { export default function AccountSettingsPage() {
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
const [preview, setPreview] = useState<string | null>(null); const [preview, setPreview] = useState<string | null>(null);
useEffect(() => {
const user = JSON.parse(localStorage.getItem("user") || "{}");
if (user?.avatarUrl) setPreview(user.avatarUrl);
}, []);
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (!file) return; if (!file) return;
@ -16,11 +22,13 @@ export default function AccountSettingsPage() {
const handleUpload = async () => { const handleUpload = async () => {
if (!image) return; if (!image) return;
try { try {
const token = localStorage.getItem("token")||"" const token = localStorage.getItem("token") || "";
const data = await uploadAvatar(token,image); await uploadAvatar( image);
const user = JSON.parse(localStorage.getItem("user") || "{}");
user.avatarUrl = data.url; // avatar yüklendikten sonra güncel kullanıcıyı backendden çek
localStorage.setItem("user", JSON.stringify(user)); const updatedUser = await me();
localStorage.setItem("user", JSON.stringify(updatedUser));
setPreview(updatedUser.avatarUrl);
alert("Profil fotoğrafı güncellendi"); alert("Profil fotoğrafı güncellendi");
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -37,7 +45,7 @@ export default function AccountSettingsPage() {
{preview ? ( {preview ? (
<img <img
src={preview} src={preview}
alt="Preview" alt="Profil Fotoğrafı"
className="w-32 h-32 rounded-full object-cover mb-3" className="w-32 h-32 rounded-full object-cover mb-3"
/> />
) : ( ) : (

View File

@ -1,33 +1,19 @@
// src/pages/DealPage.tsx
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useParams } from "react-router-dom" import { useParams } from "react-router-dom"
import { getDeal } from "../api/deal/getDeal" import { getDeal } from "../api/deal/getDeal"
import MainLayout from "../layouts/MainLayout" import MainLayout from "../layouts/MainLayout"
import DealDetailsLayout from "../layouts/DealDetailsLayout"
import DealImages from "../components/DealScreen/DealImages" import DealImages from "../components/DealScreen/DealImages"
import DealDetails from "../components/DealScreen/DealDetails" import DealDetails from "../components/DealScreen/DealDetails"
import DealDescription from "../components/DealScreen/DealDescription" import DealDescription from "../components/DealScreen/DealDescription"
import DealComments from "../components/DealScreen/DealComments" import DealComments from "../components/DealScreen/DealComments"
import type { Deal } from "../models/Deal"
type Deal = { type DealPageProps = {
id: number onRequireLogin: () => void
title: string
price: string
store: string
link: string
image: string
description: string
postedBy: string
postedAgo: string
comments: {
id: number
author: string
text: string
postedAgo: string
}[]
} }
export default function DealPage() { export default function DealPage({ onRequireLogin }: DealPageProps) {
const { id } = useParams() const { id } = useParams()
const [deal, setDeal] = useState<Deal | null>(null) const [deal, setDeal] = useState<Deal | null>(null)
@ -35,28 +21,41 @@ export default function DealPage() {
if (!id) return if (!id) return
getDeal(Number(id)) getDeal(Number(id))
.then(setDeal) .then(setDeal)
.catch((err) => console.error(err)) .catch((err) => console.error("Deal yüklenemedi:", err))
}, [id]) }, [id])
if (!deal) return <p className="p-4">Yükleniyor...</p> if (!deal) return <p className="p-4">Yükleniyor...</p>
return ( return (
<MainLayout> <MainLayout>
<DealDetailsLayout <div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
Image={<DealImages imageUrl={deal.image} />} {/* Sol: görsel */}
Details={ <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 <DealDetails
title={deal.title} title={deal.title}
price={deal.price} price={deal.price?.toString() || "-"}
store={deal.store} store={deal.url || ""}
link={deal.link} link={deal.url || ""}
postedBy={deal.postedBy} postedBy={deal.user?.username || "Anonim"}
postedAgo={deal.postedAgo} postedAgo={deal.createdAt}
/> />
} </div>
Description={<DealDescription description={deal.description} />}
Comments={<DealComments dealId={deal.id} />} {/* 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> </MainLayout>
) )
} }

View File

@ -1,10 +0,0 @@
import { useEffect, useState } from "react";
import MainLayout from "../layouts/MainLayout";
import PageLayout from "../layouts/HomeLayout";
export default function DealsPage() {
const [deals, setDeals] = useState([]);
}

View File

@ -1,6 +1,6 @@
// src/pages/Home.tsx
import { useEffect, useState, useRef } from "react" import { useEffect, useState, useRef } from "react"
import MainLayout from "../layouts/MainLayout" import MainLayout from "../layouts/MainLayout"
import HomeLayout from "../layouts/HomeLayout"
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 { timeAgo } from "../utils/timeAgo" import { timeAgo } from "../utils/timeAgo"
@ -10,15 +10,18 @@ type Deal = {
title: string title: string
description: string description: string
url: string url: string
imageUrl: string images: { imageUrl: string }[]
price: number price: number
score: number score: number
createdAt: string createdAt: string
user?: { username: string } user?: { username: string }
} }
export default function Home() { type HomeProps = {
onRequireLogin: () => void
}
export default function HomePage({ onRequireLogin }: HomeProps) {
const [deals, setDeals] = useState<Deal[]>([]) const [deals, setDeals] = useState<Deal[]>([])
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true) const [hasMore, setHasMore] = useState(true)
@ -26,7 +29,6 @@ export default function Home() {
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const observerRef = useRef<HTMLDivElement | null>(null) const observerRef = useRef<HTMLDivElement | null>(null)
// Sayfa değiştiğinde yeni verileri çek
useEffect(() => { useEffect(() => {
const loadDeals = async () => { const loadDeals = async () => {
if (loading || !hasMore) return if (loading || !hasMore) return
@ -36,7 +38,6 @@ export default function Home() {
if (newDeals.length === 0) { if (newDeals.length === 0) {
setHasMore(false) setHasMore(false)
} else { } else {
// yinelenen veriyi engelle
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 = newDeals.filter((d: Deal) => !existingIds.has(d.id))
@ -50,9 +51,8 @@ export default function Home() {
} }
} }
loadDeals() loadDeals()
}, [page]) // page değişince tekrar çalışır }, [page])
// Scroll gözlemleyici
useEffect(() => { useEffect(() => {
const observer = new IntersectionObserver( const observer = new IntersectionObserver(
(entries) => { (entries) => {
@ -70,29 +70,38 @@ export default function Home() {
return ( return (
<MainLayout> <MainLayout>
<HomeLayout> <div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
{deals.map((deal) => ( {/* SOL: 3/4 - Deal listesi */}
<DealCardMain <div className="lg:col-span-3 space-y-4">
key={deal.id} {deals.map((deal) => (
id={deal.id} <DealCardMain
image={deal.images[0].imageUrl} key={deal.id}
title={deal.title} id={deal.id}
price={`${deal.price}`} image={deal.images[0]?.imageUrl || "/placeholder.png"}
store={deal.url ? new URL(deal.url).hostname : "Bilinmiyor"} title={deal.title}
postedBy={deal.user?.username || "unknown"} price={`${deal.price}`}
score={deal.score} store={deal.url ? new URL(deal.url).hostname : "Bilinmiyor"}
comments={0} postedBy={deal.user?.username || "unknown"}
postedAgo={timeAgo(deal.createdAt)} score={deal.score}
/> comments={0}
))} postedAgo={timeAgo(deal.createdAt)}
{loading && <p className="text-center py-4">Yükleniyor...</p>} onRequireLogin={onRequireLogin}
{!hasMore && ( />
<p className="text-center py-4 text-muted-foreground"> ))}
Tüm fırsatlar yüklendi. {loading && <p className="text-center py-4">Yükleniyor...</p>}
</p> {!hasMore && (
)} <p className="text-center py-4 text-muted-foreground">
<div ref={observerRef} className="h-8" /> Tüm fırsatlar yüklendi.
</HomeLayout> </p>
)}
<div ref={observerRef} className="h-8" />
</div>
{/* SAĞ: sidebar alanı */}
<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>
</aside>
</div>
</MainLayout> </MainLayout>
) )
} }

130
src/pages/ProfilePage.tsx Normal file
View File

@ -0,0 +1,130 @@
import { useState, useEffect } from "react"
import { useParams } from "react-router-dom"
import MainLayout from "../layouts/MainLayout"
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"
export default function ProfilePage() {
const { userName } = useParams<{ userName: string }>()
const [activeTab, setActiveTab] = useState<"deals" | "comments">("deals")
const [user, setUser] = useState<PublicUser | null>(null)
const [deals, setDeals] = useState<Deal[]>([])
const [comments, setComments] = useState<Comment[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
if (!userName) return
const loadUser = async () => {
setLoading(true)
try {
const { user, deals, comments } = await fetchUserProfile(userName)
setUser(user)
setDeals(deals)
setComments(comments)
} catch (err: any) {
console.error(err)
setError(err.message)
} finally {
setLoading(false)
}
}
loadUser()
}, [userName])
if (loading) return <p className="p-4 text-center">Yükleniyor...</p>
if (error) return <p className="p-4 text-center text-red-600">{error}</p>
if (!user) return <p className="p-4 text-center">Kullanıcı bulunamadı.</p>
return (
<MainLayout>
<div className="max-w-5xl mx-auto px-4 py-8 space-y-8">
{/* ÜST: profil bilgisi */}
<div className="bg-surface/50 rounded-lg p-6 flex flex-col items-center text-center shadow-sm">
<img
src={user.avatarUrl || `${import.meta.env.BASE_URL}placeholders/placeholder-profile.png`}
alt="avatar"
className="w-24 h-24 rounded-full mb-3 border"
/>
<h1 className="text-xl font-semibold">{user.username}</h1>
<p className="text-xs text-muted-foreground mt-1">
Katılma: {new Date(user.createdAt).toLocaleDateString("tr-TR")}
</p>
</div>
{/* MENÜ */}
<div className="border-b border-border flex justify-center gap-8 text-sm font-medium">
<button
onClick={() => setActiveTab("deals")}
className={`py-2 ${
activeTab === "deals"
? "text-primary border-b-2 border-primary"
: "text-muted-foreground"
}`}
>
Paylaşımlar
</button>
<button
onClick={() => setActiveTab("comments")}
className={`py-2 ${
activeTab === "comments"
? "text-primary border-b-2 border-primary"
: "text-muted-foreground"
}`}
>
Yorumlar
</button>
</div>
{/* İÇERİK */}
<div className="min-h-[300px]">
{activeTab === "deals" ? (
<div className="space-y-4">
{deals.length > 0 ? (
deals.map((deal) => {
const firstImage = deal.images?.[0]?.imageUrl || "/placeholder.png"
const postedAgo = new Date(deal.createdAt).toLocaleDateString("tr-TR")
return (
<DealCardMain
key={deal.id}
id={deal.id}
image={firstImage}
title={deal.title}
price={`${deal.price ?? 0}`}
store={ "Bilinmiyor"}
postedBy={user.username}
score={deal.score}
comments={0}
postedAgo={postedAgo}
onRequireLogin={() => {}}
/>
)
})
) : (
<p className="text-center text-muted-foreground py-8">
Henüz paylaşım yok.
</p>
)}
</div>
) : (
<div className="space-y-4">
{comments.length > 0 ? (
comments.map((c) => <CommentCard key={c.id} comment={c} />)
) : (
<p className="text-center text-muted-foreground py-8">
Henüz yorum yok.
</p>
)}
</div>
)}
</div>
</div>
</MainLayout>
)
}

94
src/pages/SearchPage.tsx Normal file
View File

@ -0,0 +1,94 @@
// src/pages/SearchPage.tsx
import { useEffect, useState, useRef } from "react"
import { useSearchParams } from "react-router-dom"
import MainLayout from "../layouts/MainLayout"
import DealCardMain from "../components/Shared/DealCardMain"
import { timeAgo } from "../utils/timeAgo"
import { searchDeals } from "../api/deal/searchDeal"
export default function SearchPage({ onRequireLogin }: { onRequireLogin: () => void }) {
const [searchParams] = useSearchParams()
const query = searchParams.get("query") || ""
const [deals, setDeals] = useState<any[]>([])
const [page, setPage] = useState(1)
const [hasMore, setHasMore] = useState(true)
const [loading, setLoading] = useState(false)
const observerRef = useRef<HTMLDivElement | null>(null)
useEffect(() => {
setDeals([])
setPage(1)
setHasMore(true)
}, [query])
useEffect(() => {
const load = async () => {
if (!query || loading || !hasMore) return
setLoading(true)
try {
const newDeals = await searchDeals(query, page)
if (newDeals.length === 0) setHasMore(false)
else {
setDeals((prev) => {
const ids = new Set(prev.map((d) => d.id))
return [...prev, ...newDeals.filter((d: any) => !ids.has(d.id))]
})
}
} finally {
setLoading(false)
}
}
load()
}, [page, query])
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !loading) {
setPage((p) => p + 1)
}
},
{ threshold: 1 }
)
if (observerRef.current) observer.observe(observerRef.current)
return () => observer.disconnect()
}, [hasMore, loading])
return (
<MainLayout>
<div className="max-w-[1400px] mx-auto px-4 py-8 grid grid-cols-1 lg:grid-cols-4 gap-6">
<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>
</aside>
<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"}
title={deal.title}
price={`${deal.price}`}
store={deal.url ? new URL(deal.url).hostname : "Bilinmiyor"}
postedBy={deal.user?.username || "unknown"}
score={deal.score}
comments={0}
postedAgo={timeAgo(deal.createdAt)}
onRequireLogin={onRequireLogin}
/>
))}
{loading && <p className="text-center py-4">Yükleniyor...</p>}
{!hasMore && <p className="text-center py-4 text-muted-foreground">Tüm sonuçlar yüklendi.</p>}
<div ref={observerRef} className="h-8" />
</div>
</div>
</MainLayout>
)
}

View File

@ -18,7 +18,7 @@ export default function SubmitDealPage() {
} }
try { try {
await createDeal(token, { await createDeal( {
title, title,
description, description,
url, url,

View File

@ -0,0 +1,41 @@
import { getUser } from "../api/user/getUser"
import type { PublicUser, Deal, Comment } from "../models"
export type UserProfile = {
user: PublicUser
deals: Deal[]
comments: Comment[]
}
export async function fetchUserProfile(userName: string): Promise<UserProfile> {
const data = await getUser(userName)
const user: PublicUser = {
username: data.user.username,
avatarUrl: data.user.avatarUrl,
createdAt: data.user.createdAt,
}
const deals: Deal[] = data.deals.map((d: any) => ({
id: d.id,
title: d.title,
price: d.price,
store: d.store,
score: d.score,
createdAt: d.createdAt,
images:
d.images?.map((img: any) => ({
imageUrl: img.imageUrl,
order: img.order,
})) || [],
}))
const comments: Comment[] = data.comments.map((c: any) => ({
id: c.id,
text: c.text,
createdAt: c.createdAt,
deal: { title: c.deal?.title || "Bilinmeyen paylaşım" },
}))
return { user, deals, comments }
}