Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
20a48ff
refactor: extract delayed loading function to seperate hook
emmafre Nov 9, 2026
0c414ad
refactor: extract element visibility to a seperate hook
emmafre Nov 9, 2026
a1fa227
refactor: extract handlers
emmafre Nov 9, 2026
0d65fc9
refactor: extract scroll restoration
emmafre Nov 9, 2026
2528d31
docs: add comments explaining the central function
emmafre Nov 10, 2026
1ad0b3f
refactor: rearrange and comment code
emmafre Nov 10, 2026
70687b7
refactor: extract successMessage on gallery page
emmafre Nov 10, 2026
fb2326b
refactor: extract gallery name functions to a seperate hook
emmafre Nov 10, 2026
1831769
refactor: rename HomePage.tsx to ArtCataloguePage
emmafre Nov 10, 2026
a52a6e3
refactor: move gallery hooks to new hook folder
emmafre Nov 10, 2026
b8dbcce
refactor: update import paths
emmafre Nov 10, 2026
ee6c716
fix: update tailwind to newer versions
emmafre Nov 10, 2026
f3f57ce
refactor: extract gallery header to a seperate component
emmafre Nov 10, 2026
7441a72
refactor: move gallery components to new gallery folder
emmafre Nov 10, 2026
63a0ac8
refactor: add AddToGalleryModal.tsx to gallery folder
emmafre Nov 10, 2026
a66ea02
refactor: update paths
emmafre Nov 10, 2026
eb19c3f
refactor: rearrange and comment GallertPage.tsx
emmafre Nov 10, 2026
4991588
refactor: extract ArtworkDetails
emmafre Nov 10, 2026
064aa9a
refactor: move artwork related files to designated folder
emmafre Nov 10, 2026
5828473
refactor: update path
emmafre Nov 10, 2026
70c9006
refactor: extract Register functions to seperate hook
emmafre Nov 10, 2026
7b62078
refactor: extract login functions
emmafre Nov 10, 2026
da0cfa3
refactor: extract image functions from ProfilePage
emmafre Nov 10, 2026
4a2d760
refactor: extract gallery functions to seperate hook
emmafre Nov 10, 2026
bbe1fe0
refactor: extract profile header
emmafre Nov 10, 2026
e9d8633
refactor: extract successmessage and move files
emmafre Nov 10, 2026
2e9812b
refactor: remove unused file
emmafre Nov 10, 2026
4b98d5b
fix: update imports and tests
emmafre Nov 10, 2026
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
8 changes: 5 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Routes, Route, useLocation } from "react-router-dom";
import "./index.css";
import HomePage from "./pages/HomePage";
import ArtCataloguePage from "./pages/ArtCataloguePage";
import Login from "./pages/LoginPage";
import CreateAccount from "./pages/CreateAccountPage";
import ProfilePage from "./pages/ProfilePage";
Expand All @@ -17,10 +17,12 @@ function App() {
const showFooter = pathname !== "/login" && pathname !== "/create-account";

return (
<div className={`flex min-h-dvh flex-col transition-colors duration-300 ${isDarkMode ? "dark bg-[#171615] text-[#f3f0ea] [color-scheme:dark]" : "bg-white text-[#292624] [color-scheme:light]"}`}>
<div
className={`flex min-h-dvh flex-col transition-colors duration-300 ${isDarkMode ? "dark bg-[#171615] text-[#f3f0ea] scheme-dark" : "bg-white text-[#292624] scheme-light"}`}
>
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/" element={<ArtCataloguePage />} />
<Route path="/login" element={<Login />} />
<Route path="/create-account" element={<CreateAccount />} />
<Route
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState } from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Plus } from "lucide-react";
import type { Artwork } from "../types/Artwork";
import { useAuthStore } from "../store/authStore";
import AddToGalleryModal from "./AddToGalleryModal";
import type { Artwork } from "../../types/Artwork";
import { useAuthStore } from "../../store/authStore";
import AddToGalleryModal from "../gallery/AddToGalleryModal";
import "./ArtworkCard.css";

type ArtworkCardProps = {
Expand All @@ -15,7 +15,11 @@ function ArtworkImage({ src, alt }: { src: string; alt: string }) {
const [hasError, setHasError] = useState(false);

if (hasError) {
return <div className="artwork-card__placeholder dark:bg-[#302e2a] dark:text-gray-300">Image unavailable</div>;
return (
<div className="artwork-card__placeholder dark:bg-[#302e2a] dark:text-gray-300">
Image unavailable
</div>
);
}

return (
Expand All @@ -31,21 +35,28 @@ function ArtworkImage({ src, alt }: { src: string; alt: string }) {
);
}

export default function ArtworkCard({ artwork, showGalleryAction = true }: ArtworkCardProps) {
export default function ArtworkCard({
artwork,
showGalleryAction = true,
}: ArtworkCardProps) {
const artist = artwork.artist_display?.trim() || "Unknown artist";
const location = useLocation();
const navigate = useNavigate();
const isLoggedIn = useAuthStore((state) => Boolean(state.token));
const [showGalleryModal, setShowGalleryModal] = useState(false);

// Made with AI help.
// Made with AI help.
function rememberCataloguePosition() {
try {
sessionStorage.setItem("catalogue-return", JSON.stringify({
url: `${location.pathname}${location.search}`,
scrollY: window.scrollY,
}));
sessionStorage.setItem(
"catalogue-return",
JSON.stringify({
url: `${location.pathname}${location.search}`,
scrollY: window.scrollY,
}),
);
} catch {
// Catch
}
}

Expand All @@ -64,7 +75,9 @@ export default function ArtworkCard({ artwork, showGalleryAction = true }: Artwo
alt={artwork.image_url || `${artwork.title} by ${artist}`}
/>
) : (
<div className="artwork-card__placeholder dark:bg-[#302e2a] dark:text-gray-300">Image unavailable</div>
<div className="artwork-card__placeholder dark:bg-[#302e2a] dark:text-gray-300">
Image unavailable
</div>
)}

<h3 className="artwork-card__title">{artwork.title}</h3>
Expand All @@ -74,7 +87,11 @@ export default function ArtworkCard({ artwork, showGalleryAction = true }: Artwo
{showGalleryAction && (
<button
type="button"
aria-label={isLoggedIn ? `Add ${artwork.title} to a gallery` : "Sign in to add this artwork to a gallery"}
aria-label={
isLoggedIn
? `Add ${artwork.title} to a gallery`
: "Sign in to add this artwork to a gallery"
}
title={isLoggedIn ? "Add to gallery" : "Sign in to add to a gallery"}
onClick={() => {
if (isLoggedIn) setShowGalleryModal(true);
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/components/artwork/ArtworkDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { Artwork } from "../../types/Artwork";

type ArtworkDetailsProps = {
artwork: Artwork;
className?: string;
};

// Displays artwork details on both mobile and desktop
export default function ArtworkDetails({
artwork,
className = "",
}: ArtworkDetailsProps) {
const place = [artwork.city, artwork.country].filter(Boolean).join(", ");

const details = [
{ label: "Medium", value: artwork.medium },
{ label: "Place", value: place },
{ label: "Culture", value: artwork.culture },
{ label: "Period", value: artwork.period },
{ label: "Dimensions", value: artwork.dimensions },
].filter((detail) => detail.value);

return (
<section className={className}>
<h2 className="font-serif text-3xl">Details</h2>

<div className="mt-4 border-t border-gray-300 dark:border-[#504b46]">
{details.map((detail) => (
<div
key={detail.label}
className="grid grid-cols-[110px_1fr] gap-6 py-4 sm:grid-cols-[200px_1fr] lg:grid-cols-[110px_1fr]"
>
<p className="font-medium">{detail.label}</p>

<p className="text-gray-500 dark:text-gray-400">{detail.value}</p>
</div>
))}
</div>
</section>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Artwork } from "../types/Artwork";
import type { Artwork } from "../../types/Artwork";
import type { ReactNode } from "react";
import ArtworkCard from "./ArtworkCard";
import "./ArtworkGrid.css";
Expand All @@ -22,7 +22,10 @@ export default function ArtworkGrid({
<ul className="artwork-grid" aria-label="Artwork results" role="list">
{artworks.map((artwork) => (
<li key={artwork.id} className="relative">
<ArtworkCard artwork={artwork} showGalleryAction={showGalleryActions} />
<ArtworkCard
artwork={artwork}
showGalleryAction={showGalleryActions}
/>
{renderAction?.(artwork)}
</li>
))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import type { Gallery } from "../types/Gallery";
import { useAuthStore } from "../store/authStore";
import type { Gallery } from "../../types/Gallery";
import { useAuthStore } from "../../store/authStore";

type AddToGalleryModalProps = {
artworkId: number;
Expand Down Expand Up @@ -151,7 +151,9 @@ export default function AddToGalleryModal({
</div>

{loading ? (
<p className="mt-6 text-gray-500 dark:text-gray-400">Loading galleries...</p>
<p className="mt-6 text-gray-500 dark:text-gray-400">
Loading galleries...
</p>
) : galleries.length === 0 ? (
<p className="mt-6 text-gray-500 dark:text-gray-400">
You have not created any galleries yet.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { LucideX } from "lucide-react";
import { useState } from "react";
import { useAuthStore } from "../store/authStore";
import { useAuthStore } from "../../store/authStore";

type CreateGalleryModalProps = {
onClose: () => void;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export default function DeleteGalleryDialog({

<p
id="delete-gallery-description"
className="mt-4 break-words text-sm leading-relaxed"
className="mt-4 wrap-break-word text-sm leading-relaxed"
>
Delete “{title}”? This cannot be undone. Artworks will remain in the
catalogue and in your other galleries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,7 @@ export default function GalleryCard({
</div>
)}

<h2 className="mt-3 text-lg font-bold [overflow-wrap:anywhere]">
{title}
</h2>
<h2 className="mt-3 text-lg font-bold wrap-anywhere">{title}</h2>

<p className="text-sm text-gray-700 dark:text-gray-300">
{artworkCount} {artworkCount === 1 ? "artwork" : "artworks"}
Expand Down
130 changes: 130 additions & 0 deletions frontend/src/components/gallery/GalleryHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { Pencil } from "lucide-react";

type GalleryHeaderProps = {
title: string;
totalCount: number;
isEditing: boolean;
isRenaming: boolean;
titleInput: string;
renaming: boolean;
renameError: string;
onTitleChange: (value: string) => void;
onToggleEditing: () => void;
onStartRenaming: (title: string) => void;
onCancelRenaming: () => void;
onRename: () => void;
};

// Displays gallery details and controls for editing the gallery name
export default function GalleryHeader({
title,
totalCount,
isEditing,
isRenaming,
titleInput,
renaming,
renameError,
onTitleChange,
onToggleEditing,
onStartRenaming,
onCancelRenaming,
onRename,
}: GalleryHeaderProps) {
return (
<header className="mb-8 flex flex-col gap-3 border-b border-gray-300 pb-6 dark:border-[#504b46] sm:flex-row sm:items-end sm:justify-between sm:gap-8">
<div className="min-w-0 flex-1">
{isRenaming ? (
<form
onSubmit={(event) => {
event.preventDefault();
onRename();
}}
onKeyDown={(event) => {
if (event.key === "Escape" && !renaming) {
event.preventDefault();
onCancelRenaming();
}
}}
className="flex max-w-xl flex-wrap items-center gap-3"
>
<label htmlFor="gallery-title" className="sr-only">
Gallery name
</label>

<input
ref={(input) => input?.focus()}
id="gallery-title"
name="title"
type="text"
value={titleInput}
onChange={(event) => onTitleChange(event.target.value)}
required
disabled={renaming}
className="min-h-11 w-full rounded border border-gray-300 bg-white px-3 py-2 font-serif text-3xl text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700 disabled:opacity-50 dark:border-[#504b46] dark:bg-[#242321] dark:text-[#e6dfd6] sm:text-4xl"
/>

<button
type="submit"
disabled={renaming || !titleInput.trim()}
className="min-h-11 cursor-pointer rounded border border-gray-300 px-4 py-2 text-sm hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-[#302e2a]"
>
{renaming ? "Saving..." : "Save"}
</button>

<button
type="button"
onClick={onCancelRenaming}
disabled={renaming}
className="min-h-11 cursor-pointer rounded border border-gray-300 px-4 py-2 text-sm hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700 disabled:opacity-50 dark:hover:bg-[#302e2a]"
>
Cancel
</button>
</form>
) : (
<div className="flex items-center gap-3">
<h1 className="min-w-0 wrap-break-word font-serif text-4xl font-normal leading-tight sm:text-5xl">
{title}
</h1>

{isEditing && (
<button
type="button"
onClick={() => onStartRenaming(title)}
aria-label="Rename gallery"
title="Rename gallery"
className="inline-flex min-h-11 min-w-11 shrink-0 cursor-pointer items-center justify-center rounded-full text-gray-600 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700 dark:text-gray-300 dark:hover:bg-[#302e2a]"
>
<Pencil size={20} aria-hidden="true" />
</button>
)}
</div>
)}

{isRenaming && renameError && (
<p
role="alert"
className="mt-3 text-sm text-red-700 dark:text-red-400"
>
Could not rename gallery: {renameError}
</p>
)}
</div>

<div className="flex shrink-0 flex-wrap items-center gap-4">
<p className="shrink-0 text-sm text-gray-600 sm:pb-1 sm:text-base">
{totalCount} {totalCount === 1 ? "artwork" : "artworks"}
</p>

<button
type="button"
onClick={onToggleEditing}
disabled={renaming}
aria-pressed={isEditing}
className="min-h-11 cursor-pointer rounded border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-700"
>
{isEditing ? "Done" : "Edit gallery"}
</button>
</div>
</header>
);
}
Loading