From a6bbbf59f268949c386054d7e973593d3aacfb52 Mon Sep 17 00:00:00 2001
From: Kristian Gjertsen
Date: Mon, 9 Nov 2026 18:17:36 +0100
Subject: [PATCH 01/15] Renamed file NavBar to Header
---
frontend/src/components/Header.tsx | 142 +++++++++++++++++++++++++++++
1 file changed, 142 insertions(+)
create mode 100644 frontend/src/components/Header.tsx
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
new file mode 100644
index 0000000..012edd1
--- /dev/null
+++ b/frontend/src/components/Header.tsx
@@ -0,0 +1,142 @@
+import { useState } from "react";
+import { useBuild } from "@/context/BuildContext";
+
+type NavBarProps = {
+ search?: string;
+ setSearch?: (value: string) => void;
+};
+
+export default function NavBar({
+ search: externalSearch,
+ setSearch: setExternalSearch,
+}: NavBarProps) {
+ const [localSearch, setLocalSearch] = useState(
+ () => new URLSearchParams(window.location.search).get("search") ?? "",
+ );
+ const search = externalSearch ?? localSearch;
+ const setSearch = setExternalSearch ?? setLocalSearch;
+ const { parts } = useBuild();
+ const selectedCount = Object.values(parts).length;
+
+ const params = new URLSearchParams(window.location.search);
+ const currentPage = params.get("page") ?? "home";
+
+ return (
+
+ );
+}
From 5535290d82295b14181cfcd7eb39acdb62b56751 Mon Sep 17 00:00:00 2001
From: Kristian Gjertsen
Date: Mon, 9 Nov 2026 18:23:55 +0100
Subject: [PATCH 02/15] Implemented react-router for navigation instead og
query- parametre
---
frontend/package-lock.json | 45 +++++++-
frontend/package.json | 1 +
frontend/src/App.tsx | 38 +++----
frontend/src/components/Header.tsx | 164 ++++++++++-------------------
frontend/src/components/NavBar.tsx | 142 -------------------------
frontend/src/main.tsx | 11 +-
6 files changed, 126 insertions(+), 275 deletions(-)
delete mode 100644 frontend/src/components/NavBar.tsx
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b260a60..981b214 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -12,6 +12,7 @@
"lucide-react": "^1.64.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
+ "react-router": "^8.5.1",
"tailwind-merge": "^3.7.0",
"tailwindcss": "^4.3.3"
},
@@ -935,6 +936,12 @@
"url": "https://github.com/sponsors/oxc-project"
}
},
+ "node_modules/@remix-run/route-pattern": {
+ "version": "0.22.1",
+ "resolved": "https://registry.npmjs.org/@remix-run/route-pattern/-/route-pattern-0.22.1.tgz",
+ "integrity": "sha512-czdGJWh09Lapvcqt7Vb09KlrU2PhRJ8xQaI+AItTuD9aDJ5MAgxnS38lnys6Ll+6RJEqPiaUqTwIQhVLwFvv+w==",
+ "license": "MIT"
+ },
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.15",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz",
@@ -1892,7 +1899,7 @@
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.3.0"
@@ -2503,6 +2510,12 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/cookie-es": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz",
+ "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==",
+ "license": "MIT"
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4025,6 +4038,36 @@
"license": "MIT",
"peer": true
},
+ "node_modules/react-router": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.5.1.tgz",
+ "integrity": "sha512-5u1eQ46QjuXjNfEnLwzrnou1Bm5dquO3xjVYkkMYCudtkicD2AQHtx+fvXzPATtPJx8TW6DR1x1TDRqsfHyZBw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/route-pattern": "^0.22.1",
+ "cookie-es": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=22.22.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=19.2.0",
+ "@types/react-dom": ">=19.2.0",
+ "react": ">=19.2.7",
+ "react-dom": ">=19.2.7"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index ac94c40..2bca6c4 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -15,6 +15,7 @@
"lucide-react": "^1.64.0",
"react": "^19.2.8",
"react-dom": "^19.2.8",
+ "react-router": "^8.5.1",
"tailwind-merge": "^3.7.0",
"tailwindcss": "^4.3.3"
},
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 3e0db5d..e76f513 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -1,38 +1,34 @@
+import { useState } from "react";
import "./App.css";
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/NavBar";
+import NavBar from "./components/Header";
import Footer from "./components/Footer";
import BackToTopButton from "./components/BackToTopButton";
+import { Routes, Route } from "react-router";
function App() {
- const params = new URLSearchParams(window.location.search);
- const page = params.get("page");
- const category = params.get("category");
- const search = (params.get("search") ?? "").trim();
-
- let content = ;
-
- if (page === "builder") {
- content = ;
- } else if (page === "community") {
- content = ;
- } else if (category !== null) {
- content = ;
- } else if (page === "explore") {
- content = ;
- }
+ const [search, setSearch] = useState("");
return (
-
- {content}
-
+
+
+ } />
+ }
+ />
+ } />
+ } />
+ 404 - Page not found} />
+
+
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx
index 012edd1..15c7256 100644
--- a/frontend/src/components/Header.tsx
+++ b/frontend/src/components/Header.tsx
@@ -1,141 +1,89 @@
import { useState } from "react";
+import { Link, NavLink, useNavigate } from "react-router";
+import { Search, Computer } from "lucide-react";
import { useBuild } from "@/context/BuildContext";
+const navigation = [
+ { to: "/", label: "Home" },
+ { to: "/explore", label: "Explore components" },
+ { to: "/builder", label: "PC Builder" },
+ { to: "/community", label: "Community Page" },
+];
+
type NavBarProps = {
- search?: string;
- setSearch?: (value: string) => void;
+ onSearch: (value: string) => void;
};
-export default function NavBar({
- search: externalSearch,
- setSearch: setExternalSearch,
-}: NavBarProps) {
- const [localSearch, setLocalSearch] = useState(
- () => new URLSearchParams(window.location.search).get("search") ?? "",
- );
- const search = externalSearch ?? localSearch;
- const setSearch = setExternalSearch ?? setLocalSearch;
+export default function NavBar({ onSearch }: NavBarProps) {
+ const [search, setSearch] = useState("");
+ const navigate = useNavigate();
const { parts } = useBuild();
const selectedCount = Object.values(parts).length;
- const params = new URLSearchParams(window.location.search);
- const currentPage = params.get("page") ?? "home";
-
return (
-
} />
diff --git a/frontend/src/components/DesktopComponentDetailPage.tsx b/frontend/src/components/DesktopComponentDetailPage.tsx
index 3783266..534d8e3 100644
--- a/frontend/src/components/DesktopComponentDetailPage.tsx
+++ b/frontend/src/components/DesktopComponentDetailPage.tsx
@@ -1,10 +1,11 @@
import { useBuild } from '@/context/BuildContext'
import { Trash2, X } from 'lucide-react'
import { useId, useState } from 'react'
+import { Link } from 'react-router'
import type { PcPart } from '@/types/types'
import { categories } from '@/types/categories'
import { formatSpecification, getSpecifications } from '@/utils/partSpecifications'
-import AddToBuildButton from './AddToBuildButton'
+import AddToBuildButton from './buttons/AddToBuildButton'
export default function DesktopComponentDetailPage({ part }: { part: PcPart }) {
const [expanded, setExpanded] = useState(false)
@@ -23,7 +24,7 @@ export default function DesktopComponentDetailPage({ part }: { part: PcPart }) {
-
- Home
+ event.currentTarget.closest("dialog")?.close()} className="inline-flex min-h-11 items-center px-1 text-gray-400 hover:text-white">Home
- ›
-
@@ -33,9 +34,9 @@ export default function DesktopComponentDetailPage({ part }: { part: PcPart }) {
- /
-
-
+ event.currentTarget.closest("dialog")?.close()} className="inline-flex min-h-11 items-center px-1 hover:text-white">
{category}
-
+
- /
- {part.name}
diff --git a/frontend/src/components/FilterComponent.tsx b/frontend/src/components/FilterComponent.tsx
index 6cbb75e..7cc02af 100644
--- a/frontend/src/components/FilterComponent.tsx
+++ b/frontend/src/components/FilterComponent.tsx
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import type { PartsOptions } from "@/api/parts";
import { categories } from "@/types/categories";
-import Button from "./Button";
+import Button from "./buttons/Button";
// Sort and both filters sit inline only when there is room next to the search field.
const DESKTOP_FILTER_WIDTH = 1200;
diff --git a/frontend/src/components/MobileComponentDetailPage.tsx b/frontend/src/components/MobileComponentDetailPage.tsx
index 6aadf5c..1cbe16a 100644
--- a/frontend/src/components/MobileComponentDetailPage.tsx
+++ b/frontend/src/components/MobileComponentDetailPage.tsx
@@ -5,7 +5,7 @@ import {
formatSpecification,
getSpecifications,
} from "@/utils/partSpecifications";
-import AddToBuildButton from "./AddToBuildButton";
+import AddToBuildButton from "./buttons/AddToBuildButton";
export default function ComponentDetailPage({ part }: { part: PcPart }) {
const imageUrl = part.image_url?.trim() ?? "";
diff --git a/frontend/src/components/PcPartCard.tsx b/frontend/src/components/PcPartCard.tsx
index 1bada4c..fd9837a 100644
--- a/frontend/src/components/PcPartCard.tsx
+++ b/frontend/src/components/PcPartCard.tsx
@@ -2,8 +2,8 @@ import { useState, useRef, useEffect } from "react";
import type { PcPart } from "@/types/types";
import MobileComponentDetailPage from "./MobileComponentDetailPage";
import DesktopComponentDetailPage from "./DesktopComponentDetailPage";
-import AddToBuildButton from "./AddToBuildButton";
-import Button from "@/components/Button";
+import AddToBuildButton from "./buttons/AddToBuildButton";
+import Button from "@/components/buttons/Button";
export default function PcPartCard({ part }: { part: PcPart }) {
const [failedImage, setFailedImage] = useState("");
diff --git a/frontend/src/components/AddToBuildButton.tsx b/frontend/src/components/buttons/AddToBuildButton.tsx
similarity index 100%
rename from frontend/src/components/AddToBuildButton.tsx
rename to frontend/src/components/buttons/AddToBuildButton.tsx
diff --git a/frontend/src/components/BackToTopButton.tsx b/frontend/src/components/buttons/BackToTopButton.tsx
similarity index 100%
rename from frontend/src/components/BackToTopButton.tsx
rename to frontend/src/components/buttons/BackToTopButton.tsx
diff --git a/frontend/src/components/Button.tsx b/frontend/src/components/buttons/Button.tsx
similarity index 100%
rename from frontend/src/components/Button.tsx
rename to frontend/src/components/buttons/Button.tsx
diff --git a/frontend/src/components/Footer.tsx b/frontend/src/components/pageSections/Footer.tsx
similarity index 100%
rename from frontend/src/components/Footer.tsx
rename to frontend/src/components/pageSections/Footer.tsx
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/pageSections/Header.tsx
similarity index 100%
rename from frontend/src/components/Header.tsx
rename to frontend/src/components/pageSections/Header.tsx
diff --git a/frontend/src/pages/categoryPage/categoryPage.tsx b/frontend/src/pages/categoryPage/categoryPage.tsx
index 1167f15..11d417d 100644
--- a/frontend/src/pages/categoryPage/categoryPage.tsx
+++ b/frontend/src/pages/categoryPage/categoryPage.tsx
@@ -2,6 +2,7 @@ import AllPartsPreview from "../../components/AllPartsPreview";
import { categories } from "@/types/categories";
import FilterComponent from "@/components/FilterComponent";
import { useState } from "react";
+import { Link } from "react-router";
import { ArrowLeft } from "lucide-react";
import type { PartsOptions } from "@/api/parts";
@@ -26,13 +27,13 @@ export default function CategoryPage({
return (
-
Back to home
-
+
{invalidCategory
diff --git a/frontend/src/pages/communityPage/communityPage.tsx b/frontend/src/pages/communityPage/communityPage.tsx
index f7a5a18..6888197 100644
--- a/frontend/src/pages/communityPage/communityPage.tsx
+++ b/frontend/src/pages/communityPage/communityPage.tsx
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import { getBuilds, type SavedBuild } from '@/api/builds'
-import Button from '@/components/Button'
+import Button from '@/components/buttons/Button'
import BuildParts from './components/BuildParts'
export default function CommunityPage() {
diff --git a/frontend/src/pages/homePage/components/BuildPCGraphic.tsx b/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
index 0501d4d..c1f8d7b 100644
--- a/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
+++ b/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
@@ -1,4 +1,4 @@
-import Button from "@/components/Button";
+import Button from "@/components/buttons/Button";
import heroImage from "@/assets/hero_image.webp";
export default function BuildPCGraphic() {
diff --git a/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx b/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx
index bbd2b1c..1960d3c 100644
--- a/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx
+++ b/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx
@@ -1,5 +1,5 @@
import { useRef } from "react";
-import Button from "@/components/Button";
+import Button from "@/components/buttons/Button";
import { useBuild } from "@/context/BuildContext";
export default function ClearBuild() {
diff --git a/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx b/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx
index fd1641f..fe01d1f 100644
--- a/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx
+++ b/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx
@@ -5,7 +5,7 @@ import {
getSpecifications,
} from "@/utils/partSpecifications";
import { useBuild } from "@/context/BuildContext";
-import Button from "@/components/Button";
+import Button from "@/components/buttons/Button";
import { ArrowLeftRight, Plus, Trash2 } from "lucide-react";
type ComponentSelectionComponentProps = {
diff --git a/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx b/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx
index 5d36536..8c8bb62 100644
--- a/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx
+++ b/frontend/src/pages/pcBuilderPage/components/PublishBuild.tsx
@@ -1,7 +1,7 @@
import { useRef, useState } from 'react'
import { useBuild } from '@/context/BuildContext'
import { publishBuild } from '@/api/builds'
-import Button from '@/components/Button'
+import Button from '@/components/buttons/Button'
export default function PublishBuild() {
const { parts } = useBuild()
From 3b973eb235373ee8156fbd5b41d979020a69d586 Mon Sep 17 00:00:00 2001
From: Kristian Gjertsen
Date: Mon, 9 Nov 2026 18:36:08 +0100
Subject: [PATCH 05/15] Refactor navigation to use react-router in multiple
components, replacing query parameters with Link and useNavigate for improved
routing
---
.../components/buttons/AddToBuildButton.tsx | 7 +++---
frontend/src/main.tsx | 1 -
.../homePage/components/BuildPCGraphic.tsx | 10 ++++----
.../homePage/components/CategorySelection.tsx | 13 +++++-----
frontend/src/pages/homePage/homePage.tsx | 7 +++---
.../ComponentSelectionComponent.tsx | 24 ++++++++++---------
.../pcBuilderPage/components/PublishBuild.tsx | 3 ++-
7 files changed, 36 insertions(+), 29 deletions(-)
diff --git a/frontend/src/components/buttons/AddToBuildButton.tsx b/frontend/src/components/buttons/AddToBuildButton.tsx
index fef6868..7d1ec93 100644
--- a/frontend/src/components/buttons/AddToBuildButton.tsx
+++ b/frontend/src/components/buttons/AddToBuildButton.tsx
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import type { PcPart } from "@/types/types";
import { useBuild } from "@/context/BuildContext";
-import { flushSync } from "react-dom";
+import { useNavigate } from "react-router";
import { twMerge } from "tailwind-merge";
import Button from "./Button";
@@ -16,6 +16,7 @@ export default function AddToBuildButton({
children,
className,
}: AddToBuildButtonProps) {
+ const navigate = useNavigate();
const { parts, addPart, removePart } = useBuild();
const inBuild = parts[part.category]?.id === part.id;
@@ -31,8 +32,8 @@ export default function AddToBuildButton({
removePart(part.category);
return;
}
- flushSync(() => addPart(part)); //add the part to the build
- window.location.href = `?page=builder&highlight=${encodeURIComponent(part.category)}`;
+ addPart(part);
+ navigate("/builder", { state: { highlight: part.category } });
}}
>
{children ?? (inBuild ? "Remove from build" : "Add to build")}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index f6bba9a..5b1dbfe 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -13,6 +13,5 @@ createRoot(document.getElementById("root")!).render(
-
,
);
diff --git a/frontend/src/pages/homePage/components/BuildPCGraphic.tsx b/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
index c1f8d7b..add805e 100644
--- a/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
+++ b/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
@@ -1,7 +1,9 @@
+import { Link, useNavigate } from "react-router";
import Button from "@/components/buttons/Button";
import heroImage from "@/assets/hero_image.webp";
export default function BuildPCGraphic() {
+ const navigate = useNavigate();
return (
{
- window.location.href = "?page=builder";
+ navigate("/builder");
}}
>
→ Start building
-
Explore components
-
+
);
diff --git a/frontend/src/pages/homePage/components/CategorySelection.tsx b/frontend/src/pages/homePage/components/CategorySelection.tsx
index 536f6db..d6dae1f 100644
--- a/frontend/src/pages/homePage/components/CategorySelection.tsx
+++ b/frontend/src/pages/homePage/components/CategorySelection.tsx
@@ -1,3 +1,4 @@
+import { Link } from "react-router";
import { categories } from "@/types/categories";
export default function CategorySelection() {
@@ -13,23 +14,23 @@ export default function CategorySelection() {
-
View all components →
-
+
diff --git a/frontend/src/pages/homePage/homePage.tsx b/frontend/src/pages/homePage/homePage.tsx
index c3458a8..ca1d905 100644
--- a/frontend/src/pages/homePage/homePage.tsx
+++ b/frontend/src/pages/homePage/homePage.tsx
@@ -1,3 +1,4 @@
+import { Link } from "react-router";
import { useState } from "react";
import AllPartsPreview from "../../components/AllPartsPreview";
import type { PartsOptions } from "@/api/parts";
@@ -24,12 +25,12 @@ export default function HomePage() {
-
Browse processors →
-
+
diff --git a/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx b/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx
index fe01d1f..dd4f2c5 100644
--- a/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx
+++ b/frontend/src/pages/pcBuilderPage/components/ComponentSelectionComponent.tsx
@@ -1,3 +1,4 @@
+import { useLocation, useNavigate } from "react-router";
import type { PcPart } from "@/types/types";
import { useEffect, useRef, useState } from "react";
import {
@@ -19,22 +20,17 @@ export default function ComponentSelectionComponent({
category,
categoryValue,
}: ComponentSelectionComponentProps) {
+ const navigate = useNavigate();
+ const location = useLocation();
const { removePart } = useBuild();
const [failedImage, setFailedImage] = useState("");
const imageUrl = part?.image_url?.trim() ?? "";
const sectionRef = useRef(null);
- const [highlighted] = useState(
- () =>
- new URLSearchParams(window.location.search).get("highlight") ===
- categoryValue,
- );
+ const highlighted = location.state?.highlight === categoryValue;
- // Move screen to the center of the new component added
+ // Move screen to the center of the new component added.
useEffect(() => {
if (!highlighted) return;
- const url = new URL(window.location.href);
- url.searchParams.delete("highlight");
- window.history.replaceState(window.history.state, "", url);
const frame = requestAnimationFrame(() => {
sectionRef.current?.scrollIntoView({
behavior: "smooth",
@@ -47,6 +43,12 @@ export default function ComponentSelectionComponent({
return (
{
+ if (event.target !== event.currentTarget || !highlighted) return;
+ const state = { ...location.state };
+ delete state.highlight;
+ navigate(location.pathname, { replace: true, state });
+ }}
className={`scroll-mt-24 flex min-h-30 w-full flex-col sm:flex-row items-stretch sm:items-center sm:justify-between gap-3 sm:gap-6 px-3 py-3 sm:py-1 border-b border-gray-600 text-sm
${part ? "bg-[#303B3F]" : "bg-background"}
${highlighted ? "animate-[build-added_2.2s]" : ""}`}
@@ -87,7 +89,7 @@ export default function ComponentSelectionComponent({