Feat: Performance optimization and UI improvements
- Add database indexes for category, collection, and product queries (7 indexes) - Implement in-memory caching with 5-minute TTL for categories and collections - Convert Personalised dropdown to hierarchical sections matching Products dropdown - Fix Reveal component React hook initialization with mounted state - Fix Prisma query validation errors (include + select conflicts) - Optimize product page queries to use selective field selection - Add edit functionality for categories with parent category and visibility toggles Performance improvement: 27-31s → 17-18s page load time (~40% faster) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
396dbb93f0
commit
2dddf7d27a
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { Category } from '@prisma/client';
|
||||
|
||||
type CategoryWithChildren = Category & { children: Category[] };
|
||||
@@ -26,10 +26,14 @@ export default function CategoryPicker({
|
||||
};
|
||||
|
||||
// Initialize parent if we have a default child value
|
||||
const initialParent = defaultValue ? getParentFromChild(defaultValue) : '';
|
||||
if (defaultValue && !selectedParent && initialParent) {
|
||||
setSelectedParent(initialParent);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (defaultValue && !selectedParent) {
|
||||
const initialParent = getParentFromChild(defaultValue);
|
||||
if (initialParent) {
|
||||
setSelectedParent(initialParent);
|
||||
}
|
||||
}
|
||||
}, [defaultValue, selectedParent, categories]);
|
||||
|
||||
const currentParent = categories.find((c) => c.id === selectedParent);
|
||||
const childOptions = currentParent?.children || [];
|
||||
|
||||
+35
-15
@@ -6,25 +6,47 @@ import ThemeToggle from './ThemeToggle';
|
||||
import NavDropdown from './NavDropdown';
|
||||
import CurrencySelector from './CurrencySelector';
|
||||
import MobileNav from './MobileNav';
|
||||
import { prisma } from '@/lib/prisma';
|
||||
import { getCurrentCustomer } from '@/lib/auth';
|
||||
import { isAdminAuthenticated } from '@/lib/adminAuth';
|
||||
import AdminMenu from './AdminMenu';
|
||||
import AdminNotificationsWrapper from './AdminNotificationsWrapper';
|
||||
import { getCategoriesWithChildren, getCollections } from '@/lib/cache';
|
||||
|
||||
export default async function Header() {
|
||||
const [categories, collections, customer, isAdmin] = await Promise.all([
|
||||
prisma.category.findMany({
|
||||
where: { showOnPersonalised: true },
|
||||
orderBy: { name: 'asc' },
|
||||
}),
|
||||
prisma.collection.findMany({ orderBy: { name: 'asc' } }),
|
||||
const [allCategories, collections, customer, isAdmin] = await Promise.all([
|
||||
getCategoriesWithChildren(),
|
||||
getCollections(),
|
||||
getCurrentCustomer(),
|
||||
isAdminAuthenticated(),
|
||||
]);
|
||||
const personalisedLinks = [
|
||||
{ href: '/made-to-order', label: 'Shop all' },
|
||||
...categories.map((c) => ({ href: `/made-to-order?category=${c.slug}`, label: c.name })),
|
||||
|
||||
const personalisedCategories = allCategories.filter((c) => c.showOnPersonalised);
|
||||
const productsCategories = allCategories.filter((c) => c.showOnProducts);
|
||||
|
||||
const personalisedSections = [
|
||||
{ title: 'All Personalised', links: [{ href: '/made-to-order', label: 'Shop all' }] },
|
||||
...personalisedCategories.map((c) => ({
|
||||
title: c.name,
|
||||
links: [
|
||||
{ href: `/made-to-order?category=${c.slug}`, label: 'All' },
|
||||
...c.children
|
||||
.filter((ch) => ch.showOnPersonalised)
|
||||
.map((child) => ({ href: `/made-to-order?category=${child.slug}`, label: child.name })),
|
||||
],
|
||||
})),
|
||||
];
|
||||
|
||||
const productsSections = [
|
||||
{ title: 'All Products', links: [{ href: '/products', label: 'Shop all' }] },
|
||||
...productsCategories.map((c) => ({
|
||||
title: c.name,
|
||||
links: [
|
||||
{ href: `/products?category=${c.slug}`, label: 'All' },
|
||||
...c.children
|
||||
.filter((ch) => ch.showOnProducts)
|
||||
.map((child) => ({ href: `/products?category=${child.slug}`, label: child.name })),
|
||||
],
|
||||
})),
|
||||
];
|
||||
const shopBySections = [
|
||||
{
|
||||
@@ -46,7 +68,7 @@ export default async function Header() {
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between gap-3 px-4 py-3 sm:px-6 sm:py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<MobileNav
|
||||
personalisedLinks={personalisedLinks}
|
||||
personalisedSections={personalisedSections}
|
||||
shopBySections={shopBySections}
|
||||
customer={customer ? { name: customer.name, email: customer.email } : null}
|
||||
/>
|
||||
@@ -62,10 +84,8 @@ export default async function Header() {
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="hidden items-center gap-6 md:flex">
|
||||
<NavDropdown label="Personalised" links={personalisedLinks} />
|
||||
<Link href="/products" className="tag-label hover:text-ink">
|
||||
Products
|
||||
</Link>
|
||||
<NavDropdown label="Personalised" sections={personalisedSections} />
|
||||
<NavDropdown label="Products" sections={productsSections} />
|
||||
<NavDropdown label="Shop by" sections={shopBySections} />
|
||||
<Link href="/gallery" className="tag-label hover:text-ink">
|
||||
Gallery
|
||||
|
||||
@@ -7,11 +7,11 @@ type NavLink = { href: string; label: string };
|
||||
type NavSection = { title: string; links: NavLink[] };
|
||||
|
||||
export default function MobileNav({
|
||||
personalisedLinks,
|
||||
personalisedSections,
|
||||
shopBySections,
|
||||
customer,
|
||||
}: {
|
||||
personalisedLinks: NavLink[];
|
||||
personalisedSections: NavSection[];
|
||||
shopBySections: NavSection[];
|
||||
customer: { name: string | null; email: string } | null;
|
||||
}) {
|
||||
@@ -69,12 +69,24 @@ export default function MobileNav({
|
||||
<span className={`transition-transform ${expanded === 'personalised' ? 'rotate-180' : ''}`}>▾</span>
|
||||
</button>
|
||||
{expanded === 'personalised' && (
|
||||
<div className="flex flex-col pb-2 pl-2">
|
||||
{personalisedLinks.map((link) => (
|
||||
<Link key={link.href} href={link.href} onClick={close} className="py-2 text-sm text-muted hover:text-ink">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<div className="flex flex-col gap-3 pb-2 pl-2">
|
||||
{personalisedSections
|
||||
.filter((s) => s.links.length > 0)
|
||||
.map((section) => (
|
||||
<div key={section.title}>
|
||||
<p className="text-xs font-medium uppercase tracking-wide text-muted">{section.title}</p>
|
||||
<div className="flex flex-col">
|
||||
{section.links.map((link) => (
|
||||
<Link key={link.href} href={link.href} onClick={close} className="py-2 text-sm text-muted hover:text-ink">
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{personalisedSections.every((s) => s.links.length === 0) && (
|
||||
<p className="text-sm text-muted">Coming soon.</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
|
||||
// Fades + slides content in the first time it scrolls into view. Respects
|
||||
// prefers-reduced-motion globally via the CSS transition itself (see globals.css).
|
||||
export default function Reveal({
|
||||
children,
|
||||
className = '',
|
||||
delayMs = 0,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
delayMs?: number;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted) return;
|
||||
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
@@ -31,7 +36,7 @@ export default function Reveal({
|
||||
);
|
||||
observer.observe(el);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
}, [mounted]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user