Skip to content

Commit a579f39

Browse files
author
Sandi Karajic
committed
add searchbar and fix breadcrumbs
1 parent 1dbfdfe commit a579f39

9 files changed

Lines changed: 198 additions & 11 deletions

File tree

biome.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
"recommended": true,
1919
"a11y": {
2020
"useFocusableInteractive": "off",
21-
"useSemanticElements": "off"
21+
"useSemanticElements": "off",
22+
"noLabelWithoutControl": "off"
2223
}
2324
}
2425
},

index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<meta name="color-scheme" content="light dark" />
77
<link rel="icon" href="/favicon.ico" type="image/x-icon">
88
<meta name="description" content="A community made to manage a custom API that serves static data.">
9-
<title>RAW - CommunityDragon</title>
9+
<title>Explore - CommunityDragon</title>
1010
<script>
1111
(() => {
1212
try {

src/components/global/breadcrumbs.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Home, type LucideIcon } from "lucide-react";
12
import {
23
Breadcrumb,
34
BreadcrumbEllipsis,
@@ -20,11 +21,11 @@ interface Props {
2021
}
2122

2223
type crumb = {
23-
label: string;
24+
label: string | LucideIcon;
2425
href: string;
2526
};
2627

27-
export function PathBreadcrumbs({ path = "/" }: Props) {
28+
export const Breadcrumbs: React.FC<Props> = ({ path = "/" }) => {
2829
const segments = path.split("/").filter((segment) => segment !== "");
2930

3031
const breadcrumbs: crumb[] = segments.map((segment, index) => {
@@ -35,8 +36,10 @@ export function PathBreadcrumbs({ path = "/" }: Props) {
3536
};
3637
});
3738

38-
const allItems =
39-
breadcrumbs.length === 0 ? [{ label: "Home", href: "/" }] : breadcrumbs;
39+
const allItems = [
40+
{ label: <Home size={16} strokeWidth={1} />, href: "/" },
41+
...breadcrumbs,
42+
];
4043

4144
const maxVisible = 6;
4245
const isTooLong = allItems.length > maxVisible;
@@ -92,4 +95,4 @@ export function PathBreadcrumbs({ path = "/" }: Props) {
9295
</BreadcrumbList>
9396
</Breadcrumb>
9497
);
95-
}
98+
};

src/components/global/main.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Separator } from "@components/ui/separator";
2-
import { PathBreadcrumbs } from "./breadcrumbs";
2+
import { Breadcrumbs } from "./breadcrumbs";
3+
import { Search } from "./search";
34

45
interface Props {
56
path?: string;
@@ -8,9 +9,10 @@ interface Props {
89

910
export const Main: React.FC<Props> = ({ path, children }) => (
1011
<div className="relative">
11-
<div className="flex flex-col gap-4 max-w-5xl m-auto pb-12 -mt-20">
12+
<div className="flex flex-col gap-4 max-w-4xl m-auto pb-12 -mt-20">
13+
<Search />
1214
<Separator />
13-
<PathBreadcrumbs path={path ?? "/"} />
15+
{(path ?? "/") === "/" ? null : <Breadcrumbs path={path} />}
1416
{children}
1517
</div>
1618
</div>

src/components/global/navbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const NavBar: React.FC = () => {
3535
navigationMenuTriggerStyle(),
3636
"bg-transparent",
3737
)}
38-
render={<a href="https://www.communitydragon.org">Home</a>}
38+
render={<a href="/">Home</a>}
3939
/>
4040
</NavigationMenuItem>
4141

src/components/global/search.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { Button } from "@components/ui/button";
2+
import { ButtonGroup } from "@components/ui/button-group";
3+
import { LoaderCircleIcon, SearchIcon } from "lucide-react";
4+
import { useEffect, useId, useState } from "react";
5+
import { Input } from "@/components/ui/input";
6+
7+
export const Search: React.FC = () => {
8+
const [value, setValue] = useState("");
9+
const [isLoading, setIsLoading] = useState(false);
10+
11+
const id = useId();
12+
13+
useEffect(() => {
14+
if (value) {
15+
setIsLoading(true);
16+
17+
const timer = setTimeout(() => {
18+
setIsLoading(false);
19+
}, 500);
20+
21+
return () => clearTimeout(timer);
22+
}
23+
24+
setIsLoading(false);
25+
}, [value]);
26+
27+
return (
28+
<div className="w-full flex gap-2">
29+
<div className="relative grow">
30+
<div className="text-muted-foreground pointer-events-none absolute inset-y-0 left-0 flex items-center justify-center pl-3 peer-disabled:opacity-50">
31+
<SearchIcon className="size-4" />
32+
<span className="sr-only">Search</span>
33+
</div>
34+
<Input
35+
id={id}
36+
type="search"
37+
placeholder="Search..."
38+
value={value}
39+
onChange={(e) => setValue(e.target.value)}
40+
className="peer px-9 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none"
41+
/>
42+
{isLoading && (
43+
<div className="text-muted-foreground pointer-events-none absolute inset-y-0 right-0 flex items-center justify-center pr-3 peer-disabled:opacity-50">
44+
<LoaderCircleIcon className="size-4 animate-spin" />
45+
<span className="sr-only">Loading...</span>
46+
</div>
47+
)}
48+
</div>
49+
<div className="relative">
50+
<ButtonGroup>
51+
<Button variant="secondary">Local</Button>
52+
<Button variant="secondary">Global</Button>
53+
</ButtonGroup>
54+
</div>
55+
</div>
56+
);
57+
};

src/components/ui/button-group.tsx

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { mergeProps } from "@base-ui/react/merge-props";
2+
import { useRender } from "@base-ui/react/use-render";
3+
import { cva, type VariantProps } from "class-variance-authority";
4+
import { Separator } from "@/components/ui/separator";
5+
import { cn } from "@/lib/utils";
6+
7+
const buttonGroupVariants = cva(
8+
"has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md flex w-fit items-stretch *:focus-visible:z-10 *:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
9+
{
10+
variants: {
11+
orientation: {
12+
horizontal:
13+
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-md! [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0 *:data-slot:rounded-r-none",
14+
vertical:
15+
"[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-md! flex-col [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0 *:data-slot:rounded-b-none",
16+
},
17+
},
18+
defaultVariants: {
19+
orientation: "horizontal",
20+
},
21+
},
22+
);
23+
24+
function ButtonGroup({
25+
className,
26+
orientation,
27+
...props
28+
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
29+
return (
30+
<div
31+
role="group"
32+
data-slot="button-group"
33+
data-orientation={orientation}
34+
className={cn(buttonGroupVariants({ orientation }), className)}
35+
{...props}
36+
/>
37+
);
38+
}
39+
40+
function ButtonGroupText({
41+
className,
42+
render,
43+
...props
44+
}: useRender.ComponentProps<"div">) {
45+
return useRender({
46+
defaultTagName: "div",
47+
props: mergeProps<"div">(
48+
{
49+
className: cn(
50+
"bg-muted gap-2 rounded-md border px-2.5 text-sm font-medium shadow-xs [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
51+
className,
52+
),
53+
},
54+
props,
55+
),
56+
render,
57+
state: {
58+
slot: "button-group-text",
59+
},
60+
});
61+
}
62+
63+
function ButtonGroupSeparator({
64+
className,
65+
orientation = "vertical",
66+
...props
67+
}: React.ComponentProps<typeof Separator>) {
68+
return (
69+
<Separator
70+
data-slot="button-group-separator"
71+
orientation={orientation}
72+
className={cn(
73+
"bg-input relative self-stretch data-horizontal:mx-px data-horizontal:w-auto data-vertical:my-px data-vertical:h-auto",
74+
className,
75+
)}
76+
{...props}
77+
/>
78+
);
79+
}
80+
81+
export {
82+
ButtonGroup,
83+
ButtonGroupSeparator,
84+
ButtonGroupText,
85+
buttonGroupVariants,
86+
};

src/components/ui/input.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Input as InputPrimitive } from "@base-ui/react/input";
2+
import type * as React from "react";
3+
4+
import { cn } from "@/lib/utils";
5+
6+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
7+
return (
8+
<InputPrimitive
9+
type={type}
10+
data-slot="input"
11+
className={cn(
12+
"dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 file:text-foreground placeholder:text-muted-foreground h-9 w-full min-w-0 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm",
13+
className,
14+
)}
15+
{...props}
16+
/>
17+
);
18+
}
19+
20+
export { Input };

src/components/ui/label.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type * as React from "react";
2+
3+
import { cn } from "@/lib/utils";
4+
5+
function Label({ className, ...props }: React.ComponentProps<"label">) {
6+
return (
7+
<label
8+
data-slot="label"
9+
className={cn(
10+
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
11+
className,
12+
)}
13+
{...props}
14+
/>
15+
);
16+
}
17+
18+
export { Label };

0 commit comments

Comments
 (0)