shortcut-next 0.2.2 → 0.2.6
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 +5 -2
- package/templates/base/@core/configs/clientConfig.ts +1 -1
- package/templates/base/@core/context/AuthContext.tsx +21 -28
- package/templates/base/@core/hooks/useAbility.ts +58 -0
- package/templates/base/app/(dashboard)/dashboard/page.tsx +104 -0
- package/templates/base/app/(dashboard)/layout.tsx +97 -0
- package/templates/base/app/home/page.tsx +112 -0
- package/templates/base/app/login/page.tsx +296 -0
- package/templates/base/app/unauthorized/page.tsx +120 -0
- package/templates/base/components/MSWProvider.tsx +54 -0
- package/templates/base/components/auth/LoginForm.tsx +279 -0
- package/templates/base/components/auth/SignupForm.tsx +348 -0
- package/templates/base/components/loaders/Spinner.tsx +5 -24
- package/templates/base/components/ui/ErrorMessage.tsx +17 -0
- package/templates/base/components/ui/FormFieldWrapper.tsx +27 -0
- package/templates/base/docs/AuthorizationDocumentation.md +348 -0
- package/templates/base/lib/abilities/checkAuthorization.ts +74 -0
- package/templates/base/lib/abilities/index.ts +27 -0
- package/templates/base/lib/abilities/roles.ts +75 -0
- package/templates/base/lib/abilities/routeMap.ts +35 -0
- package/templates/base/lib/abilities/routeMatcher.ts +117 -0
- package/templates/base/lib/abilities/types.ts +68 -0
- package/templates/base/lib/mocks/browser.ts +11 -0
- package/templates/base/lib/mocks/db.ts +124 -0
- package/templates/base/lib/mocks/handlers/auth.ts +203 -0
- package/templates/base/lib/mocks/handlers/index.ts +16 -0
- package/templates/base/lib/mocks/index.ts +34 -0
- package/templates/base/lib/mocks/jwt.ts +99 -0
- package/templates/base/middleware.ts +147 -0
- package/templates/base/package-lock.json +725 -2
- package/templates/base/package.json +13 -2
- package/templates/base/providers/AppProviders.tsx +8 -5
- package/templates/base/public/locales/ar.json +73 -0
- package/templates/base/public/locales/en.json +73 -0
- package/templates/base/public/mockServiceWorker.js +349 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Box, Stack, Typography } from '@mui/material'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { motion, AnimatePresence } from 'framer-motion'
|
|
6
|
+
import { useTranslation } from 'react-i18next'
|
|
7
|
+
import { Icon } from '@iconify/react'
|
|
8
|
+
import LoginForm from '@/components/auth/LoginForm'
|
|
9
|
+
import SignupForm from '@/components/auth/SignupForm'
|
|
10
|
+
import LanguageDropdown from '@/@core/components/LanguageDropdown'
|
|
11
|
+
import useLanguage from '@/@core/hooks/useLanguage'
|
|
12
|
+
|
|
13
|
+
const MotionBox = motion.create(Box)
|
|
14
|
+
|
|
15
|
+
const HeroCard = ({ icon, title, desc, delay = 0 }: { icon: string; title: string; desc: string; delay?: number }) => (
|
|
16
|
+
<MotionBox
|
|
17
|
+
initial={{ opacity: 0, x: -30 }}
|
|
18
|
+
animate={{ opacity: 1, x: 0 }}
|
|
19
|
+
transition={{ delay, duration: 0.6 }}
|
|
20
|
+
sx={{
|
|
21
|
+
p: 4,
|
|
22
|
+
borderRadius: 5,
|
|
23
|
+
bgcolor: (theme) => (theme.palette.mode === 'light' ? 'rgba(0, 0, 0, 0.02)' : 'rgba(255, 255, 255, 0.03)'),
|
|
24
|
+
border: (theme) => `1px solid ${theme.palette.divider}`,
|
|
25
|
+
backdropFilter: 'blur(20px)',
|
|
26
|
+
width: '100%',
|
|
27
|
+
maxWidth: 380,
|
|
28
|
+
display: 'flex',
|
|
29
|
+
gap: 3,
|
|
30
|
+
alignItems: 'flex-start',
|
|
31
|
+
boxShadow: (theme) => theme.shadows[theme.palette.mode === 'light' ? 2 : 10],
|
|
32
|
+
'&:hover': {
|
|
33
|
+
borderColor: 'primary.main',
|
|
34
|
+
bgcolor: (theme) => (theme.palette.mode === 'light' ? 'rgba(0,0,0,0.02)' : 'rgba(255, 255, 255, 0.05)'),
|
|
35
|
+
transform: 'translateY(-5px)',
|
|
36
|
+
transition: 'all 0.3s ease'
|
|
37
|
+
}
|
|
38
|
+
}}
|
|
39
|
+
>
|
|
40
|
+
<Box
|
|
41
|
+
sx={{
|
|
42
|
+
p: 2,
|
|
43
|
+
borderRadius: 5,
|
|
44
|
+
bgcolor: 'primary.main',
|
|
45
|
+
display: 'flex',
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
justifyContent: 'center',
|
|
48
|
+
boxShadow: (theme) => `0 8px 16px ${theme.palette.primary.main}40`
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<Icon icon={icon} color="white" fontSize={24} />
|
|
52
|
+
</Box>
|
|
53
|
+
<Box>
|
|
54
|
+
<Typography variant="h6" fontWeight={800} color="text.primary" gutterBottom>
|
|
55
|
+
{title}
|
|
56
|
+
</Typography>
|
|
57
|
+
<Typography variant="body2" sx={{ color: 'text.secondary', lineHeight: 1.6 }}>
|
|
58
|
+
{desc}
|
|
59
|
+
</Typography>
|
|
60
|
+
</Box>
|
|
61
|
+
</MotionBox>
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
const HeroBefore = () => {
|
|
65
|
+
const { t } = useTranslation()
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Stack
|
|
69
|
+
spacing={8}
|
|
70
|
+
sx={{ p: { xs: 6, md: 12 }, height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
|
|
71
|
+
>
|
|
72
|
+
<Box>
|
|
73
|
+
<Typography
|
|
74
|
+
variant="overline"
|
|
75
|
+
sx={{ color: 'primary.main', fontWeight: 900, letterSpacing: 2, mb: 1, display: 'block' }}
|
|
76
|
+
>
|
|
77
|
+
{t('login.heroBefore.overline', 'Welcome Back')}
|
|
78
|
+
</Typography>
|
|
79
|
+
<Typography
|
|
80
|
+
variant="h2"
|
|
81
|
+
sx={{ fontWeight: 900, mb: 2, letterSpacing: -1.5, fontSize: { md: '3rem', lg: '3.75rem' } }}
|
|
82
|
+
>
|
|
83
|
+
{t('login.heroBefore.title', 'Sign in to your account')}
|
|
84
|
+
</Typography>
|
|
85
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', maxWidth: 450, fontWeight: 400 }}>
|
|
86
|
+
{t('login.heroBefore.subtitle', 'Access your dashboard and manage your business')}
|
|
87
|
+
</Typography>
|
|
88
|
+
</Box>
|
|
89
|
+
|
|
90
|
+
<Stack spacing={3}>
|
|
91
|
+
<HeroCard
|
|
92
|
+
icon="lucide:trending-up"
|
|
93
|
+
title={t('login.heroBefore.card1.title', 'Analytics Dashboard')}
|
|
94
|
+
desc={t('login.heroBefore.card1.desc', 'Track your performance with real-time insights')}
|
|
95
|
+
delay={0.2}
|
|
96
|
+
/>
|
|
97
|
+
<HeroCard
|
|
98
|
+
icon="lucide:shield-check"
|
|
99
|
+
title={t('login.heroBefore.card2.title', 'Secure Access')}
|
|
100
|
+
desc={t('login.heroBefore.card2.desc', 'Your data is protected with enterprise-grade security')}
|
|
101
|
+
delay={0.3}
|
|
102
|
+
/>
|
|
103
|
+
</Stack>
|
|
104
|
+
</Stack>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const HeroAfter = () => {
|
|
109
|
+
const { t } = useTranslation()
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Stack
|
|
113
|
+
spacing={8}
|
|
114
|
+
sx={{ p: { xs: 6, md: 12 }, height: '100%', display: 'flex', flexDirection: 'column', justifyContent: 'center' }}
|
|
115
|
+
>
|
|
116
|
+
<Box>
|
|
117
|
+
<Typography
|
|
118
|
+
variant="overline"
|
|
119
|
+
sx={{ color: 'primary.main', fontWeight: 900, letterSpacing: 2, mb: 1, display: 'block' }}
|
|
120
|
+
>
|
|
121
|
+
{t('login.heroAfter.overline', 'Get Started')}
|
|
122
|
+
</Typography>
|
|
123
|
+
<Typography
|
|
124
|
+
variant="h2"
|
|
125
|
+
sx={{
|
|
126
|
+
fontWeight: 900,
|
|
127
|
+
color: 'text.primary',
|
|
128
|
+
mb: 2,
|
|
129
|
+
letterSpacing: -1.5,
|
|
130
|
+
fontSize: { md: '3rem', lg: '3.75rem' }
|
|
131
|
+
}}
|
|
132
|
+
>
|
|
133
|
+
{t('login.heroAfter.title', 'Create your account')}
|
|
134
|
+
</Typography>
|
|
135
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', maxWidth: 450, fontWeight: 400 }}>
|
|
136
|
+
{t('login.heroAfter.subtitle', 'Join thousands of businesses already using our platform')}
|
|
137
|
+
</Typography>
|
|
138
|
+
</Box>
|
|
139
|
+
|
|
140
|
+
<Stack spacing={3}>
|
|
141
|
+
<HeroCard
|
|
142
|
+
icon="lucide:zap"
|
|
143
|
+
title={t('login.heroAfter.card1.title', 'Quick Setup')}
|
|
144
|
+
desc={t('login.heroAfter.card1.desc', 'Get started in minutes with our simple onboarding')}
|
|
145
|
+
delay={0.2}
|
|
146
|
+
/>
|
|
147
|
+
<HeroCard
|
|
148
|
+
icon="lucide:users"
|
|
149
|
+
title={t('login.heroAfter.card2.title', 'Team Collaboration')}
|
|
150
|
+
desc={t('login.heroAfter.card2.desc', 'Invite your team and work together seamlessly')}
|
|
151
|
+
delay={0.3}
|
|
152
|
+
/>
|
|
153
|
+
</Stack>
|
|
154
|
+
</Stack>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export default function LoginPage() {
|
|
159
|
+
const [currentView, setCurrentView] = useState<'login' | 'signup'>('login')
|
|
160
|
+
const [showEmailForm, setShowEmailForm] = useState(false)
|
|
161
|
+
const { language } = useLanguage()
|
|
162
|
+
const isRTL = language === 'ar'
|
|
163
|
+
|
|
164
|
+
const handleSocialLogin = (provider: 'google' | 'microsoft') => {
|
|
165
|
+
console.log(`Login with ${provider}`)
|
|
166
|
+
// TODO: Implement social login
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const handleSwitchToSignup = () => {
|
|
170
|
+
setShowEmailForm(false)
|
|
171
|
+
setCurrentView('signup')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const handleSwitchToLogin = () => {
|
|
175
|
+
setShowEmailForm(false)
|
|
176
|
+
setCurrentView('login')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sliderLeft = currentView === 'login' ? '47%' : '0%'
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Box
|
|
183
|
+
sx={{
|
|
184
|
+
position: 'relative',
|
|
185
|
+
height: '100vh',
|
|
186
|
+
bgcolor: 'background.default',
|
|
187
|
+
overflow: 'hidden'
|
|
188
|
+
}}
|
|
189
|
+
>
|
|
190
|
+
{/* Hero Areas (Background Layer) - Hidden on mobile */}
|
|
191
|
+
<Box sx={{ position: 'relative', width: '100%', height: '100%', display: { xs: 'none', md: 'flex' }, zIndex: 1 }}>
|
|
192
|
+
<MotionBox
|
|
193
|
+
initial={false}
|
|
194
|
+
animate={{
|
|
195
|
+
opacity: currentView === 'login' ? 1 : 0,
|
|
196
|
+
x: currentView === 'login' ? 0 : -80,
|
|
197
|
+
scale: currentView === 'login' ? 1 : 0.95
|
|
198
|
+
}}
|
|
199
|
+
transition={{ duration: 0.6, ease: 'easeInOut' }}
|
|
200
|
+
sx={{ position: 'absolute', top: 0, left: 0, width: '47%', height: '100%' }}
|
|
201
|
+
>
|
|
202
|
+
<HeroBefore />
|
|
203
|
+
</MotionBox>
|
|
204
|
+
|
|
205
|
+
<MotionBox
|
|
206
|
+
initial={false}
|
|
207
|
+
animate={{
|
|
208
|
+
opacity: currentView === 'signup' ? 1 : 0,
|
|
209
|
+
x: currentView === 'signup' ? 0 : 80,
|
|
210
|
+
scale: currentView === 'signup' ? 1 : 0.95
|
|
211
|
+
}}
|
|
212
|
+
transition={{ duration: 0.6, ease: 'easeInOut' }}
|
|
213
|
+
sx={{ position: 'absolute', top: 0, right: 0, width: '47%', height: '100%' }}
|
|
214
|
+
>
|
|
215
|
+
<HeroAfter />
|
|
216
|
+
</MotionBox>
|
|
217
|
+
</Box>
|
|
218
|
+
|
|
219
|
+
{/* Main Slider Panel */}
|
|
220
|
+
<MotionBox
|
|
221
|
+
initial={false}
|
|
222
|
+
animate={{
|
|
223
|
+
[isRTL ? 'right' : 'left']: sliderLeft
|
|
224
|
+
}}
|
|
225
|
+
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
|
|
226
|
+
sx={{
|
|
227
|
+
position: 'absolute',
|
|
228
|
+
top: 0,
|
|
229
|
+
width: { xs: '100%', md: '53%' },
|
|
230
|
+
height: '100%',
|
|
231
|
+
bgcolor: (theme) => (theme.palette.mode === 'light' ? 'rgba(255, 255, 255, 0.8)' : 'rgba(15, 11, 12, 0.75)'),
|
|
232
|
+
backdropFilter: 'blur(40px) saturate(180%)',
|
|
233
|
+
display: 'flex',
|
|
234
|
+
alignItems: 'center',
|
|
235
|
+
justifyContent: 'center',
|
|
236
|
+
px: { xs: 3, md: 12 },
|
|
237
|
+
zIndex: 10,
|
|
238
|
+
borderLeft: (theme) => (currentView === 'login' ? `1px solid ${theme.palette.divider}` : 'none'),
|
|
239
|
+
borderRight: (theme) => (currentView === 'signup' ? `1px solid ${theme.palette.divider}` : 'none'),
|
|
240
|
+
boxShadow: (theme) => theme.shadows[20],
|
|
241
|
+
overflowY: 'auto'
|
|
242
|
+
}}
|
|
243
|
+
>
|
|
244
|
+
<Box sx={{ width: '100%', maxWidth: 480, py: 12 }}>
|
|
245
|
+
{/* Header */}
|
|
246
|
+
<Box
|
|
247
|
+
sx={{
|
|
248
|
+
position: 'absolute',
|
|
249
|
+
top: (theme) => theme.spacing(4),
|
|
250
|
+
left: (theme) => theme.spacing(4),
|
|
251
|
+
right: (theme) => theme.spacing(4),
|
|
252
|
+
zIndex: 11,
|
|
253
|
+
display: 'flex',
|
|
254
|
+
alignItems: 'center',
|
|
255
|
+
justifyContent: 'space-between'
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
<Typography variant="h5" fontWeight={800} color="primary.main">
|
|
259
|
+
Logo
|
|
260
|
+
</Typography>
|
|
261
|
+
<LanguageDropdown />
|
|
262
|
+
</Box>
|
|
263
|
+
|
|
264
|
+
<AnimatePresence mode="wait">
|
|
265
|
+
{currentView === 'login' ? (
|
|
266
|
+
<MotionBox
|
|
267
|
+
key="login-panel"
|
|
268
|
+
initial={{ opacity: 0, x: 20 }}
|
|
269
|
+
animate={{ opacity: 1, x: 0 }}
|
|
270
|
+
exit={{ opacity: 0, x: -20 }}
|
|
271
|
+
transition={{ duration: 0.5 }}
|
|
272
|
+
>
|
|
273
|
+
<LoginForm
|
|
274
|
+
showEmailForm={showEmailForm}
|
|
275
|
+
setShowEmailForm={setShowEmailForm}
|
|
276
|
+
onSwitchToSignup={handleSwitchToSignup}
|
|
277
|
+
onSocialLogin={handleSocialLogin}
|
|
278
|
+
/>
|
|
279
|
+
</MotionBox>
|
|
280
|
+
) : (
|
|
281
|
+
<MotionBox
|
|
282
|
+
key="signup-panel"
|
|
283
|
+
initial={{ opacity: 0, x: -20 }}
|
|
284
|
+
animate={{ opacity: 1, x: 0 }}
|
|
285
|
+
exit={{ opacity: 0, x: 20 }}
|
|
286
|
+
transition={{ duration: 0.5 }}
|
|
287
|
+
>
|
|
288
|
+
<SignupForm onSwitchToLogin={handleSwitchToLogin} />
|
|
289
|
+
</MotionBox>
|
|
290
|
+
)}
|
|
291
|
+
</AnimatePresence>
|
|
292
|
+
</Box>
|
|
293
|
+
</MotionBox>
|
|
294
|
+
</Box>
|
|
295
|
+
)
|
|
296
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useSearchParams, useRouter } from 'next/navigation'
|
|
4
|
+
import { Box, Button, Container, Typography, Stack, Paper } from '@mui/material'
|
|
5
|
+
import { ShieldX, ArrowLeft, Home } from 'lucide-react'
|
|
6
|
+
import { useTranslation } from 'react-i18next'
|
|
7
|
+
import { Suspense } from 'react'
|
|
8
|
+
|
|
9
|
+
function UnauthorizedContent() {
|
|
10
|
+
const searchParams = useSearchParams()
|
|
11
|
+
const router = useRouter()
|
|
12
|
+
const { t } = useTranslation()
|
|
13
|
+
|
|
14
|
+
const resource = searchParams.get('resource')
|
|
15
|
+
const action = searchParams.get('action')
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Container maxWidth='sm'>
|
|
19
|
+
<Box
|
|
20
|
+
sx={{
|
|
21
|
+
minHeight: '100vh',
|
|
22
|
+
display: 'flex',
|
|
23
|
+
alignItems: 'center',
|
|
24
|
+
justifyContent: 'center'
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
<Paper
|
|
28
|
+
elevation={0}
|
|
29
|
+
sx={{
|
|
30
|
+
p: 6,
|
|
31
|
+
textAlign: 'center',
|
|
32
|
+
bgcolor: 'background.paper',
|
|
33
|
+
border: '1px solid',
|
|
34
|
+
borderColor: 'divider',
|
|
35
|
+
borderRadius: 2
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<Box
|
|
39
|
+
sx={{
|
|
40
|
+
width: 80,
|
|
41
|
+
height: 80,
|
|
42
|
+
borderRadius: '50%',
|
|
43
|
+
bgcolor: 'error.lighter',
|
|
44
|
+
display: 'flex',
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
justifyContent: 'center',
|
|
47
|
+
mx: 'auto',
|
|
48
|
+
mb: 3
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<ShieldX size={40} color='var(--mui-palette-error-main)' />
|
|
52
|
+
</Box>
|
|
53
|
+
|
|
54
|
+
<Typography variant='h4' fontWeight={700} gutterBottom>
|
|
55
|
+
{t('errors.unauthorized.title', 'Access Denied')}
|
|
56
|
+
</Typography>
|
|
57
|
+
|
|
58
|
+
<Typography variant='body1' color='text.secondary' sx={{ mb: 1 }}>
|
|
59
|
+
{t('errors.unauthorized.message', "You don't have permission to access this page.")}
|
|
60
|
+
</Typography>
|
|
61
|
+
|
|
62
|
+
{(resource || action) && (
|
|
63
|
+
<Typography variant='body2' color='text.disabled' sx={{ mb: 4 }}>
|
|
64
|
+
{resource && action
|
|
65
|
+
? t('errors.unauthorized.resourceAction', {
|
|
66
|
+
resource,
|
|
67
|
+
action,
|
|
68
|
+
defaultValue: `Required: ${action} access to ${resource}`
|
|
69
|
+
})
|
|
70
|
+
: resource
|
|
71
|
+
? t('errors.unauthorized.resource', {
|
|
72
|
+
resource,
|
|
73
|
+
defaultValue: `Required access to: ${resource}`
|
|
74
|
+
})
|
|
75
|
+
: null}
|
|
76
|
+
</Typography>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
<Stack direction='row' spacing={2} justifyContent='center' sx={{ mt: 4 }}>
|
|
80
|
+
<Button variant='outlined' startIcon={<ArrowLeft size={18} />} onClick={() => router.back()}>
|
|
81
|
+
{t('common.goBack', 'Go Back')}
|
|
82
|
+
</Button>
|
|
83
|
+
<Button variant='contained' startIcon={<Home size={18} />} onClick={() => router.push('/dashboard')}>
|
|
84
|
+
{t('common.goHome', 'Go to Dashboard')}
|
|
85
|
+
</Button>
|
|
86
|
+
</Stack>
|
|
87
|
+
</Paper>
|
|
88
|
+
</Box>
|
|
89
|
+
</Container>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Unauthorized Page
|
|
95
|
+
*
|
|
96
|
+
* Displayed when a user tries to access a route they don't have permission for.
|
|
97
|
+
* Shows the required resource/action and provides navigation options.
|
|
98
|
+
*/
|
|
99
|
+
export default function UnauthorizedPage() {
|
|
100
|
+
return (
|
|
101
|
+
<Suspense
|
|
102
|
+
fallback={
|
|
103
|
+
<Container maxWidth='sm'>
|
|
104
|
+
<Box
|
|
105
|
+
sx={{
|
|
106
|
+
minHeight: '100vh',
|
|
107
|
+
display: 'flex',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
justifyContent: 'center'
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<Typography>Loading...</Typography>
|
|
113
|
+
</Box>
|
|
114
|
+
</Container>
|
|
115
|
+
}
|
|
116
|
+
>
|
|
117
|
+
<UnauthorizedContent />
|
|
118
|
+
</Suspense>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
interface MSWProviderProps {
|
|
6
|
+
children: React.ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* MSW Provider Component
|
|
11
|
+
*
|
|
12
|
+
* Initializes Mock Service Worker in development mode.
|
|
13
|
+
* Shows a loading state while MSW is starting to prevent
|
|
14
|
+
* API calls before mocks are ready.
|
|
15
|
+
*/
|
|
16
|
+
export default function MSWProvider({ children }: MSWProviderProps) {
|
|
17
|
+
const [isReady, setIsReady] = useState(false)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
async function init() {
|
|
21
|
+
// Only initialize MSW in development
|
|
22
|
+
if (process.env.NODE_ENV === 'development') {
|
|
23
|
+
// Check if mocks are disabled
|
|
24
|
+
if (process.env.NEXT_PUBLIC_ENABLE_MOCKS === 'false') {
|
|
25
|
+
setIsReady(true)
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const { initMocks } = await import('@/lib/mocks')
|
|
31
|
+
await initMocks()
|
|
32
|
+
console.log('[MSW] Ready to intercept requests')
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.warn('[MSW] Failed to initialize:', error)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
setIsReady(true)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
init()
|
|
41
|
+
}, [])
|
|
42
|
+
|
|
43
|
+
// In production, render immediately
|
|
44
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
45
|
+
return <>{children}</>
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// In development, wait for MSW to be ready
|
|
49
|
+
if (!isReady) {
|
|
50
|
+
return null // Or a loading spinner if preferred
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return <>{children}</>
|
|
54
|
+
}
|