Skip to content

Commit 5fa4ae1

Browse files
hh
1 parent 38481f4 commit 5fa4ae1

13 files changed

Lines changed: 1153 additions & 631 deletions

File tree

components/Layout.js

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,87 @@
1-
import { useState } from 'react';
1+
import {
2+
createContext,
3+
useCallback,
4+
useContext,
5+
useEffect,
6+
useMemo,
7+
useState,
8+
} from 'react';
29
import Link from 'next/link';
310
import { useRouter } from 'next/router';
411
import { Dialog, DialogBackdrop, DialogPanel, TransitionChild } from '@headlessui/react';
512
import { Bars3Icon as MenuIcon, XMarkIcon as X } from '@heroicons/react/24/outline';
613
import SideNav from '@/components/Navigation/SideNav';
714
import YugiohSiteBackground from '@/components/Yugioh/YugiohSiteBackground';
815

16+
const ShellSlotsContext = createContext( null );
17+
18+
const EMPTY_SLOTS = {
19+
header: null,
20+
rightSidebar: null,
21+
footer: null,
22+
};
23+
24+
export function useAppShellSlots( slots ) {
25+
const context = useContext( ShellSlotsContext );
26+
27+
useEffect( () => {
28+
if ( !context ) {
29+
return undefined;
30+
}
31+
32+
context.setSlots( {
33+
...EMPTY_SLOTS,
34+
...slots,
35+
} );
36+
37+
return () => {
38+
context.clearSlots();
39+
};
40+
}, [
41+
context,
42+
slots?.header,
43+
slots?.rightSidebar,
44+
slots?.footer,
45+
] );
46+
}
947

1048
export default function Layout( { children } ) {
1149
const [ isSidebarOpen, setIsSidebarOpen ] = useState( false );
50+
const [ slots, setSlots ] = useState( EMPTY_SLOTS );
1251
const router = useRouter();
1352
const showYugiohBackground = router.pathname.startsWith( '/yugioh' );
53+
const hasHeaderSlot = Boolean( slots.header );
54+
const hasRightSidebar = Boolean( slots.rightSidebar );
55+
const hasFooterSlot = Boolean( slots.footer );
56+
57+
const clearSlots = useCallback( () => {
58+
setSlots( EMPTY_SLOTS );
59+
}, [] );
60+
61+
const contextValue = useMemo(
62+
() => ( {
63+
setSlots,
64+
clearSlots,
65+
} ),
66+
[ clearSlots ]
67+
);
1468

1569
const toggleSidebar = () => {
16-
setIsSidebarOpen( !isSidebarOpen );
70+
setIsSidebarOpen( ( open ) => !open );
1771
};
1872

73+
useEffect( () => {
74+
clearSlots();
75+
setIsSidebarOpen( false );
76+
}, [ clearSlots, router.asPath ] );
77+
1978
return (
20-
<div className={ `relative mx-auto min-h-screen w-full glass backdrop ${ showYugiohBackground ? "overflow-x-hidden" : "" }` }>
21-
{ showYugiohBackground ? <YugiohSiteBackground /> : null }
22-
<div className="relative z-10">
79+
<ShellSlotsContext.Provider value={ contextValue }>
80+
<div
81+
className={ `app-shell parent relative mx-auto min-h-screen w-full glass backdrop ${ showYugiohBackground ? "overflow-x-hidden" : "" } ${ hasHeaderSlot ? "app-shell--has-header" : "" } ${ hasRightSidebar ? "app-shell--has-right" : "" } ${ hasFooterSlot ? "app-shell--has-footer" : "" }` }
82+
>
83+
{ showYugiohBackground ? <YugiohSiteBackground /> : null }
84+
2385
<Dialog open={ isSidebarOpen } onClose={ setIsSidebarOpen } className="relative z-50 lg:hidden">
2486
<DialogBackdrop
2587
transition
@@ -46,23 +108,17 @@ export default function Layout( { children } ) {
46108
</DialogPanel>
47109
</div>
48110
</Dialog>
49-
<div className="glass hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col dark:bg-gray-900">
50-
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 px-6 dark:border-white/10 dark:bg-black/10">
51-
<nav className="flex flex-1 flex-col mt-10">
52-
<SideNav />
53-
</nav>
54-
</div>
55-
</div>
56-
<div className="lg:pl-72 w-full">
57-
<div className="sticky top-0 z-40 flex items-center gap-x-6 glass backdrop px-4 py-4 shadow-xs sm:px-6 lg:hidden dark:bg-gray-900 dark:shadow-none dark:after:pointer-events-none dark:after:absolute dark:after:inset-0 dark:after:border-b dark:after:border-white/10 dark:after:bg-black/10">
111+
112+
<header className="app-shell-header section">
113+
<div className="app-shell-mobile-header">
58114
<button
59115
type="button"
60116
onClick={ toggleSidebar }
61-
className=" text-gray-700 hover:text-gray-900 lg:hidden dark:text-gray-400 dark:hover:text-white justify-self-end place-items-end"
117+
className="text-gray-700 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
62118
title="Open sidebar"
63119
>
64120
<span className="sr-only">Open sidebar</span>
65-
<MenuIcon color={ "white" } aria-hidden="true" className="size-6" />
121+
<MenuIcon color="white" aria-hidden="true" className="size-6" />
66122
</button>
67123
<Link href="/" className="flex-1 text-sm font-semibold leading-6 text-shadow text-white">
68124
CARD PRICE APP
@@ -71,13 +127,45 @@ export default function Layout( { children } ) {
71127
<span className="rounded-full border border-dashed border-white py-1 px-1 font-black text-shadow text-sm">PIC</span>
72128
</div>
73129
</div>
74-
<main className="min-h-screen mx-auto">
75-
<div className="max-w-full w-auto">
76-
{ children }
130+
131+
<div className="app-shell-desktop-header">
132+
<Link href="/" className="text-sm font-semibold leading-6 text-shadow text-white">
133+
CARD PRICE APP
134+
</Link>
135+
<div className="flex items-center">
136+
<span className="rounded-full border border-dashed border-white py-1 px-1 font-black text-shadow text-sm">PIC</span>
137+
</div>
138+
</div>
139+
140+
{ hasHeaderSlot && (
141+
<div className="app-shell-header-slot">
142+
{ slots.header }
77143
</div>
78-
</main>
79-
</div>
144+
) }
145+
</header>
146+
147+
<aside className="app-shell-left left-side section">
148+
<div className="app-shell-left-inner">
149+
<nav className="flex flex-1 flex-col">
150+
<SideNav />
151+
</nav>
152+
</div>
153+
</aside>
154+
155+
<main className="app-shell-main section">
156+
{ children }
157+
</main>
158+
159+
{ hasRightSidebar && (
160+
<aside className="app-shell-right right-side section">
161+
{ slots.rightSidebar }
162+
</aside>
163+
) }
164+
165+
<footer id="app-shell-footer" className="app-shell-footer section">
166+
{ slots.footer }
167+
</footer>
80168
</div>
81-
</div>
169+
</ShellSlotsContext.Provider>
82170
);
83171
};

components/Navigation/Breadcrumb.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import Link from 'next/link';
33
import { useRouter } from 'next/router';
44

5-
const Breadcrumb = ( { items = null } ) => {
5+
const Breadcrumb = ( { items = null, className = "" } ) => {
66
const router = useRouter();
77
const { pathname, query, isReady } = router;
88
const [ hasMounted, setHasMounted ] = useState( false );
@@ -109,7 +109,7 @@ const Breadcrumb = ( { items = null } ) => {
109109

110110
return (
111111
<nav
112-
className="object-start self-start items-start mx-auto w-full max-w-7xl px-4 pt-4 sm:px-6 lg:px-8"
112+
className={ className || "object-start self-start items-start mx-auto w-full max-w-7xl px-4 py-3 sm:px-6 lg:px-8" }
113113
aria-label="Breadcrumb"
114114
>
115115
<ol className="flex flex-wrap items-center gap-2 rounded-2xl border border-white/10 bg-black/40 px-4 py-3 text-sm text-white/80 shadow-2xl backdrop-blur">

components/Notification.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const Notification = ( { show = false, setShow = () => { }, message = "" } ) =>
6363
</div>
6464
);
6565

66-
return createPortal( toast, document.body );
66+
return createPortal( toast, document.getElementById( "app-shell-footer" ) || document.body );
6767
};
6868

6969
export default Notification;

components/Yugioh/Card.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ const Card = ( { cardData } ) => {
3535
<Image
3636
as="image"
3737
unoptimized="true"
38-
className="object-cover lg:object-scale-down object-top w-full mx-auto h-96 aspect-square"
38+
className="object-cover lg:object-scale-down object-top w-full mx-auto h-64 aspect-square"
3939
src={ imageSrc }
4040
alt={ `Card Image - ${ productName }` }
4141
width={ 1600 }
42-
height={ 450 }
42+
height={ 500 }
4343
priority={ true }
4444
draggable={ false }
4545
/>

components/Yugioh/CardFilter.js

Lines changed: 90 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use client';
22

3-
import { useState } from 'react';
4-
import { Dialog, DialogPanel, DialogTitle } from '@headlessui/react';
5-
import { X } from 'lucide-react';
3+
import { useMemo, useState } from 'react';
4+
import { Dialog, DialogBackdrop, DialogPanel, DialogTitle } from '@headlessui/react';
5+
import { Minus, Plus, X } from 'lucide-react';
66
import { YUGIOH_FILTER_SECTIONS } from '@/constants/yugiohFilters';
77

88
const CardFilter = ( {
9-
filters, // { rarity: [], condition: [] }
9+
filters = {}, // { rarity: [], condition: [] }
1010
updateFilters, // (filterType, updatedValues) => void
11+
clearFilters,
12+
sections = YUGIOH_FILTER_SECTIONS,
1113
open, // boolean (optional controlled prop)
1214
setOpen, // (open: boolean) => void (optional controlled setter)
1315
isModalOpen, // legacy boolean prop
@@ -36,6 +38,13 @@ const CardFilter = ( {
3638
const openFilters = () => resolvedSetOpen( true );
3739
const closeFilters = () => resolvedSetOpen( false );
3840

41+
const activeCount = useMemo( () => {
42+
return Object.values( filters || {} ).reduce(
43+
( total, entries ) => total + ( Array.isArray( entries ) ? entries.length : 0 ),
44+
0,
45+
);
46+
}, [ filters ] );
47+
3948
const handleCheckboxChange = async ( event, filterType ) => {
4049
const { value, checked } = event.target;
4150
const prev = filters[ filterType ] || [];
@@ -61,68 +70,102 @@ const CardFilter = ( {
6170
return (
6271
<div>
6372
{ triggerContent }
64-
<Dialog open={ resolvedOpen } onClose={ resolvedSetOpen } className="relative z-10 ">
65-
<div className="fixed inset-0" />
66-
73+
<Dialog open={ resolvedOpen } onClose={ resolvedSetOpen } className="relative z-[120]">
74+
<DialogBackdrop
75+
transition
76+
className="fixed inset-0 bg-black/70 backdrop-blur-sm transition-opacity duration-300 data-closed:opacity-0"
77+
/>
6778
<div className="fixed inset-0 overflow-hidden">
6879
<div className="absolute inset-0 overflow-hidden">
69-
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-12 sm:pl-2 glass backdrop">
80+
<div className="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-0">
7081
<DialogPanel
7182
transition
72-
className="pointer-events-auto w-screen max-w-md transform transition duration-500 ease-in-out data-closed:translate-x-full sm:duration-700"
83+
className="pointer-events-auto w-screen max-w-md transform transition duration-300 ease-out data-closed:translate-x-full"
7384
>
74-
<div className="relative flex h-full flex-col overflow-y-auto py-6 shadow-xl dark:bg-gray-800 dark:after:absolute dark:after:inset-y-0 dark:after:left-0 dark:after:w-px dark:after:bg-white/10">
75-
<div className="px-4 sm:px-6">
76-
<div className="flex items-start justify-between">
77-
<DialogTitle className="text-base font-semibold text-white">
78-
{ title }
79-
</DialogTitle>
80-
<div className="ml-3 flex h-7 items-center">
85+
<div className="relative flex h-full flex-col overflow-y-auto border-l border-white/10 bg-slate-950/92 py-6 text-white shadow-2xl backdrop-blur-xl">
86+
<div className="px-5 sm:px-6">
87+
<div className="flex items-start justify-between gap-3">
88+
<div>
89+
<p className="text-xs uppercase tracking-wide text-white/50">Filters</p>
90+
<DialogTitle className="text-lg font-semibold text-white">
91+
{ title }
92+
</DialogTitle>
93+
{ activeCount > 0 && (
94+
<p className="text-xs text-indigo-100/80">{ activeCount } active</p>
95+
) }
96+
</div>
97+
<div className="flex items-center gap-2">
98+
{ typeof clearFilters === 'function' && activeCount > 0 && (
99+
<button
100+
type="button"
101+
onClick={ clearFilters }
102+
className="rounded-full border border-white/10 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white/70 transition hover:border-white/40 hover:text-white"
103+
>
104+
Clear All
105+
</button>
106+
) }
81107
<button
82108
type="button"
83109
title={ "Close" }
84110
onClick={ closeFilters }
85-
className="relative rounded-md text-white hover:text-gray-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 dark:hover:text-white dark:focus-visible:outline-indigo-500"
111+
className="relative rounded-md p-1 text-white/70 transition hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
86112
>
87-
<span className="absolute -inset-2.5" />
88113
<span className="sr-only">Close filters</span>
89-
<X color={ `red` } aria-hidden="true" className="size-6" />
114+
<X aria-hidden="true" className="size-5" />
90115
</button>
91116
</div>
92117
</div>
93118
</div>
94-
<div className="relative mt-6 flex-1 px-4 ">
95-
<div className="space-y-6">
96-
{ YUGIOH_FILTER_SECTIONS.map( filter => (
97-
<div key={ filter.id } className="space-y-3">
98-
<p className="text-sm font-semibold text-white">{ filter.label }</p>
99-
<div className="space-y-2">
100-
{ filter.options.map( value => (
101-
<label
102-
key={ value }
103-
htmlFor={ `${ filter.id }-${ value }` }
104-
className="flex items-start gap-2 text-sm text-gray-200"
105-
>
106-
<input
107-
type="checkbox"
108-
id={ `${ filter.id }-${ value }` }
109-
value={ value }
110-
checked={ filters[ filter.id ]?.includes( value ) || false }
111-
onChange={ e => handleCheckboxChange( e, filter.id ) }
112-
className="mt-1 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500 dark:border-gray-600 dark:bg-gray-700"
113-
/>
114-
<span>{ value }</span>
115-
</label>
116-
) ) }
117-
</div>
118-
</div>
119-
) ) }
119+
<div className="relative mt-6 flex-1 px-5 sm:px-6">
120+
<div className="space-y-4">
121+
{ sections.map( filter => {
122+
const activeValues = new Set( filters[ filter.id ] || [] );
123+
124+
return (
125+
<details
126+
key={ filter.id }
127+
className="group rounded-2xl border border-white/10 bg-white/5"
128+
open
129+
>
130+
<summary className="flex cursor-pointer items-center justify-between px-4 py-3 text-sm font-semibold tracking-wide text-white/80">
131+
<span>{ filter.label }</span>
132+
<span className="text-white/60 group-open:hidden">
133+
<Plus size={ 16 } />
134+
</span>
135+
<span className="hidden text-white/60 group-open:block">
136+
<Minus size={ 16 } />
137+
</span>
138+
</summary>
139+
<div className="border-t border-white/5 px-4 py-3 text-sm">
140+
<div className="max-h-64 space-y-2 overflow-y-auto pr-1">
141+
{ filter.options.map( value => (
142+
<label
143+
key={ value }
144+
htmlFor={ `${ filter.id }-${ value }` }
145+
className="flex items-center gap-3 rounded-lg border border-white/5 bg-white/5 px-3 py-2 text-white/80 transition hover:border-white/30"
146+
>
147+
<input
148+
type="checkbox"
149+
id={ `${ filter.id }-${ value }` }
150+
value={ value }
151+
checked={ activeValues.has( value ) }
152+
onChange={ e => handleCheckboxChange( e, filter.id ) }
153+
className="h-4 w-4 rounded border-gray-300 text-indigo-500 focus:ring-indigo-400"
154+
/>
155+
<span className="text-sm">{ value }</span>
156+
</label>
157+
) ) }
158+
</div>
159+
</div>
160+
</details>
161+
);
162+
} ) }
120163
</div>
121164

122-
<div className="mt-6 border-t border-gray-200 pt-4 dark:border-white/10">
165+
<div className="mt-6 border-t border-white/10 pt-4">
123166
<button
124167
type="button"
125-
className="flex w-full justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
168+
className="flex w-full justify-center rounded-md bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
126169
onClick={ closeFilters }
127170
>
128171
Apply Filters

0 commit comments

Comments
 (0)