create-nala 3.1.1 → 3.1.2

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.
@@ -0,0 +1,278 @@
1
+ <script setup lang="ts">
2
+ import { ref, onUnmounted } from 'vue'
3
+ import { useRouter } from 'vue-router'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'
5
+ import { Button } from '@/components/ui/button'
6
+ import { Input } from '@/components/ui/input'
7
+ import { Label } from '@/components/ui/label'
8
+ import { Alert, AlertDescription } from '@/components/ui/alert'
9
+ import { toast } from '@/components/ui/sonner'
10
+ import {
11
+ Sparkles,
12
+ Mail,
13
+ ArrowRight,
14
+ ArrowLeft,
15
+ Loader2,
16
+ CheckCircle2,
17
+ ExternalLink,
18
+ RotateCw,
19
+ AlertCircle,
20
+ Clock,
21
+ Shield,
22
+ } from '@lucide/vue'
23
+
24
+ const router = useRouter()
25
+
26
+ const email = ref('')
27
+ const isSubmitted = ref(false)
28
+ const isSending = ref(false)
29
+ const isResending = ref(false)
30
+ const errorMessage = ref('')
31
+ const countdown = ref(60)
32
+ let timer: ReturnType<typeof setInterval> | null = null
33
+
34
+ const startCountdown = () => {
35
+ countdown.value = 60
36
+ if (timer) clearInterval(timer)
37
+ timer = setInterval(() => {
38
+ if (countdown.value > 0) {
39
+ countdown.value--
40
+ } else {
41
+ if (timer) clearInterval(timer)
42
+ }
43
+ }, 1000)
44
+ }
45
+
46
+ onUnmounted(() => {
47
+ if (timer) clearInterval(timer)
48
+ })
49
+
50
+ const handleSendMagicLink = () => {
51
+ const trimmed = email.value.trim()
52
+ if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
53
+ errorMessage.value = 'Please enter a valid email address.'
54
+ return
55
+ }
56
+
57
+ isSending.value = true
58
+ errorMessage.value = ''
59
+
60
+ setTimeout(() => {
61
+ isSending.value = false
62
+ isSubmitted.value = true
63
+ startCountdown()
64
+ toast.success('Magic link has been dispatched to your inbox!')
65
+ }, 1000)
66
+ }
67
+
68
+ const handleResend = () => {
69
+ if (countdown.value > 0 || isResending.value) return
70
+ isResending.value = true
71
+ errorMessage.value = ''
72
+
73
+ setTimeout(() => {
74
+ isResending.value = false
75
+ startCountdown()
76
+ toast.info('A fresh magic link has been sent!')
77
+ }, 900)
78
+ }
79
+
80
+ const openEmailClient = (provider: 'gmail' | 'outlook') => {
81
+ if (provider === 'gmail') {
82
+ window.open('https://mail.google.com', '_blank', 'noopener,noreferrer')
83
+ } else {
84
+ window.open('https://outlook.live.com', '_blank', 'noopener,noreferrer')
85
+ }
86
+ }
87
+
88
+ const simulateInstantLogin = () => {
89
+ toast.success('Simulated magic link clicked from email client!')
90
+ router.push('/')
91
+ }
92
+ </script>
93
+
94
+ <template>
95
+ <Card flush class="w-full max-w-md shadow-xl border-border/80 bg-card">
96
+ <!-- State 2: Magic Link Dispatched (Awaiting Email Click) -->
97
+ <template v-if="isSubmitted">
98
+ <CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-2 text-center">
99
+ <div class="relative mx-auto mb-2 flex h-14 w-14 items-center justify-center rounded-2xl bg-primary/10 text-primary">
100
+ <Mail class="h-7 w-7 animate-pulse" />
101
+ <div class="absolute -top-1 -right-1 flex h-5 w-5 items-center justify-center rounded-full bg-emerald-500 text-white shadow-xs">
102
+ <CheckCircle2 class="h-3.5 w-3.5" />
103
+ </div>
104
+ </div>
105
+ <CardTitle class="text-2xl font-bold tracking-tight">Check your email</CardTitle>
106
+ <CardDescription class="text-sm">
107
+ We've sent a passwordless sign-in link to
108
+ <span class="font-semibold text-foreground">{{ email }}</span>.
109
+ </CardDescription>
110
+ </CardHeader>
111
+
112
+ <CardContent class="px-6 sm:px-8 pb-6 space-y-4">
113
+ <!-- Explanatory note -->
114
+ <div class="rounded-lg border border-border/70 bg-muted/20 p-3.5 text-xs text-muted-foreground space-y-1.5">
115
+ <div class="flex items-center gap-1.5 font-medium text-foreground">
116
+ <Shield class="h-3.5 w-3.5 text-primary" />
117
+ <span>Secure Passwordless Authentication</span>
118
+ </div>
119
+ <p>
120
+ Click the button inside the email to sign in instantly. The link expires in 15 minutes and can only be used once.
121
+ </p>
122
+ </div>
123
+
124
+ <!-- Direct Email Providers Launcher -->
125
+ <div class="space-y-2 pt-1">
126
+ <span class="text-[11px] font-medium text-muted-foreground">Quick open mailbox:</span>
127
+ <div class="grid grid-cols-2 gap-2">
128
+ <Button
129
+ type="button"
130
+ variant="outline"
131
+ size="sm"
132
+ class="h-9 text-xs justify-center cursor-pointer"
133
+ @click="openEmailClient('gmail')"
134
+ >
135
+ Open Gmail
136
+ <ExternalLink class="h-3.5 w-3.5 ml-1.5 opacity-70" />
137
+ </Button>
138
+ <Button
139
+ type="button"
140
+ variant="outline"
141
+ size="sm"
142
+ class="h-9 text-xs justify-center cursor-pointer"
143
+ @click="openEmailClient('outlook')"
144
+ >
145
+ Open Outlook
146
+ <ExternalLink class="h-3.5 w-3.5 ml-1.5 opacity-70" />
147
+ </Button>
148
+ </div>
149
+ </div>
150
+
151
+ <!-- Simulated One-Click Callback Demo -->
152
+ <div class="rounded-lg border border-primary/20 bg-primary/5 p-3 text-xs space-y-2">
153
+ <div class="flex items-center justify-between text-[11px] text-primary font-medium">
154
+ <span>Developer Sandbox Demo</span>
155
+ <Sparkles class="h-3.5 w-3.5" />
156
+ </div>
157
+ <p class="text-[11px] text-muted-foreground">
158
+ Testing locally without access to an actual mail inbox? Simulate clicking the magic link callback below:
159
+ </p>
160
+ <Button
161
+ type="button"
162
+ class="w-full h-8 text-xs font-medium cursor-pointer"
163
+ @click="simulateInstantLogin"
164
+ >
165
+ Simulate Email Link Click
166
+ <ArrowRight class="h-3.5 w-3.5 ml-1.5" />
167
+ </Button>
168
+ </div>
169
+
170
+ <!-- Resend Countdown -->
171
+ <div class="pt-2 text-center">
172
+ <p class="text-xs text-muted-foreground">
173
+ Didn't receive the email?
174
+ <button
175
+ v-if="countdown === 0"
176
+ type="button"
177
+ class="text-primary hover:underline font-medium ml-1 inline-flex items-center gap-1 cursor-pointer"
178
+ :disabled="isResending"
179
+ @click="handleResend"
180
+ >
181
+ <RotateCw v-if="isResending" class="h-3 w-3 animate-spin" />
182
+ Resend magic link
183
+ </button>
184
+ <span v-else class="text-foreground/70 font-mono ml-1 inline-flex items-center gap-1">
185
+ <Clock class="h-3 w-3 text-muted-foreground" />
186
+ Resend in {{ countdown }}s
187
+ </span>
188
+ </p>
189
+ </div>
190
+ </CardContent>
191
+
192
+ <CardFooter class="flex justify-center border-t border-border/60 py-4 bg-muted/10">
193
+ <button
194
+ type="button"
195
+ class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
196
+ @click="isSubmitted = false"
197
+ >
198
+ <ArrowLeft class="h-3.5 w-3.5" />
199
+ Use a different email address
200
+ </button>
201
+ </CardFooter>
202
+ </template>
203
+
204
+ <!-- State 1: Magic Link Request Form -->
205
+ <template v-else>
206
+ <CardHeader class="pt-8 pb-6 px-6 sm:px-8 space-y-1.5 text-center">
207
+ <div class="mx-auto mb-2 flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10 text-primary">
208
+ <Sparkles class="h-6 w-6" />
209
+ </div>
210
+ <CardTitle class="text-2xl font-bold tracking-tight">Passwordless Sign In</CardTitle>
211
+ <CardDescription class="text-sm">
212
+ Enter your email address and we'll send you a secure magic link to sign in instantly without a password.
213
+ </CardDescription>
214
+ </CardHeader>
215
+
216
+ <CardContent class="px-6 sm:px-8 pb-6 space-y-4">
217
+ <!-- Error Alert Banner -->
218
+ <Alert v-if="errorMessage" variant="destructive" class="py-2.5 px-3.5">
219
+ <AlertCircle class="h-4 w-4" />
220
+ <AlertDescription class="text-xs font-medium ml-2">
221
+ {{ errorMessage }}
222
+ </AlertDescription>
223
+ </Alert>
224
+
225
+ <form @submit.prevent="handleSendMagicLink" class="space-y-4">
226
+ <div class="space-y-1.5">
227
+ <Label for="magic-email" class="text-xs font-medium">Email address</Label>
228
+ <div class="relative flex items-center">
229
+ <Mail class="absolute left-3 h-4 w-4 text-muted-foreground pointer-events-none" />
230
+ <Input
231
+ id="magic-email"
232
+ v-model="email"
233
+ type="email"
234
+ placeholder="name@example.com"
235
+ class="pl-9 h-10 text-sm"
236
+ :disabled="isSending"
237
+ autofocus
238
+ />
239
+ </div>
240
+ </div>
241
+
242
+ <Button
243
+ type="submit"
244
+ class="w-full font-medium h-10 cursor-pointer"
245
+ :disabled="!email.trim() || isSending"
246
+ >
247
+ <Loader2 v-if="isSending" class="h-4 w-4 animate-spin mr-2" />
248
+ <span v-else class="flex items-center justify-center gap-1.5">
249
+ Send Magic Link
250
+ <ArrowRight class="h-3.5 w-3.5" />
251
+ </span>
252
+ </Button>
253
+ </form>
254
+
255
+ <!-- Demo auto-fill hint -->
256
+ <div class="pt-1 text-center">
257
+ <button
258
+ type="button"
259
+ class="text-[11px] text-muted-foreground hover:text-primary transition-colors cursor-pointer"
260
+ @click="email = 'alex.morgan@example.com'"
261
+ >
262
+ Click to fill demo: <span class="font-mono text-foreground">alex.morgan@example.com</span>
263
+ </button>
264
+ </div>
265
+ </CardContent>
266
+
267
+ <CardFooter class="flex justify-center border-t border-border/60 py-4 bg-muted/10">
268
+ <router-link
269
+ to="/auth/login"
270
+ class="inline-flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
271
+ >
272
+ <ArrowLeft class="h-3.5 w-3.5" />
273
+ Return to password sign in
274
+ </router-link>
275
+ </CardFooter>
276
+ </template>
277
+ </Card>
278
+ </template>