Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Frontend] Implement some UX improvements #698

Merged
merged 4 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-collapsible": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-label": "^2.0.2",
Expand All @@ -33,6 +34,8 @@
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.3",
"@tailwindcss/typography": "^0.5.15",
"@tanstack/react-query": "^5.35.1",
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/components/Answers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export function Answers({ questionId }: AnswersProps) {
onDownvote={() => handleVote(answer.id, -1)}
/>
))}
{answers.length === 0 && (
<span>
{" "}
This question doesn't have an answer yet. Contribute to the discussion
by answering this question.
</span>
)}
</div>
);
}
354 changes: 193 additions & 161 deletions frontend/src/components/DifficultyBar.tsx
Original file line number Diff line number Diff line change
@@ -1,161 +1,193 @@
import React, {useState} from "react";
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "@/components/ui/tooltip";
import { useRateQuestion} from "@/services/api/programmingForumComponents";
import { Button } from "@/components/ui/button";
import useAuthStore from "@/services/auth";


type DifficultyBarProps = {
easyCount: number;
mediumCount: number;
hardCount: number;
questionId: number;
};

export const DifficultyBar: React.FC<DifficultyBarProps> = ({
easyCount,
mediumCount,
hardCount,
questionId,
}) => {
const { token } = useAuthStore(); // Fetch authentication status
const [votedDifficulty, setVotedDifficulty] = useState<string | null>(null); // Track user's vote
const [localCounts, setLocalCounts] = useState({
easy: easyCount,
medium: mediumCount,
hard: hardCount,
});

const { mutateAsync: rateQuestion } = useRateQuestion();

// Helper to calculate percentage
const calculatePercentage = (count: number) =>
totalVotes > 0 ? ((count / totalVotes) * 100).toFixed(1) : "0";

const handleVote = async (difficulty: "EASY" | "MEDIUM" | "HARD") => {
if (!token) {
alert("You must be logged in to vote!");
return;
}

try {
const response = await rateQuestion({
pathParams: { id: questionId },
body: { difficulty: difficulty },
});

const { easyCount, mediumCount, hardCount } = response.data;

setLocalCounts({
easy: easyCount ?? 0,
medium: mediumCount ?? 0,
hard: hardCount ?? 0,
});

setVotedDifficulty(difficulty);
} catch (error) {
console.error("Failed to vote:", error);
alert("There was an issue submitting your vote. Please try again.");
}
};

const totalVotes = localCounts.easy + localCounts.medium + localCounts.hard;

const getHighestVotedDifficulty = () => {
const counts = [
{ level: "Easy", count: localCounts.easy },
{ level: "Medium", count: localCounts.medium },
{ level: "Hard", count: localCounts.hard },
];
const highest = counts.reduce((prev, current) => //any equality -> pick higher difficulty level
prev.count > current.count ? prev : current
);
return highest.level;
};

return (
<div className="mt-6">
<TooltipProvider>
{/* Voting Form */}
<div className="flex flex-col items-center mb-4">
<h2 className="text-lg font-semibold mb-2">Vote for Difficulty Level:</h2>
<div className="flex flex-row gap-2">
<Button
variant="outline"
onClick={() => handleVote("EASY")}
disabled={!token || votedDifficulty === "EASY"}
>
Easy
</Button>
<Button
variant="outline"
onClick={() => handleVote("MEDIUM")}
disabled={!token || votedDifficulty === "MEDIUM"}
>
Medium
</Button>
<Button
variant="outline"
onClick={() => handleVote("HARD")}
disabled={!token || votedDifficulty === "HARD"}
>
Hard
</Button>
</div>
</div>

{/* Difficulty Bar */}
<div>
<h2 className="mb-2 text-lg font-semibold">
The community finds this question: <strong>{getHighestVotedDifficulty()}</strong> difficulty.
</h2>
<div className="text-sm text-gray-600 mb-2">
<span>Easy: {localCounts.easy} votes</span>,{" "}
<span>Medium: {localCounts.medium} votes</span>,{" "}
<span>Hard: {localCounts.hard} votes</span>
</div>
<div className="relative flex w-full h-6 rounded border border-gray-300 overflow-hidden">
{/* Easy Section */}
<Tooltip>
<TooltipTrigger asChild>
<div
className="bg-green-500"
style={{ width: `${calculatePercentage(localCounts.easy)}%` }}
></div>
</TooltipTrigger>
<TooltipContent>
{localCounts.easy} Easy votes
</TooltipContent>
</Tooltip>

{/* Medium Section */}
<Tooltip>
<TooltipTrigger asChild>
<div
className="bg-yellow-500"
style={{ width: `${calculatePercentage(localCounts.medium)}%` }}
></div>
</TooltipTrigger>
<TooltipContent>
{localCounts.medium} Medium votes
</TooltipContent>
</Tooltip>

{/* Hard Section */}
<Tooltip>
<TooltipTrigger asChild>
<div
className="bg-red-500"
style={{ width: `${calculatePercentage(localCounts.hard)}%` }}
></div>
</TooltipTrigger>
<TooltipContent>
{localCounts.hard} Hard votes
</TooltipContent>
</Tooltip>
</div>
</div>
</TooltipProvider>
</div>
);
};
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useRateQuestion } from "@/services/api/programmingForumComponents";
import useAuthStore from "@/services/auth";
import React, { useState } from "react";
import { Card } from "./ui/card";
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";

type DifficultyBarProps = {
easyCount: number;
mediumCount: number;
hardCount: number;
selfDifficultyVote: "EASY" | "MEDIUM" | "HARD" | null;
questionId: number;
onVote: () => void;
};

export const DifficultyBar: React.FC<DifficultyBarProps> = ({
selfDifficultyVote,
easyCount,
mediumCount,
hardCount,
questionId,
onVote,
}) => {
const { token } = useAuthStore(); // Fetch authentication status
const [localCounts, setLocalCounts] = useState({
easy: easyCount,
medium: mediumCount,
hard: hardCount,
});

const { mutateAsync: rateQuestion } = useRateQuestion();

// Helper to calculate percentage
const calculatePercentage = (count: number) =>
totalVotes > 0 ? ((count / totalVotes) * 100).toFixed(1) : "0";

const handleVote = async (difficulty: "EASY" | "MEDIUM" | "HARD") => {
if (!token) {
alert("You must be logged in to vote!");
return;
}

try {
const response = await rateQuestion({
pathParams: { id: questionId },
body: { difficulty: difficulty },
});
onVote?.();

const { easyCount, mediumCount, hardCount } = response.data;

setLocalCounts({
easy: easyCount ?? 0,
medium: mediumCount ?? 0,
hard: hardCount ?? 0,
});
} catch (error) {
console.error("Failed to vote:", error);
alert("There was an issue submitting your vote. Please try again.");
}
};

const totalVotes = localCounts.easy + localCounts.medium + localCounts.hard;

const getHighestDifficultyVote = () => {
const counts = [
{ level: "Easy", count: localCounts.easy },
{ level: "Medium", count: localCounts.medium },
{ level: "Hard", count: localCounts.hard },
];
const highest = counts.reduce(
(
prev,
current, //any equality -> pick higher difficulty level
) => (prev.count > current.count ? prev : current),
);
return highest.level;
};

return (
<Card className="my-4 border-none bg-neutral-100 px-6 py-8 shadow-sm">
<h2 className="text-xl">Difficulty voting</h2>
<TooltipProvider>
{/* Voting Form */}
<div className="my-4 flex items-center gap-2">
<span className="text-">Vote for Difficulty Level:</span>
<ToggleGroup
value={selfDifficultyVote || undefined}
onValueChange={(val) =>
val && handleVote(val as "EASY" | "MEDIUM" | "HARD")
}
type="single"
size="lg"
variant="outline"
>
<ToggleGroupItem
value="EASY"
aria-label="Vote easy difficulty"
className={
selfDifficultyVote === "EASY"
? "data-[state=on]:bg-green-200"
: ""
}
>
Easy
</ToggleGroupItem>
<ToggleGroupItem
value="MEDIUM"
className={
selfDifficultyVote === "MEDIUM"
? "data-[state=on]:bg-yellow-200"
: ""
}
aria-label="Vote medium difficulty"
>
Medium
</ToggleGroupItem>
<ToggleGroupItem
value="HARD"
aria-label="Vote hard difficulty"
className={
selfDifficultyVote === "HARD"
? "data-[state=on]:bg-red-100"
: ""
}
>
Hard
</ToggleGroupItem>
</ToggleGroup>
</div>

{/* Difficulty Bar */}
<div>
<h2 className="mb-2 text-lg font-semibold">
The community finds this question:{" "}
<strong>{getHighestDifficultyVote()}</strong> difficulty.
</h2>
<div className="mb-2 text-sm text-gray-600">
<span>Easy: {localCounts.easy} votes</span>,{" "}
<span>Medium: {localCounts.medium} votes</span>,{" "}
<span>Hard: {localCounts.hard} votes</span>
</div>
<div className="relative flex h-6 w-full overflow-hidden rounded border border-gray-300">
{/* Easy Section */}
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<div
className="bg-green-500"
style={{
width: `${calculatePercentage(localCounts.easy)}%`,
}}
></div>
</TooltipTrigger>
<TooltipContent>{localCounts.easy} Easy votes</TooltipContent>
</Tooltip>

{/* Medium Section */}
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<div
className="bg-yellow-500"
style={{
width: `${calculatePercentage(localCounts.medium)}%`,
}}
></div>
</TooltipTrigger>
<TooltipContent>{localCounts.medium} Medium votes</TooltipContent>
</Tooltip>

{/* Hard Section */}
<Tooltip delayDuration={200}>
<TooltipTrigger asChild>
<div
className="bg-red-500"
style={{
width: `${calculatePercentage(localCounts.hard)}%`,
}}
></div>
</TooltipTrigger>
<TooltipContent>{localCounts.hard} Hard votes</TooltipContent>
</Tooltip>
</div>
</div>
</TooltipProvider>
</Card>
);
};
9 changes: 9 additions & 0 deletions frontend/src/components/ui/collapsible.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";

const Collapsible = CollapsiblePrimitive.Root;

const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;

const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;

export { Collapsible, CollapsibleTrigger, CollapsibleContent };
Loading
Loading