From a84aa2df09284b29b0779bbc4cb7a759987fd64a Mon Sep 17 00:00:00 2001 From: Nazire Date: Tue, 5 Nov 2024 18:08:59 +0300 Subject: [PATCH 01/12] adjusted colors to meet the contrast requirements of WCAG --- frontend/src/components/AnswerCard.tsx | 6 ++--- frontend/src/components/AnswerItem.tsx | 2 +- frontend/src/components/ErrorBoundary.tsx | 2 +- frontend/src/components/QuestionCard.tsx | 6 ++--- frontend/src/components/Tag.tsx | 2 +- frontend/src/components/TagCard.tsx | 6 ++--- frontend/src/components/ui/tabs.tsx | 2 +- frontend/src/index.css | 32 +++++++++++------------ frontend/src/routes/profile.tsx | 10 +++---- mobile/components/AnswerCard.tsx | 6 ++--- mobile/components/AnswerItem.tsx | 2 +- mobile/components/ErrorBoundary.tsx | 2 +- mobile/components/QuestionCard.tsx | 6 ++--- mobile/components/TagCard.tsx | 6 ++--- 14 files changed, 45 insertions(+), 45 deletions(-) diff --git a/frontend/src/components/AnswerCard.tsx b/frontend/src/components/AnswerCard.tsx index 292df202..10b7fcce 100644 --- a/frontend/src/components/AnswerCard.tsx +++ b/frontend/src/components/AnswerCard.tsx @@ -35,11 +35,11 @@ export const AnswerCard: React.FC = ({ height={24} className="mt-2 flex-shrink-0" /> -

+

{content}

-
+
{votes} votes @@ -55,7 +55,7 @@ export const AnswerCard: React.FC = ({ Go to answer diff --git a/frontend/src/components/AnswerItem.tsx b/frontend/src/components/AnswerItem.tsx index 7aace7a8..51c3de90 100644 --- a/frontend/src/components/AnswerItem.tsx +++ b/frontend/src/components/AnswerItem.tsx @@ -61,7 +61,7 @@ export const AnswerItem: React.FC = ({ /> {answer.author?.name} - + Answered: {new Date(answer.createdAt || "").toLocaleDateString()}
diff --git a/frontend/src/components/ErrorBoundary.tsx b/frontend/src/components/ErrorBoundary.tsx index 1110fae8..c06baf34 100644 --- a/frontend/src/components/ErrorBoundary.tsx +++ b/frontend/src/components/ErrorBoundary.tsx @@ -33,7 +33,7 @@ class ErrorBoundary extends Component {

Oops! Something went wrong.

-

+

We apologize for the inconvenience. Please try refreshing the page or contact support if the problem persists.

diff --git a/frontend/src/components/QuestionCard.tsx b/frontend/src/components/QuestionCard.tsx index 4312700f..d0158d11 100644 --- a/frontend/src/components/QuestionCard.tsx +++ b/frontend/src/components/QuestionCard.tsx @@ -31,10 +31,10 @@ export const QuestionCard: React.FC = ({

{title}

-

+

{content}

-
+
{votes} votes @@ -54,7 +54,7 @@ export const QuestionCard: React.FC = ({ Go to question diff --git a/frontend/src/components/Tag.tsx b/frontend/src/components/Tag.tsx index 2a805904..63cee25d 100644 --- a/frontend/src/components/Tag.tsx +++ b/frontend/src/components/Tag.tsx @@ -30,7 +30,7 @@ export const Tag = ({
-

{description}

+

{description}

{!!token && ( diff --git a/frontend/src/components/TagCard.tsx b/frontend/src/components/TagCard.tsx index a7657bf7..57ad5535 100644 --- a/frontend/src/components/TagCard.tsx +++ b/frontend/src/components/TagCard.tsx @@ -16,11 +16,11 @@ export const TagCard: React.FC = ({ tag }) => { {tag.name}
-

+

{tag.description}

-
+
{tag.followersCount} followers @@ -42,7 +42,7 @@ export const TagCard: React.FC = ({ tag }) => { )} View tag diff --git a/frontend/src/components/ui/tabs.tsx b/frontend/src/components/ui/tabs.tsx index e66f8e2c..36a00f43 100644 --- a/frontend/src/components/ui/tabs.tsx +++ b/frontend/src/components/ui/tabs.tsx @@ -27,7 +27,7 @@ const TabsTrigger = React.forwardRef<
{profile.questionCount}
-
Questions
+
Questions
{profile.answerCount}
-
Answers
+
Answers
{profile.followersCount}
-
Followers
+
Followers
{profile.followingCount}
-
Following
+
Following
@@ -124,7 +124,7 @@ export default function Profile() {

{profile.bio ?? "Empty bio."} diff --git a/mobile/components/AnswerCard.tsx b/mobile/components/AnswerCard.tsx index 6ecda594..5c85cda0 100644 --- a/mobile/components/AnswerCard.tsx +++ b/mobile/components/AnswerCard.tsx @@ -34,11 +34,11 @@ export const AnswerCard: React.FC = ({ -

+

{content}

- + {votes} votes @@ -54,7 +54,7 @@ export const AnswerCard: React.FC = ({ Go to answer diff --git a/mobile/components/AnswerItem.tsx b/mobile/components/AnswerItem.tsx index 13b2977a..7657f168 100644 --- a/mobile/components/AnswerItem.tsx +++ b/mobile/components/AnswerItem.tsx @@ -64,7 +64,7 @@ export const AnswerItem: React.FC = ({ /> {answer.author?.name} - + Answered: {new Date(answer.createdAt || "").toLocaleDateString()} diff --git a/mobile/components/ErrorBoundary.tsx b/mobile/components/ErrorBoundary.tsx index c874d884..1f515028 100644 --- a/mobile/components/ErrorBoundary.tsx +++ b/mobile/components/ErrorBoundary.tsx @@ -33,7 +33,7 @@ class ErrorBoundary extends Component { Oops! Something went wrong. -

+

We apologize for the inconvenience. Please try refreshing the page or contact support if the problem persists.

diff --git a/mobile/components/QuestionCard.tsx b/mobile/components/QuestionCard.tsx index a9f97380..aa533738 100644 --- a/mobile/components/QuestionCard.tsx +++ b/mobile/components/QuestionCard.tsx @@ -33,10 +33,10 @@ export const QuestionCard: React.FC = ({ {title} - + {content} - + {votes} votes @@ -56,7 +56,7 @@ export const QuestionCard: React.FC = ({ Go to question diff --git a/mobile/components/TagCard.tsx b/mobile/components/TagCard.tsx index dac3188b..4fc6e4c6 100644 --- a/mobile/components/TagCard.tsx +++ b/mobile/components/TagCard.tsx @@ -18,11 +18,11 @@ export const TagCard: React.FC = ({ tag }) => { {tag.name} - + {tag.description} - + {tag.followersCount} followers @@ -46,7 +46,7 @@ export const TagCard: React.FC = ({ tag }) => { )} View tag From 4f06341e0903a579a4d34b36106b106b8de6fc6b Mon Sep 17 00:00:00 2001 From: Nazire Date: Fri, 22 Nov 2024 10:51:25 +0300 Subject: [PATCH 02/12] added question create --- frontend/src/routes/create-question.tsx | 220 ++++++++++++++++++++++++ frontend/src/routes/index.tsx | 5 + 2 files changed, 225 insertions(+) create mode 100644 frontend/src/routes/create-question.tsx diff --git a/frontend/src/routes/create-question.tsx b/frontend/src/routes/create-question.tsx new file mode 100644 index 00000000..95b4bbb4 --- /dev/null +++ b/frontend/src/routes/create-question.tsx @@ -0,0 +1,220 @@ +import * as React from "react"; +import { useForm, useFieldArray } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import { useCreateQuestion, useSearchTags } from "@/services/api/programmingForumComponents"; +import { useEffect, useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; + +const newQuestionSchema = z.object({ + title: z.string().min(1, "Title is required").max(200, "Max 200 characters"), + content: z.string().min(1, "Content is required").max(100000, "Max 100,000 characters"), + tags: z + .array(z.object({ id: z.string(), name: z.string() })) + .min(1, "At least one tag is required"), + difficulty: z.enum(["Easy", "Medium", "Hard"], "Select a difficulty level"), +}); + +type NewQuestion = z.infer; + +export default function QuestionCreationPage() { + const form = useForm({ + resolver: zodResolver(newQuestionSchema), + defaultValues: { + title: "", + content: "", + tags: [], + difficulty: "Easy", + }, + }); + + const { handleSubmit, control, setValue } = form; + const { fields, append, remove } = useFieldArray({ control, name: "tags" }); + const navigate = useNavigate(); + const [params] = useSearchParams(); + const [availableTags, setAvailableTags] = useState<{ id: string; name: string }[]>([]); + const [searchQuery, setSearchQuery] = useState(""); + const queryClient = useQueryClient(); + + const tagId = params.get("tagId"); + + const { data: tagSearchData } = useSearchTags( + { queryParams: { q: searchQuery, pageSize: 20 } }, + { enabled: !!searchQuery } + ); + + useEffect(() => { + if (tagSearchData?.data?.items) { + setAvailableTags( + tagSearchData.data.items.filter( + (tag) => + !fields.some((selectedTag) => selectedTag.id === tag.id) // Remove already selected tags from availableTags + ) + ); + } + }, [tagSearchData, fields]); + + const { mutateAsync } = useCreateQuestion({ + onSuccess: (data) => { + const newQuestion = data.data; + + // Redirect to the new question page + navigate(`/question/${newQuestion.id}`); + + // Update the user's profile with the new question + queryClient.setQueryData(["getUserProfile", { userId: newQuestion.author.id }], (oldData: any) => { + if (!oldData) return; + return { + ...oldData, + data: { + ...oldData.data, + questions: [...oldData.data.questions, newQuestion], + }, + }; + }); + + // Update the questions in the tags associated with this question + newQuestion.tags.forEach((tag: { id: string }) => { + queryClient.setQueryData(["getTagDetails", { tagId: tag.id }], (oldData: any) => { + if (!oldData) return; + return { + ...oldData, + data: { + ...oldData.data, + questions: [...oldData.data.questions, newQuestion], + }, + }; + }); + }); + }, + }); + + const handleTagSelect = (tagId: string) => { + const selectedTag = availableTags.find((tag) => tag.id === tagId); + if (selectedTag) { + append(selectedTag); + setSearchQuery(""); // Reset search query + setAvailableTags((prev) => prev.filter((tag) => tag.id !== tagId)); // Remove the selected tag from available + } + }; + + const handleTagRemove = (index: number) => { + const removedTag = fields[index]; + remove(index); + setAvailableTags((prev) => [...prev, removedTag]); // Re-add the removed tag to availableTags + }; + + return ( +
+ { + const tagIds = values.tags.map((tag) => tag.id); + await mutateAsync({ body: { ...values, tags: tagIds } }); + })} + className="flex flex-col gap-6" + > + {/* Title */} + ( + + + + + + + )} + /> + + {/* Content */} + ( + + + + {contentLength} / 1000 + + + ); + +} \ No newline at end of file diff --git a/mobile/components/ContentWithSnippets.tsx b/mobile/components/ContentWithSnippets.tsx index 552703e1..938de201 100644 --- a/mobile/components/ContentWithSnippets.tsx +++ b/mobile/components/ContentWithSnippets.tsx @@ -2,6 +2,7 @@ import React from "react"; import { View } from "react-native"; import { CodeSnippet } from "./CodeSnippet"; import { Text } from "./ui"; + interface ContentWithSnippetsProps { content: string; } @@ -51,12 +52,14 @@ export const ContentWithSnippets: React.FC = ({ ); } - return ( - - {part.code} - - ); - return {part.content}; + if (part.type === "text") { + return ( + + {part.content} + + ); + } + return null; }); }, [content]); From e7c6b914212bde1e1235aab1094a1c9b7fb1ff04 Mon Sep 17 00:00:00 2001 From: Nazire Date: Mon, 25 Nov 2024 18:38:28 +0300 Subject: [PATCH 09/12] couldnt fix tag selector --- frontend/package.json | 9 +- frontend/src/components/multi-select.tsx | 273 +++++++++++++++++++++++ frontend/src/components/ui/badge.tsx | 36 +++ frontend/src/components/ui/command.tsx | 151 +++++++++++++ frontend/src/components/ui/separator.tsx | 31 +++ frontend/src/routes/create-question.tsx | 189 ++++------------ 6 files changed, 539 insertions(+), 150 deletions(-) create mode 100644 frontend/src/components/multi-select.tsx create mode 100644 frontend/src/components/ui/badge.tsx create mode 100644 frontend/src/components/ui/command.tsx create mode 100644 frontend/src/components/ui/separator.tsx diff --git a/frontend/package.json b/frontend/package.json index 783edf87..6567ced3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,12 +24,13 @@ "@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-dialog": "^1.0.5", + "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-popover": "^1.1.2", "@radix-ui/react-select": "^2.1.2", - "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-separator": "^1.1.0", + "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.1.3", @@ -38,6 +39,7 @@ "@types/react-syntax-highlighter": "^15.5.13", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "cmdk": "1.0.0", "country-emoji": "^1.5.6", "date-fns": "^3.6.0", "lucide-react": "^0.376.0", @@ -70,7 +72,6 @@ "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/ui": "^2.1.2", "autoprefixer": "^10.4.19", - "canvas": "^2.11.2", "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-react": "^7.37.1", diff --git a/frontend/src/components/multi-select.tsx b/frontend/src/components/multi-select.tsx new file mode 100644 index 00000000..670a4822 --- /dev/null +++ b/frontend/src/components/multi-select.tsx @@ -0,0 +1,273 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; +import { CheckIcon, XCircle, ChevronDown, XIcon } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, +} from "@/components/ui/command"; + +const multiSelectVariants = cva( + "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", + { + variants: { + variant: { + default: + "border-foreground/10 text-foreground bg-card hover:bg-card/80", + secondary: + "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + inverted: "inverted", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +interface MultiSelectProps + extends React.ButtonHTMLAttributes, + VariantProps { + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; + onValueChange: (value: string[]) => void; + defaultValue?: string[]; + placeholder?: string; + animation?: number; + maxCount?: number; + modalPopover?: boolean; + asChild?: boolean; + className?: string; +} + +export const MultiSelect = React.forwardRef< + HTMLButtonElement, + MultiSelectProps +>( + ( + { + options, + onValueChange, + variant, + defaultValue = [], + placeholder = "Select options", + animation = 0, + maxCount = 3, + modalPopover = false, + asChild = false, + className, + ...props + }, + ref + ) => { + const [selectedValues, setSelectedValues] = + React.useState(defaultValue); + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + + const toggleOption = (optionValue: string | undefined) => { + if (!optionValue) { + console.warn(`Attempted to toggle non-existent value: ${optionValue}`); + return; + } + + const isSelected = selectedValues.includes(optionValue); + const newSelectedValues = isSelected + ? selectedValues.filter((value) => value !== optionValue) // Deselect + : [...selectedValues, optionValue]; // Select + + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const handleClear = () => { + setSelectedValues([]); + onValueChange([]); + }; + + const handleTogglePopover = () => { + setIsPopoverOpen((prev) => !prev); + }; + + const clearExtraOptions = () => { + const newSelectedValues = selectedValues.slice(0, maxCount); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const toggleAll = () => { + if (selectedValues.length === options.length) { + handleClear(); + } else { + const allValues = options.map((option) => option.value); + setSelectedValues(allValues); + onValueChange(allValues); + } + }; + + return ( + + + + + + + + + No results found. + + +
+ +
+ (Select All) +
+ {options.map((option, index) => { + const uniqueKey = option.value || `option-${index}`; // Generate a unique key + const isSelected = selectedValues.includes(option.value); // Check if this option is selected + + return ( + toggleOption(option.value)} // Toggle this option + className="cursor-pointer" + > +
+ +
+ {option.icon && ( + + )} + {option.label || "Unknown"} {/* Display label */} +
+ ); +})} +
+
+
+
+
+ ); + } +); + +MultiSelect.displayName = "MultiSelect"; diff --git a/frontend/src/components/ui/badge.tsx b/frontend/src/components/ui/badge.tsx new file mode 100644 index 00000000..f000e3ef --- /dev/null +++ b/frontend/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/frontend/src/components/ui/command.tsx b/frontend/src/components/ui/command.tsx new file mode 100644 index 00000000..f0e4e3fb --- /dev/null +++ b/frontend/src/components/ui/command.tsx @@ -0,0 +1,151 @@ +import * as React from "react" +import { type DialogProps } from "@radix-ui/react-dialog" +import { Command as CommandPrimitive } from "cmdk" +import { Search } from "lucide-react" + +import { cn } from "@/lib/utils" +import { Dialog, DialogContent } from "@/components/ui/dialog" + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Command.displayName = CommandPrimitive.displayName + +const CommandDialog = ({ children, ...props }: DialogProps) => { + return ( + + + + {children} + + + + ) +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)) + +CommandInput.displayName = CommandPrimitive.Input.displayName + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandList.displayName = CommandPrimitive.List.displayName + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)) + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandGroup.displayName = CommandPrimitive.Group.displayName + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +CommandSeparator.displayName = CommandPrimitive.Separator.displayName + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) + +CommandItem.displayName = CommandPrimitive.Item.displayName + +const CommandShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +CommandShortcut.displayName = "CommandShortcut" + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +} diff --git a/frontend/src/components/ui/separator.tsx b/frontend/src/components/ui/separator.tsx new file mode 100644 index 00000000..12d81c4a --- /dev/null +++ b/frontend/src/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/frontend/src/routes/create-question.tsx b/frontend/src/routes/create-question.tsx index b4eadd88..7dc2b777 100644 --- a/frontend/src/routes/create-question.tsx +++ b/frontend/src/routes/create-question.tsx @@ -2,32 +2,24 @@ import { Button } from "@/components/ui/button"; import { Form, FormControl, - FormDescription, FormField, FormItem, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; +import { MultiSelect } from "@/components/multi-select"; import { - GetUserProfileResponse, useCreateQuestion, useSearchTags, } from "@/services/api/programmingForumComponents"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; -import { useFieldArray, useForm } from "react-hook-form"; -import { useNavigate } from "react-router-dom"; +import { useForm } from "react-hook-form"; import { z } from "zod"; +// Schema validation for the form const newQuestionSchema = z.object({ title: z.string().min(1, "Title is required").max(200, "Max 200 characters"), content: z @@ -56,92 +48,35 @@ export default function QuestionCreationPage() { }); const { handleSubmit, control } = form; - const { fields, append, remove } = useFieldArray({ control, name: "tags" }); - const navigate = useNavigate(); + const queryClient = useQueryClient(); + const [availableTags, setAvailableTags] = useState< { id: string; name: string }[] >([]); - const [searchQuery, setSearchQuery] = useState(""); - const queryClient = useQueryClient(); const { data: tagSearchData } = useSearchTags( - { queryParams: { q: searchQuery, pageSize: 20 } }, - { enabled: !!searchQuery }, + { queryParams: { q: "", pageSize: 1000 } }, + { enabled: true } ); useEffect(() => { if (tagSearchData?.data?.items) { - setAvailableTags( - tagSearchData.data.items.filter( - (tag) => !fields.some((selectedTag) => selectedTag.id === tag.id), // Remove already selected tags from availableTags - ), - ); + setAvailableTags(tagSearchData.data.items); } - }, [tagSearchData, fields]); + }, [tagSearchData]); const { mutateAsync } = useCreateQuestion({ onSuccess: (data) => { const newQuestion = data.data; - // Redirect to the new question page - navigate(`/question/${newQuestion.id}`); - - // Update the user's profile with the new question - queryClient.setQueryData( - ["getUserProfile", { userId: newQuestion.author.id }], - (oldData: GetUserProfileResponse) => { - if (!oldData) return; - return { - ...oldData, - data: { - ...oldData.data, - questions: [...(oldData.data.questions || []), newQuestion], - }, - }; - }, - ); - - // Update the questions in the tags associated with this question - newQuestion.tags.forEach((tag: { id?: string }) => { - queryClient.setQueryData( - ["getTagDetails", { tagId: tag.id }], - (oldData: GetUserProfileResponse) => { - if (!oldData) return; - return { - ...oldData, - data: { - ...oldData.data, - questions: [...(oldData.data.questions || []), newQuestion], - }, - }; - }, - ); - }); + queryClient.invalidateQueries(["getUserProfile"]); + queryClient.invalidateQueries(["getTagDetails"]); }, }); - const handleTagSelect = (tagId: string) => { - const selectedTag = availableTags.find((tag) => tag.id === tagId); - if (selectedTag) { - append(selectedTag); - setSearchQuery(""); // Reset search query - setAvailableTags((prev) => prev.filter((tag) => tag.id !== tagId)); // Remove the selected tag from available - } - }; - - const handleTagRemove = (index: number) => { - const removedTag = fields[index]; - remove(index); - setAvailableTags((prev) => [...prev, removedTag]); // Re-add the removed tag to availableTags - }; - return ( -
-
-
-

Create a new question

-
-
+
+

Create a new question

{ @@ -182,74 +117,37 @@ export default function QuestionCreationPage() { )} /> - {/* Difficulty Level */} + + {/* Tags */} ( - + ({ + value: tag.id, + label: tag.name, + }))} + value={field.value.map((tag) => tag.id)} // Bind selected values correctly + onValueChange={(selectedIds) => { + // Map selected IDs back to tag objects + const selectedTags = selectedIds + .map((id) => + availableTags.find((tag) => tag.id === id) + ) + .filter(Boolean); + field.onChange(selectedTags); + }} + placeholder="Select Tags" + /> - - Please select the difficulty level of your question - )} /> - {/* Tags */} - - - setSearchQuery(e.target.value)} - /> - - - - - {/* Selected Tags */} -
- {fields.map((tag, index) => ( -
- {tag.name} - -
- ))} -
- {/* Difficulty */} ( - + )} /> - {/* Submit Button */} + {/* Submit */} From 06590eb09cc084608b2736ebe613d2b987039f26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Sun, 24 Nov 2024 22:17:24 +0300 Subject: [PATCH 10/12] remove pre-wrap --- frontend/src/components/ContentWithSnippets.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/components/ContentWithSnippets.tsx b/frontend/src/components/ContentWithSnippets.tsx index 2f4f5003..88d1bd70 100644 --- a/frontend/src/components/ContentWithSnippets.tsx +++ b/frontend/src/components/ContentWithSnippets.tsx @@ -55,9 +55,5 @@ export const ContentWithSnippets: React.FC = ({ }); }, [content]); - return ( -
- {renderedContent} -
- ); + return
{renderedContent}
; }; From cd8abc40e3e1f228acbac9f2f722e2007abf91d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Mon, 25 Nov 2024 19:00:15 +0300 Subject: [PATCH 11/12] fix: frontend multi select not working properly --- frontend/package.json | 1 + frontend/src/components/multi-select.tsx | 123 ++++---- frontend/src/routes/create-question.tsx | 66 ++-- frontend/yarn.lock | 364 ++++++++++++++++++++++- 4 files changed, 461 insertions(+), 93 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 6567ced3..5b24eb45 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -72,6 +72,7 @@ "@vitejs/plugin-react-swc": "^3.5.0", "@vitest/ui": "^2.1.2", "autoprefixer": "^10.4.19", + "canvas": "^2.11.2", "eslint": "^9.12.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-react": "^7.37.1", diff --git a/frontend/src/components/multi-select.tsx b/frontend/src/components/multi-select.tsx index 670a4822..0140b195 100644 --- a/frontend/src/components/multi-select.tsx +++ b/frontend/src/components/multi-select.tsx @@ -1,16 +1,9 @@ -import * as React from "react"; import { cva, type VariantProps } from "class-variance-authority"; -import { CheckIcon, XCircle, ChevronDown, XIcon } from "lucide-react"; +import { CheckIcon, ChevronDown, XCircle, XIcon } from "lucide-react"; +import * as React from "react"; -import { cn } from "@/lib/utils"; -import { Separator } from "@/components/ui/separator"; -import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; +import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, @@ -18,11 +11,17 @@ import { CommandInput, CommandItem, CommandList, - CommandSeparator, } from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; const multiSelectVariants = cva( - "m-1 transition ease-in-out delay-150 hover:-translate-y-1 hover:scale-110 duration-300", + "m-1 transition ease-in-out delay-150 duration-300", { variants: { variant: { @@ -38,7 +37,7 @@ const multiSelectVariants = cva( defaultVariants: { variant: "default", }, - } + }, ); interface MultiSelectProps @@ -77,7 +76,7 @@ export const MultiSelect = React.forwardRef< className, ...props }, - ref + ref, ) => { const [selectedValues, setSelectedValues] = React.useState(defaultValue); @@ -88,16 +87,16 @@ export const MultiSelect = React.forwardRef< console.warn(`Attempted to toggle non-existent value: ${optionValue}`); return; } - + const isSelected = selectedValues.includes(optionValue); const newSelectedValues = isSelected ? selectedValues.filter((value) => value !== optionValue) // Deselect : [...selectedValues, optionValue]; // Select - + setSelectedValues(newSelectedValues); onValueChange(newSelectedValues); }; - + const handleClear = () => { setSelectedValues([]); onValueChange([]); @@ -135,12 +134,12 @@ export const MultiSelect = React.forwardRef< {...props} onClick={handleTogglePopover} className={cn( - "flex w-full p-1 rounded-md border min-h-10 h-auto items-center justify-between bg-inherit hover:bg-inherit [&_svg]:pointer-events-auto", - className + "flex h-auto min-h-10 w-full items-center justify-between rounded-md border bg-inherit p-1 hover:bg-inherit [&_svg]:pointer-events-auto", + className, )} > {selectedValues.length > 0 ? ( -
+
{selectedValues.slice(0, maxCount).map((value) => { const option = options.find((o) => o.value === value); @@ -148,12 +147,10 @@ export const MultiSelect = React.forwardRef< return ( {IconComponent && ( - + )} {option?.label || "Unknown"} maxCount && ( {`+ ${selectedValues.length - maxCount} more`} @@ -185,7 +182,7 @@ export const MultiSelect = React.forwardRef<
{ event.stopPropagation(); handleClear(); @@ -193,17 +190,17 @@ export const MultiSelect = React.forwardRef< /> - +
) : ( -
- +
+ {placeholder} - +
)} @@ -211,6 +208,7 @@ export const MultiSelect = React.forwardRef< + No results found. @@ -224,7 +222,7 @@ export const MultiSelect = React.forwardRef< "mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary", selectedValues.length === options.length ? "bg-primary text-primary-foreground" - : "opacity-50 [&_svg]:invisible" + : "opacity-50 [&_svg]:invisible", )} > @@ -232,42 +230,45 @@ export const MultiSelect = React.forwardRef< (Select All) {options.map((option, index) => { - const uniqueKey = option.value || `option-${index}`; // Generate a unique key - const isSelected = selectedValues.includes(option.value); // Check if this option is selected + const uniqueKey = option.value || `option-${index}`; // Generate a unique key + const isSelected = selectedValues.includes(option.value); // Check if this option is selected - return ( - toggleOption(option.value)} // Toggle this option - className="cursor-pointer" - > -
- -
- {option.icon && ( - - )} - {option.label || "Unknown"} {/* Display label */} -
- ); -})} + return ( + toggleOption(option.value)} // Toggle this option + className="cursor-pointer" + > +
+ +
+ {option.icon && ( + + )} + + {option.label || "Unknown"} + {" "} + {/* Display label */} +
+ ); + })}
); - } + }, ); MultiSelect.displayName = "MultiSelect"; diff --git a/frontend/src/routes/create-question.tsx b/frontend/src/routes/create-question.tsx index 7dc2b777..2fd5c09a 100644 --- a/frontend/src/routes/create-question.tsx +++ b/frontend/src/routes/create-question.tsx @@ -1,3 +1,4 @@ +import { MultiSelect } from "@/components/multi-select"; import { Button } from "@/components/ui/button"; import { Form, @@ -8,15 +9,17 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; -import { MultiSelect } from "@/components/multi-select"; import { useCreateQuestion, useSearchTags, } from "@/services/api/programmingForumComponents"; +import { queryKeyFn } from "@/services/api/programmingForumContext"; +import { TagDetails } from "@/services/api/programmingForumSchemas"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; import { z } from "zod"; // Schema validation for the form @@ -27,9 +30,9 @@ const newQuestionSchema = z.object({ .min(1, "Content is required") .max(100000, "Max 100,000 characters"), tags: z - .array(z.object({ id: z.string(), name: z.string() })) + .array(z.object({ tagId: z.number(), name: z.string() })) .min(1, "At least one tag is required"), - difficulty: z.enum(["Easy", "Medium", "Hard"], { + difficultyLevel: z.enum(["EASY", "MEDIUM", "HARD"], { errorMap: () => ({ message: "Select a difficulty level" }), }), }); @@ -43,7 +46,7 @@ export default function QuestionCreationPage() { title: "", content: "", tags: [], - difficulty: "Easy", + difficultyLevel: "EASY", }, }); @@ -51,26 +54,33 @@ export default function QuestionCreationPage() { const queryClient = useQueryClient(); const [availableTags, setAvailableTags] = useState< - { id: string; name: string }[] + { tagId: string; name: string }[] >([]); const { data: tagSearchData } = useSearchTags( - { queryParams: { q: "", pageSize: 1000 } }, - { enabled: true } + { queryParams: { q: "", pageSize: 1000 } }, + { enabled: true }, ); useEffect(() => { - if (tagSearchData?.data?.items) { - setAvailableTags(tagSearchData.data.items); + if (tagSearchData?.data) { + setAvailableTags((tagSearchData.data as { items: TagDetails[] }).items); } }, [tagSearchData]); + const navigate = useNavigate(); + const { mutateAsync } = useCreateQuestion({ - onSuccess: (data) => { - const newQuestion = data.data; + onSuccess: (result) => { + queryClient.invalidateQueries( + queryKeyFn({ + path: "/users/me", + operationId: "getMe", + variables: {}, + }) as any, + ); - queryClient.invalidateQueries(["getUserProfile"]); - queryClient.invalidateQueries(["getTagDetails"]); + navigate(`/question/${result.data.id}`); }, }); @@ -80,9 +90,12 @@ export default function QuestionCreationPage() {
{ - const tagIds = values.tags.map((tag) => tag.id); + const tagIds = values.tags.map((tag) => tag.tagId); await mutateAsync({ - body: { ...values, tagIds: tagIds.map(Number) }, + body: { + ...values, + tagIds: tagIds.map(Number), + }, }); })} className="flex flex-col gap-6" @@ -127,18 +140,21 @@ export default function QuestionCreationPage() { ({ - value: tag.id, + value: tag.tagId, label: tag.name, }))} - value={field.value.map((tag) => tag.id)} // Bind selected values correctly + value={field.value.map((tag) => String(tag.tagId))} // Bind selected values correctly onValueChange={(selectedIds) => { // Map selected IDs back to tag objects const selectedTags = selectedIds .map((id) => - availableTags.find((tag) => tag.id === id) + availableTags.find( + (tag) => Number(tag.tagId) === Number(id), + ), ) - .filter(Boolean); - field.onChange(selectedTags); + .filter(Boolean); + console.log(selectedTags); + field.onChange(selectedTags); }} placeholder="Select Tags" /> @@ -151,18 +167,18 @@ export default function QuestionCreationPage() { {/* Difficulty */} ( diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 19b6ece4..a5f5a772 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -238,6 +238,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.13.10": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10c0/12c01357e0345f89f4f7e8c0e81921f2a3e3e101f06e8eaa18a382b517376520cd2fa8c237726eb094dab25532855df28a7baaf1c26342b52782f6936b07c287 + languageName: node + linkType: hard + "@babel/template@npm:^7.25.7": version: 7.25.7 resolution: "@babel/template@npm:7.25.7" @@ -807,6 +816,15 @@ __metadata: languageName: node linkType: hard +"@radix-ui/primitive@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/primitive@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + checksum: 10c0/912216455537db3ca77f3e7f70174fb2b454fbd4a37a0acb7cfadad9ab6131abdfb787472242574460a3c301edf45738340cc84f6717982710082840fde7d916 + languageName: node + linkType: hard + "@radix-ui/primitive@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/primitive@npm:1.1.0" @@ -949,6 +967,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-compose-refs@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-compose-refs@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/be06f8dab35b5a1bffa7a5982fb26218ddade1acb751288333e3b89d7b4a7dfb5a6371be83876dac0ec2ebe0866d295e8618b778608e1965342986ea448040ec + languageName: node + linkType: hard + "@radix-ui/react-compose-refs@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-compose-refs@npm:1.1.0" @@ -962,6 +995,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-context@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-context@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/3de5761b32cc70cd61715527f29d8c699c01ab28c195ced972ccbc7025763a373a68f18c9f948c7a7b922e469fd2df7fee5f7536e3f7bad44ffc06d959359333 + languageName: node + linkType: hard + "@radix-ui/react-context@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-context@npm:1.1.0" @@ -988,7 +1036,40 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-dialog@npm:^1.0.5": +"@radix-ui/react-dialog@npm:1.0.5": + version: 1.0.5 + resolution: "@radix-ui/react-dialog@npm:1.0.5" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-context": "npm:1.0.1" + "@radix-ui/react-dismissable-layer": "npm:1.0.5" + "@radix-ui/react-focus-guards": "npm:1.0.1" + "@radix-ui/react-focus-scope": "npm:1.0.4" + "@radix-ui/react-id": "npm:1.0.1" + "@radix-ui/react-portal": "npm:1.0.4" + "@radix-ui/react-presence": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-slot": "npm:1.0.2" + "@radix-ui/react-use-controllable-state": "npm:1.0.1" + aria-hidden: "npm:^1.1.1" + react-remove-scroll: "npm:2.5.5" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/c5b3069397379e79857a3203f3ead4d12d87736b59899f02a63e620a07dd1e6704e15523926cdf8e39afe1c945a7ff0f2533c5ea5be1e17c3114820300a51133 + languageName: node + linkType: hard + +"@radix-ui/react-dialog@npm:^1.1.2": version: 1.1.2 resolution: "@radix-ui/react-dialog@npm:1.1.2" dependencies: @@ -1033,6 +1114,30 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-dismissable-layer@npm:1.0.5": + version: 1.0.5 + resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/primitive": "npm:1.0.1" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + "@radix-ui/react-use-escape-keydown": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/7e4308867aecfb07b506330c1964d94a52247ab9453725613cd326762aa13e483423c250f107219c131b0449600eb8d1576ce3159c2b96e8c978f75e46062cb2 + languageName: node + linkType: hard + "@radix-ui/react-dismissable-layer@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/react-dismissable-layer@npm:1.1.1" @@ -1081,6 +1186,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-focus-guards@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-focus-guards@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/d5fd4e5aa9d9a87c8ad490b3b4992d6f1d9eddf18e56df2a2bcf8744c4332b275d73377fd193df3e6ba0ad9608dc497709beca5c64de2b834d5f5350b3c9a272 + languageName: node + linkType: hard + "@radix-ui/react-focus-guards@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/react-focus-guards@npm:1.1.1" @@ -1094,6 +1214,28 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-focus-scope@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-focus-scope@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-primitive": "npm:1.0.3" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/2fce0bafcab4e16cf4ed7560bda40654223f3d0add6b231e1c607433030c14e6249818b444b7b58ee7a6ff6bbf8e192c9c81d22c3a5c88c2daade9d1f881b5be + languageName: node + linkType: hard + "@radix-ui/react-focus-scope@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-focus-scope@npm:1.1.0" @@ -1115,6 +1257,22 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-id@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-id@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/e2859ca58bea171c956098ace7ecf615cf9432f58a118b779a14720746b3adcf0351c36c75de131548672d3cd290ca238198acbd33b88dc4706f98312e9317ad + languageName: node + linkType: hard + "@radix-ui/react-id@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-id@npm:1.1.0" @@ -1185,7 +1343,7 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-popover@npm:^1.0.7": +"@radix-ui/react-popover@npm:^1.1.2": version: 1.1.2 resolution: "@radix-ui/react-popover@npm:1.1.2" dependencies: @@ -1246,6 +1404,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-portal@npm:1.0.4": + version: 1.0.4 + resolution: "@radix-ui/react-portal@npm:1.0.4" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/fed32f8148b833fe852fb5e2f859979ffdf2fb9a9ef46583b9b52915d764ad36ba5c958a64e61d23395628ccc09d678229ee94cd112941e8fe2575021f820c29 + languageName: node + linkType: hard + "@radix-ui/react-portal@npm:1.1.2": version: 1.1.2 resolution: "@radix-ui/react-portal@npm:1.1.2" @@ -1266,6 +1444,27 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-presence@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-presence@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + "@radix-ui/react-use-layout-effect": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/90780618b265fe794a8f1ddaa5bfd3c71a1127fa79330a14d32722e6265b44452a9dd36efe4e769129d33e57f979f6b8713e2cbf2e2755326aa3b0f337185b6e + languageName: node + linkType: hard + "@radix-ui/react-presence@npm:1.1.1": version: 1.1.1 resolution: "@radix-ui/react-presence@npm:1.1.1" @@ -1286,6 +1485,26 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-primitive@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-primitive@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-slot": "npm:1.0.2" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/67a66ff8898a5e7739eda228ab6f5ce808858da1dce967014138d87e72b6bbfc93dc1467c706d98d1a2b93bf0b6e09233d1a24d31c78227b078444c1a69c42be + languageName: node + linkType: hard + "@radix-ui/react-primitive@npm:2.0.0": version: 2.0.0 resolution: "@radix-ui/react-primitive@npm:2.0.0" @@ -1371,7 +1590,42 @@ __metadata: languageName: node linkType: hard -"@radix-ui/react-slot@npm:1.1.0, @radix-ui/react-slot@npm:^1.0.2": +"@radix-ui/react-separator@npm:^1.1.0": + version: 1.1.0 + resolution: "@radix-ui/react-separator@npm:1.1.0" + dependencies: + "@radix-ui/react-primitive": "npm:2.0.0" + peerDependencies: + "@types/react": "*" + "@types/react-dom": "*" + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + "@types/react": + optional: true + "@types/react-dom": + optional: true + checksum: 10c0/0ca9e25db27b6b001f3c0c50b2df9d6cf070b949f183043e263115d694a25b7268fecd670572469a512e556deca25ebb08b3aec4a870f0309eed728eef19ab8a + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.0.2": + version: 1.0.2 + resolution: "@radix-ui/react-slot@npm:1.0.2" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-compose-refs": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/3af6ea4891e6fa8091e666802adffe7718b3cd390a10fa9229a5f40f8efded9f3918ea01b046103d93923d41cc32119505ebb6bde76cad07a87b6cf4f2119347 + languageName: node + linkType: hard + +"@radix-ui/react-slot@npm:1.1.0, @radix-ui/react-slot@npm:^1.1.0": version: 1.1.0 resolution: "@radix-ui/react-slot@npm:1.1.0" dependencies: @@ -1472,6 +1726,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-callback-ref@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/331b432be1edc960ca148637ae6087220873ee828ceb13bd155926ef8f49e862812de5b379129f6aaefcd11be53715f3237e6caa9a33d9c0abfff43f3ba58938 + languageName: node + linkType: hard + "@radix-ui/react-use-callback-ref@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-use-callback-ref@npm:1.1.0" @@ -1485,6 +1754,22 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-controllable-state@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/29b069dbf09e48bca321af6272574ad0fc7283174e7d092731a10663fe00c0e6b4bde5e1b5ea67725fe48dcbe8026e7ff0d69d42891c62cbb9ca408498171fbe + languageName: node + linkType: hard + "@radix-ui/react-use-controllable-state@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-use-controllable-state@npm:1.1.0" @@ -1500,6 +1785,22 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-escape-keydown@npm:1.0.3": + version: 1.0.3 + resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3" + dependencies: + "@babel/runtime": "npm:^7.13.10" + "@radix-ui/react-use-callback-ref": "npm:1.0.1" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/3c94c78902dcb40b60083ee2184614f45c95a189178f52d89323b467bd04bcf5fdb1bc4d43debecd7f0b572c3843c7e04edbcb56f40a4b4b43936fb2770fb8ad + languageName: node + linkType: hard + "@radix-ui/react-use-escape-keydown@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-use-escape-keydown@npm:1.1.0" @@ -1515,6 +1816,21 @@ __metadata: languageName: node linkType: hard +"@radix-ui/react-use-layout-effect@npm:1.0.1": + version: 1.0.1 + resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1" + dependencies: + "@babel/runtime": "npm:^7.13.10" + peerDependencies: + "@types/react": "*" + react: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/13cd0c38395c5838bc9a18238020d3bcf67fb340039e6d1cbf438be1b91d64cf6900b78121f3dc9219faeb40dcc7b523ce0f17e4a41631655690e5a30a40886a + languageName: node + linkType: hard + "@radix-ui/react-use-layout-effect@npm:1.1.0": version: 1.1.0 resolution: "@radix-ui/react-use-layout-effect@npm:1.1.0" @@ -3351,6 +3667,19 @@ __metadata: languageName: node linkType: hard +"cmdk@npm:1.0.0": + version: 1.0.0 + resolution: "cmdk@npm:1.0.0" + dependencies: + "@radix-ui/react-dialog": "npm:1.0.5" + "@radix-ui/react-primitive": "npm:1.0.3" + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 10c0/bf1c9cfce46f2f507ab95735fa08c9aa27e76ecdff87720cc51ae89dbf4814b7559668458f66ff4c3932a88a6b9d8817be05c3cc4ff98bc40c3645acf4a97376 + languageName: node + linkType: hard + "code-excerpt@npm:^3.0.0": version: 3.0.0 resolution: "code-excerpt@npm:3.0.0" @@ -4502,12 +4831,13 @@ __metadata: "@radix-ui/react-accordion": "npm:^1.1.2" "@radix-ui/react-aspect-ratio": "npm:^1.0.3" "@radix-ui/react-avatar": "npm:^1.0.4" - "@radix-ui/react-dialog": "npm:^1.0.5" + "@radix-ui/react-dialog": "npm:^1.1.2" "@radix-ui/react-dropdown-menu": "npm:^2.0.6" "@radix-ui/react-label": "npm:^2.0.2" - "@radix-ui/react-popover": "npm:^1.0.7" + "@radix-ui/react-popover": "npm:^1.1.2" "@radix-ui/react-select": "npm:^2.1.2" - "@radix-ui/react-slot": "npm:^1.0.2" + "@radix-ui/react-separator": "npm:^1.1.0" + "@radix-ui/react-slot": "npm:^1.1.0" "@radix-ui/react-tabs": "npm:^1.0.4" "@radix-ui/react-toast": "npm:^1.1.5" "@radix-ui/react-tooltip": "npm:^1.1.3" @@ -4529,6 +4859,7 @@ __metadata: canvas: "npm:^2.11.2" class-variance-authority: "npm:^0.7.0" clsx: "npm:^2.1.1" + cmdk: "npm:1.0.0" country-emoji: "npm:^1.5.6" date-fns: "npm:^3.6.0" eslint: "npm:^9.12.0" @@ -7625,7 +7956,7 @@ __metadata: languageName: node linkType: hard -"react-remove-scroll-bar@npm:^2.3.6": +"react-remove-scroll-bar@npm:^2.3.3, react-remove-scroll-bar@npm:^2.3.6": version: 2.3.6 resolution: "react-remove-scroll-bar@npm:2.3.6" dependencies: @@ -7641,6 +7972,25 @@ __metadata: languageName: node linkType: hard +"react-remove-scroll@npm:2.5.5": + version: 2.5.5 + resolution: "react-remove-scroll@npm:2.5.5" + dependencies: + react-remove-scroll-bar: "npm:^2.3.3" + react-style-singleton: "npm:^2.2.1" + tslib: "npm:^2.1.0" + use-callback-ref: "npm:^1.3.0" + use-sidecar: "npm:^1.1.2" + peerDependencies: + "@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 10c0/4952657e6a7b9d661d4ad4dfcef81b9c7fa493e35164abff99c35c0b27b3d172ef7ad70c09416dc44dd14ff2e6b38a5ec7da27e27e90a15cbad36b8fd2fd8054 + languageName: node + linkType: hard + "react-remove-scroll@npm:2.6.0": version: 2.6.0 resolution: "react-remove-scroll@npm:2.6.0" From 1a51d5433678a7a6b708007a71379a778f2762a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20Efe=20Ak=C3=A7a?= Date: Mon, 25 Nov 2024 19:04:19 +0300 Subject: [PATCH 12/12] fix: lint errors --- frontend/src/components/multi-select.tsx | 546 +++++++++++------------ frontend/src/routes/create-question.tsx | 4 +- 2 files changed, 274 insertions(+), 276 deletions(-) diff --git a/frontend/src/components/multi-select.tsx b/frontend/src/components/multi-select.tsx index 0140b195..9ac8bd92 100644 --- a/frontend/src/components/multi-select.tsx +++ b/frontend/src/components/multi-select.tsx @@ -1,274 +1,272 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import { CheckIcon, ChevronDown, XCircle, XIcon } from "lucide-react"; -import * as React from "react"; - -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; - -const multiSelectVariants = cva( - "m-1 transition ease-in-out delay-150 duration-300", - { - variants: { - variant: { - default: - "border-foreground/10 text-foreground bg-card hover:bg-card/80", - secondary: - "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - inverted: "inverted", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -interface MultiSelectProps - extends React.ButtonHTMLAttributes, - VariantProps { - options: { - label: string; - value: string; - icon?: React.ComponentType<{ className?: string }>; - }[]; - onValueChange: (value: string[]) => void; - defaultValue?: string[]; - placeholder?: string; - animation?: number; - maxCount?: number; - modalPopover?: boolean; - asChild?: boolean; - className?: string; -} - -export const MultiSelect = React.forwardRef< - HTMLButtonElement, - MultiSelectProps ->( - ( - { - options, - onValueChange, - variant, - defaultValue = [], - placeholder = "Select options", - animation = 0, - maxCount = 3, - modalPopover = false, - asChild = false, - className, - ...props - }, - ref, - ) => { - const [selectedValues, setSelectedValues] = - React.useState(defaultValue); - const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); - - const toggleOption = (optionValue: string | undefined) => { - if (!optionValue) { - console.warn(`Attempted to toggle non-existent value: ${optionValue}`); - return; - } - - const isSelected = selectedValues.includes(optionValue); - const newSelectedValues = isSelected - ? selectedValues.filter((value) => value !== optionValue) // Deselect - : [...selectedValues, optionValue]; // Select - - setSelectedValues(newSelectedValues); - onValueChange(newSelectedValues); - }; - - const handleClear = () => { - setSelectedValues([]); - onValueChange([]); - }; - - const handleTogglePopover = () => { - setIsPopoverOpen((prev) => !prev); - }; - - const clearExtraOptions = () => { - const newSelectedValues = selectedValues.slice(0, maxCount); - setSelectedValues(newSelectedValues); - onValueChange(newSelectedValues); - }; - - const toggleAll = () => { - if (selectedValues.length === options.length) { - handleClear(); - } else { - const allValues = options.map((option) => option.value); - setSelectedValues(allValues); - onValueChange(allValues); - } - }; - - return ( - - - - - - - - - - No results found. - - -
- -
- (Select All) -
- {options.map((option, index) => { - const uniqueKey = option.value || `option-${index}`; // Generate a unique key - const isSelected = selectedValues.includes(option.value); // Check if this option is selected - - return ( - toggleOption(option.value)} // Toggle this option - className="cursor-pointer" - > -
- -
- {option.icon && ( - - )} - - {option.label || "Unknown"} - {" "} - {/* Display label */} -
- ); - })} -
-
-
-
-
- ); - }, -); - -MultiSelect.displayName = "MultiSelect"; +import { cva, type VariantProps } from "class-variance-authority"; +import { CheckIcon, ChevronDown, XCircle, XIcon } from "lucide-react"; +import * as React from "react"; + +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; + +const multiSelectVariants = cva( + "m-1 transition ease-in-out delay-150 duration-300", + { + variants: { + variant: { + default: + "border-foreground/10 text-foreground bg-card hover:bg-card/80", + secondary: + "border-foreground/10 bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + inverted: "inverted", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +interface MultiSelectProps + extends React.ButtonHTMLAttributes, + VariantProps { + options: { + label: string; + value: string; + icon?: React.ComponentType<{ className?: string }>; + }[]; + onValueChange: (value: string[]) => void; + defaultValue?: string[]; + placeholder?: string; + animation?: number; + maxCount?: number; + modalPopover?: boolean; + asChild?: boolean; + className?: string; +} + +export const MultiSelect = React.forwardRef< + HTMLButtonElement, + MultiSelectProps +>( + ( + { + options, + onValueChange, + variant, + defaultValue = [], + placeholder = "Select options", + maxCount = 3, + modalPopover = false, + className, + ...props + }, + ref, + ) => { + const [selectedValues, setSelectedValues] = + React.useState(defaultValue); + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + + const toggleOption = (optionValue: string | undefined) => { + if (!optionValue) { + console.warn(`Attempted to toggle non-existent value: ${optionValue}`); + return; + } + + const isSelected = selectedValues.includes(optionValue); + const newSelectedValues = isSelected + ? selectedValues.filter((value) => value !== optionValue) // Deselect + : [...selectedValues, optionValue]; // Select + + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const handleClear = () => { + setSelectedValues([]); + onValueChange([]); + }; + + const handleTogglePopover = () => { + setIsPopoverOpen((prev) => !prev); + }; + + const clearExtraOptions = () => { + const newSelectedValues = selectedValues.slice(0, maxCount); + setSelectedValues(newSelectedValues); + onValueChange(newSelectedValues); + }; + + const toggleAll = () => { + if (selectedValues.length === options.length) { + handleClear(); + } else { + const allValues = options.map((option) => option.value); + setSelectedValues(allValues); + onValueChange(allValues); + } + }; + + return ( + + + + + + + + + + No results found. + + +
+ +
+ (Select All) +
+ {options.map((option, index) => { + const uniqueKey = option.value || `option-${index}`; // Generate a unique key + const isSelected = selectedValues.includes(option.value); // Check if this option is selected + + return ( + toggleOption(option.value)} // Toggle this option + className="cursor-pointer" + > +
+ +
+ {option.icon && ( + + )} + + {option.label || "Unknown"} + {" "} + {/* Display label */} +
+ ); + })} +
+
+
+
+
+ ); + }, +); + +MultiSelect.displayName = "MultiSelect"; diff --git a/frontend/src/routes/create-question.tsx b/frontend/src/routes/create-question.tsx index 2fd5c09a..3933f910 100644 --- a/frontend/src/routes/create-question.tsx +++ b/frontend/src/routes/create-question.tsx @@ -16,7 +16,7 @@ import { import { queryKeyFn } from "@/services/api/programmingForumContext"; import { TagDetails } from "@/services/api/programmingForumSchemas"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useQueryClient } from "@tanstack/react-query"; +import { InvalidateQueryFilters, useQueryClient } from "@tanstack/react-query"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; @@ -77,7 +77,7 @@ export default function QuestionCreationPage() { path: "/users/me", operationId: "getMe", variables: {}, - }) as any, + }) as unknown as InvalidateQueryFilters, ); navigate(`/question/${result.data.id}`);