Skip to content

Commit

Permalink
Merge pull request #553 from bounswe/frontend/feature/502/QuestionCreate
Browse files Browse the repository at this point in the history
  • Loading branch information
mmtftr authored Nov 25, 2024
2 parents 93404f8 + 1a51d54 commit f823793
Show file tree
Hide file tree
Showing 14 changed files with 1,037 additions and 35 deletions.
8 changes: 2 additions & 6 deletions dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,10 @@ services:
- PROXY_API=http://backend:8080
ports:
- "5173:5173"
volumes:
- ./frontend/src:/app/src
develop:
watch:
- action: sync
x-initialSync: true
path: ./frontend
target: /app
ignore:
- ./frontend/node_modules/
- action: rebuild
path: ./frontend/package.json
volumes:
Expand Down
8 changes: 5 additions & 3 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/components/ContentWithSnippets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,5 @@ export const ContentWithSnippets: React.FC<ContentWithSnippetsProps> = ({
});
}, [content]);

return (
<div className="prose max-w-full whitespace-pre-wrap">
{renderedContent}
</div>
);
return <div className="prose max-w-full">{renderedContent}</div>;
};
8 changes: 5 additions & 3 deletions frontend/src/components/CreateTagForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ export function CreateTagForm({ onCreateSuccess }: CreateTagFormProps) {

return (
<form onSubmit={handleSubmit} className="mt-6 flex flex-col gap-4">

<Input
value={name}
onChange={(e) => setName(e.target.value)}
Expand All @@ -51,10 +50,13 @@ export function CreateTagForm({ onCreateSuccess }: CreateTagFormProps) {
/>

<div className="flex justify-end">
<Button type="submit" disabled={isPending || !name.trim() || !description.trim()}>
<Button
type="submit"
disabled={isPending || !name.trim() || !description.trim()}
>
{isPending ? "Creating..." : "Create Tag"}
</Button>
</div>
</form>
);
}
}
4 changes: 2 additions & 2 deletions frontend/src/components/CreateTagPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CreateTagForm } from "@/components/CreateTagForm"; // Import the Create
export default function CreateTagPage() {
return (
<div className="container py-8">
<h1 className="text-2xl font-bold mb-4">Create a New Tag</h1>
<h1 className="mb-4 text-2xl font-bold">Create a New Tag</h1>

<CreateTagForm
onCreateSuccess={() => {
Expand All @@ -13,4 +13,4 @@ export default function CreateTagPage() {
/>
</div>
);
}
}
5 changes: 4 additions & 1 deletion frontend/src/components/NavbarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ import { SearchBar } from "./SearchBar";
import { Button } from "./ui/button";
import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet";

const links = [{ name: "Home", path: "/" }, { name: "Tags", path: "/tags" } ] as const;
const links = [
{ name: "Home", path: "/" },
{ name: "Tags", path: "/tags" },
] as const;

export const NavbarLayout = () => {
const navigation = useNavigation();
Expand Down
272 changes: 272 additions & 0 deletions frontend/src/components/multi-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +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<HTMLButtonElement>,
VariantProps<typeof multiSelectVariants> {
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<string[]>(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 (
<Popover
open={isPopoverOpen}
onOpenChange={setIsPopoverOpen}
modal={modalPopover}
>
<PopoverTrigger asChild>
<Button
ref={ref}
{...props}
onClick={handleTogglePopover}
className={cn(
"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 ? (
<div className="flex w-full items-center justify-between">
<div className="flex flex-wrap items-center">
{selectedValues.slice(0, maxCount).map((value) => {
const option = options.find((o) => o.value === value);
const IconComponent = option?.icon;
return (
<Badge
key={`badge-${value}`}
className={cn(multiSelectVariants({ variant }))}
>
{IconComponent && (
<IconComponent className="mr-2 h-4 w-4" />
)}
{option?.label || "Unknown"}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
toggleOption(value);
}}
/>
</Badge>
);
})}
{selectedValues.length > maxCount && (
<Badge
className={cn(
"border-foreground/1 bg-transparent text-foreground hover:bg-transparent",
)}
>
{`+ ${selectedValues.length - maxCount} more`}
<XCircle
className="ml-2 h-4 w-4 cursor-pointer"
onClick={(event) => {
event.stopPropagation();
clearExtraOptions();
}}
/>
</Badge>
)}
</div>
<div className="flex items-center justify-between">
<XIcon
className="mx-2 h-4 cursor-pointer text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
handleClear();
}}
/>
<Separator
orientation="vertical"
className="flex h-full min-h-6"
/>
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground" />
</div>
</div>
) : (
<div className="mx-auto flex w-full items-center justify-between">
<span className="mx-3 text-sm text-muted-foreground">
{placeholder}
</span>
<ChevronDown className="mx-2 h-4 cursor-pointer text-muted-foreground" />
</div>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Command>
<CommandInput placeholder="Search..." />

<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup>
<CommandItem
key="select-all"
onSelect={toggleAll}
className="cursor-pointer"
>
<div
className={cn(
"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",
)}
>
<CheckIcon className="h-4 w-4" />
</div>
<span>(Select All)</span>
</CommandItem>
{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 (
<CommandItem
key={`command-item-${uniqueKey}`} // Unique key for each CommandItem
onSelect={() => toggleOption(option.value)} // Toggle this option
className="cursor-pointer"
>
<div
className={cn(
"mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary",
isSelected
? "bg-primary text-primary-foreground"
: "opacity-50 [&_svg]:invisible",
)}
>
<CheckIcon className="h-4 w-4" />
</div>
{option.icon && (
<option.icon
key={`icon-${uniqueKey}`} // Unique key for the icon
className="mr-2 h-4 w-4 text-muted-foreground"
/>
)}
<span key={`label-${uniqueKey}`}>
{option.label || "Unknown"}
</span>{" "}
{/* Display label */}
</CommandItem>
);
})}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
},
);

MultiSelect.displayName = "MultiSelect";
2 changes: 1 addition & 1 deletion frontend/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";

import { cn } from "@/lib/utils";

Expand Down
Loading

0 comments on commit f823793

Please sign in to comment.