diff --git a/package-lock.json b/package-lock.json
index 18b0875..a5381be 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,8 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router-dom": "^7.9.4"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
@@ -2208,6 +2209,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3500,6 +3510,44 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-router": {
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
+ "integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.1",
+ "set-cookie-parser": "^2.6.0"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "7.9.4",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
+ "integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.9.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -3602,6 +3650,12 @@
"semver": "bin/semver.js"
}
},
+ "node_modules/set-cookie-parser": {
+ "version": "2.7.2",
+ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
+ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
+ "license": "MIT"
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
diff --git a/package.json b/package.json
index 71b602e..fb8bd09 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,8 @@
"dependencies": {
"@tailwindcss/vite": "^4.1.16",
"react": "^19.1.1",
- "react-dom": "^19.1.1"
+ "react-dom": "^19.1.1",
+ "react-router-dom": "^7.9.4"
},
"devDependencies": {
"@eslint/js": "^9.36.0",
diff --git a/src/App.css b/src/App.css
index b9d355d..3d552a6 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,42 +1,2 @@
-#root {
- max-width: 1280px;
- margin: 0 auto;
- padding: 2rem;
- text-align: center;
-}
+@import "tailwindcss";
-.logo {
- height: 6em;
- padding: 1.5em;
- will-change: filter;
- transition: filter 300ms;
-}
-.logo:hover {
- filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
- filter: drop-shadow(0 0 2em #61dafbaa);
-}
-
-@keyframes logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
-
-@media (prefers-reduced-motion: no-preference) {
- a:nth-of-type(2) .logo {
- animation: logo-spin infinite 20s linear;
- }
-}
-
-.card {
- padding: 2em;
-}
-
-.read-the-docs {
- color: #888;
-}
diff --git a/src/App.tsx b/src/App.tsx
index 3d7ded3..55437f9 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,33 +1,17 @@
import { useState } from 'react'
-import reactLogo from './assets/react.svg'
-import viteLogo from '/vite.svg'
import './App.css'
+import Navbar from './components/Layout/Navbar/Navbar'
+
function App() {
const [count, setCount] = useState(0)
-
return (
<>
-
- Vite + React
-
-
-
- Edit src/App.tsx and save to test HMR
-
-
-
- Click on the Vite and React logos to learn more
-
+
+
+
+
+
>
)
}
diff --git a/src/components/Auth/LoginModal.tsx b/src/components/Auth/LoginModal.tsx
new file mode 100644
index 0000000..e854b97
--- /dev/null
+++ b/src/components/Auth/LoginModal.tsx
@@ -0,0 +1,80 @@
+import { useState } from "react";
+
+type LoginModalProps = {
+ onClose: () => void;
+ onLogin: (email: string, password: string) => void;
+ onRegister: (username: string, email: string, password: string) => void;
+};
+
+export default function LoginModal({ onClose, onLogin, onRegister }: LoginModalProps) {
+ const [isRegister, setIsRegister] = useState(false);
+ const [username, setUsername] =useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+
+ const handleSubmit = () => {
+ if (isRegister) {
+ if (!username || !email || !password) return;
+ onRegister(username, email, password);
+ } else {
+ if (!email || !password) return;
+ onLogin(email, password);
+ }
+ };
+
+ return (
+
+
+
+
+ {isRegister ? "Kayıt Ol" : "Giriş Yap"}
+
+
+ {isRegister && (
+
setUsername(e.target.value)}
+ className="w-full mb-3 p-3 rounded bg-surface border border-neutral-700 text-text placeholder-text-muted"
+ />
+ )}
+
+
setEmail(e.target.value)}
+ className="w-full mb-3 p-3 rounded bg-surface border border-neutral-700 text-text placeholder-text-muted"
+ />
+
+
setPassword(e.target.value)}
+ className="w-full mb-5 p-3 rounded bg-surface border border-neutral-700 text-text placeholder-text-muted"
+ />
+
+
+
+
+
+
+ {isRegister ? "Zaten hesabın var mı?" : "Hesabın yok mu?"}
+
+
+
+
+ );
+}
diff --git a/src/components/Layout/Footer.tsx b/src/components/Layout/Footer.tsx
new file mode 100644
index 0000000..5337696
--- /dev/null
+++ b/src/components/Layout/Footer.tsx
@@ -0,0 +1,9 @@
+export default function Footer() {
+ return (
+
+ );
+}
diff --git a/src/components/Layout/Navbar/Navbar.tsx b/src/components/Layout/Navbar/Navbar.tsx
new file mode 100644
index 0000000..1420407
--- /dev/null
+++ b/src/components/Layout/Navbar/Navbar.tsx
@@ -0,0 +1,28 @@
+import UserInfo from "./UserInfo";
+
+export default function Navbar() {
+ return (
+ // Dış katman: arka plan tam genişlikte
+
+ );
+}
diff --git a/src/components/Layout/Navbar/Post.tsx b/src/components/Layout/Navbar/Post.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/components/Layout/Navbar/UserInfo.tsx b/src/components/Layout/Navbar/UserInfo.tsx
new file mode 100644
index 0000000..dedd7de
--- /dev/null
+++ b/src/components/Layout/Navbar/UserInfo.tsx
@@ -0,0 +1,93 @@
+import { useState, useEffect } from "react";
+import LoginModal from "../../Auth/LoginModal";
+
+type User = {
+ id: number;
+ username: string;
+ email?: string;
+ avatarUrl?: string;
+};
+
+export default function UserInfo() {
+ const [user, setUser] = useState(null);
+ const [showModal, setShowModal] = useState(false);
+
+ useEffect(() => {
+ const storedUser = localStorage.getItem("user");
+ if (storedUser) setUser(JSON.parse(storedUser));
+ }, []);
+ const handleRegister = async (username: string, email: string, password: string) => {
+ const res = await fetch("http://localhost:3000/api/auth/register", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username, email, password }),
+ });
+ const data = await res.json();
+ // ...
+ };
+ const handleLogin = async (email: string, password: string) => {
+ try {
+ const res = await fetch("http://localhost:3000/api/auth/login", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ email, password }),
+ });
+ const data = await res.json();
+
+ if (res.ok) {
+ localStorage.setItem("token", data.token);
+ localStorage.setItem("user", JSON.stringify(data.user));
+ setUser(data.user);
+ setShowModal(false);
+ } else {
+ alert(data.message || "Giriş başarısız");
+ }
+ } catch {
+ alert("Sunucu hatası");
+ }
+ };
+
+ const handleLogout = () => {
+ localStorage.removeItem("token");
+ localStorage.removeItem("user");
+ setUser(null);
+ };
+
+ return (
+
+ {user ? (
+
+
+

+
{user.username}
+
+
+
+ ) : (
+
+ )}
+
+ {showModal && (
+
setShowModal(false)}
+ onLogin={handleLogin}
+ onRegister={handleRegister}
+ />
+ )}
+
+ );
+}
diff --git a/src/components/MainScreen/ListedDeal.tsx b/src/components/MainScreen/ListedDeal.tsx
new file mode 100644
index 0000000..62dbc6b
--- /dev/null
+++ b/src/components/MainScreen/ListedDeal.tsx
@@ -0,0 +1,52 @@
+type ListedDealProps = {
+ deal: {
+ id: number;
+ title: string;
+ description: string;
+ image: string;
+ price: number;
+ upvotes?: number;
+ comments?: number;
+ };
+};
+
+
+
+export default function ListedDeal({ deal }: ListedDealProps) {
+ return (
+
+ {/* Görsel */}
+
+

+
+
+ {/* İçerik */}
+
+
+
+ {deal.title}
+
+
+ {deal.description}
+
+
+
+ {/* Alt bilgi */}
+
+
+ {deal.price.toLocaleString()} ₺
+
+
+
+ 🔥 {deal.upvotes ?? 0}
+ 💬 {deal.comments ?? 0}
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/Shared/DealCardMain.tsx b/src/components/Shared/DealCardMain.tsx
new file mode 100644
index 0000000..dcf3c46
--- /dev/null
+++ b/src/components/Shared/DealCardMain.tsx
@@ -0,0 +1,125 @@
+import { useState, useEffect } from "react";
+
+type DealCardProps = {
+ id: number;
+ image: string;
+ title: string;
+ price: string;
+ store: string;
+ postedBy: string;
+ score: number;
+ comments: number;
+ postedAgo: string;
+};
+
+export default function DealCardMain({
+ id,
+ image,
+ title,
+ price,
+ store,
+ postedBy,
+ score,
+ comments,
+ postedAgo,
+}: DealCardProps) {
+ const [currentScore, setCurrentScore] = useState(score);
+ const [voting, setVoting] = useState(false);
+
+ // dışarıdan gelen score güncellenirse state'i de güncelle
+ useEffect(() => {
+ setCurrentScore(score);
+ }, [score]);
+
+ const handleVote = async (type: "UP" | "DOWN") => {
+ const token = localStorage.getItem("token");
+ if (!token) {
+ alert("Giriş yapmalısın");
+ return;
+ }
+
+ setVoting(true);
+ try {
+ const res = await fetch("http://localhost:3000/api/deal-votes", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ body: JSON.stringify({ dealId: id, voteType: type }),
+ });
+
+ const data = await res.json();
+ if (res.ok && typeof data.score === "number") {
+ setCurrentScore(data.score);
+ } else {
+ alert(data.error || "Oy gönderilemedi");
+ }
+ } catch (err) {
+ console.error("Vote error:", err);
+ alert("Sunucu hatası");
+ } finally {
+ setVoting(false);
+ }
+ };
+
+ return (
+
+
+

+
+
+
+
+
+
+
+ {currentScore ?? 0}°
+
+
+ Posted {postedAgo}
+
+
+
+ {title}
+
+
+
+ {price}
+ {store}
+
+
+
+ {postedBy} tarafından paylaşıldı.
+
+
+
+
+
+ 💬 {comments}
+ 🔗 Paylaş
+
+
+
+
+
+ );
+}
diff --git a/src/global.css b/src/global.css
new file mode 100644
index 0000000..7a46428
--- /dev/null
+++ b/src/global.css
@@ -0,0 +1,19 @@
+@import url('https://fonts.googleapis.com/css2?family=Rubik:wght@400;500;600;700&display=swap');
+
+@import "tailwindcss";
+
+@theme {
+ --color-background: #121212;
+ --color-surface: #1E1E1E;
+ --color-primary: #FF6B00;
+ --color-primary-hover: #E65A00;
+ --color-accent: #FFD166;
+ --color-text: #FFFFFF;
+ --color-text-muted: #B3B3B3;
+ --color-success: #00C851;
+ --color-danger: #FF4444;
+}
+
+body {
+ @apply bg-background text-text font-sans;
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
deleted file mode 100644
index 08a3ac9..0000000
--- a/src/index.css
+++ /dev/null
@@ -1,68 +0,0 @@
-:root {
- font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
- line-height: 1.5;
- font-weight: 400;
-
- color-scheme: light dark;
- color: rgba(255, 255, 255, 0.87);
- background-color: #242424;
-
- font-synthesis: none;
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-a {
- font-weight: 500;
- color: #646cff;
- text-decoration: inherit;
-}
-a:hover {
- color: #535bf2;
-}
-
-body {
- margin: 0;
- display: flex;
- place-items: center;
- min-width: 320px;
- min-height: 100vh;
-}
-
-h1 {
- font-size: 3.2em;
- line-height: 1.1;
-}
-
-button {
- border-radius: 8px;
- border: 1px solid transparent;
- padding: 0.6em 1.2em;
- font-size: 1em;
- font-weight: 500;
- font-family: inherit;
- background-color: #1a1a1a;
- cursor: pointer;
- transition: border-color 0.25s;
-}
-button:hover {
- border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
- outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
- :root {
- color: #213547;
- background-color: #ffffff;
- }
- a:hover {
- color: #747bff;
- }
- button {
- background-color: #f9f9f9;
- }
-}
diff --git a/src/layouts/HomeLayout.tsx b/src/layouts/HomeLayout.tsx
new file mode 100644
index 0000000..6895f74
--- /dev/null
+++ b/src/layouts/HomeLayout.tsx
@@ -0,0 +1,19 @@
+export default function PageLayout({
+ children,
+ sidebar,
+}: {
+ children: React.ReactNode
+ sidebar?: React.ReactNode
+}) {
+ return (
+
+ {/* SOL: 3/4 - deal listesi */}
+
{children}
+
+ {/* SAĞ: 1/4 - isteğe bağlı alan */}
+
+
+ )
+}
diff --git a/src/layouts/MainLayout.tsx b/src/layouts/MainLayout.tsx
new file mode 100644
index 0000000..055415f
--- /dev/null
+++ b/src/layouts/MainLayout.tsx
@@ -0,0 +1,35 @@
+import Navbar from "../components/Layout/Navbar/Navbar";
+import Footer from "../components/Layout/Footer";
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export default function MainLayout({ children }: Props) {
+ return (
+
+
+ {/* NAVBAR - tam genişlikte arka plan, ortalı içerik */}
+
+
+ {/* ANA İÇERİK */}
+
+
+ {children}
+
+
+
+ {/* FOOTER - tam genişlikte arka plan, ortalı içerik */}
+
+
+
+ );
+}
diff --git a/src/main.tsx b/src/main.tsx
index bef5202..7f151d4 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,10 +1,15 @@
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
+import React from "react";
+import ReactDOM from "react-dom/client";
+import { BrowserRouter, Routes, Route } from "react-router-dom"; // <-- Burası önemli
+import HomePage from "./pages/HomePage";
+import "./global.css";
-createRoot(document.getElementById('root')!).render(
-
-
- ,
-)
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+ } />
+
+
+
+);
diff --git a/src/pages/DealsPage.tsx b/src/pages/DealsPage.tsx
new file mode 100644
index 0000000..607d304
--- /dev/null
+++ b/src/pages/DealsPage.tsx
@@ -0,0 +1,10 @@
+import { useEffect, useState } from "react";
+import MainLayout from "../layouts/MainLayout";
+import PageLayout from "../layouts/HomeLayout";
+
+export default function DealsPage() {
+ const [deals, setDeals] = useState([]);
+
+
+
+}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000..98744d9
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,56 @@
+import { useEffect, useState } from "react";
+import MainLayout from "../layouts/MainLayout";
+import HomeLayout from "../layouts/HomeLayout";
+import DealCardMain from "../components/Shared/DealCardMain";
+
+type Deal = {
+ id: number;
+ title: string;
+ description: string;
+ url: string;
+ imageUrl: string;
+ price: number;
+ score:number;
+ createdAt: string;
+ user?: { username: string };
+};
+
+
+export default function Home() {
+ const [deals, setDeals] = useState([]);
+
+ useEffect(() => {
+ async function fetchDeals() {
+ try {
+ const res = await fetch("http://localhost:3000/api/deals");
+ const data = await res.json();
+ setDeals(data);
+ } catch (err) {
+ console.error("Error fetching deals:", err);
+ }
+ }
+ fetchDeals();
+ }, []);
+
+ return (
+
+
+ {deals.map((deal) => (
+
+
+ ))}
+
+
+ );
+}