Skip to content

Commit

Permalink
Merge pull request #698 from bounswe/feature/frontend/ux-improvements
Browse files Browse the repository at this point in the history
[Frontend] Implement some UX improvements
  • Loading branch information
mmtftr authored Dec 16, 2024
2 parents a431ed6 + fd22ad6 commit 42aae89
Show file tree
Hide file tree
Showing 10 changed files with 609 additions and 221 deletions.
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

0 comments on commit 42aae89

Please sign in to comment.