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 logo - - - React logo - -
-

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} + {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 */} +
+ {deal.title} +
+ + {/* İç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 ( +
+
+ {title} +
+ +
+
+
+ + + {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) => ( + + + ))} + + + ); +}