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.
Files changed (35) hide show
  1. package/package.json +5 -2
  2. package/templates/base/@core/configs/clientConfig.ts +1 -1
  3. package/templates/base/@core/context/AuthContext.tsx +21 -28
  4. package/templates/base/@core/hooks/useAbility.ts +58 -0
  5. package/templates/base/app/(dashboard)/dashboard/page.tsx +104 -0
  6. package/templates/base/app/(dashboard)/layout.tsx +97 -0
  7. package/templates/base/app/home/page.tsx +112 -0
  8. package/templates/base/app/login/page.tsx +296 -0
  9. package/templates/base/app/unauthorized/page.tsx +120 -0
  10. package/templates/base/components/MSWProvider.tsx +54 -0
  11. package/templates/base/components/auth/LoginForm.tsx +279 -0
  12. package/templates/base/components/auth/SignupForm.tsx +348 -0
  13. package/templates/base/components/loaders/Spinner.tsx +5 -24
  14. package/templates/base/components/ui/ErrorMessage.tsx +17 -0
  15. package/templates/base/components/ui/FormFieldWrapper.tsx +27 -0
  16. package/templates/base/docs/AuthorizationDocumentation.md +348 -0
  17. package/templates/base/lib/abilities/checkAuthorization.ts +74 -0
  18. package/templates/base/lib/abilities/index.ts +27 -0
  19. package/templates/base/lib/abilities/roles.ts +75 -0
  20. package/templates/base/lib/abilities/routeMap.ts +35 -0
  21. package/templates/base/lib/abilities/routeMatcher.ts +117 -0
  22. package/templates/base/lib/abilities/types.ts +68 -0
  23. package/templates/base/lib/mocks/browser.ts +11 -0
  24. package/templates/base/lib/mocks/db.ts +124 -0
  25. package/templates/base/lib/mocks/handlers/auth.ts +203 -0
  26. package/templates/base/lib/mocks/handlers/index.ts +16 -0
  27. package/templates/base/lib/mocks/index.ts +34 -0
  28. package/templates/base/lib/mocks/jwt.ts +99 -0
  29. package/templates/base/middleware.ts +147 -0
  30. package/templates/base/package-lock.json +725 -2
  31. package/templates/base/package.json +13 -2
  32. package/templates/base/providers/AppProviders.tsx +8 -5
  33. package/templates/base/public/locales/ar.json +73 -0
  34. package/templates/base/public/locales/en.json +73 -0
  35. 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
+ }