Skip to content

Commit

Permalink
lab(client): lab6 work (#613)
Browse files Browse the repository at this point in the history
* feat(client): implement create modal

* fix(client): syntax error fixed

* feat(client): implement create post

* feat(client): created vote mock endpoints

* feat(client): implement like forum question and its answers

* feat(client): implement bookmark

* feat(client): make add post aria and keyboard naviagble

* feat(cleint): implement answer forum question

* style(client): add gap between radio buttons
  • Loading branch information
hks1444 authored Nov 19, 2024
1 parent a540279 commit ac8c70a
Show file tree
Hide file tree
Showing 14 changed files with 850 additions and 104 deletions.
18 changes: 15 additions & 3 deletions client/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export const buttonInnerRing = cva(
"inset-[1px]",
"width-full",
"height-full",
"rounded-[7px]",
"border",
"transition-all",
],
Expand All @@ -24,9 +23,14 @@ export const buttonInnerRing = cva(
tertiary: ["border-white/50"],
destructive: ["border-red-400"],
},
rounded: {
default: ["rounded-[7px]"],
full: ["rounded-full"],
},
},
defaultVariants: {
intent: "primary",
rounded: "default",
},
},
);
Expand All @@ -38,9 +42,7 @@ export const buttonClass = cva(
"flex",
"items-center",
"justify-center",
"rounded-2",
"group",
"relative",
"transition-all",
"duration-100",
"focus-visible:ring-slate-300",
Expand Down Expand Up @@ -111,11 +113,21 @@ export const buttonClass = cva(
small: ["text-xs", "py-1", "px-2"],
medium: ["text-sm", "py-2", "px-4"],
},
rounded: {
default: ["rounded-2"],
full: ["rounded-full"],
},
position: {
fixed: ["fixed"],
relative: ["relative"],
},
},
defaultVariants: {
intent: "primary",
size: "medium",
icon: "none",
rounded: "default",
position: "relative",
},
},
);
75 changes: 60 additions & 15 deletions client/src/components/forum-answer-card.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,58 @@
import { Button, Separator } from "@ariakit/react";
import { RiArrowDownLine, RiArrowUpLine } from "@remixicon/react";
import { useState } from "react";
import { useRouteLoaderData } from "react-router-typesafe";
import { homeLoader } from "../routes/Home.data";
import { Answer } from "../types/post";
import { getRelativeTime, logger } from "../utils";
import { BASE_URL, getRelativeTime, logger } from "../utils";
import { Avatar } from "./avatar";

type forumAnswerCardProps = {
type ForumAnswerCardProps = {
answer: Answer;
key: string;
};

export const ForumAnswerCard = ({ answer, key }: forumAnswerCardProps) => {
export const ForumAnswerCard = ({ answer, key }: ForumAnswerCardProps) => {
const [userVote, setUserVote] = useState<"upvote" | "downvote" | null>(
answer.userVote || null,
);
const [numVotes, setNumVotes] = useState(
answer.num_likes - answer.num_dislikes,
);
const { user, logged_in } =
useRouteLoaderData<typeof homeLoader>("home-main");
const handleVote = async (
e: React.MouseEvent,
voteType: "upvote" | "downvote",
) => {
e.preventDefault();
logger.log("Vote clicked");
if (!logged_in) return;

try {
const response = await fetch(
`${BASE_URL}/answers/${answer.id}/vote`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ voteType }),
},
);

if (response.ok) {
const updatedAnswer = await response.json();
setUserVote(
voteType === updatedAnswer.userVote ? voteType : null,
);
setNumVotes(
updatedAnswer.num_likes - updatedAnswer.num_dislikes,
);
}
} catch (error) {
console.error("Vote failed:", error);
}
};

return (
<div
key={key}
Expand Down Expand Up @@ -38,23 +81,25 @@ export const ForumAnswerCard = ({ answer, key }: forumAnswerCardProps) => {
<Separator className="w-full border-slate-200" />
<div className="flex w-full flex-row justify-end">
<div className="flex flex-row items-center gap-2">
<Button className="flex size-8 items-center justify-center rounded-2 bg-slate-100">
<Button
aria-label="Upvote"
onClick={(e) => handleVote(e, "upvote")}
className="flex size-8 items-center justify-center rounded-2 bg-slate-100"
>
<RiArrowUpLine
className="size-5 text-slate-900"
onClick={() => {
logger.log("upvoted");
}}
className={`size-5 ${userVote === "upvote" ? "text-orange-500" : "text-slate-900"}`}
/>
</Button>
<p className="text-sm text-slate-900">
{answer.num_likes - answer.num_dislikes}
<p className="w-6 text-center text-sm text-slate-900">
{numVotes}
</p>
<Button className="flex size-8 items-center justify-center rounded-2 border border-slate-200">
<Button
aria-label="Downvote"
onClick={(e) => handleVote(e, "downvote")}
className="flex size-8 items-center justify-center rounded-2 border border-slate-200"
>
<RiArrowDownLine
className="size-5 text-slate-900"
onClick={() => {
logger.log("downvoted");
}}
className={`size-5 ${userVote === "downvote" ? "text-indigo-900" : "text-slate-900"}`}
/>
</Button>
</div>
Expand Down
121 changes: 88 additions & 33 deletions client/src/components/forum-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,81 @@ import {
RiArrowUpLine,
RiBookmark2Line,
} from "@remixicon/react";
import { useState } from "react";
import { Link } from "react-router-dom";
import { Post } from "../routes/Forum.data";
import { logger } from "../utils";
import { useRouteLoaderData } from "react-router-typesafe";
import { homeLoader } from "../routes/Home.data";
import { PostOverview } from "../types/post";
import { BASE_URL } from "../utils";
import { Avatar } from "./avatar";

type forumCardProps = {
post: Post;
type ForumCardProps = {
post: PostOverview;
key: string;
};

export const ForumCard = ({ post, key }: forumCardProps) => {
export const ForumCard = ({ post, key }: ForumCardProps) => {
const [userVote, setUserVote] = useState<"upvote" | "downvote" | null>(
post.userVote || null,
);
const [bookmark, setBookmark] = useState(post.bookmark);
const [numVotes, setNumVotes] = useState(
post.num_likes - post.num_dislikes,
);
const { user, logged_in } =
useRouteLoaderData<typeof homeLoader>("home-main");
const handleVote = async (
e: React.MouseEvent,
voteType: "upvote" | "downvote",
) => {
e.preventDefault(); // Prevent link navigation
e.stopPropagation(); // Stop event bubbling
if (!logged_in) return;

try {
const response = await fetch(`${BASE_URL}/forum/${post.id}/vote`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ voteType }),
});

if (response.ok) {
const updatedPost = await response.json();
setUserVote(
voteType === updatedPost.userVote ? voteType : null,
);
setNumVotes(updatedPost.num_likes - updatedPost.num_dislikes);
}
} catch (error) {
console.error("Vote failed:", error);
}
};
const handleBookmark = async (e: React.MouseEvent) => {
e.preventDefault(); // Prevent link navigation
e.stopPropagation(); // Stop event bubbling
if (!logged_in) return;

try {
const response = await fetch(
`${BASE_URL}/forum/${post.id}/bookmark`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
},
);

if (response.ok) {
const updatedPost = await response.json();
setBookmark(updatedPost.bookmark);
}
} catch (error) {
console.error("Bookmark failed:", error);
}
};

return (
<Link
to={`forum/${post.id}`}
to={`/forum/${post.id}`}
key={key}
aria-label={`${post.title} by ${post.author.full_name}`}
className="relative flex w-full max-w-xl flex-col gap-3 rounded-2 bg-white px-6 pb-4 pt-6 shadow-none ring ring-slate-200 transition-all duration-200"
Expand All @@ -31,12 +92,15 @@ export const ForumCard = ({ post, key }: forumCardProps) => {
</p>
</div>
<Button
onClick={() => {
logger.log(post.id);
onClick={(e) => {
handleBookmark(e);
}}
className="flex size-9 items-center justify-center rounded-1 bg-slate-100"
>
<RiBookmark2Line className="size-5 text-slate-500" />
<RiBookmark2Line
color={bookmark ? "gold" : "text-slate-500"}
className="size-5"
/>
</Button>
</div>

Expand All @@ -50,16 +114,11 @@ export const ForumCard = ({ post, key }: forumCardProps) => {
</p>
</div>
<div className="flex flex-row gap-4">
{post.tags.map(({ name }) => {
return (
<Link
to="#"
className="rounded-2 border border-slate-200 bg-white px-2 py-1 text-xs text-slate-500"
>
{name.toLocaleUpperCase()}
</Link>
);
})}
{post.tags.map(({ name }) => (
<span className="rounded-2 border border-slate-200 bg-white px-2 py-1 text-xs text-slate-500">
{name.toLocaleUpperCase()}
</span>
))}
</div>
</div>
</div>
Expand All @@ -74,26 +133,22 @@ export const ForumCard = ({ post, key }: forumCardProps) => {
</div>
<div className="flex flex-row items-center gap-2">
<Button
aria-roledescription="Upvote"
aria-label="Upvote"
onClick={(e) => handleVote(e, "upvote")}
className="flex size-8 items-center justify-center rounded-2 bg-slate-100"
>
<RiArrowUpLine
className="size-5 text-slate-900"
onClick={() => {
logger.log("upvoted");
}}
className={`size-5 ${userVote === "upvote" ? "text-orange-500" : "text-slate-900"}`}
/>
</Button>
<p className="text-sm text-slate-900">
{post.num_likes - post.num_dislikes}
</p>
<Button className="flex size-8 items-center justify-center rounded-2 border border-slate-200">
<p className="text-slate- w-6 text-sm">{numVotes}</p>
<Button
aria-label="Downvote"
onClick={(e) => handleVote(e, "downvote")}
className="flex size-8 items-center justify-center rounded-2 border border-slate-200"
>
<RiArrowDownLine
aria-roledescription="Downvote"
className="size-5 text-slate-900"
onClick={() => {
logger.log("downvoted");
}}
className={`size-5 ${userVote === "downvote" ? "text-purple-800" : "text-slate-900"}`}
/>
</Button>
</div>
Expand Down
Loading

0 comments on commit ac8c70a

Please sign in to comment.