diff --git a/dist/index.html b/dist/index.html index e864e00a..f4a2a8b4 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4,6 +4,7 @@ + Capstone 1 diff --git a/package-lock.json b/package-lock.json index 1da56e59..eb0773ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "oauth-demo-frontend", + "name": "capstone-1-frontend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "oauth-demo-frontend", + "name": "capstone-1-frontend", "version": "1.0.0", "license": "ISC", "dependencies": { @@ -14,8 +14,10 @@ "@babel/preset-react": "^7.27.1", "axios": "^1.10.0", "babel-loader": "^10.0.0", + "chart.js": "^4.5.0", "dotenv": "^17.1.0", "react": "^19.1.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", "webpack": "^5.99.9", @@ -789,6 +791,12 @@ "tslib": "2" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@leichtgewicht/ip-codec": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", @@ -1837,6 +1845,18 @@ "node": ">=8" } }, + "node_modules/chart.js": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz", + "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==", + "license": "MIT", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -5378,6 +5398,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz", + "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", diff --git a/package.json b/package.json index da1486c6..d1508b6b 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,10 @@ "@babel/preset-react": "^7.27.1", "axios": "^1.10.0", "babel-loader": "^10.0.0", + "chart.js": "^4.5.0", "dotenv": "^17.1.0", "react": "^19.1.0", + "react-chartjs-2": "^5.3.0", "react-dom": "^19.1.0", "react-router-dom": "^7.6.3", "webpack": "^5.99.9", diff --git a/src/App.jsx b/src/App.jsx index d793b9af..369f0d0c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,38 +1,66 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, use } from "react"; import { createRoot } from "react-dom/client"; import axios from "axios"; import "./AppStyles.css"; import NavBar from "./components/NavBar"; import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; +import { API_URL } from "./shared"; +import { useNavigate } from "react-router-dom"; + import Login from "./components/Login"; import Signup from "./components/Signup"; import Home from "./components/Home"; import NotFound from "./components/NotFound"; -import { API_URL } from "./shared"; +import FriendsPage from "./components/FriendsPage"; +import Friends from "./components/Friends"; +import Profile from "./components/Profile"; +import NewPoll from "./components/NewPoll"; +import PollList from "./components/PollList"; +import UsersPage from "./components/UsersPage"; +import UserCard from "./components/UserCard"; +import DraftPoll from "./components/DraftPoll"; +import PollDetails from "./components/PollDetails"; +import AboutUs from "./components/AboutUs"; +//Alex branch const App = () => { const [user, setUser] = useState(null); + const [polls, setPolls] = useState(null); + const [loading, setLoading] = useState(true); + + const fetchPolls = async () => { + try { + const response = await axios.get(`${API_URL}/api/polls`); + setPolls(response.data); + } catch { + console.log("failed to get polls"); + setPolls([]); + } + }; const checkAuth = async () => { try { const response = await axios.get(`${API_URL}/auth/me`, { withCredentials: true, }); - setUser(response.data.user); - } catch { - console.log("Not authenticated"); + setUser(response.data); + } catch (error) { + console.error("Auth check failed:", error); setUser(null); + } finally { + setLoading(false); } }; - // Check authentication status on app load useEffect(() => { checkAuth(); + fetchPolls(); }, []); + const navigate = useNavigate(); + const handleLogout = async () => { try { - // Logout from our backend await axios.post( `${API_URL}/auth/logout`, {}, @@ -46,6 +74,8 @@ const App = () => { } }; + console.log(user); + return (
@@ -53,7 +83,23 @@ const App = () => { } /> } /> + } /> } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + } + /> + } /> + } /> } />
diff --git a/src/components/AboutUs.jsx b/src/components/AboutUs.jsx new file mode 100644 index 00000000..f8198405 --- /dev/null +++ b/src/components/AboutUs.jsx @@ -0,0 +1,37 @@ +import React from "react"; +import "./CSS/AboutUs.css"; + +const AboutUs = () => { + return ( +
+
+

About Us

+

+ This platform was built with a simple goal: to make decision-making + fair, fast, and accessible to everyone. +

+

+ Whether you're planning a night out, organizing an event, or just + settling a debate, our ranked-choice poll system ensures the results + reflect the group's true consensus. +

+
+ +
+

Why Ranked-Choice?

+

+ Traditional polls can be skewed by vote splitting or tactical voting. + With ranked-choice, every vote counts more fairly. Voters rank the + options in order of preference, and a winner is chosen through a + series of instant-runoff rounds. +

+

+ This method leads to more satisfying outcomes because it rewards broad + support, not just a passionate minority. +

+
+
+ ); +}; + +export default AboutUs; diff --git a/src/components/CSS/AboutUs.CSS b/src/components/CSS/AboutUs.CSS new file mode 100644 index 00000000..ffc15406 --- /dev/null +++ b/src/components/CSS/AboutUs.CSS @@ -0,0 +1,44 @@ +/* AboutUs.css */ + +.about-container { + padding: 60px 20px; + max-width: 1000px; + margin: 0 auto; + color: #e2e8f0; + background-color: #0f111a; + font-family: "Segoe UI", Roboto, sans-serif; +} + +.about-container h1, +.about-container h2 { + color: #ffffff; + margin-bottom: 20px; +} + +.about-container p { + color: #cbd5e1; + font-size: 17px; + line-height: 1.7; + margin-bottom: 24px; + max-width: 800px; +} + +.about-container ul { + list-style-type: disc; + padding-left: 20px; + margin-bottom: 40px; +} + +.about-container li { + font-size: 16px; + margin-bottom: 12px; +} + +.about-container a { + color: #818cf8; + text-decoration: underline; +} + +.about-container a:hover { + color: #6366f1; +} diff --git a/src/components/AuthStyles.css b/src/components/CSS/AuthStyles.css similarity index 100% rename from src/components/AuthStyles.css rename to src/components/CSS/AuthStyles.css diff --git a/src/components/CSS/Dropdown.css b/src/components/CSS/Dropdown.css new file mode 100644 index 00000000..d4ae23f7 --- /dev/null +++ b/src/components/CSS/Dropdown.css @@ -0,0 +1,37 @@ +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown-toggle { + padding: 8px 14px; + font-size: 12px; + cursor: pointer; + border: 1px solid #cccccc; + border-radius: 4px; + width: 100%; + white-space: nowrap; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + border: 1px solid #cccccc; + border-radius: 4px; + list-style: none; + padding: 0; + margin: 4px 0 0 0; + min-width: 100%; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background-color: #ffffff; +} + +.dropdown:hover .dropdown-menu { + display: block; +} + +.dropdown-item { + white-space: nowrap; +} \ No newline at end of file diff --git a/src/components/CSS/Home.css b/src/components/CSS/Home.css new file mode 100644 index 00000000..00e14615 --- /dev/null +++ b/src/components/CSS/Home.css @@ -0,0 +1,102 @@ +/* Home.css */ + +body { + margin: 0; + font-family: "Segoe UI", Roboto, sans-serif; + background-color: #0f111a; + color: #ffffff; +} + +.home-container { + padding: 60px 20px; + max-width: 1200px; + margin: 0 auto; +} + +section { + margin-bottom: 80px; +} + +h1, +h2 { + color: #ffffff; + margin-bottom: 16px; +} + +p { + color: #cbd5e1; + max-width: 700px; + line-height: 1.6; +} + +button { + background-color: #6366f1; + color: white; + border: none; + padding: 12px 20px; + font-size: 16px; + border-radius: 8px; + cursor: pointer; + margin-right: 12px; + box-shadow: 0 4px 10px rgba(99, 102, 241, 0.3); + transition: background-color 0.2s; +} + +button:hover { + background-color: #4f46e5; +} + +button:disabled { + background-color: #3f3f46; + cursor: not-allowed; + opacity: 0.6; +} + +ul { + list-style: none; + padding: 0; + display: flex; + flex-wrap: wrap; + gap: 40px; + margin-top: 20px; +} + +li { + flex: 1 1 200px; + font-size: 18px; + font-weight: 500; + color: #d1d5db; +} + +span { + color: #818cf8; + font-weight: bold; +} + +/* CTA section */ +.cta { + background: linear-gradient(to right, #1e293b, #111827); + padding: 40px 20px; + border-radius: 12px; + text-align: center; +} + +.cta h2 { + font-size: 28px; + margin-bottom: 20px; +} + +.cta div { + display: flex; + justify-content: center; + gap: 20px; + flex-wrap: wrap; +} + +.styled-paragraph { + color: #cbd5e1; + max-width: 700px; + line-height: 1.6; + font-size: 18px; + margin-bottom: 20px; +} diff --git a/src/components/CSS/IRVResults.css b/src/components/CSS/IRVResults.css new file mode 100644 index 00000000..e86fb0aa --- /dev/null +++ b/src/components/CSS/IRVResults.css @@ -0,0 +1,44 @@ + /* https://coolors.co/palette/fffcf2-ccc5b9-403d39-252422-eb5e28 */ + +.irv-results { + box-sizing: border-box; + width: 100%; + max-width: 1200px; + margin: 0 auto; + background: #fffcf2; + color: #403d39; + padding: 1.25rem 1.75rem; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(37, 36, 34, 0.12); +} + +.irv-results h3 { + color: #252422; + margin-bottom: 0.25rem; + font-size: 1.35rem; +} + +.irv-results p { + margin: 0 0 1rem; + color: #403d39; +} + +@media (max-width: 768px) { + .irv-results { + padding: 1rem; + } + .irv-results h3 { + font-size: 1.2rem; + } +} +@media (max-width: 540px) { + .irv-results { + padding: 0.75rem; + } + .irv-results h3 { + font-size: 1.1rem; + } + .irv-results p { + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/src/components/NavBarStyles.css b/src/components/CSS/NavBarStyles.css similarity index 100% rename from src/components/NavBarStyles.css rename to src/components/CSS/NavBarStyles.css diff --git a/src/components/CSS/PollCardStyles.css b/src/components/CSS/PollCardStyles.css new file mode 100644 index 00000000..4d91d8e0 --- /dev/null +++ b/src/components/CSS/PollCardStyles.css @@ -0,0 +1,96 @@ +.poll-card { + background: white; + border: 1px solid #e1e8ed; + border-radius: 12px; + padding: 16px; + margin-bottom: 12px; + transition: all 0.2s ease; + cursor: pointer; +} + +.poll-card:hover { + border-color: #1da1f2; + box-shadow: 0 2px 8px rgba(29, 161, 242, 0.1); +} + +.poll-card.poll-ended { + opacity: 0.7; + background: #f7f9fa; +} + +.poll-header { + margin-bottom: 12px; +} + +.poll-title { + font-size: 18px; + font-weight: 600; + color: #14171a; + margin: 0 0 8px 0; + line-height: 1.3; +} + +.poll-meta { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.poll-creator { + color: #657786; + font-size: 14px; + font-weight: 500; +} + +.poll-time { + background: #1da1f2; + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.poll-time.ended { + background: #657786; +} + +.poll-status { + margin-top: 8px; + display: flex; + justify-content: flex-end; +} + +.status-badge { + background: #f45d22; + color: white; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +/* Responsive design */ +@media (max-width: 768px) { + .poll-card { + padding: 12px; + } + + .poll-title { + font-size: 16px; + } + + .poll-meta { + flex-direction: column; + align-items: flex-start; + } + + .poll-time { + align-self: flex-end; + } +} \ No newline at end of file diff --git a/src/components/CSS/PollDetailsStyles.css b/src/components/CSS/PollDetailsStyles.css new file mode 100644 index 00000000..f4fcb767 --- /dev/null +++ b/src/components/CSS/PollDetailsStyles.css @@ -0,0 +1,235 @@ +.poll-details-container { + max-width: 800px; + margin: 0 auto; + padding: 20px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: #f7f9fa; + min-height: 100vh; +} + +.poll-header { + margin-bottom: 30px; + border-bottom: 2px solid #e1e8ed; + padding-bottom: 20px; + background: white; + padding: 25px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.poll-header h1 { + font-size: 28px; + color: #14171a; + margin-bottom: 10px; + font-weight: 700; + line-height: 1.3; +} + +.poll-description { + font-size: 16px; + color: #657786; + margin-bottom: 20px; + line-height: 1.6; + background: #f8f9fa; + padding: 15px; + border-radius: 8px; + border-left: 4px solid #1da1f2; +} + +.poll-meta { + display: flex; + gap: 25px; + flex-wrap: wrap; + font-size: 14px; + color: #657786; + margin-top: 15px; +} + +.poll-status { + font-weight: 600; + padding: 6px 12px; + border-radius: 16px; + background: #e8f5e8; + color: #2d7d32; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.poll-end-time { + padding: 6px 12px; + border-radius: 16px; + background: #e3f2fd; + color: #1565c0; + font-weight: 500; +} + +.poll-options { + background: white; + padding: 25px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + margin-bottom: 25px; +} + +.poll-options h3 { + font-size: 22px; + color: #14171a; + margin-bottom: 20px; + font-weight: 600; + display: flex; + align-items: center; + gap: 10px; +} + +.poll-options h3::before { + content: "πŸ“Š"; + font-size: 24px; +} + +.options-list { + display: flex; + flex-direction: column; + gap: 15px; +} + +.option-item { + display: flex; + align-items: center; + padding: 18px 20px; + border: 2px solid #e1e8ed; + border-radius: 12px; + background: white; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.option-item::before { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 4px; + background: #1da1f2; + transform: scaleY(0); + transition: transform 0.3s ease; +} + +.option-item:hover { + border-color: #1da1f2; + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(29, 161, 242, 0.2); +} + +.option-item:hover::before { + transform: scaleY(1); +} + +.option-number { + font-weight: 700; + color: #1da1f2; + margin-right: 15px; + min-width: 30px; + height: 30px; + border-radius: 50%; + background: #e8f4fd; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; +} + +.option-text { + flex-grow: 1; + font-size: 16px; + color: #14171a; + font-weight: 500; + margin-right: 15px; +} + +.vote-btn { + background: linear-gradient(135deg, #1da1f2, #1991db); + color: white; + border: none; + padding: 10px 20px; + border-radius: 25px; + cursor: pointer; + font-weight: 600; + font-size: 14px; + transition: all 0.3s ease; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: 0 2px 8px rgba(29, 161, 242, 0.3); +} + +.vote-btn:hover:not(.disabled) { + background: linear-gradient(135deg, #1991db, #1976d2); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(29, 161, 242, 0.4); +} + +.vote-btn:active:not(.disabled) { + transform: translateY(0); +} + +.vote-btn.disabled { + background: #aab8c2; + cursor: not-allowed; + box-shadow: none; + opacity: 0.6; +} + +.poll-info { + background: white; + padding: 25px; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + font-size: 14px; + color: #657786; +} + +.poll-info h4 { + color: #14171a; + margin-bottom: 15px; + font-size: 18px; + font-weight: 600; + display: flex; + align-items: center; + gap: 8px; +} + +.poll-info h4::before { + content: "ℹ️"; +} + +.poll-info p { + margin: 8px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.poll-info strong { + color: #14171a; + min-width: 140px; +} + +/* Loading and Error States */ +.poll-details-container p { + text-align: center; + font-size: 16px; + color: #657786; + margin: 50px 0; +} + +/* Status indicators */ +.poll-status:contains("Active") { + background: #e8f5e8; + color: #2d7d32; +} + +.poll-status:contains("Ended") { + background: #ffebee; + color: #c62828; +} diff --git a/src/components/CSS/ProfileStyles.CSS b/src/components/CSS/ProfileStyles.CSS new file mode 100644 index 00000000..a4783386 --- /dev/null +++ b/src/components/CSS/ProfileStyles.CSS @@ -0,0 +1,134 @@ +.profile-page { + max-width: 800px; + margin: 0 auto; + padding: 20px; +} + +.profile-header { + display: flex; + gap: 30px; + align-items: flex-start; + background: white; + border-radius: 12px; + padding: 30px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; +} + +.profile-picture { + flex-shrink: 0; +} + +.profile-img { + width: 150px; + height: 150px; + border-radius: 50%; + object-fit: cover; + border: 4px solid #f0f0f0; +} + +.profile-info { + flex: 1; +} + +.display-name { + font-size: 2rem; + font-weight: bold; + margin: 0 0 5px 0; + color: #333; +} + +.username { + font-size: 1.1rem; + color: #666; + margin: 0 0 20px 0; +} + +.stats { + display: flex; + gap: 30px; + margin-bottom: 20px; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.stat-count { + font-size: 1.5rem; + font-weight: bold; + color: #333; +} + +.stat-label { + font-size: 0.9rem; + color: #666; + margin-top: 2px; +} + +.bio { + font-size: 1rem; + color: #333; + line-height: 1.5; + margin-bottom: 20px; +} + +.profile-actions { + display: flex; + gap: 10px; +} + +.follow-btn, +.message-btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-weight: 600; + cursor: pointer; + transition: background-color 0.2s; +} + +.follow-btn { + background-color: #f0f0f0; + color: #333; + border: 1px solid #ddd; +} + +.follow-btn:hover { + background-color: #e0e0e0; +} + +.message-btn { + background-color: #f0f0f0; + color: #333; + border: 1px solid #ddd; +} + +.message-btn:hover { + background-color: #e0e0e0; +} + +/* Responsive design */ +@media (max-width: 768px) { + .profile-header { + flex-direction: column; + text-align: center; + padding: 20px; + } + + .profile-img { + width: 120px; + height: 120px; + } + + .display-name { + font-size: 1.8rem; + } + + .stats { + justify-content: center; + } +} diff --git a/src/components/CSS/UserCard.css b/src/components/CSS/UserCard.css new file mode 100644 index 00000000..a441c0d8 --- /dev/null +++ b/src/components/CSS/UserCard.css @@ -0,0 +1,24 @@ +.user-card-page { + padding: 20px; + max-width: 600px; + margin: 0 auto; +} + +.user-card-pfp { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 50%; +} + +.user-card-name { + font-size: 24px; + font-weight: bold; + margin-top: 12px; +} + +.user-card-bio { + font-size: 16px; + color: #555; + margin-top: 8px; +} diff --git a/src/components/CSS/UserPollCardStyles.css b/src/components/CSS/UserPollCardStyles.css new file mode 100644 index 00000000..4d91d8e0 --- /dev/null +++ b/src/components/CSS/UserPollCardStyles.css @@ -0,0 +1,96 @@ +.poll-card { + background: white; + border: 1px solid #e1e8ed; + border-radius: 12px; + padding: 16px; + margin-bottom: 12px; + transition: all 0.2s ease; + cursor: pointer; +} + +.poll-card:hover { + border-color: #1da1f2; + box-shadow: 0 2px 8px rgba(29, 161, 242, 0.1); +} + +.poll-card.poll-ended { + opacity: 0.7; + background: #f7f9fa; +} + +.poll-header { + margin-bottom: 12px; +} + +.poll-title { + font-size: 18px; + font-weight: 600; + color: #14171a; + margin: 0 0 8px 0; + line-height: 1.3; +} + +.poll-meta { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.poll-creator { + color: #657786; + font-size: 14px; + font-weight: 500; +} + +.poll-time { + background: #1da1f2; + color: white; + padding: 4px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.poll-time.ended { + background: #657786; +} + +.poll-status { + margin-top: 8px; + display: flex; + justify-content: flex-end; +} + +.status-badge { + background: #f45d22; + color: white; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + font-weight: 600; + text-transform: uppercase; +} + +/* Responsive design */ +@media (max-width: 768px) { + .poll-card { + padding: 12px; + } + + .poll-title { + font-size: 16px; + } + + .poll-meta { + flex-direction: column; + align-items: flex-start; + } + + .poll-time { + align-self: flex-end; + } +} \ No newline at end of file diff --git a/src/components/CSS/UsersPage.css b/src/components/CSS/UsersPage.css new file mode 100644 index 00000000..a2ffee2f --- /dev/null +++ b/src/components/CSS/UsersPage.css @@ -0,0 +1,42 @@ +.users-page { + padding: 20px; + max-width: 600px; + margin: 0 auto; +} + +.search-input { + width: 100%; + padding: 8px; + margin-bottom: 16px; + font-size: 16px; +} + +.user-list { + list-style: none; + padding: 0; +} + +.user-card { + border: 1px solid #ddd; + padding: 12px; + margin-bottom: 10px; + cursor: pointer; +} + +.user-pfp { + width: 50px; + height: 50px; + border-radius: 50%; + object-fit: cover; +} + +.user-name { + font-weight: bold; + margin: 8px 0 4px; +} + +.user-bio { + margin: 0; + font-size: 14px; + color: #555; +} diff --git a/src/components/DraftPoll.jsx b/src/components/DraftPoll.jsx new file mode 100644 index 00000000..ee6b67fa --- /dev/null +++ b/src/components/DraftPoll.jsx @@ -0,0 +1,270 @@ +import React, { useEffect, useState } from "react"; +import axios from "axios"; +import { useNavigate, useParams, Link } from "react-router-dom"; + +const DraftPoll = ({ user }) => { + const { id } = useParams(); + const navigate = useNavigate(); + const [drafts, setDrafts] = useState([]); + const [loading, setLoading] = useState(true); + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [options, setOptions] = useState(["", ""]); + const [endDate, setEndDate] = useState(""); + const [isIndefinite, setIsIndefinite] = useState(true); + const [allowAnonymous, setAllowAnonymous] = useState(false); + const [error, setError] = useState(""); + +useEffect(() => { + if (!id && user) { + axios + .get("http://localhost:8080/api/polls") + .then((res) => { + const userDrafts = res.data.filter( + (draft) => (draft.creator_id === user.id && draft.status === "draft") + ); + setDrafts(userDrafts); + setLoading(false); + }) + .catch((err) => { + console.error("Failed to load drafts:", err); + setLoading(false); + }); + } +}, [id, user]); + +useEffect(() => { + if (id) { + axios.get(`http://localhost:8080/api/polls/${id}`) + .then(res => { + const data = res.data; + setTitle(data.title || ""); + setDescription(data.description || ""); + setAllowAnonymous(data.allowAnonymous || false); + setIsIndefinite(!data.endAt); + setEndDate(data.endAt ? data.endAt.slice(0, 16) : ""); + + setOptions(data.pollOptions?.map(opt => opt.text) || ["", ""]); + }) + .catch(err => { + console.error("Error loading draft:", err); + }); + } +}, [id]); + +const deleteDraft = async (draftId) => { + if (!window.confirm("Are you sure you want to delete this draft?")) return; + + try { + await axios.delete(`http://localhost:8080/api/polls/${draftId}`); + setDrafts((prev) => prev.filter((d) => d.id !== draftId)); + } catch (err) { + console.error("Failed to delete draft:", err); + alert("Failed to delete draft"); + } +}; + + const handleOptionChange = (index, value) => { + const updated = [...options]; + updated[index] = value; + setOptions(updated); + }; + + const addOptionField = () => { + setOptions([...options, ""]); + }; + + const removeOptionField = (index) => { + const updated = options.filter((_, i) => i !== index); + setOptions(updated); + }; + + const getMinDateTime = () => { + const now = new Date(); + now.setHours(now.getHours() + 1); + return now.toISOString().slice(0, 16); + }; + + const handleAddEndDate = () => { + setIsIndefinite(false); + }; + + const handleRemoveEndDate = () => { + setIsIndefinite(true); + setEndDate(""); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + const validOptions = options.filter((opt) => opt.trim() !== ""); + if (!title.trim()) return setError("Title required."); + if (validOptions.length < 2) return setError("At least 2 options required."); + + try { + await axios.patch(`http://localhost:8080/api/polls/${id}`, { + title: title.trim(), + description: description.trim(), + allowAnonymous, + endAt: isIndefinite ? null : new Date(endDate).toISOString(), + pollOptions: validOptions.map((text, i) => ({ + text, + position: i + 1, + })), + }); + + navigate("/poll-list"); + } catch (err) { + console.error("Failed to update draft:", err); + setError("Update failed."); + } + }; + + if (!id) { + if (loading) return

Loading drafts...

; + return ( +
+

My Draft Polls

+ {drafts.length === 0 ? ( +

You don't have any saved drafts.

+ ) : ( + + )} +
+ ); + } + + return ( +
+

Edit Drafted Poll

+ {error &&

{error}

} + +
+
+ + setTitle(e.target.value)} + placeholder="Enter your poll question" + required + /> +
+ +
+ +