+
+ Public Polls
+
Login
diff --git a/src/components/NewPoll.jsx b/src/components/NewPoll.jsx
new file mode 100644
index 00000000..b22bc4b5
--- /dev/null
+++ b/src/components/NewPoll.jsx
@@ -0,0 +1,305 @@
+import React, { useState } from "react";
+import axios, { all } from "axios";
+import { useNavigate } from "react-router-dom";
+
+const NewPoll = ({ user }) => {
+ 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("");
+ const navigate = useNavigate();
+ const [restrictToUsers, setRestrictToUsers] = useState(false);
+ const [allowedUsersInput, setAllowedUsersInput] = useState("");
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ const creator_id = user.id;
+
+ const handleOptionChange = (index, value) => {
+ const updatedOptions = [...options];
+ updatedOptions[index] = value;
+ setOptions(updatedOptions);
+ };
+
+ const addOptionField = () => {
+ setOptions([...options, ""]);
+ };
+
+ const removeOptionField = (index) => {
+ const updatedOptions = options.filter((_, i) => i != index);
+ setOptions(updatedOptions);
+ };
+
+ 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();
+
+ if (!title.trim()) {
+ return setError("Poll title is required.");
+ }
+
+ if (!isIndefinite) {
+ if (!endDate) {
+ return setError(
+ "Please select an end date or remove the end date to make it indefinite."
+ );
+ }
+
+ const selectedEndDate = new Date(endDate);
+ const now = new Date();
+ if (selectedEndDate <= now) {
+ return setError("End date must be in the future.");
+ }
+ }
+
+ const validOptions = options.filter((opt) => opt.trim() !== "");
+ if (validOptions.length < 2) {
+ return setError("At least two filled options are required.");
+ }
+
+ const allowedUserIds = restrictToUsers
+ ? allowedUsersInput
+ .split(",")
+ .map((id) => parseInt(id.trim()))
+ .filter((id) => !isNaN(id))
+ : [];
+
+ try {
+ const pollData = {
+ creator_id,
+ title: title.trim(),
+ description: description.trim(),
+ allowAnonymous,
+ allowListOnly: restrictToUsers,
+ allowedUserIds,
+ pollOptions: validOptions.map((optionText, index) => ({
+ text: optionText,
+ position: index + 1,
+ })),
+ };
+
+ if (!isIndefinite && endDate) {
+ pollData.endAt = new Date(endDate).toISOString();
+ }
+
+ await axios.post("http://localhost:8080/api/polls", pollData);
+
+ navigate("/poll-list");
+ } catch (err) {
+ setError("Failed to create poll.");
+ console.error("Poll creation error:", err);
+ }
+ };
+
+ return (
+
+
Create New Poll
+ {error &&
{error}
}
+
+
+
+ );
+};
+
+export default NewPoll;
diff --git a/src/components/PollCard.jsx b/src/components/PollCard.jsx
new file mode 100644
index 00000000..611dfa7e
--- /dev/null
+++ b/src/components/PollCard.jsx
@@ -0,0 +1,104 @@
+import React, { useState, useEffect } from 'react';
+import './CSS/PollCardStyles.css';
+
+
+const PollCard = ({ poll, onClick, onDuplicate }) => {
+ const [timeLeft, setTimeLeft] = useState('');
+ const [creator, setCreator] = useState(null);
+
+ useEffect(() => {
+ const calculateTimeLeft = () => {
+ if (!poll.endAt) {
+ setTimeLeft('No end date');
+ return;
+ }
+
+ const now = new Date();
+ const endTime = new Date(poll.endAt);
+ const difference = endTime - now;
+
+ if (difference > 0) {
+ const days = Math.floor(difference / (1000 * 60 * 60 * 24));
+ const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
+ const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
+
+ if (days > 0) {
+ setTimeLeft(`${days}d ${hours}h left`);
+ } else if (hours > 0) {
+ setTimeLeft(`${hours}h ${minutes}m left`);
+ } else if (minutes > 0) {
+ setTimeLeft(`${minutes}m left`);
+ } else {
+ setTimeLeft('Less than 1m left');
+ }
+ } else {
+ setTimeLeft('Poll ended');
+ }
+ };
+
+ calculateTimeLeft();
+ const timer = setInterval(calculateTimeLeft, 60000);
+
+ return () => clearInterval(timer);
+ }, [poll.endAt]);
+
+ useEffect(() => {
+ const fetchCreator = async () => {
+ try {
+ const response = await fetch(`http://localhost:8080/api/users/${poll.creator_id}`);
+ if (response.ok) {
+ const userData = await response.json();
+ setCreator(userData);
+ } else {
+ console.error('Failed to fetch creator:', response.status);
+ setCreator({ username: 'Unknown' });
+ }
+ } catch (error) {
+ console.error('Error fetching creator:', error);
+ setCreator({ username: 'Unknown' });
+ }
+ };
+
+ if (poll.creator_id) {
+ fetchCreator();
+ }
+ }, [poll.creator_id]);
+
+ const isPollActive = poll.endAt ? new Date(poll.endAt) > new Date() : true;
+
+return (
+
+
+
{poll.title}
+
+
+ by {creator ? `@${creator.username}` : 'Loading...'}
+
+
+ {timeLeft}
+
+
+
+
+ {poll.description && (
+
+ )}
+
+
Duplicate
+
+ {!isPollActive && (
+
+ Ended
+
+ )}
+
+ );
+};
+
+export default PollCard;
\ No newline at end of file
diff --git a/src/components/PollDetails.jsx b/src/components/PollDetails.jsx
new file mode 100644
index 00000000..e94ad164
--- /dev/null
+++ b/src/components/PollDetails.jsx
@@ -0,0 +1,197 @@
+import React, { useState, useEffect } from "react";
+import { useParams } from "react-router-dom";
+import axios from "axios";
+import { API_URL } from "../shared";
+import VoteForm from "./VoteForm";
+import IRVResults from "./IRVResults";
+
+const PollDetails = ({ user }) => {
+ const { id } = useParams();
+ const [poll, setPoll] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [master, setMaster] = useState(null);
+ const [userLoading, setUserLoading] = useState(false);
+ const [showVoteForm, setShowVoteForm] = useState(false);
+
+ const fetchPoll = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get(`${API_URL}/api/polls/${id}`);
+ setPoll(response.data);
+ } catch (error) {
+ console.error("Error fetching poll:", error);
+ setError("Failed to load poll");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchUser = async (creatorId) => {
+ try {
+ setUserLoading(true);
+ const response = await axios.get(`${API_URL}/api/users/${creatorId}`);
+ setMaster(response.data);
+ } catch (error) {
+ console.error("Error fetching user:", error);
+ } finally {
+ setUserLoading(false);
+ }
+ };
+
+ const handleVoteSubmitted = (voteData) => {
+ console.log("Vote submitted:", voteData);
+ setShowVoteForm(false);
+ fetchPoll();
+ };
+
+ useEffect(() => {
+ if (id) {
+ fetchPoll();
+ }
+ }, [id]);
+
+ useEffect(() => {
+ if (poll && poll.creator_id) {
+ fetchUser(poll.creator_id);
+ }
+ }, [poll]);
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!poll) {
+ return (
+
+ );
+ }
+
+ const isPollActive = poll.endAt ? new Date(poll.endAt) > new Date() : true;
+ const canVote = isPollActive && (user || poll.allowAnonymous);
+ const showLoginPrompt = isPollActive && !user && !poll.allowAnonymous;
+
+ return (
+
+
+
+
{poll.title}
+ {poll.description && (
+
{poll.description}
+ )}
+
+
+
+ Status: {isPollActive ? "Active" : "Ended"}
+
+ {poll.endAt && (
+
+ Ends: {new Date(poll.endAt).toLocaleDateString()} at{" "}
+ {new Date(poll.endAt).toLocaleTimeString()}
+
+ )}
+ {!poll.endAt && No end date }
+
+
+
+
+
Options:
+ {poll.pollOptions && poll.pollOptions.length > 0 ? (
+
+ {poll.pollOptions
+ .sort((a, b) => a.position - b.position)
+ .map((option, index) => (
+
+ {index + 1}.
+ {option.text}
+
+ ))}
+
+ ) : (
+
No options available for this poll.
+ )}
+
+ {/* Voting section */}
+ {canVote && (
+
setShowVoteForm(!showVoteForm)}
+ className="vote-toggle-btn"
+ >
+ {showVoteForm ? "Cancel Vote" : "Vote Now"}
+
+ )}
+
+ {/* Login prompt for non-anonymous polls */}
+ {showLoginPrompt && (
+
+ )}
+
+ {/* Poll ended message */}
+ {!isPollActive && (
+
+
This poll has ended. No more votes are being accepted.
+
+ )}
+
+
+ {/* Vote form - only show if user can vote */}
+ {showVoteForm && canVote && (
+
+ )}
+
+
+
+ Anonymous voting: {" "}
+ {poll.allowAnonymous ? "Allowed" : "Not allowed"}
+
+
+ Total votes: {poll.ballots?.length || 0}
+
+
+ Created: {" "}
+ {new Date(poll.createdAt).toLocaleDateString()}
+
+
+ Created by: {" "}
+ {userLoading ? "Loading..." : master ? master.username : "Unknown"}
+
+
+
+ {/* Results section */}
+ {poll.status === "published" && poll.ballots?.length > 0 && (
+
+
Results
+
+
+ )}
+
+
+ );
+};
+
+export default PollDetails;
diff --git a/src/components/PollList.jsx b/src/components/PollList.jsx
new file mode 100644
index 00000000..6ba86a69
--- /dev/null
+++ b/src/components/PollList.jsx
@@ -0,0 +1,91 @@
+import React, { useState, useEffect } from "react";
+import axios from "axios";
+import PollCard from "./PollCard";
+import { useNavigate } from "react-router-dom";
+
+const PollList = ({ poll }) => {
+ const [polls, setPolls] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const navigate = useNavigate();
+
+ const duplicatePoll = async (poll) => {
+ try {
+ const newPollData = {
+ creator_id: poll.creator_id,
+ title: poll.title + " (Copy)",
+ description: poll.description,
+ allowAnonymous: poll.allowAnonymous,
+ endAt: poll.endAt,
+ pollOptions: poll.pollOptions.map((opt) => ({
+ text: opt.text,
+ position: opt.position,
+ })),
+ status: "draft",
+ };
+
+ const response = await axios.post(
+ "http://localhost:8080/api/polls",
+ newPollData
+ );
+ const draftId = response.data.id;
+ const fullDraftResponse = await axios.get(`http://localhost:8080/api/polls/${draftId}`);
+ navigate(`/edit-draft/${draftId}`);
+ } catch (error) {
+ console.error("Failed to duplicate poll:", error);
+ alert("Failed to duplicate poll");
+ }
+ };
+
+ if (!polls) {
+ return (
+
+ );
+ }
+ const handleUserClick = (id) => {
+ navigate(`/polls/${id}`);
+ };
+
+ useEffect(() => {
+ const fetchPolls = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get("http://localhost:8080/api/polls");
+ setPolls(response.data);
+ } catch (err) {
+ setError("Failed to fetch polls.");
+ console.error("Error fetching polls:", err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchPolls();
+ }, []);
+
+ if (loading) return
Loading polls...
;
+ if (error) return
{error}
;
+
+ return (
+
+ {polls.length === 0 ? (
+
No polls available.
+ ) : (
+ polls.map((poll) => (
+
handleUserClick(poll.id)}
+ onDuplicate={() => duplicatePoll(poll)}
+ />
+ ))
+ )}
+
+ );
+};
+
+export default PollList;
\ No newline at end of file
diff --git a/src/components/Profile.jsx b/src/components/Profile.jsx
new file mode 100644
index 00000000..5db27679
--- /dev/null
+++ b/src/components/Profile.jsx
@@ -0,0 +1,158 @@
+import React from "react";
+import "./CSS/ProfileStyles.css";
+import { useParams, useNavigate } from "react-router-dom";
+import { API_URL } from "../shared";
+import { useState, useEffect } from "react";
+import axios from "axios";
+import UserPollCard from "./UserPollCard";
+
+const ProfilePage = ({ user, authLoading }) => {
+ const [master, setMaster] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const navigate = useNavigate();
+
+ const fetchUser = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get(`${API_URL}/api/users/${user.id}`);
+ setMaster(response.data);
+ } catch (error) {
+ console.error("Error fetching user:", error);
+ setError("Failed to load user");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleUserClick = (id) => {
+ navigate(`/polls/${id}`);
+ };
+
+ useEffect(() => {
+ if (authLoading === false && !user) {
+ navigate("/login");
+ }
+ }, [user, authLoading, navigate]);
+
+ useEffect(() => {
+ if (user?.id) {
+ fetchUser();
+ }
+ }, [user?.id]);
+
+ if (authLoading) {
+ return (
+
+ );
+ }
+
+ if (!user) {
+ return (
+
+
+
Redirecting to login...
+
+
+ );
+ }
+
+ if (loading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!master) {
+ return (
+
+ );
+ }
+
+ console.log(user);
+
+ return (
+
+
+
+
+
+
+
+
{master.username}
+
@{master.username}
+
+
+
+
+ {master.polls ? master.polls.length : 0}
+
+ Polls
+
+
+ {master.followersCount || 0}
+ Followers
+
+
+ {master.followingCount || 0}
+ Following
+
+
+
+ {master.bio &&
{master.bio}
}
+
+
+ Edit Profile
+ View Drafts
+
+
+
+
+ {master.polls && master.polls.length > 0 && (
+
+
My Polls ({master.polls.length})
+
+ {master.polls.map((poll) => (
+ handleUserClick(poll.id)}
+ />
+ ))}
+
+
+ )}
+
+ );
+};
+
+export default ProfilePage;
\ No newline at end of file
diff --git a/src/components/Signup.jsx b/src/components/Signup.jsx
index 989fa096..116ffde5 100644
--- a/src/components/Signup.jsx
+++ b/src/components/Signup.jsx
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { useNavigate, Link } from "react-router-dom";
import axios from "axios";
-import "./AuthStyles.css";
+import "./CSS/AuthStyles.css";
import { API_URL } from "../shared";
const Signup = ({ setUser }) => {
diff --git a/src/components/UserCard.jsx b/src/components/UserCard.jsx
new file mode 100644
index 00000000..3bb2d1fb
--- /dev/null
+++ b/src/components/UserCard.jsx
@@ -0,0 +1,77 @@
+import React, { useState, useEffect } from "react";
+import axios from "axios";
+import { useParams, useNavigate } from "react-router-dom";
+import "./CSS/UserCard.css";
+
+const UserCard = () => {
+ console.log("UserCard rendered");
+
+ const { id } = useParams();
+ console.log("params id:", id);
+ const navigate = useNavigate();
+
+ const [user, setUser] = useState(null);
+ const [userPolls, setUserPolls] = useState([]);
+
+ useEffect(() => {
+ const fetchUserAndPolls = async () => {
+ try {
+ const [userRes, allPollsRes] = await Promise.all([
+ axios.get(`http://localhost:8080/api/users/${id}`),
+ axios.get("http://localhost:8080/api/polls"),
+ ]);
+
+ const userData = userRes.data;
+ setUser(userData);
+
+ console.log("userData.id:", userData.id);
+ console.log(
+ "poll.creator_id values:",
+ allPollsRes.data.map((p) => p.creator_id)
+ );
+
+ const pollsByUser = allPollsRes.data.filter(
+ (poll) => poll.creator_id === userData.id
+ );
+ setUserPolls(pollsByUser);
+ } catch (error) {
+ console.error("Error fetching user or polls:", error);
+ }
+ };
+
+ fetchUserAndPolls();
+ }, [id]);
+
+ if (!user) return
Loading...
;
+
+ return (
+
+
User Profile
+ {user.imageUrl && (
+
+ )}
+
{user.username}
+ {user.bio &&
{user.bio}
}
+
+
Polls by {user.username}
+ {userPolls.length === 0 ? (
+
This user hasn't created any polls yet.
+ ) : (
+
+ {userPolls.map((poll) => (
+ navigate(`/polls/${poll.id}`)}
+ style={{ cursor: "pointer" }}
+ >
+ {poll.title}
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default UserCard;
diff --git a/src/components/UserPollCard.jsx b/src/components/UserPollCard.jsx
new file mode 100644
index 00000000..f882e1a0
--- /dev/null
+++ b/src/components/UserPollCard.jsx
@@ -0,0 +1,98 @@
+import React, { useState, useEffect } from 'react';
+import './CSS/UserPollCardStyles.css';
+
+const PollCard = ({ poll, onClick }) => {
+ const [timeLeft, setTimeLeft] = useState('');
+ const [creator, setCreator] = useState(null);
+
+ useEffect(() => {
+ const calculateTimeLeft = () => {
+ if (!poll.endAt) {
+ setTimeLeft('No end date');
+ return;
+ }
+
+ const now = new Date();
+ const endTime = new Date(poll.endAt);
+ const difference = endTime - now;
+
+ if (difference > 0) {
+ const days = Math.floor(difference / (1000 * 60 * 60 * 24));
+ const hours = Math.floor((difference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
+ const minutes = Math.floor((difference % (1000 * 60 * 60)) / (1000 * 60));
+
+ if (days > 0) {
+ setTimeLeft(`${days}d ${hours}h left`);
+ } else if (hours > 0) {
+ setTimeLeft(`${hours}h ${minutes}m left`);
+ } else if (minutes > 0) {
+ setTimeLeft(`${minutes}m left`);
+ } else {
+ setTimeLeft('Less than 1m left');
+ }
+ } else {
+ setTimeLeft('Poll ended');
+ }
+ };
+
+ calculateTimeLeft();
+ const timer = setInterval(calculateTimeLeft, 60000);
+
+ return () => clearInterval(timer);
+ }, [poll.endAt]);
+
+ useEffect(() => {
+ const fetchCreator = async () => {
+ try {
+ const response = await fetch(`http://localhost:8080/api/users/${poll.creator_id}`);
+ if (response.ok) {
+ const userData = await response.json();
+ setCreator(userData);
+ } else {
+ console.error('Failed to fetch creator:', response.status);
+ setCreator({ username: 'Unknown' });
+ }
+ } catch (error) {
+ console.error('Error fetching creator:', error);
+ setCreator({ username: 'Unknown' });
+ }
+ };
+
+ if (poll.creator_id) {
+ fetchCreator();
+ }
+ }, [poll.creator_id]);
+
+ const isPollActive = poll.endAt ? new Date(poll.endAt) > new Date() : true;
+
+return (
+
+
+
{poll.title}
+
+
+ {timeLeft}
+
+
+
+
+ {poll.description && (
+
+ )}
+
+ {!isPollActive && (
+
+ Ended
+
+ )}
+
+ );
+};
+
+export default PollCard;
\ No newline at end of file
diff --git a/src/components/UsersPage.jsx b/src/components/UsersPage.jsx
new file mode 100644
index 00000000..707e4096
--- /dev/null
+++ b/src/components/UsersPage.jsx
@@ -0,0 +1,62 @@
+import React, { useState, useEffect } from "react";
+import axios from "axios";
+import { useNavigate } from "react-router-dom";
+import "./CSS/UsersPage.css";
+const UsersPage = () => {
+ const [users, setUsers] = useState([]);
+ const [search, setSearch] = useState("");
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ axios
+ .get("http://localhost:8080/api/users")
+ .then((response) => {
+ setUsers(response.data);
+ })
+ .catch((error) => {
+ console.error("Error fetching users:", error);
+ });
+ }, []);
+
+ const handleUserClick = (id) => {
+ navigate(`/users/${id}`);
+ };
+
+ const filteredUsers = users.filter((user) =>
+ user.username.toLowerCase().includes(search.toLowerCase())
+ );
+
+ return (
+
+
All Users
+
setSearch(e.target.value)}
+ />
+ {filteredUsers.length === 0 ? (
+
No users found.
+ ) : (
+
+ {filteredUsers.map((user) => (
+ handleUserClick(user.id)}
+ >
+ {user.imageUrl && (
+
+ )}
+ {user.username}
+ {user.bio && {user.bio}
}
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default UsersPage;
diff --git a/src/components/VoteForm.jsx b/src/components/VoteForm.jsx
new file mode 100644
index 00000000..a3ee30c5
--- /dev/null
+++ b/src/components/VoteForm.jsx
@@ -0,0 +1,149 @@
+import React, { useState } from "react";
+import axios from "axios";
+import { API_URL } from "../shared";
+
+const VoteForm = ({ poll, user, onVoteSubmitted }) => {
+ const [rankings, setRankings] = useState({});
+ const [submitting, setSubmitting] = useState(false);
+ const [error, setError] = useState(null);
+ const [success, setSuccess] = useState(false);
+
+ const handleRankChange = (optionId, rank) => {
+ setRankings((prev) => ({
+ ...prev,
+ [optionId]: parseInt(rank),
+ }));
+ };
+
+ const submitVote = async () => {
+ try {
+ setSubmitting(true);
+ setError(null);
+
+ const rankingsArray = Object.entries(rankings)
+ .filter(([_, rank]) => rank > 0)
+ .map(([pollOptionId, rank]) => ({
+ pollOptionId: parseInt(pollOptionId),
+ rank: rank,
+ }));
+
+ if (rankingsArray.length === 0) {
+ setError("Please rank at least one option");
+ return;
+ }
+
+ const ranks = rankingsArray.map((r) => r.rank);
+ const uniqueRanks = [...new Set(ranks)];
+ if (ranks.length !== uniqueRanks.length) {
+ setError("Each option must have a unique rank");
+ return;
+ }
+
+ if (rankingsArray.length < poll.pollOptions.length) {
+ const warningMessage = window.confirm(
+ "You have not ranked all options. Do you want to continue?"
+ );
+ if (!warningMessage) {
+ setSubmitting(false);
+ return;
+ }
+ }
+
+ const voteData = {
+ pollId: poll.id,
+ userId: user ? user.id : null,
+ rankings: rankingsArray,
+ };
+
+ const response = await axios.post(`${API_URL}/api/ballots`, voteData);
+
+ setSuccess(true);
+ setRankings({});
+
+ if (onVoteSubmitted) {
+ onVoteSubmitted(response.data);
+ const confirmationMessage = window.confirm(
+ "Vote submitted successfully!"
+ );
+ }
+ } catch (error) {
+ console.error("Error submitting vote:", error);
+ setError(error.response?.data?.error || "Failed to submit vote");
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ if (success) {
+ return (
+
+
+
β Vote submitted successfully!
+
Thank you for participating in this poll.
+ {!user && (
+
Your vote was submitted anonymously.
+ )}
+
+
+ );
+ }
+
+ return (
+
+
Rank the Options
+
Rank the options from 1 (most preferred) to {poll.pollOptions.length} (least preferred)
+
+ {!user && poll.allowAnonymous && (
+
+
π You are voting anonymously
+
+ )}
+
+ {error && (
+
+ )}
+
+
+ {poll.pollOptions
+ .sort((a, b) => a.position - b.position)
+ .map((option, index) => (
+
+
+ {index + 1}.
+ {option.text}
+
+
+
+ Rank:
+ handleRankChange(option.id, e.target.value)}
+ className="rank-select"
+ >
+ No rank
+ {Array.from({ length: poll.pollOptions.length }, (_, i) => (
+
+ {i + 1}
+
+ ))}
+
+
+
+ ))}
+
+
+
+ {submitting ? "Submitting..." : "Submit Vote"}
+
+
+ );
+};
+
+export default VoteForm;
diff --git a/src/components/css/Friends.css b/src/components/css/Friends.css
new file mode 100644
index 00000000..a722872e
--- /dev/null
+++ b/src/components/css/Friends.css
@@ -0,0 +1,64 @@
+.friends-container {
+ padding: 20px;
+}
+
+.friends-outline {
+ background: white;
+ padding: 2rem;
+ border-radius: 8px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ width: 100%;
+ max-width: 1000px;
+}
+
+.profile {
+ justify-content: column;
+ gap: 40px;
+}
+
+.image {
+ justify-content: row;
+}
+
+.image img {
+ width: 150px;
+ height: 150px;
+ padding: 10px;
+}
+
+.top-section {
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-start;
+ gap: 40px;
+ flex-wrap: wrap;
+}
+
+.friend-name {
+ margin: 0;
+ font-size: 28px;
+}
+
+.stats {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ gap: 20px;
+}
+
+.middle-section,
+.bottom-section {
+ margin-top: 30px;
+}
+
+.middle-section h3,
+.bottom-section h3 {
+ margin: 0;
+}
+
+.friend-search-input {
+ width: 100%;
+ padding: 0.5rem;
+ margin: 1rem 0;
+ font-size: 1rem;
+}