Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions frontend/src/api/parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ type ApiPart = {
export type PartsOptions = {
search?: string;
category?: PcPart["category"];
manufacturers?: string[];
sortBy?: "NAME" | "PRICE" | "MANUFACTURER" | "CATEGORY";
sortDirection?: "ASC" | "DESC";
first?: number;
Expand All @@ -35,7 +34,6 @@ export async function getParts(
{
search = "",
category,
manufacturers,
sortBy = "NAME",
sortDirection = "ASC",
first = 20,
Expand All @@ -52,7 +50,6 @@ export async function getParts(
query GetParts(
$search: String
$category: Category
$manufacturers: [String!]
$sortBy: SortField!
$sortDirection: SortDirection!
$first: Int!
Expand All @@ -63,7 +60,6 @@ export async function getParts(
after: $after
search: $search
category: $category
manufacturers: $manufacturers
sortBy: $sortBy
sortDirection: $sortDirection

Expand All @@ -89,7 +85,6 @@ export async function getParts(
variables: {
search: search.trim() || null,
category: category ?? null,
manufacturers: manufacturers?.length ? manufacturers : null,
sortBy,
sortDirection,
first,
Expand Down
79 changes: 79 additions & 0 deletions frontend/src/components/CategorySelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Link } from "react-router";
import { categories } from "@/types/categories";
import {
CircuitBoard,
Cpu,
Fan,
Gpu,
HardDrive,
MemoryStick,
PcCase,
Plug,
type LucideIcon,
} from "lucide-react";
import type { PcPart } from "@/types/types";

const categoryIcons: Record<PcPart["category"], LucideIcon> = {
GPU: Gpu,
CPU: Cpu,
RAM: MemoryStick,
Storage: HardDrive,
PSU: Plug,
PCCase: PcCase,
CPUCooler: Fan,
Motherboard: CircuitBoard,
};

type CategorySelectionProps = {
showViewAll?: boolean;
};

export default function CategorySelection({
showViewAll = true,
}: CategorySelectionProps) {
return (
<section className="mx-auto w-[90%] max-w-lg text-white lg:mx-0 lg:w-full lg:max-w-none">
<div className="mb-4 flex flex-wrap items-end justify-between gap-3">
<div className="w-full text-center lg:w-auto lg:text-left">
<h2 className="text-2xl lg:text-xl lg:font-bold">
Explore components
</h2>
<p className="mt-1 hidden text-sm text-secondary lg:block">
Find every part of your next build.
</p>
</div>

{showViewAll && (
<Link
to="/explore"
className="hidden text-sm text-accent hover:underline lg:block"
>
View all components →
</Link>
)}
</div>

<ul className="grid grid-cols-3 gap-3 lg:grid-cols-8">
{categories.map((category) => {
const Icon = categoryIcons[category.value];
return (
<li key={category.value}>
<Link
to={`/explore/category/${encodeURIComponent(category.value)}`}
className="flex min-h-24 flex-col items-center justify-center gap-2 rounded-xl bg-surface p-2 text-center text-sm hover:brightness-110"
>
<Icon
aria-hidden="true"
size={28}
strokeWidth={1.75}
className="shrink-0 text-accent"
/>
{category.label}
</Link>
</li>
);
})}
</ul>
</section>
);
}
91 changes: 27 additions & 64 deletions frontend/src/components/FilterComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type { Dispatch, SetStateAction } from "react";
import type { PartsOptions } from "@/api/parts";
import { categories } from "@/types/categories";
import Button from "./buttons/Button";
import SearchComponent from "./SearchComponent";

// Sort and both filters sit inline only when there is room next to the search field.
// Sort and category controls sit inline when there is room next to the search field.
const DESKTOP_FILTER_WIDTH = 1200;

type FilterComponentProps = {
Expand Down Expand Up @@ -49,14 +50,7 @@ export default function FilterComponent({
aria-label="Search and filter components"
className={`w-[90%] max-w-6xl text-white flex justify-center ${isDesktopSize ? "border border-gray-600 rounded-2xl bg-surface-dark p-2" : ""} items-center h-fit gap-2`}
>
<input
type="search"
aria-label="Search components"
placeholder="Search components..."
value={search}
onChange={(event) => setSearch(event.target.value)}
className="min-w-0 flex-1 rounded-xl bg-surface px-4 py-3 placeholder:text-gray-300"
/>
<SearchComponent value={search} onChange={setSearch} variant="light" />
{isDesktopSize ? (
<>
<form
Expand Down Expand Up @@ -110,23 +104,6 @@ export default function FilterComponent({
</select>
</label>

<label className="flex items-center gap-2 h-full">
Manufacturer
<input
type="text"
className="h-full rounded-lg border border-gray-500 p-2 placeholder:text-gray-300"
placeholder="e.g. MSI"
value={filtOptions.manufacturers?.[0] ?? ""}
onChange={(event) =>
updateFilters({
manufacturers: event.target.value
? [event.target.value]
: [],
})
}
/>
</label>

<button
type="button"
aria-label="Reset filters"
Expand All @@ -152,46 +129,38 @@ export default function FilterComponent({
<dialog
ref={dialogRef}
aria-labelledby="filter-title"
className="m-auto max-h-[90vh] w-[90%] max-w-sm overflow-y-auto rounded-3xl bg-surface p-6 text-white backdrop:bg-black/50"
className="m-auto max-h-[90dvh] w-[calc(100%-2rem)] max-w-sm overflow-y-auto rounded-2xl border border-gray-600 bg-surface-dark p-5 text-white shadow-2xl backdrop:bg-black/60 sm:p-6"
onClick={(event) => {
if (event.target === event.currentTarget)
dialogRef.current?.close();
}}
>
<form
className="flex min-h-[28rem] flex-col gap-6"
className="flex flex-col gap-5"
onSubmit={(event) => {
event.preventDefault();
setFiltOptions(draft);
dialogRef.current?.close();
}}
>
<header className="flex items-center justify-between">
<button
type="button"
aria-label="Reset filters"
className="cursor-pointer text-sm underline"
onClick={() => setDraft({ category: defaultCategory })}
>
Reset
</button>
<h2 id="filter-title" className="text-2xl">
Filter
<header className="flex items-center justify-between border-b border-gray-600 pb-3">
<h2 id="filter-title" className="text-xl font-semibold">
Filters
</h2>
<button
type="button"
aria-label="Close filters"
className="cursor-pointer text-3xl"
className="flex size-11 cursor-pointer items-center justify-center rounded-lg text-2xl text-gray-300 transition-colors hover:bg-surface hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
onClick={() => dialogRef.current?.close()}
>
×
</button>
</header>

<label className="flex flex-col gap-2">
<label className="flex flex-col gap-2 text-sm font-medium">
Sort by
<select
className="rounded-lg border border-gray-400 bg-surface p-2"
className="min-h-11 w-full rounded-lg border border-gray-500 bg-surface px-3 py-2 text-base font-normal focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
value={
draft.sortBy === "PRICE"
? (draft.sortDirection ?? "ASC")
Expand All @@ -211,10 +180,10 @@ export default function FilterComponent({
</select>
</label>

<label className="flex flex-col gap-2">
<label className="flex flex-col gap-2 text-sm font-medium">
Category
<select
className="rounded-lg border border-gray-400 bg-surface p-2"
className="min-h-11 w-full rounded-lg border border-gray-500 bg-surface px-3 py-2 text-base font-normal focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
value={draft.category ?? ""}
onChange={(event) =>
updateFilters({
Expand All @@ -233,26 +202,20 @@ export default function FilterComponent({
</select>
</label>

<label className="flex flex-col gap-2">
Manufacturer
<input
type="text"
className="rounded-lg border border-gray-400 p-2 placeholder:text-gray-300"
placeholder="e.g. MSI"
value={draft.manufacturers?.[0] ?? ""}
onChange={(event) =>
updateFilters({
manufacturers: event.target.value
? [event.target.value]
: [],
})
}
/>
</label>

<div className="pt-10">
<Button type="submit" className="py-3 ">
Use filter
<div className="mt-1 flex items-center gap-4 border-t border-gray-600 pt-4">
<button
type="button"
aria-label="Reset filters"
className="min-h-11 cursor-pointer rounded-lg px-3 text-sm underline underline-offset-4 hover:text-gray-300 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
onClick={() => setDraft({ category: defaultCategory })}
>
Reset
</button>
<Button
type="submit"
className="min-h-11 flex-1 py-3 font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white"
>
Apply filters
</Button>
</div>
</form>
Expand Down
57 changes: 57 additions & 0 deletions frontend/src/components/SearchComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useRef } from "react";
import { Search, X } from "lucide-react";
import { twMerge } from "tailwind-merge";

type SearchComponentProps = {
value: string;
onChange: (value: string) => void;
placeholder?: string;
label?: string;
variant?: "default" | "light";
className?: string;
};

export default function SearchComponent({
value,
onChange,
placeholder = "Search components or manufacturers...",
label = "Search components",
variant = "default",
className,
}: SearchComponentProps) {
const inputRef = useRef<HTMLInputElement>(null);

return (
<div
className={twMerge(
"flex w-full min-w-0 flex-1 items-center gap-2 rounded-lg px-3 py-2 focus-within:ring-2 focus-within:ring-accent",
variant === "light" ? "bg-surface" : "bg-background",
className,
)}
>
<Search aria-hidden="true" height={15} className="shrink-0" />
<input
ref={inputRef}
aria-label={label}
type="search"
value={value}
onChange={(event) => onChange(event.target.value)}
placeholder={placeholder}
className="w-full min-w-0 bg-transparent focus:outline-none [&::-webkit-search-cancel-button]:appearance-none"
/>
{value && (
<button
type="button"
aria-label="Clear search"
className="flex size-6 shrink-0 cursor-pointer items-center justify-center rounded text-white hover:bg-white/10 focus-visible:outline-2 focus-visible:outline-white"
onClick={() => {
onChange("");
inputRef.current?.focus();
}}
>
<X aria-hidden="true" size={16} />
</button>
)}
</div>
);
}
1 change: 0 additions & 1 deletion frontend/src/components/buttons/AddToBuildButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { ReactNode } from "react";
import { useRef, useState } from "react";
import type { PcPart } from "@/types/types";
import { useBuild } from "@/context/BuildContext";
import { useNavigate } from "react-router";
import { twMerge } from "tailwind-merge";
import Button from "./Button";
import AddToBuildFeedback from "../AddToBuildFeedback";
Expand Down
18 changes: 4 additions & 14 deletions frontend/src/components/pageSections/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { useState, type FormEvent } from "react";
import { Link, NavLink, useNavigate } from "react-router";
import { Menu, Search, X } from "lucide-react";
import { Menu, X } from "lucide-react";
import { useBuild } from "@/context/BuildContext";
import SearchComponent from "@/components/SearchComponent";

const navigation = [
{ to: "/", label: "Home" },
Expand Down Expand Up @@ -63,20 +64,9 @@ export default function NavBar({ onSearch }: NavBarProps) {
<form
role="search"
onSubmit={handleSearch}
className="hidden lg:flex min-w-0 flex-1 self-center items-center gap-2 rounded-lg bg-background px-3 py-2 focus-within:ring-2 focus-within:ring-accent"
className="hidden lg:flex min-w-0 flex-1 self-center"
>
<label htmlFor="header-search" className="sr-only">
Search components
</label>
<Search aria-hidden="true" height={15} className="shrink-0" />
<input
id="header-search"
type="search"
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Search components"
className="min-w-0 w-full focus:outline-none"
/>
<SearchComponent value={search} onChange={setSearch} />
</form>

<Link
Expand Down
2 changes: 0 additions & 2 deletions frontend/src/hooks/useParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ const catalogueCache = new Map<
export function useParts({
search = "",
category,
manufacturers,
sortBy = "NAME",
sortDirection = "ASC",
first = 20,
Expand All @@ -31,7 +30,6 @@ export function useParts({
// Include every filter and pagination option so only identical queries share data.
search,
category,
manufacturers: manufacturers ?? [],
sortBy,
sortDirection,
first,
Expand Down
Loading