Skip to content

Commit

Permalink
feat(client): implement create a forum question with dummy tags
Browse files Browse the repository at this point in the history
  • Loading branch information
umitcan07 committed Nov 20, 2024
1 parent 96ffe25 commit 2f054b9
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 104 deletions.
11 changes: 11 additions & 0 deletions client/src/components/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const toggleButtonClass = cva(
bookmark: [],
upvote: [],
downvote: [],
delete: [],
},
state: {
on: [],
Expand Down Expand Up @@ -207,6 +208,16 @@ export const toggleButtonClass = cva(
state: "off",
class: ["bg-slate-100", "text-slate-800"],
},
{
intent: "delete",
state: "on",
class: ["bg-red-600", "text-white"],
},
{
intent: "delete",
state: "off",
class: ["bg-slate-100", "text-slate-800"],
},
],
},
);
82 changes: 58 additions & 24 deletions client/src/components/forum-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import {
RiArrowDownLine,
RiArrowUpLine,
RiBookmark2Line,
RiDeleteBin4Line,
} from "@remixicon/react";
import { Link, useFetcher } from "react-router-dom";

import { ForumQuestion } from "../routes/Forum/Forum.schema";
import {
bookmarkForumAction,
deleteForumAction,
downvoteForumAction,
upvoteForumAction,
} from "../routes/Forum/Question.data";
Expand All @@ -23,6 +25,7 @@ export const ForumQuestionCard = ({ question }: ForumCardProps) => {
const upvoteFetcher = useFetcher<typeof upvoteForumAction>();
const downvoteFetcher = useFetcher<typeof downvoteForumAction>();
const bookmarkFetcher = useFetcher<typeof bookmarkForumAction>();
const deleteFetcher = useFetcher<typeof deleteForumAction>();

return (
<div 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 hover:ring-slate-300">
Expand All @@ -34,31 +37,62 @@ export const ForumQuestionCard = ({ question }: ForumCardProps) => {
{question.author.username}
</p>
</div>
<bookmarkFetcher.Form
method="POST"
action={`/forum/${question.id}/bookmark`}
>
<input
type="hidden"
name="post_id"
value={question.id}
/>
<input
type="hidden"
name="is_bookmarked"
value={question.is_bookmarked || 0}
/>
<Button
type="submit"
aria-label="Bookmark"
className={toggleButtonClass({
intent: "bookmark",
state: question.is_bookmarked ? "on" : "off",
})}
<div className="flex flex-row items-center justify-end gap-3">
{question.is_bookmarked && (
<deleteFetcher.Form
method="POST"
action={`/forum/${question.id}/delete`}
>
<input
type="hidden"
name="post_id"
value={question.id}
/>
<input
type="hidden"
name="is_bookmarked"
value={question.is_bookmarked || 0}
/>
<Button
type="submit"
aria-label="Bookmark"
className={toggleButtonClass({
intent: "delete",
state: "on",
})}
>
<RiDeleteBin4Line size={16} />
</Button>
</deleteFetcher.Form>
)}
<bookmarkFetcher.Form
method="POST"
action={`/forum/${question.id}/bookmark`}
>
<RiBookmark2Line size={16} />
</Button>
</bookmarkFetcher.Form>
<input
type="hidden"
name="post_id"
value={question.id}
/>
<input
type="hidden"
name="is_bookmarked"
value={question.is_bookmarked || 0}
/>
<Button
type="submit"
aria-label="Bookmark"
className={toggleButtonClass({
intent: "bookmark",
state: question.is_bookmarked
? "on"
: "off",
})}
>
<RiBookmark2Line size={16} />
</Button>
</bookmarkFetcher.Form>
</div>
</div>

<div className="flex flex-col gap-4">
Expand Down
16 changes: 7 additions & 9 deletions client/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import { loginAction, loginLoader } from "./routes/Auth/Login.data";
import { logoutLoader } from "./routes/Auth/Logout.data";
import { registerAction } from "./routes/Auth/Register.data";

import { forumLoader } from "./routes/Forum/Forum.data";
import { forumCreateAction, forumLoader } from "./routes/Forum/Forum.data";
import {
answerForumAction,
bookmarkForumAction,
deleteForumAction,
downvoteForumAction,
downvoteForumAnswerAction,
forumQuestionLoader,
Expand Down Expand Up @@ -56,14 +57,7 @@ export const routes: RouteObject[] = [
path: "forum",
element: <Forum />,
loader: forumLoader,
children: [
{
path: "create",
action: () => {
return true;
},
},
],
action: forumCreateAction,
},
{
path: "forum/:questionId",
Expand All @@ -86,6 +80,10 @@ export const routes: RouteObject[] = [
path: "answer",
action: answerForumAction,
},
{
path: "delete",
action: deleteForumAction,
},
{
path: "upvoteAnswer",
action: upvoteForumAnswerAction,
Expand Down
39 changes: 38 additions & 1 deletion client/src/routes/Forum/Forum.data.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LoaderFunction } from "react-router";
import { ActionFunction, LoaderFunction } from "react-router";
import { safeParse } from "valibot";
import apiClient from "../../api";
import { logger } from "../../utils";
Expand Down Expand Up @@ -30,3 +30,40 @@ export const forumLoader = (async ({ request }) => {
throw new Error("Failed to load forum questions");
}
}) satisfies LoaderFunction;

export const forumCreateAction = (async ({ request }) => {
try {
const formData = await request.formData();
const formEntries = Object.fromEntries(formData);

// Get the select element
const selectElement = document.getElementById(
"tags",
) as HTMLSelectElement;

// Get all selected options and transform them into the required format
const transformedTags = Array.from(selectElement.selectedOptions).map(
(option) => ({
name: option.text,
linked_data_id: option.dataset.linkedId,
description: option.dataset.description,
}),
);

// Create a new object without the original tags
const { tags: _, ...restEntries } = formEntries;

// Create the final payload with the transformed tags
const payload = {
...restEntries,
tags: transformedTags,
};

const response = await apiClient.post("/forum-questions/", payload);

return response;
} catch (error) {
logger.error("Error creating forum question", error);
throw new Error("Failed to create forum question");
}
}) satisfies ActionFunction;
122 changes: 75 additions & 47 deletions client/src/routes/Forum/Forum.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Dialog, DialogHeading } from "@ariakit/react";
import { Button } from "@ariakit/react";
import { RiAddLine } from "@remixicon/react";
import { useState } from "react";
import { Form, useSearchParams } from "react-router-dom";
Expand Down Expand Up @@ -42,6 +42,80 @@ export const Forum = () => {
return (
<div className="container flex max-w-screen-xl flex-col items-stretch gap-8 py-12">
<PageHead title="Forum" description={description} />
<aside className="max-w-lg">
<Form
aria-labelledby="add-new-post"
className="w-full"
method="POST"
>
<div className="flex w-full flex-col gap-4">
<input
aria-label="Title"
type="text"
name="title"
placeholder="Question Title"
className={inputClass()}
required
maxLength={100}
minLength={1}
/>
<textarea
name="question"
aria-label="Question"
placeholder="Write your question here..."
className={inputClass()}
required
maxLength={1000}
minLength={1}
rows={4}
/>
<div className="flex flex-col gap-2">
<label htmlFor="tags">Tags</label>
<select
id="tags"
name="tags"
multiple
className={`${inputClass()} w-full`}
required
>
<option
value="tag1"
data-linked-id="ld_tag1"
data-description="Description for tag 1"
>
Tag 1
</option>
<option
value="tag2"
data-linked-id="ld_tag2"
data-description="Description for tag 2"
>
Tag 2
</option>
<option
value="tag3"
data-linked-id="ld_tag3"
data-description="Description for tag 3"
>
Tag 3
</option>
</select>
</div>
<button
type="submit"
className={buttonClass({ intent: "primary" })}
onClick={() => setCreatingPost(false)}
>
<div
className={buttonInnerRing({
intent: "primary",
})}
/>
<span>Post</span>
</button>
</div>
</Form>
</aside>
<main className="flex flex-col items-stretch justify-stretch gap-10">
<div className="flex flex-col gap-4">
{/* Pagination Controls */}
Expand Down Expand Up @@ -129,52 +203,6 @@ export const Forum = () => {
<RiAddLine color="white" size="24px"></RiAddLine>
</Button>
</div>
{/* Dialog for Creating Post */}
<Dialog
open={creatingPost}
onClose={() => setCreatingPost(false)}
backdrop={<div className="backdrop" />}
className="dialog"
>
<DialogHeading className="heading">
Create New Question
</DialogHeading>
<Form
aria-labelledby="add-new-post"
className="w-full"
method="POST"
action="/forum"
>
<div className="flex w-full flex-col gap-4">
<input
aria-label="Title"
type="text"
name="title"
placeholder="Post Title"
className={`${inputClass()} w-full`}
required
/>
<textarea
name="body"
aria-label="Question Body"
placeholder="Question Body"
className={`${inputClass()} w-full`}
></textarea>
<button
type="submit"
className={buttonClass({ intent: "primary" })}
onClick={() => setCreatingPost(false)}
>
<div
className={buttonInnerRing({
intent: "primary",
})}
/>
<span>Post</span>
</button>
</div>
</Form>
</Dialog>
</div>
);
};
Loading

0 comments on commit 2f054b9

Please sign in to comment.