Skip to content

Commit db84539

Browse files
feat: Added tags to projects section and configured relationships
1 parent 5db6a82 commit db84539

File tree

9 files changed

+785
-20
lines changed

9 files changed

+785
-20
lines changed

client/package-lock.json

Lines changed: 377 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"axios": "^1.7.4",
2525
"class-variance-authority": "^0.7.0",
2626
"clsx": "^2.1.1",
27+
"cmdk": "^1.0.0",
2728
"cobe": "^0.6.3",
2829
"framer-motion": "^11.3.8",
2930
"lucide-react": "^0.411.0",
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React, { useState } from 'react';
2+
3+
const predefinedTags = ['React', 'Vue', 'Angular', 'Svelte', 'Node.js', 'Django', 'Flask'];
4+
5+
const FancyMultiSelect = ({ onChange }) => {
6+
const [selectedTags, setSelectedTags] = useState([]);
7+
const [query, setQuery] = useState('');
8+
9+
const handleTagClick = (tag) => {
10+
setSelectedTags((prev) => {
11+
if (prev.includes(tag)) {
12+
return prev.filter((t) => t !== tag);
13+
} else {
14+
return [...prev, tag];
15+
}
16+
});
17+
};
18+
19+
const handleInputChange = (e) => {
20+
setQuery(e.target.value);
21+
};
22+
23+
const filteredTags = predefinedTags.filter((tag) =>
24+
tag.toLowerCase().includes(query.toLowerCase())
25+
);
26+
27+
React.useEffect(() => {
28+
onChange(selectedTags);
29+
}, [selectedTags, onChange]);
30+
31+
return (
32+
<div>
33+
<input
34+
type="text"
35+
value={query}
36+
onChange={handleInputChange}
37+
placeholder="Search or add tags..."
38+
className="border rounded px-2 py-1"
39+
/>
40+
<div className="mt-2">
41+
{filteredTags.map((tag) => (
42+
<button
43+
key={tag}
44+
onClick={() => handleTagClick(tag)}
45+
className={`border rounded px-2 py-1 mr-2 mb-2 ${
46+
selectedTags.includes(tag) ? 'bg-blue-500 text-white' : 'bg-gray-200'
47+
}`}
48+
>
49+
{tag}
50+
</button>
51+
))}
52+
</div>
53+
</div>
54+
);
55+
};
56+
57+
export default FancyMultiSelect;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import { X } from "lucide-react";
5+
import { Badge } from "../ui/badge";
6+
import {
7+
Command,
8+
CommandGroup,
9+
CommandItem,
10+
CommandList,
11+
} from "../ui/command";
12+
import { Command as CommandPrimitive } from "cmdk";
13+
14+
// hardcoded later to be fetched from server which will analyze all project and create necessary tags
15+
const TAGS = [
16+
{ value: "react", label: "React" },
17+
{ value: "node.js", label: "Node.js" },
18+
{ value: "javascript", label: "JavaScript" },
19+
{ value: "python", label: "Python" },
20+
{ value: "ai", label: "AI" },
21+
{ value: "ml", label: "Machine Learning" },
22+
{ value: "fintech", label: "Fintech" },
23+
];
24+
25+
export default function TagInput({ selectedTags, onTagsChange }) {
26+
const inputRef = React.useRef(null);
27+
const [open, setOpen] = React.useState(false);
28+
const [inputValue, setInputValue] = React.useState("");
29+
30+
const handleUnselect = React.useCallback((tag) => {
31+
onTagsChange(selectedTags.filter((s) => s.value !== tag.value));
32+
}, [selectedTags, onTagsChange]);
33+
34+
const handleKeyDown = React.useCallback((e) => {
35+
const input = inputRef.current;
36+
if (input) {
37+
if (e.key === "Delete" || e.key === "Backspace") {
38+
if (input.value === "") {
39+
onTagsChange(selectedTags.slice(0, -1));
40+
}
41+
}
42+
if (e.key === "Escape") {
43+
input.blur();
44+
}
45+
}
46+
}, [selectedTags, onTagsChange]);
47+
48+
const selectables = TAGS.filter(tag => !selectedTags.includes(tag));
49+
50+
return (
51+
<Command onKeyDown={handleKeyDown} className="overflow-visible bg-transparent">
52+
<div className="group rounded-md border border-input px-3 py-2 text-sm ring-offset-background focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
53+
<div className="flex flex-wrap gap-1">
54+
{selectedTags.map((tag) => (
55+
<Badge key={tag.value} variant="secondary">
56+
{tag.label}
57+
<button
58+
className="ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2"
59+
onKeyDown={(e) => {
60+
if (e.key === "Enter") {
61+
handleUnselect(tag);
62+
}
63+
}}
64+
onMouseDown={(e) => {
65+
e.preventDefault();
66+
e.stopPropagation();
67+
}}
68+
onClick={() => handleUnselect(tag)}
69+
>
70+
<X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
71+
</button>
72+
</Badge>
73+
))}
74+
<CommandPrimitive.Input
75+
ref={inputRef}
76+
value={inputValue}
77+
onValueChange={setInputValue}
78+
onBlur={() => setOpen(false)}
79+
onFocus={() => setOpen(true)}
80+
placeholder="Select tags..."
81+
className="ml-2 flex-1 bg-transparent outline-none placeholder:text-muted-foreground"
82+
/>
83+
</div>
84+
</div>
85+
<div className="relative mt-2">
86+
<CommandList>
87+
{open && selectables.length > 0 ? (
88+
<div className="absolute top-0 z-10 w-full rounded-md border bg-popover text-popover-foreground shadow-md outline-none animate-in">
89+
<CommandGroup className="h-full overflow-auto">
90+
{selectables.map((tag) => (
91+
<CommandItem
92+
key={tag.value}
93+
onMouseDown={(e) => {
94+
e.preventDefault();
95+
e.stopPropagation();
96+
}}
97+
onSelect={() => {
98+
setInputValue("");
99+
onTagsChange([...selectedTags, tag]);
100+
}}
101+
className="cursor-pointer"
102+
>
103+
{tag.label}
104+
</CommandItem>
105+
))}
106+
</CommandGroup>
107+
</div>
108+
) : null}
109+
</CommandList>
110+
</div>
111+
</Command>
112+
);
113+
}
114+

client/src/components/ui/badge.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import * as React from "react"
2+
import { VariantProps, cva } from "class-variance-authority"
3+
4+
import { cn } from "@/lib/utils"
5+
6+
const badgeVariants = cva(
7+
"inline-flex items-center border rounded-full 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",
8+
{
9+
variants: {
10+
variant: {
11+
default:
12+
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
13+
secondary:
14+
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
15+
destructive:
16+
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
17+
outline: "text-foreground",
18+
},
19+
},
20+
defaultVariants: {
21+
variant: "default",
22+
},
23+
}
24+
)
25+
26+
export interface BadgeProps
27+
extends React.HTMLAttributes<HTMLDivElement>,
28+
VariantProps<typeof badgeVariants> {}
29+
30+
function Badge({ className, variant, ...props }: BadgeProps) {
31+
return (
32+
<div className={cn(badgeVariants({ variant }), className)} {...props} />
33+
)
34+
}
35+
36+
export { Badge, badgeVariants }
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import type { DialogProps } from "@radix-ui/react-dialog";
5+
import { Command as CommandPrimitive } from "cmdk";
6+
import { Search } from "lucide-react";
7+
8+
import { cn } from "@/lib/utils";
9+
import { Dialog, DialogContent } from "@/components/ui/dialog";
10+
11+
const Command = React.forwardRef<
12+
React.ElementRef<typeof CommandPrimitive>,
13+
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14+
>(({ className, ...props }, ref) => (
15+
<CommandPrimitive
16+
ref={ref}
17+
className={cn(
18+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19+
className
20+
)}
21+
{...props}
22+
/>
23+
));
24+
Command.displayName = CommandPrimitive.displayName;
25+
26+
interface CommandDialogProps extends DialogProps {}
27+
28+
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
29+
return (
30+
<Dialog {...props}>
31+
<DialogContent className="overflow-hidden p-0 shadow-2xl">
32+
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
33+
{children}
34+
</Command>
35+
</DialogContent>
36+
</Dialog>
37+
);
38+
};
39+
40+
const CommandInput = React.forwardRef<
41+
React.ElementRef<typeof CommandPrimitive.Input>,
42+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
43+
>(({ className, ...props }, ref) => (
44+
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
45+
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
46+
<CommandPrimitive.Input
47+
ref={ref}
48+
className={cn(
49+
"placeholder:text-foreground-muted flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
50+
className
51+
)}
52+
{...props}
53+
/>
54+
</div>
55+
));
56+
57+
CommandInput.displayName = CommandPrimitive.Input.displayName;
58+
59+
const CommandList = React.forwardRef<
60+
React.ElementRef<typeof CommandPrimitive.List>,
61+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
62+
>(({ className, ...props }, ref) => (
63+
<CommandPrimitive.List
64+
ref={ref}
65+
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
66+
{...props}
67+
/>
68+
));
69+
70+
CommandList.displayName = CommandPrimitive.List.displayName;
71+
72+
const CommandEmpty = React.forwardRef<
73+
React.ElementRef<typeof CommandPrimitive.Empty>,
74+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
75+
>((props, ref) => (
76+
<CommandPrimitive.Empty
77+
ref={ref}
78+
className="py-6 text-center text-sm"
79+
{...props}
80+
/>
81+
));
82+
83+
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
84+
85+
const CommandGroup = React.forwardRef<
86+
React.ElementRef<typeof CommandPrimitive.Group>,
87+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
88+
>(({ className, ...props }, ref) => (
89+
<CommandPrimitive.Group
90+
ref={ref}
91+
className={cn(
92+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
93+
className
94+
)}
95+
{...props}
96+
/>
97+
));
98+
99+
CommandGroup.displayName = CommandPrimitive.Group.displayName;
100+
101+
const CommandSeparator = React.forwardRef<
102+
React.ElementRef<typeof CommandPrimitive.Separator>,
103+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
104+
>(({ className, ...props }, ref) => (
105+
<CommandPrimitive.Separator
106+
ref={ref}
107+
className={cn("-mx-1 h-px bg-border", className)}
108+
{...props}
109+
/>
110+
));
111+
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
112+
113+
const CommandItem = React.forwardRef<
114+
React.ElementRef<typeof CommandPrimitive.Item>,
115+
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
116+
>(({ className, ...props }, ref) => (
117+
<CommandPrimitive.Item
118+
ref={ref}
119+
className={cn(
120+
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
121+
className
122+
)}
123+
{...props}
124+
/>
125+
));
126+
127+
CommandItem.displayName = CommandPrimitive.Item.displayName;
128+
129+
const CommandShortcut = ({
130+
className,
131+
...props
132+
}: React.HTMLAttributes<HTMLSpanElement>) => {
133+
return (
134+
<span
135+
className={cn(
136+
"ml-auto text-xs tracking-widest text-muted-foreground",
137+
className
138+
)}
139+
{...props}
140+
/>
141+
);
142+
};
143+
CommandShortcut.displayName = "CommandShortcut";
144+
145+
export {
146+
Command,
147+
CommandDialog,
148+
CommandInput,
149+
CommandList,
150+
CommandEmpty,
151+
CommandGroup,
152+
CommandItem,
153+
CommandShortcut,
154+
CommandSeparator,
155+
};

0 commit comments

Comments
 (0)