diff --git a/backend/db/schema.sql b/backend/db/schema.sql
index 65bb812..c7d5284 100644
--- a/backend/db/schema.sql
+++ b/backend/db/schema.sql
@@ -66,6 +66,8 @@ CREATE TABLE IF NOT EXISTS builds (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
tittel varchar(50) NOT NULL CHECK (length(trim(tittel)) > 0),
created_at timestamptz NOT NULL DEFAULT now(),
+ owner_id bigint REFERENCES users(id),
+ is_published boolean NOT NULL DEFAULT false,
comment varchar(200),
cpu_id uuid REFERENCES parts(id),
@@ -78,4 +80,4 @@ CREATE TABLE IF NOT EXISTS builds (
cooler_id uuid REFERENCES parts(id)
);
CREATE INDEX IF NOT EXISTS builds_owner_id_idx
- ON builds(owner_id);
\ No newline at end of file
+ ON builds(owner_id);
diff --git a/backend/server/package.json b/backend/server/package.json
index ea01ffc..680afa5 100644
--- a/backend/server/package.json
+++ b/backend/server/package.json
@@ -44,5 +44,12 @@
},
"overrides": {
"minimatch": "^11.0.1"
+ },
+ "allowScripts": {
+ "@apollo/protobufjs@1.2.8": true,
+ "@firebase/util@1.15.3": true,
+ "esbuild@0.28.2": true,
+ "protobufjs@7.6.6": true,
+ "fsevents@2.3.3": true
}
}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 016b702..09cba57 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -6,6 +6,7 @@ import CommunityPage from "./pages/communityPage/communityPage";
import Layout from "./components/Layout";
import LoginPage from "./pages/loginPage/loginPage";
import AccountPage from "./pages/accountPage/accountPage";
+import NotFoundPage from "./pages/notFoundPage/notFoundPage";
import { Routes, Route, useLocation, useParams } from "react-router";
import { useSelector } from "react-redux";
import type { RootState } from "./state/store";
@@ -34,10 +35,7 @@ function App() {
} />
} />
} />
- 404 - Page not found
}
- />
+ } />
);
diff --git a/frontend/src/components/AddToBuildFeedback.tsx b/frontend/src/components/AddToBuildFeedback.tsx
index 058ef52..caa9078 100644
--- a/frontend/src/components/AddToBuildFeedback.tsx
+++ b/frontend/src/components/AddToBuildFeedback.tsx
@@ -1,3 +1,4 @@
+import Button from "./buttons/Button";
import type { PcPart } from "@/types/types";
import { useEffect, useId, useRef } from "react";
import { ArrowRight, X } from "lucide-react";
@@ -23,10 +24,6 @@ export default function AddToBuildFeedback({
const savedPart = useSelector((state) =>
selectPartByCategory(state, part.category),
);
- const secondaryButton =
- "cursor-pointer rounded-lg border border-secondary/50 px-4 py-2 text-xs font-medium text-white transition-colors hover:bg-surface-dark";
- const primaryButton =
- "cursor-pointer rounded-lg border border-accent bg-accent px-5 py-2 text-xs font-semibold text-background transition-colors hover:bg-accent/90";
useEffect(() => {
dialogRef.current?.showModal();
@@ -125,16 +122,19 @@ export default function AddToBuildFeedback({
)}
{stage === "confirm" && (
-
-
+
+
)}
diff --git a/frontend/src/components/AllPartsPreview.tsx b/frontend/src/components/AllPartsPreview.tsx
index c87bac5..c9d7ca0 100644
--- a/frontend/src/components/AllPartsPreview.tsx
+++ b/frontend/src/components/AllPartsPreview.tsx
@@ -1,3 +1,4 @@
+import Button from "./buttons/Button";
import { useParts } from "@/hooks/useParts";
import PcPartCard from "@/components/PcPartCard";
import PcPartCardSkeleton from "@/components/PcPartCardSkeleton";
@@ -179,7 +180,6 @@ function PartsResults({
return (
-
{!featured && (
@@ -187,7 +187,7 @@ function PartsResults({
)}
- {viewenabled && (
+ {viewenabled && (
)}
@@ -271,9 +271,9 @@ function PartsResults({
)}
{(!featured || error) && (
-
+
)}
);
diff --git a/frontend/src/components/AuthButton.tsx b/frontend/src/components/AuthButton.tsx
index cb0da12..c3f483e 100644
--- a/frontend/src/components/AuthButton.tsx
+++ b/frontend/src/components/AuthButton.tsx
@@ -9,9 +9,9 @@ export default function AuthButton() {
return (
-
+
{user ? "My account" : "Log in"}
);
diff --git a/frontend/src/components/DesktopComponentDetailPage.tsx b/frontend/src/components/DesktopComponentDetailPage.tsx
index 716032a..bcb43a1 100644
--- a/frontend/src/components/DesktopComponentDetailPage.tsx
+++ b/frontend/src/components/DesktopComponentDetailPage.tsx
@@ -1,4 +1,5 @@
-import { Trash2, X } from "lucide-react";
+import TextButton from "@/components/buttons/TextButton";
+import { X } from "lucide-react";
import { useId, useState } from "react";
import { Link } from "react-router";
import type { PcPart } from "@/types/types";
@@ -8,14 +9,11 @@ import {
getSpecifications,
} from "@/utils/partSpecifications";
import AddToBuildButton from "./buttons/AddToBuildButton";
-import { useSelector } from "react-redux";
-import { selectIsPartInBuild } from "@/state/selectedPartsSlice";
export default function DesktopComponentDetailPage({ part }: { part: PcPart }) {
const [expanded, setExpanded] = useState(false);
const [failedImage, setFailedImage] = useState("");
const specificationsId = useId();
- const inBuild = useSelector((state) => selectIsPartInBuild(state, part));
const specifications = getSpecifications(part);
const category =
categories.find((item) => item.value === part.category)?.label ??
@@ -47,12 +45,12 @@ export default function DesktopComponentDetailPage({ part }: { part: PcPart }) {
/
@@ -130,19 +128,7 @@ export default function DesktopComponentDetailPage({ part }: { part: PcPart }) {
? "Price unavailable"
: `€${part.price_eur.toFixed(2)}`}
-
- {inBuild ? (
-
- ) : (
-
- +
-
- )}
- {inBuild ? "Remove from build" : "Add to build"}
-
+
@@ -198,12 +184,12 @@ export default function DesktopComponentDetailPage({ part }: { part: PcPart }) {
{specifications.length > 4 && (
-
+
)}
diff --git a/frontend/src/components/FilterComponent.tsx b/frontend/src/components/FilterComponent.tsx
index 6f6ac83..2465fcc 100644
--- a/frontend/src/components/FilterComponent.tsx
+++ b/frontend/src/components/FilterComponent.tsx
@@ -1,3 +1,4 @@
+import TextButton from "@/components/buttons/TextButton";
import { useEffect, useRef, useState } from "react";
import type { Dispatch, SetStateAction } from "react";
import type { PartsOptions } from "@/api/parts";
@@ -104,14 +105,13 @@ export default function FilterComponent({
-
+
>
) : (
@@ -203,14 +203,14 @@ export default function FilterComponent({
-
+
{action && (
-
{action.label}
-
+
)}
}
/>
diff --git a/frontend/src/components/buttons/AddToBuildButton.tsx b/frontend/src/components/buttons/AddToBuildButton.tsx
index c4f1e18..8177964 100644
--- a/frontend/src/components/buttons/AddToBuildButton.tsx
+++ b/frontend/src/components/buttons/AddToBuildButton.tsx
@@ -1,4 +1,4 @@
-import type { ReactNode } from "react";
+import { Trash2 } from "lucide-react";
import { useRef, useState } from "react";
import type { PcPart } from "@/types/types";
import { twMerge } from "tailwind-merge";
@@ -17,13 +17,13 @@ import { showNotification } from "@/state/notificationSlice";
type AddToBuildButtonProps = {
part: PcPart;
- children?: ReactNode;
+ showIcon?: boolean;
className?: string;
};
export default function AddToBuildButton({
part,
- children,
+ showIcon = false,
className,
}: AddToBuildButtonProps) {
const [showFeedback, setShowFeedback] = useState(false);
@@ -95,14 +95,22 @@ export default function AddToBuildButton({
return (
<>
- {children ?? (inBuild ? "Remove from build" : "Add to build")}
+ {showIcon &&
+ (inBuild ? (
+
+ ) : (
+
+ +
+
+ ))}
+ {inBuild ? "Remove from build" : "Add to build"}
{showFeedback && (stage !== "confirm" || !isCategoryEmpty) && (
diff --git a/frontend/src/components/buttons/BackToTopButton.tsx b/frontend/src/components/buttons/BackToTopButton.tsx
index a515e84..5d88680 100644
--- a/frontend/src/components/buttons/BackToTopButton.tsx
+++ b/frontend/src/components/buttons/BackToTopButton.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { ArrowUp } from "lucide-react";
+import Button from "./Button";
// Show the button once the user has scrolled about one screen down.
const SHOW_AFTER_PX = 600;
@@ -15,9 +16,8 @@ export default function BackToTopButton() {
}, []);
return (
- {
window.scrollTo({
top: 0,
@@ -34,6 +34,6 @@ export default function BackToTopButton() {
>
Back to top
-
+
);
}
diff --git a/frontend/src/components/buttons/Button.tsx b/frontend/src/components/buttons/Button.tsx
index fb27531..1108ceb 100644
--- a/frontend/src/components/buttons/Button.tsx
+++ b/frontend/src/components/buttons/Button.tsx
@@ -1,35 +1,71 @@
-import type { ReactNode } from "react";
+import type { ComponentPropsWithRef, ReactNode } from "react";
+import { Link, type LinkProps } from "react-router";
import { twMerge } from "tailwind-merge";
-//Button element with style, and possible to asign specific style when using it
-type ButtonProps = {
+const variantStyles = {
+ text: "w-auto rounded border-0 bg-transparent p-0 font-normal text-accent underline decoration-1 underline-offset-4 transition-colors enabled:hover:text-white",
+ danger: "border-red-600 bg-red-600 text-white hover:border-red-300",
+ default: "border-orange-400 bg-orange-400 text-black hover:border-black",
+ dark: "border-black bg-black text-orange-400 hover:border-orange-400",
+ secondary:
+ "rounded-2xl border border-[#687274] bg-[#252d30] px-8 py-4 text-lg font-semibold text-white hover:border-white",
+};
+
+type SharedProps = {
children: ReactNode;
className?: string;
- onClick?: () => void;
- type?: "button" | "submit";
- disabled?: boolean;
+ variant?: keyof typeof variantStyles;
};
+type ButtonProps = SharedProps &
+ (
+ | (Omit, keyof SharedProps> & {
+ to?: never;
+ })
+ | (Omit & { disabled?: never })
+ );
+
export default function Button({
children,
- className = "",
- onClick,
- type = "button",
- disabled = false,
+ className,
+ variant = "default",
+ ...props
}: ButtonProps) {
+ const baseStyles = twMerge(
+ "w-full rounded-lg border-2 transition-[border-color] duration-200 px-2 py-1.5 text-sm focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent disabled:opacity-50",
+ variantStyles[variant],
+ );
+
+ if (props.to !== undefined) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ const { onClick, type = "button", disabled = false, ...buttonProps } = props;
const inactive = disabled || (!onClick && type !== "submit");
return (
{
event.stopPropagation();
- onClick?.();
+ onClick?.(event);
}}
className={twMerge(
- "w-full rounded-lg bg-accent px-2 py-1.5 text-sm text-black",
+ baseStyles,
disabled
? "cursor-not-allowed"
: inactive
diff --git a/frontend/src/components/buttons/TextButton.tsx b/frontend/src/components/buttons/TextButton.tsx
new file mode 100644
index 0000000..f667d58
--- /dev/null
+++ b/frontend/src/components/buttons/TextButton.tsx
@@ -0,0 +1,12 @@
+import type { ComponentPropsWithRef } from "react";
+import Button from "./Button";
+
+type TextButtonProps = ComponentPropsWithRef<"button">;
+
+export default function TextButton(props: TextButtonProps) {
+ return (
+
+ {props.children}
+
+ );
+}
diff --git a/frontend/src/components/pageSections/Header.tsx b/frontend/src/components/pageSections/Header.tsx
index 22aa0cb..6906fd2 100644
--- a/frontend/src/components/pageSections/Header.tsx
+++ b/frontend/src/components/pageSections/Header.tsx
@@ -73,7 +73,7 @@ export default function NavBar() {
setMenuOpen(false)}
- className="ml-auto flex shrink-0 items-center gap-2"
+ className="ml-auto inline-flex min-h-11 shrink-0 self-center items-center gap-2 rounded-lg text-sm font-normal text-white transition-colors duration-200 hover:text-accent focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent"
>
{logoutError && (
@@ -148,14 +149,14 @@ function AccountContent() {
- resetBuild()}
- className={`${buttonStyle} bg-accent text-black hover:bg-orange-300 motion-safe:hover:scale-[1.02]`}
+ className="h-11 w-auto gap-2 px-4 py-2 text-sm font-medium focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent"
>
Create a build
-
+
{notice && (
@@ -179,17 +180,17 @@ function AccountContent() {
{error}
- {
setLoading(true);
setError("");
setRetry((value) => value + 1);
}}
- className={`${buttonStyle} mt-4 bg-accent text-black hover:bg-orange-300`}
+ className="mt-4 h-11 w-auto px-4 py-2"
>
Try again
-
+
)}
@@ -265,22 +266,24 @@ function AccountContent() {
{offset > 0 && (
- changePage(offset - 20)}
- className={`${buttonStyle} border border-gray-500 hover:bg-surface`}
+ variant="secondary"
+ className="h-11 w-auto rounded-lg px-4 py-2 text-sm font-medium"
>
Previous
-
+
)}
{builds.length > 20 && (
- changePage(offset + 20)}
- className={`${buttonStyle} ml-auto border border-gray-500 hover:bg-surface`}
+ variant="secondary"
+ className="ml-auto h-11 w-auto rounded-lg px-4 py-2 text-sm font-medium"
>
Next
-
+
)}
>
diff --git a/frontend/src/pages/accountPage/components/DeleteBuildButton.tsx b/frontend/src/pages/accountPage/components/DeleteBuildButton.tsx
index 9092622..37975c8 100644
--- a/frontend/src/pages/accountPage/components/DeleteBuildButton.tsx
+++ b/frontend/src/pages/accountPage/components/DeleteBuildButton.tsx
@@ -1,3 +1,4 @@
+import Button from "@/components/buttons/Button";
import { useId, useRef, useState } from "react";
import { LoaderCircle, Trash2 } from "lucide-react";
import { deleteBuild } from "@/api/builds";
@@ -39,7 +40,7 @@ export default function DeleteBuildButton({ id, title, onDeleted }: Props) {
return (
<>
- {
@@ -47,11 +48,12 @@ export default function DeleteBuildButton({ id, title, onDeleted }: Props) {
dialogRef.current?.showModal();
cancelRef.current?.focus();
}}
- className="relative z-10 inline-flex cursor-pointer items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-200 transition-colors hover:bg-red-950/40 hover:text-red-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
+ variant="danger"
+ className="relative z-10 inline-flex h-11 w-auto items-center justify-center gap-2 px-3 py-2"
>
Delete
-
+
>
diff --git a/frontend/src/pages/homePage/components/BuildPCGraphic.tsx b/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
index add805e..b8f350e 100644
--- a/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
+++ b/frontend/src/pages/homePage/components/BuildPCGraphic.tsx
@@ -1,9 +1,7 @@
-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 (
-
+
{
- navigate("/builder");
- }}
+ className="h-11 px-4 py-2 text-sm font-medium lg:w-auto"
+ to="/builder"
>
- → Start building
+ Start building
-
Explore components
-
+
);
diff --git a/frontend/src/pages/loginPage/loginPage.tsx b/frontend/src/pages/loginPage/loginPage.tsx
index 6363c1f..a7856cb 100644
--- a/frontend/src/pages/loginPage/loginPage.tsx
+++ b/frontend/src/pages/loginPage/loginPage.tsx
@@ -1,3 +1,5 @@
+import TextButton from "@/components/buttons/TextButton";
+import Button from "@/components/buttons/Button";
import { Link, useNavigate } from "react-router";
import { useState } from "react";
import { FirebaseError } from "firebase/app";
@@ -88,7 +90,10 @@ export default function LoginPage() {
return (
-
+
{registering ? "Create account" : "Welcome back"}
@@ -135,10 +140,10 @@ export default function LoginPage() {
/>
-
{pending && (
+
@@ -194,7 +199,7 @@ export default function LoginPage() {
{registering ? "Already have an account? " : "Need an account? "}
- {
@@ -202,10 +207,9 @@ export default function LoginPage() {
setPassword("");
setError("");
}}
- className="cursor-pointer rounded font-medium text-accent underline-offset-4 transition-colors hover:text-white hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-accent disabled:cursor-wait disabled:opacity-50"
>
{registering ? "Sign in" : "Register"}
-
+
{(pending || googlePending) && (
diff --git a/frontend/src/pages/notFoundPage/notFoundPage.tsx b/frontend/src/pages/notFoundPage/notFoundPage.tsx
new file mode 100644
index 0000000..eb17008
--- /dev/null
+++ b/frontend/src/pages/notFoundPage/notFoundPage.tsx
@@ -0,0 +1,13 @@
+import Button from "@/components/buttons/Button";
+
+export default function NotFoundPage() {
+ return (
+
+ 404
+ Page not found.
+
+ Back to home
+
+
+ );
+}
diff --git a/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx b/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx
index 89df7e4..f7c1830 100644
--- a/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx
+++ b/frontend/src/pages/pcBuilderPage/components/ClearBuild.tsx
@@ -1,3 +1,4 @@
+import TextButton from "@/components/buttons/TextButton";
import { useRef } from "react";
import Button from "@/components/buttons/Button";
import { resetBuild } from "@/state/selectedPartsSlice";
@@ -12,16 +13,16 @@ export default function ClearBuild() {
//Small popup component that ask if user wants to delete build
return (
<>
-
{
if (Object.keys(parts).length === 0) return;
dialogRef.current?.showModal();
}}
>
Clear build
-
+