Skip to content

Commit 5677af7

Browse files
feat(settings): move layout switch to settings modal (#452)
* feat(settings): move layout switch to settings modal - Add SelectLayout component with side-by-side layout display - Integrate SelectLayout as first item in Settings modal - Replace column layout with horizontal grid with thumbnails - Works in both DEMO and NON-DEMO modes - Layout change is immediate with toast notification * fix(ui): correct invalid Tailwind CSS gradient syntax in select-layout Replace invalid bg-linear-to-br and bg-linear-to-t with correct bg-gradient-to-br and bg-gradient-to-t syntax. - Fix gradient syntax in layout preview background - Fix gradient syntax in hover overlay
1 parent 944f44e commit 5677af7

File tree

2 files changed

+276
-59
lines changed

2 files changed

+276
-59
lines changed

components/settings-modal.tsx

Lines changed: 70 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,49 @@
1-
"use client";
2-
3-
import { useEffect } from "react";
4-
import { X, Settings } from "lucide-react";
5-
import { createPortal } from "react-dom";
6-
import { cn, isDemoMode } from "@/lib/utils";
7-
import { useSettingsModal } from "@/hooks/use-settings-modal";
8-
import { useTranslations } from "next-intl";
9-
import SelectContainerWidth from "@/components/ui/select-container-width";
10-
import SelectPaginationType from "@/components/ui/select-pagination-type";
11-
import SelectDatabaseMode from "@/components/ui/select-database-mode";
12-
import SelectCheckoutProvider from "@/components/ui/select-checkout-provider";
13-
import { DatabaseStatusWarning } from "@/components/ui/database-status-warning";
14-
import { useFocusManagement } from "@/components/ui/accessibility";
1+
'use client';
2+
3+
import { useEffect } from 'react';
4+
import { X, Settings } from 'lucide-react';
5+
import { createPortal } from 'react-dom';
6+
import { cn, isDemoMode } from '@/lib/utils';
7+
import { useSettingsModal } from '@/hooks/use-settings-modal';
8+
import { useTranslations } from 'next-intl';
9+
import SelectLayout from '@/components/ui/select-layout';
10+
import SelectContainerWidth from '@/components/ui/select-container-width';
11+
import SelectPaginationType from '@/components/ui/select-pagination-type';
12+
import SelectDatabaseMode from '@/components/ui/select-database-mode';
13+
import SelectCheckoutProvider from '@/components/ui/select-checkout-provider';
14+
import { DatabaseStatusWarning } from '@/components/ui/database-status-warning';
15+
import { useFocusManagement } from '@/components/ui/accessibility';
1516

1617
const BACKDROP_CLASSES = cn(
17-
"fixed inset-0",
18-
"bg-gradient-to-br from-black/50 via-black/60 to-black/70",
19-
"dark:bg-gradient-to-br dark:from-black/70 dark:via-black/80 dark:to-black/90",
20-
"backdrop-blur-2xl backdrop-saturate-150",
21-
"z-[9998]",
22-
"transition-all duration-300 ease-out"
18+
'fixed inset-0',
19+
'bg-gradient-to-br from-black/50 via-black/60 to-black/70',
20+
'dark:bg-gradient-to-br dark:from-black/70 dark:via-black/80 dark:to-black/90',
21+
'backdrop-blur-2xl backdrop-saturate-150',
22+
'z-[9998]',
23+
'transition-all duration-300 ease-out'
2324
);
2425

2526
const MODAL_CLASSES = cn(
26-
"fixed top-1/2 left-1/2",
27-
"transform -translate-x-1/2 -translate-y-1/2",
28-
"w-full max-w-2xl",
29-
"max-h-[90vh]",
30-
"bg-white/70 dark:bg-gray-900/70",
31-
"backdrop-blur-2xl backdrop-saturate-200",
32-
"border border-white/20 dark:border-white/10",
33-
"ring-1 ring-theme-primary-500/10 dark:ring-theme-primary-400/10",
34-
"rounded-2xl shadow-2xl shadow-black/20 dark:shadow-black/60",
35-
"z-[9999]",
36-
"overflow-visible",
37-
"transition-all duration-300 ease-out",
38-
"animate-fade-in-up"
27+
'fixed top-1/2 left-1/2',
28+
'transform -translate-x-1/2 -translate-y-1/2',
29+
'w-full max-w-2xl',
30+
'max-h-[90vh]',
31+
'bg-white/70 dark:bg-gray-900/70',
32+
'backdrop-blur-2xl backdrop-saturate-200',
33+
'border border-white/20 dark:border-white/10',
34+
'ring-1 ring-theme-primary-500/10 dark:ring-theme-primary-400/10',
35+
'rounded-2xl shadow-2xl shadow-black/20 dark:shadow-black/60',
36+
'z-[9999]',
37+
'overflow-visible',
38+
'transition-all duration-300 ease-out',
39+
'animate-fade-in-up'
3940
);
4041

41-
const DIVIDER_CLASSES = cn("border-t border-gray-200 dark:border-gray-700");
42+
const DIVIDER_CLASSES = cn('border-t border-gray-200 dark:border-gray-700');
4243

4344
export function SettingsModal() {
4445
const { isOpen, closeModal } = useSettingsModal();
45-
const t = useTranslations("settings");
46+
const t = useTranslations('settings');
4647
const { focusRef, setFocus, trapFocus } = useFocusManagement();
4748
const isDemo = isDemoMode();
4849

@@ -54,15 +55,15 @@ export function SettingsModal() {
5455

5556
// Add keyboard listener for focus trap
5657
const handleKeyDown = (e: KeyboardEvent) => trapFocus(e);
57-
document.addEventListener("keydown", handleKeyDown);
58+
document.addEventListener('keydown', handleKeyDown);
5859

5960
return () => {
60-
document.removeEventListener("keydown", handleKeyDown);
61+
document.removeEventListener('keydown', handleKeyDown);
6162
};
6263
}
6364
}, [isOpen, setFocus, trapFocus]);
6465

65-
if (!isOpen || typeof window === "undefined") {
66+
if (!isOpen || typeof window === 'undefined') {
6667
return null;
6768
}
6869

@@ -81,36 +82,43 @@ export function SettingsModal() {
8182
tabIndex={-1}
8283
>
8384
{/* Modal Header */}
84-
<div className={cn(
85-
"flex items-center justify-between px-6 py-4",
86-
"border-b border-gray-200 dark:border-gray-700",
87-
"bg-gradient-to-r from-gray-50/50 to-white",
88-
"dark:from-gray-800/50 dark:to-gray-900/50",
89-
"shadow-sm"
90-
)}>
85+
<div
86+
className={cn(
87+
'flex items-center justify-between px-6 py-4',
88+
'border-b border-gray-200 dark:border-gray-700',
89+
'bg-gradient-to-r from-gray-50/50 to-white',
90+
'dark:from-gray-800/50 dark:to-gray-900/50',
91+
'shadow-sm'
92+
)}
93+
>
9194
<div className="flex items-center gap-3">
92-
<div className={cn(
93-
"p-2 rounded-lg",
94-
"bg-gradient-to-br from-theme-primary-100 to-theme-primary-200",
95-
"dark:from-theme-primary-900/30 dark:to-theme-primary-800/30",
96-
"border border-theme-primary-300/50 dark:border-theme-primary-600/50"
97-
)}>
95+
<div
96+
className={cn(
97+
'p-2 rounded-lg',
98+
'bg-gradient-to-br from-theme-primary-100 to-theme-primary-200',
99+
'dark:from-theme-primary-900/30 dark:to-theme-primary-800/30',
100+
'border border-theme-primary-300/50 dark:border-theme-primary-600/50'
101+
)}
102+
>
98103
<Settings className="h-5 w-5 text-theme-primary-600 dark:text-theme-primary-400" />
99104
</div>
100-
<h2 id="settings-title" className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
101-
{t("SETTINGS")}
105+
<h2
106+
id="settings-title"
107+
className="text-2xl font-bold tracking-tight text-gray-900 dark:text-white"
108+
>
109+
{t('SETTINGS')}
102110
</h2>
103111
</div>
104112
<button
105113
onClick={closeModal}
106114
className={cn(
107-
"p-2 rounded-lg transition-all duration-200",
108-
"text-gray-500 hover:text-gray-700",
109-
"dark:text-gray-400 dark:hover:text-gray-200",
110-
"hover:bg-gray-100 dark:hover:bg-gray-800",
111-
"hover:scale-110"
115+
'p-2 rounded-lg transition-all duration-200',
116+
'text-gray-500 hover:text-gray-700',
117+
'dark:text-gray-400 dark:hover:text-gray-200',
118+
'hover:bg-gray-100 dark:hover:bg-gray-800',
119+
'hover:scale-110'
112120
)}
113-
aria-label={t("CLOSE_SETTINGS")}
121+
aria-label={t('CLOSE_SETTINGS')}
114122
type="button"
115123
>
116124
<X className="h-5 w-5" />
@@ -119,6 +127,9 @@ export function SettingsModal() {
119127

120128
{/* Modal Content */}
121129
<div className="px-6 py-8 space-y-5">
130+
{/* Layout Section - Always show */}
131+
<SelectLayout />
132+
122133
{/* Container Width Section - Always show */}
123134
<SelectContainerWidth />
124135

components/ui/select-layout.tsx

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
'use client';
2+
3+
import React, { useMemo } from 'react';
4+
import { Layout, Sparkles } from 'lucide-react';
5+
import { useTranslations } from 'next-intl';
6+
import { cn } from '@/lib/utils';
7+
import { toast } from 'sonner';
8+
import { useLayoutTheme, LayoutHome } from '@/components/context/LayoutThemeContext';
9+
import { useTheme } from 'next-themes';
10+
import Image from 'next/image';
11+
12+
interface SelectLayoutProps {
13+
className?: string;
14+
disabled?: boolean;
15+
}
16+
17+
const SelectLayout: React.FC<SelectLayoutProps> = ({ className, disabled = false }) => {
18+
const { layoutHome, setLayoutHome } = useLayoutTheme();
19+
const { theme, resolvedTheme } = useTheme();
20+
const t = useTranslations('common');
21+
22+
// Determine if we're in dark mode
23+
const isDark = resolvedTheme === 'dark' || (theme === 'system' && resolvedTheme === 'dark');
24+
25+
const layouts = useMemo(
26+
() => [
27+
{
28+
key: LayoutHome.HOME_ONE,
29+
name: 'Home 1',
30+
label: t('CLASSIC_DESIGN'),
31+
description: t('CLASSIC_LAYOUT_DESC'),
32+
icon: <Layout className="w-4 h-4" />,
33+
imageSrc: isDark ? '/home-1.png' : '/home-light-1.png'
34+
},
35+
{
36+
key: LayoutHome.HOME_TWO,
37+
name: 'Home 2',
38+
label: t('MODERN_GRID'),
39+
description: t('GRID_LAYOUT_DESC'),
40+
icon: <Sparkles className="w-4 h-4" />,
41+
imageSrc: isDark ? '/home-2.png' : '/home-light-2.png'
42+
}
43+
],
44+
[isDark, t]
45+
);
46+
47+
const handleLayoutChange = (layout: LayoutHome) => {
48+
if (disabled || layout === layoutHome) return;
49+
setLayoutHome(layout);
50+
51+
// Toast notification
52+
const selectedLayout = layouts.find((l) => l.key === layout);
53+
toast.success(selectedLayout?.label || layout, {
54+
duration: 2000,
55+
description: selectedLayout?.description
56+
});
57+
};
58+
59+
return (
60+
<div
61+
className={cn(
62+
// Structure
63+
'group p-5 rounded-xl',
64+
65+
// Blue/Purple gradient - layout/design feel
66+
'bg-gradient-to-br from-blue-50/80 via-purple-50/60 to-indigo-50/40',
67+
'dark:from-blue-950/40 dark:via-purple-950/30 dark:to-indigo-950/20',
68+
69+
// Glassmorphism
70+
'backdrop-blur-xl backdrop-saturate-150',
71+
72+
// Border with blue tones
73+
'border border-blue-200/40 dark:border-blue-800/30',
74+
75+
// Enhanced shadow
76+
'shadow-lg shadow-black/5 dark:shadow-black/20',
77+
78+
// Spring animation on hover
79+
'transition-all duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)]',
80+
81+
// Hover effects - lift and enhanced border
82+
'hover:scale-[1.02] hover:-translate-y-1',
83+
'hover:shadow-2xl hover:shadow-blue-500/10',
84+
'hover:border-blue-300/60 dark:hover:border-blue-700/50',
85+
86+
// Press feedback
87+
'active:scale-[0.98]',
88+
89+
// Animation entrance
90+
'animate-fade-in-up',
91+
92+
className
93+
)}
94+
>
95+
<div className="flex flex-col gap-4">
96+
{/* Icon + Title/Description */}
97+
<div className="flex items-start gap-3">
98+
{/* Icon container with blue gradient and glassmorphism */}
99+
<div
100+
className={cn(
101+
'p-2 rounded-lg flex-shrink-0',
102+
'bg-gradient-to-br from-blue-100 to-purple-200',
103+
'dark:from-blue-900/40 dark:to-purple-900/40',
104+
'backdrop-blur-md',
105+
'border border-blue-300/50 dark:border-blue-700/50',
106+
'shadow-inner',
107+
// Icon animation
108+
'transition-transform duration-700 ease-in-out',
109+
'group-hover:scale-110 group-hover:rotate-3'
110+
)}
111+
>
112+
<Layout className="h-5 w-5 text-blue-700 dark:text-blue-300" />
113+
</div>
114+
115+
{/* Text content with improved typography */}
116+
<div className="flex-1 min-w-0">
117+
<h3 className="text-base font-semibold tracking-tight leading-tight text-gray-900 dark:text-gray-100">
118+
{t('LAYOUT')}
119+
</h3>
120+
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mt-1">
121+
{t('CHOOSE_PREFERRED_DESIGN')}
122+
</p>
123+
</div>
124+
</div>
125+
126+
{/* Layout options - side by side */}
127+
<div className="grid grid-cols-2 gap-3">
128+
{layouts.map((layout) => {
129+
const isActive = layoutHome === layout.key;
130+
return (
131+
<button
132+
key={layout.key}
133+
onClick={() => handleLayoutChange(layout.key)}
134+
disabled={disabled}
135+
className={cn(
136+
'relative flex flex-col items-center gap-3 p-4 rounded-xl',
137+
'transition-all duration-300',
138+
'border-2',
139+
'overflow-hidden',
140+
'group/layout',
141+
isActive
142+
? 'bg-gradient-to-br from-theme-primary-50/50 via-white to-theme-primary-100/30 dark:from-gray-800 dark:via-gray-900 dark:to-theme-primary-950/30 border-theme-primary-400/50 dark:border-theme-primary-500/50 shadow-lg shadow-theme-primary-200/30 dark:shadow-theme-primary-900/20'
143+
: 'bg-white/80 dark:bg-gray-800/80 border-gray-200/50 dark:border-gray-700/50 hover:border-theme-primary-300 dark:hover:border-theme-primary-600 hover:shadow-md',
144+
disabled && 'opacity-50 cursor-not-allowed',
145+
!disabled && 'hover:scale-[1.02] active:scale-[0.98]'
146+
)}
147+
>
148+
{/* Active indicator */}
149+
{isActive && (
150+
<div className="absolute top-2 right-2 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-gray-900 animate-pulse z-10" />
151+
)}
152+
153+
{/* Layout preview image */}
154+
<div className="relative w-full h-32 rounded-lg overflow-hidden">
155+
<div
156+
className={cn(
157+
'absolute inset-0',
158+
layout.key === LayoutHome.HOME_ONE
159+
? 'bg-gradient-to-br from-theme-primary-100/20 to-theme-primary-200/20 dark:from-theme-primary-900/20 dark:to-theme-primary-800/20'
160+
: 'bg-gradient-to-br from-purple-100/20 to-pink-100/20 dark:from-purple-900/20 dark:to-pink-900/20'
161+
)}
162+
/>
163+
<Image
164+
src={layout.imageSrc}
165+
alt={`${layout.name} Layout Preview`}
166+
fill
167+
className="object-cover object-top transition-all duration-500 group-hover/layout:scale-110"
168+
sizes="(max-width: 768px) 50vw, 200px"
169+
/>
170+
{/* Overlay on hover */}
171+
<div className="absolute inset-0 bg-gradient-to-t from-black/50 via-black/20 to-transparent opacity-0 group-hover/layout:opacity-100 transition-opacity duration-300" />
172+
</div>
173+
174+
{/* Layout label and icon */}
175+
<div className="flex items-center gap-2">
176+
<div
177+
className={cn(
178+
'p-1.5 rounded-md transition-colors',
179+
isActive
180+
? 'bg-gradient-to-br from-theme-primary-500 to-theme-primary-600 text-white'
181+
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
182+
)}
183+
>
184+
{layout.icon}
185+
</div>
186+
<span
187+
className={cn(
188+
'text-sm font-semibold',
189+
isActive
190+
? 'text-theme-primary-600 dark:text-theme-primary-400'
191+
: 'text-gray-700 dark:text-gray-300'
192+
)}
193+
>
194+
{layout.label}
195+
</span>
196+
</div>
197+
</button>
198+
);
199+
})}
200+
</div>
201+
</div>
202+
</div>
203+
);
204+
};
205+
206+
export default SelectLayout;

0 commit comments

Comments
 (0)