metabinaries 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +32 -0
- package/src/constants.js +94 -0
- package/src/index.js +219 -0
- package/src/templates/app-clean.js +1066 -0
- package/src/templates/configs.js +391 -0
- package/src/templates/core-clean.js +269 -0
- package/src/templates/feature-ai-chat.js +1786 -0
- package/src/templates/folder-structure.js +29 -0
- package/src/templates/layout-clean.js +788 -0
- package/src/templates/misc.js +275 -0
- package/src/templates/packages.js +74 -0
- package/src/templates/ui-2.js +585 -0
- package/src/templates/ui-3.js +606 -0
- package/src/templates/ui-4.js +1025 -0
- package/src/templates/ui.js +777 -0
- package/src/utils.js +38 -0
|
@@ -0,0 +1,788 @@
|
|
|
1
|
+
// Clean Layout & Components (Simple Frontend Only)
|
|
2
|
+
export const layoutTemplates = {
|
|
3
|
+
'lib/providers.tsx': (projectName, options = {}) => {
|
|
4
|
+
const { includeRedux = false, includeAIChat = false } = options;
|
|
5
|
+
const useRedux = includeRedux || includeAIChat;
|
|
6
|
+
|
|
7
|
+
return `'use client';
|
|
8
|
+
|
|
9
|
+
import { ThemeProvider as NextThemeProvider } from 'next-themes';
|
|
10
|
+
${useRedux ? "import { StoreProvider } from '@/store/features/aiChat/Provider';" : ""}
|
|
11
|
+
import { ReactNode } from 'react';
|
|
12
|
+
import { Toaster } from '@/components/ui/sonner';
|
|
13
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
14
|
+
|
|
15
|
+
const queryClient = new QueryClient({
|
|
16
|
+
defaultOptions: {
|
|
17
|
+
queries: {
|
|
18
|
+
staleTime: 5 * 1000,
|
|
19
|
+
retry: 1,
|
|
20
|
+
refetchOnWindowFocus: false,
|
|
21
|
+
refetchOnReconnect: true,
|
|
22
|
+
},
|
|
23
|
+
mutations: {
|
|
24
|
+
retry: 1,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
interface ProvidersProps {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function Providers({ children }: ProvidersProps) {
|
|
34
|
+
return (
|
|
35
|
+
<QueryClientProvider client={queryClient}>
|
|
36
|
+
${useRedux ? "<StoreProvider>" : ""}
|
|
37
|
+
<NextThemeProvider
|
|
38
|
+
attribute='class'
|
|
39
|
+
defaultTheme='system'
|
|
40
|
+
enableSystem
|
|
41
|
+
disableTransitionOnChange
|
|
42
|
+
themes={['light', 'dark', 'system']}
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
<Toaster
|
|
46
|
+
position='bottom-right'
|
|
47
|
+
expand={true}
|
|
48
|
+
richColors={true}
|
|
49
|
+
closeButton={true}
|
|
50
|
+
/>
|
|
51
|
+
</NextThemeProvider>
|
|
52
|
+
${useRedux ? "</StoreProvider>" : ""}
|
|
53
|
+
</QueryClientProvider>
|
|
54
|
+
);
|
|
55
|
+
}`;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
'components/layout/LanguageSwitcher.tsx': `'use client';
|
|
59
|
+
|
|
60
|
+
import { Globe } from 'lucide-react';
|
|
61
|
+
import { Button } from '@/components/ui/button';
|
|
62
|
+
import {
|
|
63
|
+
DropdownMenu,
|
|
64
|
+
DropdownMenuContent,
|
|
65
|
+
DropdownMenuItem,
|
|
66
|
+
DropdownMenuTrigger,
|
|
67
|
+
} from '@/components/ui/dropdown-menu';
|
|
68
|
+
import { useParams, useSearchParams } from 'next/navigation';
|
|
69
|
+
import { useRouter, usePathname } from '@/i18n/navigation';
|
|
70
|
+
import { cn } from '@/lib/utils';
|
|
71
|
+
import { useTransition } from 'react';
|
|
72
|
+
|
|
73
|
+
type Locale = 'en' | 'ar';
|
|
74
|
+
|
|
75
|
+
const LANGUAGES = [
|
|
76
|
+
{ code: 'en', name: 'English', flag: '🇬🇧' },
|
|
77
|
+
{ code: 'ar', name: 'العربية', flag: '🇸🇦', dir: 'rtl' },
|
|
78
|
+
] as const;
|
|
79
|
+
|
|
80
|
+
export function LanguageSwitcher() {
|
|
81
|
+
const params = useParams();
|
|
82
|
+
const searchParams = useSearchParams();
|
|
83
|
+
const router = useRouter();
|
|
84
|
+
const pathname = usePathname();
|
|
85
|
+
const [isPending, startTransition] = useTransition();
|
|
86
|
+
|
|
87
|
+
const currentLocale = (params?.locale as Locale) || 'en';
|
|
88
|
+
|
|
89
|
+
const handleLocaleChange = (newLocale: Locale) => {
|
|
90
|
+
if (newLocale === currentLocale || isPending) return;
|
|
91
|
+
|
|
92
|
+
startTransition(() => {
|
|
93
|
+
// Build query string from current search params
|
|
94
|
+
const queryString = searchParams.toString();
|
|
95
|
+
const pathnameWithQuery = queryString
|
|
96
|
+
? \`\${pathname}?\${queryString}\`
|
|
97
|
+
: pathname;
|
|
98
|
+
|
|
99
|
+
router.replace(
|
|
100
|
+
pathnameWithQuery,
|
|
101
|
+
{ locale: newLocale }
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<DropdownMenu>
|
|
108
|
+
<DropdownMenuTrigger asChild>
|
|
109
|
+
<Button
|
|
110
|
+
variant="ghost"
|
|
111
|
+
size="sm"
|
|
112
|
+
title="Change Language"
|
|
113
|
+
disabled={isPending}
|
|
114
|
+
className="relative text-foreground hover:bg-accent transition-colors gap-2 px-3 h-10 focus-visible:outline-none focus:ring-0 focus:ring-offset-0"
|
|
115
|
+
aria-label="Change language"
|
|
116
|
+
>
|
|
117
|
+
<Globe className={cn(
|
|
118
|
+
'h-[18px] w-[18px] text-muted-foreground',
|
|
119
|
+
isPending && 'animate-spin'
|
|
120
|
+
)} />
|
|
121
|
+
<span className="text-sm font-semibold uppercase tracking-wide">
|
|
122
|
+
{currentLocale}
|
|
123
|
+
</span>
|
|
124
|
+
<span className="sr-only">Change Language</span>
|
|
125
|
+
</Button>
|
|
126
|
+
</DropdownMenuTrigger>
|
|
127
|
+
<DropdownMenuContent align="end" className="min-w-[160px]" onCloseAutoFocus={(e) => e.preventDefault()}>
|
|
128
|
+
{LANGUAGES.map((lang) => {
|
|
129
|
+
const isActive = currentLocale === lang.code;
|
|
130
|
+
return (
|
|
131
|
+
<DropdownMenuItem
|
|
132
|
+
key={lang.code}
|
|
133
|
+
onClick={() => handleLocaleChange(lang.code as Locale)}
|
|
134
|
+
disabled={isPending}
|
|
135
|
+
className={cn(
|
|
136
|
+
'cursor-pointer justify-between',
|
|
137
|
+
isActive && 'bg-muted font-medium'
|
|
138
|
+
)}
|
|
139
|
+
>
|
|
140
|
+
<span className="flex items-center gap-2">
|
|
141
|
+
<span className="text-lg">{lang.flag}</span>
|
|
142
|
+
<span>{lang.name}</span>
|
|
143
|
+
</span>
|
|
144
|
+
{isActive && (
|
|
145
|
+
<span className="text-primary font-bold">✓</span>
|
|
146
|
+
)}
|
|
147
|
+
</DropdownMenuItem>
|
|
148
|
+
);
|
|
149
|
+
})}
|
|
150
|
+
</DropdownMenuContent>
|
|
151
|
+
</DropdownMenu>
|
|
152
|
+
);
|
|
153
|
+
}`,
|
|
154
|
+
|
|
155
|
+
'components/layout/ThemeSwitcher.tsx': `'use client';
|
|
156
|
+
|
|
157
|
+
import { Moon, Sun, Monitor } from 'lucide-react';
|
|
158
|
+
import { useTheme } from 'next-themes';
|
|
159
|
+
import { Button } from '@/components/ui/button';
|
|
160
|
+
import {
|
|
161
|
+
DropdownMenu,
|
|
162
|
+
DropdownMenuContent,
|
|
163
|
+
DropdownMenuItem,
|
|
164
|
+
DropdownMenuTrigger,
|
|
165
|
+
} from '@/components/ui/dropdown-menu';
|
|
166
|
+
|
|
167
|
+
export function ThemeSwitcher() {
|
|
168
|
+
const { theme, setTheme } = useTheme();
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<DropdownMenu>
|
|
172
|
+
<DropdownMenuTrigger asChild>
|
|
173
|
+
<Button
|
|
174
|
+
variant='ghost'
|
|
175
|
+
size='sm'
|
|
176
|
+
title='Toggle theme'
|
|
177
|
+
className='text-foreground hover:bg-accent h-10 w-10 p-0 rounded-full transition-colors focus-visible:outline-none focus:ring-0 focus:ring-offset-0'
|
|
178
|
+
>
|
|
179
|
+
<Sun className='h-[18px] w-[18px] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0 text-muted-foreground' />
|
|
180
|
+
<Moon className='absolute h-[18px] w-[18px] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100 text-muted-foreground' />
|
|
181
|
+
<span className='sr-only'>Toggle theme</span>
|
|
182
|
+
</Button>
|
|
183
|
+
</DropdownMenuTrigger>
|
|
184
|
+
<DropdownMenuContent align='end' onCloseAutoFocus={(e) => e.preventDefault()}>
|
|
185
|
+
<DropdownMenuItem onClick={() => setTheme('light')}>
|
|
186
|
+
<Sun className='mr-2 h-4 w-4' />
|
|
187
|
+
Light
|
|
188
|
+
{theme === 'light' && <span className='ml-auto'>✓</span>}
|
|
189
|
+
</DropdownMenuItem>
|
|
190
|
+
<DropdownMenuItem onClick={() => setTheme('dark')}>
|
|
191
|
+
<Moon className='mr-2 h-4 w-4' />
|
|
192
|
+
Dark
|
|
193
|
+
{theme === 'dark' && <span className='ml-auto'>✓</span>}
|
|
194
|
+
</DropdownMenuItem>
|
|
195
|
+
<DropdownMenuItem onClick={() => setTheme('system')}>
|
|
196
|
+
<Monitor className='mr-2 h-4 w-4' />
|
|
197
|
+
System
|
|
198
|
+
{theme === 'system' && <span className='ml-auto'>✓</span>}
|
|
199
|
+
</DropdownMenuItem>
|
|
200
|
+
</DropdownMenuContent>
|
|
201
|
+
</DropdownMenu>
|
|
202
|
+
);
|
|
203
|
+
}`,
|
|
204
|
+
|
|
205
|
+
'components/layout/Header.tsx': `'use client';
|
|
206
|
+
|
|
207
|
+
import Link from 'next/link';
|
|
208
|
+
import * as React from 'react';
|
|
209
|
+
import { usePathname } from 'next/navigation';
|
|
210
|
+
import { LanguageSwitcher } from './LanguageSwitcher';
|
|
211
|
+
import { ThemeSwitcher } from './ThemeSwitcher';
|
|
212
|
+
import { Button } from '@/components/ui/button';
|
|
213
|
+
import { Menu, X, ArrowRight } from 'lucide-react';
|
|
214
|
+
import { useState } from 'react';
|
|
215
|
+
import Logo from '@/components/ui/logo';
|
|
216
|
+
import { cn } from '@/lib/utils';
|
|
217
|
+
|
|
218
|
+
const defaultNavigation = [
|
|
219
|
+
{ name: 'Home', href: '/' },
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
export function Header({
|
|
223
|
+
siteName = 'MetaBinaries',
|
|
224
|
+
navigation = defaultNavigation,
|
|
225
|
+
}) {
|
|
226
|
+
const pathname = usePathname();
|
|
227
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
228
|
+
|
|
229
|
+
const locale = pathname?.split('/')[1] || 'en';
|
|
230
|
+
const currentPath = pathname?.replace(\`/\${locale}\`, '') || '/';
|
|
231
|
+
|
|
232
|
+
return (
|
|
233
|
+
<header className='sticky top-0 z-50 w-full bg-background/80 backdrop-blur-md border-b border-border/40 py-4'>
|
|
234
|
+
<div className='w-full flex h-14 items-center justify-between px-4 sm:px-12'>
|
|
235
|
+
|
|
236
|
+
<Link
|
|
237
|
+
href={\`/\${locale}\`}
|
|
238
|
+
className='flex items-center gap-3 group shrink-0'
|
|
239
|
+
>
|
|
240
|
+
<Logo className='w-auto h-auto' />
|
|
241
|
+
<span className='text-xl font-bold tracking-tight text-foreground'>
|
|
242
|
+
{siteName}
|
|
243
|
+
</span>
|
|
244
|
+
</Link>
|
|
245
|
+
|
|
246
|
+
{/* Desktop Nav */}
|
|
247
|
+
<nav className='hidden md:flex items-center gap-1 p-1'>
|
|
248
|
+
{navigation.map(item => {
|
|
249
|
+
const isActive =
|
|
250
|
+
currentPath === item.href ||
|
|
251
|
+
(item.href !== '/' && currentPath.startsWith(item.href));
|
|
252
|
+
return (
|
|
253
|
+
<Link
|
|
254
|
+
key={item.name}
|
|
255
|
+
href={\`/\${locale}\${item.href}\`}
|
|
256
|
+
className={cn(
|
|
257
|
+
'px-6 py-2 rounded-full text-sm font-medium transition-all duration-300',
|
|
258
|
+
isActive
|
|
259
|
+
? 'text-blue-600 bg-blue-500/10 dark:text-blue-400'
|
|
260
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
261
|
+
)}
|
|
262
|
+
>
|
|
263
|
+
{item.name}
|
|
264
|
+
</Link>
|
|
265
|
+
);
|
|
266
|
+
})}
|
|
267
|
+
</nav>
|
|
268
|
+
|
|
269
|
+
<div className='flex items-center gap-4'>
|
|
270
|
+
<div className='hidden sm:flex items-center gap-2'>
|
|
271
|
+
<React.Suspense fallback={<div className='w-8 h-8' />}>
|
|
272
|
+
<LanguageSwitcher />
|
|
273
|
+
</React.Suspense>
|
|
274
|
+
<div className='w-[1px] h-4 bg-border mx-1' />
|
|
275
|
+
<ThemeSwitcher />
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<Button className='hidden md:flex rounded-full bg-blue-600 hover:bg-blue-700 text-white px-6 h-11 border-none font-semibold shadow-lg shadow-blue-500/20 gap-2 transition-all hover:translate-y-[-1px] active:scale-95'>
|
|
279
|
+
Start Project
|
|
280
|
+
<ArrowRight className='w-4 h-4' />
|
|
281
|
+
</Button>
|
|
282
|
+
|
|
283
|
+
<Button
|
|
284
|
+
variant='ghost'
|
|
285
|
+
size='icon'
|
|
286
|
+
className='md:hidden rounded-full text-foreground hover:bg-accent'
|
|
287
|
+
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
288
|
+
>
|
|
289
|
+
{mobileMenuOpen ? (
|
|
290
|
+
<X className='w-6 h-6' />
|
|
291
|
+
) : (
|
|
292
|
+
<Menu className='w-6 h-6' />
|
|
293
|
+
)}
|
|
294
|
+
</Button>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
|
|
298
|
+
{/* Mobile Menu */}
|
|
299
|
+
{mobileMenuOpen && (
|
|
300
|
+
<div className='md:hidden absolute top-full left-0 w-full bg-background border-b border-border animate-in fade-in slide-in-from-top-4'>
|
|
301
|
+
<div className='container py-8 flex flex-col gap-6'>
|
|
302
|
+
{navigation.map(item => (
|
|
303
|
+
<Link
|
|
304
|
+
key={item.name}
|
|
305
|
+
href={\`/\${locale}\${item.href}\`}
|
|
306
|
+
onClick={() => setMobileMenuOpen(false)}
|
|
307
|
+
className='text-xl font-medium px-4 py-3 text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition-colors'
|
|
308
|
+
>
|
|
309
|
+
{item.name}
|
|
310
|
+
</Link>
|
|
311
|
+
))}
|
|
312
|
+
<div className='flex items-center justify-between gap-4 px-4 pt-6 border-t border-border'>
|
|
313
|
+
<div className='flex items-center gap-4'>
|
|
314
|
+
<React.Suspense fallback={<div className='w-8 h-8' />}>
|
|
315
|
+
<LanguageSwitcher />
|
|
316
|
+
</React.Suspense>
|
|
317
|
+
<ThemeSwitcher />
|
|
318
|
+
</div>
|
|
319
|
+
<Button className='rounded-xl bg-blue-600 px-8 text-white'>
|
|
320
|
+
Get Started
|
|
321
|
+
</Button>
|
|
322
|
+
</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
)}
|
|
326
|
+
</header>
|
|
327
|
+
);
|
|
328
|
+
}`,
|
|
329
|
+
|
|
330
|
+
'components/layout/Footer.tsx': `'use client';
|
|
331
|
+
|
|
332
|
+
import React from 'react';
|
|
333
|
+
import Link from 'next/link';
|
|
334
|
+
import { Facebook, Twitter, Instagram, Github, Mail } from 'lucide-react';
|
|
335
|
+
import Logo from '@/components/ui/logo';
|
|
336
|
+
|
|
337
|
+
interface FooterProps {
|
|
338
|
+
siteName?: string;
|
|
339
|
+
description?: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function Footer({
|
|
343
|
+
siteName = 'MetaBinaries',
|
|
344
|
+
description = 'Empowering businesses with modern digital solutions and intelligent workspaces.'
|
|
345
|
+
}: FooterProps) {
|
|
346
|
+
const currentYear = new Date().getFullYear();
|
|
347
|
+
|
|
348
|
+
const footerLinks = [
|
|
349
|
+
{
|
|
350
|
+
title: 'Product',
|
|
351
|
+
links: [
|
|
352
|
+
{ name: 'Features', href: '/#features' },
|
|
353
|
+
{ name: 'Documentation', href: 'https://nextjs.org/docs' },
|
|
354
|
+
],
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
title: 'Company',
|
|
358
|
+
links: [
|
|
359
|
+
{ name: 'Privacy Policy', href: '/privacy' },
|
|
360
|
+
{ name: 'Terms', href: '/terms' },
|
|
361
|
+
],
|
|
362
|
+
},
|
|
363
|
+
];
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<footer className='bg-slate-50 dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 transition-colors duration-300'>
|
|
367
|
+
<div className='container mx-auto px-4 pt-16 pb-8'>
|
|
368
|
+
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-12 mb-12'>
|
|
369
|
+
{/* Brand Section */}
|
|
370
|
+
<div className='flex flex-col gap-4'>
|
|
371
|
+
<Link href='/' className='flex items-center gap-2 group'>
|
|
372
|
+
<Logo className='w-10 h-10 group-hover:scale-110 transition-transform' />
|
|
373
|
+
<span className='text-2xl font-bold tracking-tight text-slate-900 dark:text-white'>
|
|
374
|
+
{siteName}
|
|
375
|
+
</span>
|
|
376
|
+
</Link>
|
|
377
|
+
<p className='text-slate-500 dark:text-slate-400 text-sm leading-relaxed max-w-xs'>
|
|
378
|
+
{description}
|
|
379
|
+
</p>
|
|
380
|
+
<div className='flex items-center gap-4 mt-2'>
|
|
381
|
+
<Link href='#' className='text-slate-400 hover:text-blue-600 transition-colors'><Facebook size={20} /></Link>
|
|
382
|
+
<Link href='#' className='text-slate-400 hover:text-blue-400 transition-colors'><Twitter size={20} /></Link>
|
|
383
|
+
<Link href='#' className='text-slate-400 hover:text-pink-600 transition-colors'><Instagram size={20} /></Link>
|
|
384
|
+
<Link href='#' className='text-slate-400 hover:text-slate-900 dark:hover:text-white transition-colors'><Github size={20} /></Link>
|
|
385
|
+
</div>
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{/* Links Sections */}
|
|
389
|
+
{footerLinks.map((section) => (
|
|
390
|
+
<div key={section.title} className='flex flex-col gap-4'>
|
|
391
|
+
<h4 className='text-sm font-bold uppercase tracking-widest text-slate-900 dark:text-white'>
|
|
392
|
+
{section.title}
|
|
393
|
+
</h4>
|
|
394
|
+
<ul className='flex flex-col gap-2'>
|
|
395
|
+
{section.links.map((link) => (
|
|
396
|
+
<li key={link.name}>
|
|
397
|
+
<Link
|
|
398
|
+
href={link.href}
|
|
399
|
+
className='text-slate-500 dark:text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 text-sm transition-colors decoration-2 underline-offset-4 hover:underline'
|
|
400
|
+
>
|
|
401
|
+
{link.name}
|
|
402
|
+
</Link>
|
|
403
|
+
</li>
|
|
404
|
+
))}
|
|
405
|
+
</ul>
|
|
406
|
+
</div>
|
|
407
|
+
))}
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
{/* Bottom Bar */}
|
|
411
|
+
<div className='pt-8 border-t border-slate-200 dark:border-slate-800 flex flex-col md:flex-row justify-between items-center gap-4'>
|
|
412
|
+
<p className='text-slate-400 text-xs'>
|
|
413
|
+
© {currentYear} {siteName}. All rights reserved.
|
|
414
|
+
</p>
|
|
415
|
+
<div className='flex items-center gap-6'>
|
|
416
|
+
<Link href='#' className='text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 text-xs flex items-center gap-1 transition-colors'>
|
|
417
|
+
<Mail size={14} />
|
|
418
|
+
hello@{siteName.toLowerCase()}.com
|
|
419
|
+
</Link>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
</footer>
|
|
424
|
+
);
|
|
425
|
+
}`,
|
|
426
|
+
|
|
427
|
+
'components/ui/logo.tsx': `'use client';
|
|
428
|
+
|
|
429
|
+
import React from 'react';
|
|
430
|
+
import { cn } from '@/lib/utils';
|
|
431
|
+
|
|
432
|
+
interface LogoProps {
|
|
433
|
+
className?: string;
|
|
434
|
+
showText?: boolean;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const Logo: React.FC<LogoProps> = ({ className }) => {
|
|
438
|
+
return (
|
|
439
|
+
<div className={cn('flex items-center gap-2', className)}>
|
|
440
|
+
<div className='relative w-8 h-8 flex items-center justify-center bg-blue-600 rounded-lg shrink-0 overflow-hidden'>
|
|
441
|
+
<div className='w-3 h-3 bg-white rounded-[2px]' />
|
|
442
|
+
{/* Subtle inner detail */}
|
|
443
|
+
<div className='absolute inset-0 border-[3px] border-white/10 rounded-lg' />
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
export default Logo;`,
|
|
450
|
+
|
|
451
|
+
'components/layout/MainMenu.tsx': `'use client';
|
|
452
|
+
import React from 'react';
|
|
453
|
+
import Link from 'next/link';
|
|
454
|
+
import { X } from 'lucide-react';
|
|
455
|
+
|
|
456
|
+
export interface MenuLink {
|
|
457
|
+
name: string;
|
|
458
|
+
url: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export interface MenuSection {
|
|
462
|
+
title: string;
|
|
463
|
+
links: MenuLink[];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
interface MainMenuProps {
|
|
467
|
+
open: boolean;
|
|
468
|
+
onClose: () => void;
|
|
469
|
+
sections: MenuSection[];
|
|
470
|
+
isRTL?: boolean;
|
|
471
|
+
className?: string;
|
|
472
|
+
overlayClassName?: string;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export const MainMenu: React.FC<MainMenuProps> = ({
|
|
476
|
+
open,
|
|
477
|
+
onClose,
|
|
478
|
+
sections,
|
|
479
|
+
isRTL = false,
|
|
480
|
+
className = '',
|
|
481
|
+
overlayClassName = '',
|
|
482
|
+
}) => {
|
|
483
|
+
if (!open) return null;
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<div
|
|
487
|
+
className={\`fixed inset-0 z-50 pt-16 xl:pt-20 bg-black/95 backdrop-blur-md flex animate-in fade-in slide-in-from-bottom-8 overflow-y-auto \${overlayClassName}\`}
|
|
488
|
+
>
|
|
489
|
+
<button
|
|
490
|
+
className={\`fixed top-4 xl:top-6 \${isRTL ? 'left-4 xl:left-6' : 'right-4 xl:right-6'
|
|
491
|
+
} text-white hover:bg-white/10 p-2 rounded-full transition-colors z-[60]\`}
|
|
492
|
+
onClick={onClose}
|
|
493
|
+
aria-label='Close menu'
|
|
494
|
+
>
|
|
495
|
+
<X className='w-6 h-6' />
|
|
496
|
+
</button>
|
|
497
|
+
|
|
498
|
+
<div className={\`container mx-auto px-6 py-12 \${className}\`}>
|
|
499
|
+
<div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-12'>
|
|
500
|
+
{sections.map((section, idx) => (
|
|
501
|
+
<div key={idx} className='flex flex-col'>
|
|
502
|
+
<h3 className='text-white text-xl font-bold mb-6 pb-2 border-b border-white/20 uppercase tracking-wider'>
|
|
503
|
+
{section.title}
|
|
504
|
+
</h3>
|
|
505
|
+
<div className='flex flex-col space-y-4'>
|
|
506
|
+
{section.links.map((link, lIdx) => (
|
|
507
|
+
<Link
|
|
508
|
+
key={lIdx}
|
|
509
|
+
href={link.url}
|
|
510
|
+
onClick={onClose}
|
|
511
|
+
className='text-white/70 hover:text-white transition-colors text-lg hover:translate-x-1 duration-200 inline-block'
|
|
512
|
+
>
|
|
513
|
+
{link.name}
|
|
514
|
+
</Link>
|
|
515
|
+
))}
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
))}
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
};
|
|
524
|
+
export default MainMenu;`,
|
|
525
|
+
|
|
526
|
+
'components/layout/MobileNavbar.tsx': `'use client';
|
|
527
|
+
import Link from 'next/link';
|
|
528
|
+
import { Button } from '@/components/ui/button';
|
|
529
|
+
import { MenuIcon, UserIcon, Search, LogOut, X, Shield } from 'lucide-react';
|
|
530
|
+
import { useState } from 'react';
|
|
531
|
+
import { useTranslations, useLocale } from 'next-intl';
|
|
532
|
+
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
|
533
|
+
import Logo from '@/components/ui/logo';
|
|
534
|
+
|
|
535
|
+
const MobileNavbar = () => {
|
|
536
|
+
const t = useTranslations('aiChat'); // Fallback to aiChat or similar if wanted
|
|
537
|
+
const locale = useLocale();
|
|
538
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
539
|
+
const router = useRouter();
|
|
540
|
+
const pathname = usePathname();
|
|
541
|
+
const searchParams = useSearchParams();
|
|
542
|
+
|
|
543
|
+
const currentLocale = pathname?.split('/')[1] === 'ar' ? 'ar' : 'en';
|
|
544
|
+
const otherLocale = currentLocale === 'en' ? 'ar' : 'en';
|
|
545
|
+
const basePath = pathname?.replace(/^\\/(en|ar)/, '') || '/';
|
|
546
|
+
|
|
547
|
+
const searchParamsString = searchParams.toString();
|
|
548
|
+
const queryString = searchParamsString ? \`?\${searchParamsString}\` : '';
|
|
549
|
+
|
|
550
|
+
const currentSection =
|
|
551
|
+
pathname
|
|
552
|
+
?.replace(/^\\/(en|ar)/, '')
|
|
553
|
+
.split('/')
|
|
554
|
+
.filter(Boolean)[0] || '';
|
|
555
|
+
|
|
556
|
+
const handleLinkClick = () => {
|
|
557
|
+
setMenuOpen(false);
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
const isRTL = locale === 'ar';
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
<>
|
|
564
|
+
<header className='lg:hidden w-full flex sticky top-0 z-50'>
|
|
565
|
+
<nav className='w-full flex items-center justify-between px-3 md:px-6 bg-blue-600 border-b-blue-500 border-b-2 h-14 sm:h-16 text-white'>
|
|
566
|
+
<Link href={\`/\${currentLocale}\`} onClick={handleLinkClick} className='flex-shrink-0'>
|
|
567
|
+
<Logo className='w-10 h-10 sm:w-12' />
|
|
568
|
+
</Link>
|
|
569
|
+
|
|
570
|
+
<div className='flex items-center gap-2'>
|
|
571
|
+
<button
|
|
572
|
+
className='flex items-center gap-1 px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition text-sm'
|
|
573
|
+
onClick={() =>
|
|
574
|
+
router.push(\`/\${otherLocale}\${basePath}\${queryString}\`)
|
|
575
|
+
}
|
|
576
|
+
>
|
|
577
|
+
{otherLocale === 'ar' ? '🇸🇦' : '🇬🇧'}
|
|
578
|
+
</button>
|
|
579
|
+
+
|
|
580
|
+
<button
|
|
581
|
+
className='p-2 rounded-full hover:bg-white/10 transition'
|
|
582
|
+
onClick={() => setMenuOpen(!menuOpen)}
|
|
583
|
+
>
|
|
584
|
+
{menuOpen ? <X className='w-5 h-5' /> : <MenuIcon className='w-5 h-5' />}
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
</nav>
|
|
588
|
+
</header>
|
|
589
|
+
|
|
590
|
+
{menuOpen && (
|
|
591
|
+
<div className='lg:hidden fixed inset-0 z-50 pt-16 bg-blue-600 bg-opacity-95 animate-in fade-in slide-in-from-top-8'>
|
|
592
|
+
<div className='flex flex-col h-full'>
|
|
593
|
+
<div className='flex-1 px-6 pb-6 overflow-y-auto'>
|
|
594
|
+
<ul className='space-y-4 text-white'>
|
|
595
|
+
{[
|
|
596
|
+
{ name: 'Home', path: '' },
|
|
597
|
+
].map((item) => (
|
|
598
|
+
<li key={item.path}>
|
|
599
|
+
<Link
|
|
600
|
+
href={\`/\${currentLocale}/\${item.path}\`}
|
|
601
|
+
onClick={handleLinkClick}
|
|
602
|
+
className={\`block py-3 text-lg font-semibold rounded-lg transition-colors \${currentSection === item.path
|
|
603
|
+
? 'bg-white/20 text-white'
|
|
604
|
+
: 'hover:bg-white/10'
|
|
605
|
+
}\`}
|
|
606
|
+
>
|
|
607
|
+
{item.name}
|
|
608
|
+
</Link>
|
|
609
|
+
</li>
|
|
610
|
+
))}
|
|
611
|
+
</ul>
|
|
612
|
+
|
|
613
|
+
<div className='mt-8 pt-8 border-t border-white/20 space-y-4'>
|
|
614
|
+
<Button className='w-full bg-white text-blue-600 hover:bg-white/90 h-12 rounded-xl font-bold'>
|
|
615
|
+
Get Started
|
|
616
|
+
</Button>
|
|
617
|
+
</div>
|
|
618
|
+
</div>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
)}
|
|
622
|
+
</>
|
|
623
|
+
);
|
|
624
|
+
};
|
|
625
|
+
export default MobileNavbar;`,
|
|
626
|
+
|
|
627
|
+
'components/shared/loading-ui.tsx': `'use client';
|
|
628
|
+
|
|
629
|
+
import { cn } from '@/lib/utils';
|
|
630
|
+
import type { VariantProps } from 'class-variance-authority';
|
|
631
|
+
import { cva } from 'class-variance-authority';
|
|
632
|
+
import { motion } from 'framer-motion';
|
|
633
|
+
import Logo from '@/components/ui/logo';
|
|
634
|
+
|
|
635
|
+
const loadingVariants = cva(
|
|
636
|
+
'flex items-center justify-center relative overflow-hidden',
|
|
637
|
+
{
|
|
638
|
+
variants: {
|
|
639
|
+
variant: {
|
|
640
|
+
default: 'min-h-[100dvh] w-full bg-background',
|
|
641
|
+
inline: 'h-full w-full py-12',
|
|
642
|
+
overlay: 'fixed inset-0 z-[100] bg-background/80 backdrop-blur-md',
|
|
643
|
+
},
|
|
644
|
+
size: {
|
|
645
|
+
sm: 'scale-75',
|
|
646
|
+
default: 'scale-100',
|
|
647
|
+
lg: 'scale-125',
|
|
648
|
+
},
|
|
649
|
+
},
|
|
650
|
+
defaultVariants: {
|
|
651
|
+
variant: 'default',
|
|
652
|
+
size: 'default',
|
|
653
|
+
},
|
|
654
|
+
}
|
|
655
|
+
);
|
|
656
|
+
|
|
657
|
+
interface LoadingProps extends VariantProps<typeof loadingVariants> {
|
|
658
|
+
text?: string;
|
|
659
|
+
className?: string;
|
|
660
|
+
showDots?: boolean;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
export function Loading({
|
|
664
|
+
variant,
|
|
665
|
+
size,
|
|
666
|
+
text = 'Loading',
|
|
667
|
+
className,
|
|
668
|
+
showDots = true
|
|
669
|
+
}: LoadingProps) {
|
|
670
|
+
return (
|
|
671
|
+
<div
|
|
672
|
+
className={cn(loadingVariants({ variant, size }), className)}
|
|
673
|
+
role="status"
|
|
674
|
+
aria-live="polite"
|
|
675
|
+
aria-label={text}
|
|
676
|
+
>
|
|
677
|
+
{/* Dynamic Background Blobs */}
|
|
678
|
+
{variant === 'default' && (
|
|
679
|
+
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
680
|
+
<motion.div
|
|
681
|
+
animate={{
|
|
682
|
+
scale: [1, 1.2, 1],
|
|
683
|
+
x: [0, 50, 0],
|
|
684
|
+
y: [0, 30, 0],
|
|
685
|
+
}}
|
|
686
|
+
transition={{
|
|
687
|
+
duration: 8,
|
|
688
|
+
repeat: Infinity,
|
|
689
|
+
ease: "easeInOut"
|
|
690
|
+
}}
|
|
691
|
+
className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] bg-blue-500/5 rounded-full blur-[100px]"
|
|
692
|
+
/>
|
|
693
|
+
<motion.div
|
|
694
|
+
animate={{
|
|
695
|
+
scale: [1, 1.1, 1],
|
|
696
|
+
x: [0, -30, 0],
|
|
697
|
+
y: [0, -50, 0],
|
|
698
|
+
}}
|
|
699
|
+
transition={{
|
|
700
|
+
duration: 10,
|
|
701
|
+
repeat: Infinity,
|
|
702
|
+
ease: "easeInOut",
|
|
703
|
+
delay: 1
|
|
704
|
+
}}
|
|
705
|
+
className="absolute bottom-[-10%] right-[-10%] w-[50%] h-[50%] bg-blue-600/5 rounded-full blur-[100px]"
|
|
706
|
+
/>
|
|
707
|
+
</div>
|
|
708
|
+
)}
|
|
709
|
+
|
|
710
|
+
<div className="flex flex-col items-center gap-8 relative z-10">
|
|
711
|
+
{/* Modern Animated Logo Container */}
|
|
712
|
+
<div className="relative flex items-center justify-center">
|
|
713
|
+
{/* Outer Rotating Ring */}
|
|
714
|
+
<motion.div
|
|
715
|
+
animate={{ rotate: 360 }}
|
|
716
|
+
transition={{ duration: 3, repeat: Infinity, ease: "linear" }}
|
|
717
|
+
className="absolute w-24 h-24 rounded-full border-[3px] border-blue-600/10 border-t-blue-600"
|
|
718
|
+
/>
|
|
719
|
+
|
|
720
|
+
{/* Inner Pulsing Circle */}
|
|
721
|
+
<motion.div
|
|
722
|
+
animate={{ scale: [0.9, 1.05, 0.9], opacity: [0.5, 0.8, 0.5] }}
|
|
723
|
+
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
|
724
|
+
className="absolute w-16 h-16 rounded-full bg-blue-600/10"
|
|
725
|
+
/>
|
|
726
|
+
|
|
727
|
+
{/* Core Logo */}
|
|
728
|
+
<motion.div
|
|
729
|
+
initial={{ scale: 0.8, opacity: 0 }}
|
|
730
|
+
animate={{ scale: 1, opacity: 1 }}
|
|
731
|
+
transition={{ duration: 0.5 }}
|
|
732
|
+
className="relative z-20 p-4 bg-blue-600 rounded-2xl shadow-xl shadow-blue-600/20"
|
|
733
|
+
>
|
|
734
|
+
<Logo className="w-8 h-8 text-white" />
|
|
735
|
+
</motion.div>
|
|
736
|
+
</div>
|
|
737
|
+
|
|
738
|
+
{/* Text Area */}
|
|
739
|
+
{text && (
|
|
740
|
+
<div className="flex flex-col items-center gap-3">
|
|
741
|
+
<div className="flex items-center gap-2">
|
|
742
|
+
<motion.p
|
|
743
|
+
initial={{ opacity: 0, y: 10 }}
|
|
744
|
+
animate={{ opacity: 1, y: 0 }}
|
|
745
|
+
transition={{ delay: 0.2 }}
|
|
746
|
+
className="text-lg font-bold tracking-tight text-foreground"
|
|
747
|
+
>
|
|
748
|
+
{text}
|
|
749
|
+
</motion.p>
|
|
750
|
+
{showDots && (
|
|
751
|
+
<div className="flex gap-1.5 pt-1">
|
|
752
|
+
{[0, 1, 2].map((i) => (
|
|
753
|
+
<motion.span
|
|
754
|
+
key={i}
|
|
755
|
+
animate={{
|
|
756
|
+
scale: [1, 1.5, 1],
|
|
757
|
+
opacity: [0.3, 1, 0.3],
|
|
758
|
+
}}
|
|
759
|
+
transition={{
|
|
760
|
+
duration: 1,
|
|
761
|
+
repeat: Infinity,
|
|
762
|
+
delay: i * 0.2,
|
|
763
|
+
ease: "easeInOut",
|
|
764
|
+
}}
|
|
765
|
+
className="h-1.5 w-1.5 rounded-full bg-blue-600"
|
|
766
|
+
/>
|
|
767
|
+
))}
|
|
768
|
+
</div>
|
|
769
|
+
)}
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<motion.p
|
|
773
|
+
initial={{ opacity: 0 }}
|
|
774
|
+
animate={{ opacity: 1 }}
|
|
775
|
+
transition={{ delay: 0.4 }}
|
|
776
|
+
className="text-[11px] font-semibold text-muted-foreground/50 uppercase tracking-[0.3em]"
|
|
777
|
+
>
|
|
778
|
+
Building your experience
|
|
779
|
+
</motion.p>
|
|
780
|
+
</div>
|
|
781
|
+
)}
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<span className="sr-only">Please wait while content is loading</span>
|
|
785
|
+
</div>
|
|
786
|
+
);
|
|
787
|
+
}`,
|
|
788
|
+
};
|