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
43 changes: 16 additions & 27 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import HomePage from "./pages/homePage/homePage";
import CategoryPage from "./pages/categoryPage/categoryPage";
import PcBuilderPage from "./pages/pcBuilderPage/pcBuilderPage";
import CommunityPage from "./pages/communityPage/communityPage";
import NavBar from "./components/pageSections/Header";
import Footer from "./components/pageSections/Footer";
import BackToTopButton from "./components/buttons/BackToTopButton";
import Layout from "./components/Layout";
import LoginPage from "./pages/loginPage/loginPage";
import AccountPage from "./pages/accountPage/accountPage";
import { Routes, Route, useLocation, useParams } from "react-router";
import Notification from "./components/Notification";
import { useSelector } from "react-redux";
import type { RootState } from "./state/store";

Expand All @@ -28,29 +25,21 @@ function App() {
(state: RootState) => state.auth.user?.uid ?? "guest",
);
return (
<div className="flex min-h-screen flex-col">
<header className="sticky top-0 z-50 bg-background">
<NavBar />
</header>
<main className="flex-1">
<Routes key={accountKey}>
<Route path="/" element={<HomePage />} />
<Route path="/explore" element={<CategoryPage />} />
<Route
path="/explore/category/:category"
element={<CategoryRoute />}
/>
<Route path="/builder" element={<BuilderRoute />} />
<Route path="/community" element={<CommunityPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/account" element={<AccountPage />} />
<Route path="*" element={<p>404 - Page not found</p>} />
</Routes>
</main>
<Footer />
<BackToTopButton />
<Notification />
</div>
<Layout>
<Routes key={accountKey}>
<Route path="/" element={<HomePage />} />
<Route path="/explore" element={<CategoryPage />} />
<Route path="/explore/category/:category" element={<CategoryRoute />} />
<Route path="/builder" element={<BuilderRoute />} />
<Route path="/community" element={<CommunityPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/account" element={<AccountPage />} />
<Route
path="*"
element={<p className="w-full">404 - Page not found</p>}
/>
</Routes>
</Layout>
);
}
export default App;
28 changes: 28 additions & 0 deletions frontend/src/assets/dualListIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { SVGProps } from "react";

export function DualListIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
{...props}
>
{/* Left column */}
<path d="M2 5H10" />
<path d="M2 12H10" />
<path d="M2 19H10" />

{/* Right column */}
<path d="M14 5H22" />
<path d="M14 12H22" />
<path d="M14 19H22" />
</svg>
);
}
21 changes: 12 additions & 9 deletions frontend/src/components/AllPartsPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,19 @@ function PartsResults({
}, [featured, handleLoadMore, loading, error, pageInfo?.hasNextPage]);

return (
<section className="flex w-full min-w-0 max-w-7xl flex-col text-white p-2 md:p-0">
{viewenabled && (
<ViewSelector viewType={viewType} onViewChange={setViewType} />
)}
<section className="flex flex-col gap-4 w-full min-w-0 max-w-[100rem] text-white p-2 md:p-0">

{!featured && (
<p className={`mb-3 ${loading || error ? "invisible" : ""}`}>
Showing {visibleParts.length} of {totalCount} components
</p>
)}
<div className="flex items-center justify-between">
{!featured && (
<p className={` ${loading || error ? "invisible" : ""} `}>
Showing {visibleParts.length} of {totalCount} components
</p>
)}

{viewenabled && (
<ViewSelector viewType={viewType} onViewChange={setViewType} />
)}
</div>

<div
aria-busy={loading}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/CategorySelection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ 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">
<section className="mx-auto w-full 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">
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/FilterComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function FilterComponent({
return (
<section
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`}
className={`w-full max-w-[100rem] text-white flex justify-center ${isDesktopSize ? "border border-gray-600 rounded-2xl bg-surface-dark p-2" : ""} items-center h-fit gap-2`}
>
<SearchComponent value={search} onChange={setSearch} variant="light" />
{isDesktopSize ? (
Expand Down
27 changes: 27 additions & 0 deletions frontend/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { ReactNode } from "react";
import NavBar from "./pageSections/Header";
import Footer from "./pageSections/Footer";
import BackToTopButton from "./buttons/BackToTopButton";
import Notification from "./Notification";

const contentWidth = "mx-auto w-full max-w-[100rem] px-4 sm:px-6";

export default function Layout({ children }: { children: ReactNode }) {
return (
<div className="flex min-h-dvh flex-col">
<header className="sticky top-0 z-50 bg-surface shadow-2xl">
<div className={contentWidth}>
<NavBar />
</div>
</header>
<main className={`${contentWidth} min-w-0 flex-1 py-6`}>{children}</main>
<footer className="bg-surface text-white">
<div className={contentWidth}>
<Footer />
</div>
</footer>
<BackToTopButton />
<Notification />
</div>
);
}
10 changes: 7 additions & 3 deletions frontend/src/components/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,14 @@ export default function Notification() {
return;
}

const exitTransform = window.matchMedia("(min-width: 48rem)").matches
? "translateX(calc(100% + 1rem))"
: "translateY(calc(-100% - 6rem))";

exitAnimation = notification.animate(
[
{ transform: "translateX(0)", opacity: 1 },
{ transform: "translateX(calc(100% + 1rem))", opacity: 0 },
{ transform: "translate(0, 0)", opacity: 1 },
{ transform: exitTransform, opacity: 0 },
],
{ duration: 250, easing: "ease-in", fill: "forwards" },
);
Expand Down Expand Up @@ -120,7 +124,7 @@ export default function Notification() {
}}
aria-labelledby={titleId}
aria-describedby={descriptionId}
className="fixed right-4 left-auto top-20 bottom-auto m-0 h-fit max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-sm overflow-y-auto rounded-2xl border border-surface bg-background p-0 text-white shadow-lg shadow-black/20 backdrop:bg-transparent motion-safe:animate-[feedback-slide-in_250ms_ease-out]"
className="fixed right-4 left-4 top-20 bottom-auto my-0 mx-auto md:left-auto md:mx-0 h-fit max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-sm overflow-y-auto rounded-2xl border border-surface bg-background p-0 text-white shadow-lg shadow-black/20 backdrop:bg-transparent motion-safe:animate-[notification-slide-down_250ms_ease-out] md:motion-safe:animate-[feedback-slide-in_250ms_ease-out]"
>
<button
type="button"
Expand Down
18 changes: 3 additions & 15 deletions frontend/src/components/PcPartCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useEffect, useRef, useState } from "react";
import type { PcPart } from "@/types/types";
import { getPartSpecifications } from "@/api/parts";
import AddToBuildButton from "./buttons/AddToBuildButton";
import Button from "@/components/buttons/Button";
import PcPartCardLayout from "./PcPartCardLayout";

export default function PcPartCard({
Expand All @@ -18,7 +17,6 @@ export default function PcPartCard({
const [specifications, setSpecifications] = useState<
PcPart["details"] | null
>(null);
const [loadingDetails, setLoadingDetails] = useState(false);
const [detailError, setDetailError] = useState("");
const detailRequest = useRef<AbortController | null>(null);
const imageUrl = part.image_url?.trim() ?? "";
Expand All @@ -43,7 +41,6 @@ export default function PcPartCard({

const controller = new AbortController();
detailRequest.current = controller;
setLoadingDetails(true);
try {
const details = await getPartSpecifications(part.id, controller.signal);
if (controller.signal.aborted) return;
Expand All @@ -58,12 +55,12 @@ export default function PcPartCard({
} finally {
// Allow another click, including a retry if fetching failed.
detailRequest.current = null;
if (!controller.signal.aborted) setLoadingDetails(false);
}
}

return (
<PcPartCardLayout
onShowDetails={handleShowDetails}
image={
imageUrl && imageUrl !== failedImage ? (
<img
Expand All @@ -73,6 +70,7 @@ export default function PcPartCard({
draggable={false}
className="absolute inset-0 h-full w-full object-contain p-3"
onError={() => setFailedImage(imageUrl)}

/>
) : (
<p className="text-xs text-gray-500">Image unavailable</p>
Expand All @@ -86,21 +84,11 @@ export default function PcPartCard({
? "Price unavailable"
: `€ ${part.price_eur.toFixed(2)}`
}
detailsAction={
<Button
type="button"
className="truncate hover:bg-accent/20 hover:text-accent"
onClick={handleShowDetails}
disabled={loadingDetails}
>
{loadingDetails ? "Loading details..." : "Show details"}
</Button>
}
error={detailError && <p role="alert">{detailError}</p>}
buildAction={
<AddToBuildButton
part={completePart}
className="hover:bg-accent/20 hover:text-accent"
className="hover:bg-accent/20 hover:text-accent mt-auto ml-auto max-w-100"
/>
}
/>
Expand Down
62 changes: 49 additions & 13 deletions frontend/src/components/PcPartCardLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,86 @@
import type { ReactNode } from "react";

type PcPartCardLayoutProps = {
onShowDetails?: () => void;
image: ReactNode;
name: ReactNode;
nameTitle?: string;
category: ReactNode;
price: ReactNode;
detailsAction: ReactNode;
buildAction: ReactNode;
error?: ReactNode;
skeleton?: boolean;
};
//Layot for the PcPartCard and the PcPartCardSkeleton.
export default function PcPartCardLayout({
onShowDetails,
image,
name,
nameTitle,
category,
price,
detailsAction,
buildAction,
error,
skeleton = false,
}: PcPartCardLayoutProps) {
return (
<div
aria-hidden={skeleton ? true : undefined}
className="@container min-w-0"
className="@container min-w-0 cursor-pointer motion-safe:hover:scale-102 motion-safe:active:scale-98 motion-safe:transition motion-safe:duration-300"
onClick={() => {
if (!skeleton) onShowDetails?.();
}}
>
<article
<article tabIndex={skeleton ? undefined : 0}
className={`flex w-full min-w-0 flex-col overflow-hidden rounded-xl bg-surface text-white @md:flex-row ${skeleton ? "motion-safe:animate-pulse" : ""}`}
onKeyDown={(event) => {
if (skeleton || event.target !== event.currentTarget) return;
if (event.key === "Enter" || event.key === " "){
event.preventDefault();
onShowDetails?.()
}
}}
>
<div
className={`relative flex h-36 w-full items-center justify-center @md:h-auto @md:min-h-36 @md:w-44 @md:shrink-0 ${skeleton ? "bg-white/15" : "bg-white"}`}
>
{image}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-1 p-2">
<h2 className="min-h-5 truncate text-sm" title={nameTitle}>
{name}
</h2>
<p className="min-h-4 text-xs text-gray-200">{category}</p>
<p className="mb-1 min-h-7 text-xl">{price}</p>
<div className="mb-2">{detailsAction}</div>
{error}
{buildAction}

<div
className="grid min-w-0 flex-1 grid-cols-1 gap-2 p-2
@[500px]:grid-cols-[minmax(0,1fr)_auto]
@[500px]:grid-rows-[1fr_auto]
@[500px]:gap-3 @[500px]:p-3
@[800px]:grid-cols-[minmax(0,1fr)_auto_auto]
@[800px]:grid-rows-1
@[800px]:items-center @[800px]:gap-6 @[800px]:p-5"
>
<div className="min-w-0 @[500px]:col-span-2 @[800px]:col-span-1">
<h2
className="truncate text-sm @[500px]:text-lg"
title={nameTitle}
>
{name}
</h2>

<p className="text-xs text-gray-200 @[500px]:text-sm">
{category}
</p>

{error}
</div>

<p className="self-center whitespace-nowrap text-xl">
{price}
</p>

<div
className="min-w-0 self-center"
onClick={(event) => event.stopPropagation()}
>
{buildAction}
</div>
</div>
</article>
</div>
Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/PcPartCardSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export default function PcPartCardSkeleton() {
name={<span className="inline-block h-3 w-4/5 rounded bg-white/20" />}
category={<span className="inline-block h-2 w-1/3 rounded bg-white/10" />}
price={<span className="inline-block h-5 w-24 rounded bg-white/20" />}
detailsAction={actionPlaceholder}
buildAction={actionPlaceholder}
/>
);
Expand Down
9 changes: 5 additions & 4 deletions frontend/src/components/ViewSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { LayoutGrid, StretchHorizontal, Columns2 } from "lucide-react";
import { Grip, List } from "lucide-react";
import { DualListIcon } from "@/assets/dualListIcon";

const viewOptions = ["list-view", "2-grid-view", "4-grid-view"] as const;

export type ViewType = (typeof viewOptions)[number];

const viewIcons = {
"list-view": StretchHorizontal,
"2-grid-view": Columns2,
"4-grid-view": LayoutGrid,
"list-view": List,
"2-grid-view": DualListIcon,
"4-grid-view": Grip,
};

const viewLabels = {
Expand Down
Loading