@mdguggenbichler/slugbase-core 0.0.31 → 0.0.33
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/backend/dist/config/mode.d.ts +4 -4
- package/backend/dist/config/mode.d.ts.map +1 -1
- package/backend/dist/config/mode.js +4 -4
- package/backend/dist/config/mode.js.map +1 -1
- package/backend/dist/routes/auth.d.ts.map +1 -1
- package/backend/dist/routes/auth.js +33 -3
- package/backend/dist/routes/auth.js.map +1 -1
- package/frontend/src/App.tsx +3 -1
- package/frontend/src/components/FilterChips.tsx +5 -3
- package/frontend/src/components/StatCard.tsx +82 -5
- package/frontend/src/components/bookmarks/BookmarkCard.tsx +317 -210
- package/frontend/src/components/bookmarks/BookmarkTableView.tsx +47 -23
- package/frontend/src/components/collections/CollectionToolbar.tsx +294 -0
- package/frontend/src/components/collections/README.md +44 -0
- package/frontend/src/components/collections/index.ts +2 -0
- package/frontend/src/components/dashboard/DashboardHeader.tsx +16 -0
- package/frontend/src/components/dashboard/MostUsedTagsSection.tsx +49 -0
- package/frontend/src/components/dashboard/PinnedSection.tsx +110 -0
- package/frontend/src/components/dashboard/QuickAccessSection.tsx +120 -0
- package/frontend/src/components/dashboard/README.md +35 -0
- package/frontend/src/components/dashboard/StatsCardsRow.tsx +78 -0
- package/frontend/src/components/dashboard/index.ts +17 -0
- package/frontend/src/locales/de.json +11 -0
- package/frontend/src/locales/en.json +10 -0
- package/frontend/src/locales/es.json +11 -0
- package/frontend/src/locales/fr.json +2 -0
- package/frontend/src/locales/it.json +2 -0
- package/frontend/src/locales/ja.json +2 -0
- package/frontend/src/locales/nl.json +2 -0
- package/frontend/src/locales/pl.json +2 -0
- package/frontend/src/locales/pt.json +2 -0
- package/frontend/src/locales/ru.json +2 -0
- package/frontend/src/locales/zh.json +2 -0
- package/frontend/src/pages/Bookmarks.tsx +97 -214
- package/frontend/src/pages/Dashboard.tsx +99 -216
- package/frontend/src/pages/Folders.tsx +181 -251
- package/frontend/src/pages/Login.tsx +6 -3
- package/frontend/src/pages/Tags.tsx +87 -145
- package/frontend/src/pages/VerifyEmailRequired.tsx +163 -0
- package/package.json +1 -1
|
@@ -4,15 +4,14 @@ import { useTranslation } from 'react-i18next';
|
|
|
4
4
|
import api from '../api/client';
|
|
5
5
|
import ConfirmDialog from '../components/ui/ConfirmDialog';
|
|
6
6
|
import { useConfirmDialog } from '../hooks/useConfirmDialog';
|
|
7
|
-
import { Plus, Edit, Trash2, Tag as TagIcon,
|
|
7
|
+
import { Plus, Edit, Trash2, Tag as TagIcon, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
8
8
|
import TagModal from '../components/modals/TagModal';
|
|
9
9
|
import Button from '../components/ui/Button';
|
|
10
|
-
import
|
|
11
|
-
import { PageHeader } from '../components/PageHeader';
|
|
10
|
+
import { CollectionToolbar } from '../components/collections';
|
|
12
11
|
import { EmptyState } from '../components/EmptyState';
|
|
13
12
|
import { PageLoadingSkeleton } from '../components/ui/PageLoadingSkeleton';
|
|
14
13
|
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
15
|
-
import {
|
|
14
|
+
import { Card } from '../components/ui/card';
|
|
16
15
|
|
|
17
16
|
interface Tag {
|
|
18
17
|
id: string;
|
|
@@ -39,9 +38,6 @@ export default function Tags() {
|
|
|
39
38
|
const saved = localStorage.getItem('tags-view-mode');
|
|
40
39
|
return (saved === 'list' || saved === 'card') ? saved : 'card';
|
|
41
40
|
});
|
|
42
|
-
const [compactMode, setCompactMode] = useState(() => {
|
|
43
|
-
return localStorage.getItem('tags-compact-mode') === 'true';
|
|
44
|
-
});
|
|
45
41
|
|
|
46
42
|
const sortParam = searchParams.get('sort');
|
|
47
43
|
const sortBy = (sortParam === 'recently_added' || sortParam === 'alphabetical') ? sortParam : DEFAULT_SORT;
|
|
@@ -57,10 +53,6 @@ export default function Tags() {
|
|
|
57
53
|
localStorage.setItem('tags-view-mode', viewMode);
|
|
58
54
|
}, [viewMode]);
|
|
59
55
|
|
|
60
|
-
useEffect(() => {
|
|
61
|
-
localStorage.setItem('tags-compact-mode', compactMode.toString());
|
|
62
|
-
}, [compactMode]);
|
|
63
|
-
|
|
64
56
|
useEffect(() => {
|
|
65
57
|
loadTags();
|
|
66
58
|
}, [sortBy, page, pageSize]);
|
|
@@ -171,83 +163,43 @@ export default function Tags() {
|
|
|
171
163
|
|
|
172
164
|
return (
|
|
173
165
|
<div className="space-y-6 pb-24">
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
value={String(pageSize)}
|
|
212
|
-
onChange={(value) => updateParams({ limit: value })}
|
|
213
|
-
options={PAGE_SIZE_OPTIONS.map((n) => ({ value: String(n), label: String(n) }))}
|
|
214
|
-
className="min-w-[80px]"
|
|
215
|
-
/>
|
|
216
|
-
<span className="text-sm text-muted-foreground whitespace-nowrap">{t('bookmarks.perPage')}</span>
|
|
217
|
-
</div>
|
|
218
|
-
<div className="flex items-center gap-2 border-l border-border pl-3 ml-auto">
|
|
219
|
-
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-1 border border-border">
|
|
220
|
-
<button
|
|
221
|
-
onClick={() => setViewMode('card')}
|
|
222
|
-
className={`p-1.5 rounded transition-colors ${
|
|
223
|
-
viewMode === 'card' ? 'bg-background text-primary shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
|
224
|
-
}`}
|
|
225
|
-
title={t('tags.viewCard')}
|
|
226
|
-
>
|
|
227
|
-
<LayoutGrid className="h-4 w-4" />
|
|
228
|
-
</button>
|
|
229
|
-
<button
|
|
230
|
-
onClick={() => setViewMode('list')}
|
|
231
|
-
className={`p-1.5 rounded transition-colors ${
|
|
232
|
-
viewMode === 'list' ? 'bg-background text-primary shadow-sm' : 'text-muted-foreground hover:text-foreground'
|
|
233
|
-
}`}
|
|
234
|
-
title={t('tags.viewList')}
|
|
235
|
-
>
|
|
236
|
-
<List className="h-4 w-4" />
|
|
237
|
-
</button>
|
|
238
|
-
</div>
|
|
239
|
-
<button
|
|
240
|
-
onClick={() => setCompactMode(!compactMode)}
|
|
241
|
-
className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
|
|
242
|
-
compactMode ? 'bg-primary/20 text-primary' : 'bg-muted text-muted-foreground hover:bg-accent'
|
|
243
|
-
}`}
|
|
244
|
-
title={t('tags.compactMode')}
|
|
245
|
-
>
|
|
246
|
-
{t('tags.compactMode')}
|
|
247
|
-
</button>
|
|
248
|
-
</div>
|
|
249
|
-
</div>
|
|
250
|
-
</div>
|
|
166
|
+
<CollectionToolbar
|
|
167
|
+
title={t('tags.title')}
|
|
168
|
+
count={totalTags}
|
|
169
|
+
subtitle={
|
|
170
|
+
hasActiveFilters || totalTags > pageSize
|
|
171
|
+
? t('bookmarks.showingXOfY', { x: sortedTags.length, y: totalTags })
|
|
172
|
+
: undefined
|
|
173
|
+
}
|
|
174
|
+
createButton={{ label: t('tags.create'), onClick: handleCreate }}
|
|
175
|
+
filterChips={{
|
|
176
|
+
chips: filterChips,
|
|
177
|
+
onRemove: handleRemoveFilter,
|
|
178
|
+
onClearAll: handleResetFilters,
|
|
179
|
+
clearAllLabel: t('bookmarks.clearAllFilters'),
|
|
180
|
+
clearAllAriaLabel: t('bookmarks.clearAllFilters'),
|
|
181
|
+
}}
|
|
182
|
+
sort={{
|
|
183
|
+
value: sortBy,
|
|
184
|
+
onChange: (value) => updateParams({ sort: value as SortOption }),
|
|
185
|
+
options: sortOptions,
|
|
186
|
+
className: 'min-w-[160px]',
|
|
187
|
+
}}
|
|
188
|
+
perPage={{
|
|
189
|
+
value: pageSize,
|
|
190
|
+
onChange: (value) => {
|
|
191
|
+
updateParams({ limit: String(value) });
|
|
192
|
+
},
|
|
193
|
+
options: [...PAGE_SIZE_OPTIONS],
|
|
194
|
+
label: t('bookmarks.perPage'),
|
|
195
|
+
}}
|
|
196
|
+
viewMode={{
|
|
197
|
+
value: viewMode,
|
|
198
|
+
onChange: setViewMode,
|
|
199
|
+
cardLabel: t('tags.viewCard'),
|
|
200
|
+
listLabel: t('tags.viewList'),
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
251
203
|
|
|
252
204
|
{/* Tags Display */}
|
|
253
205
|
{sortedTags.length === 0 ? (
|
|
@@ -262,55 +214,48 @@ export default function Tags() {
|
|
|
262
214
|
}
|
|
263
215
|
/>
|
|
264
216
|
) : viewMode === 'card' ? (
|
|
265
|
-
<div className=
|
|
266
|
-
compactMode
|
|
267
|
-
? 'sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
|
|
268
|
-
: 'sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6'
|
|
269
|
-
}`}>
|
|
217
|
+
<div className="grid gap-3 items-start [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]">
|
|
270
218
|
{sortedTags.map((tag) => (
|
|
271
|
-
<
|
|
219
|
+
<Card
|
|
272
220
|
key={tag.id}
|
|
273
|
-
className=
|
|
221
|
+
className="group relative flex flex-col cursor-pointer rounded-lg border bg-card/95 dark:bg-card/90 transition-[border-color,box-shadow] duration-150 border-border/80 hover:border-primary/80 hover:shadow-[0_2px_6px_rgba(0,0,0,0.06)] dark:border-border/70 dark:hover:border-primary/80 dark:hover:shadow-[0_2px_6px_rgba(0,0,0,0.25)] px-3 pt-0 pb-1.5 focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2"
|
|
274
222
|
>
|
|
275
223
|
<Link
|
|
276
224
|
to={`${prefix}/bookmarks?tag_id=${tag.id}`}
|
|
277
|
-
className="
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
</div>
|
|
284
|
-
<div className="flex-1 min-w-0 pt-0.5">
|
|
285
|
-
<h3 className={`${compactMode ? 'text-xs' : 'text-sm'} font-medium text-foreground truncate`}>
|
|
286
|
-
{tag.name}
|
|
287
|
-
</h3>
|
|
288
|
-
</div>
|
|
289
|
-
</div>
|
|
225
|
+
className="absolute inset-0 rounded-lg z-0 focus:outline-none"
|
|
226
|
+
aria-label={tag.name}
|
|
227
|
+
/>
|
|
228
|
+
<header className="flex-shrink-0 flex items-center gap-1.5 min-w-0 pt-3 relative z-10">
|
|
229
|
+
<div className="flex-shrink-0 w-7 h-7 rounded-md bg-background/90 dark:bg-muted/20 flex items-center justify-center border border-border/50 overflow-hidden">
|
|
230
|
+
<TagIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
|
290
231
|
</div>
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
size="sm"
|
|
296
|
-
icon={Edit}
|
|
297
|
-
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
298
|
-
onClick={() => handleEdit(tag)}
|
|
299
|
-
className="flex-1 h-8 min-w-0 text-xs"
|
|
300
|
-
>
|
|
301
|
-
{t('common.edit')}
|
|
302
|
-
</Button>
|
|
303
|
-
<Button
|
|
304
|
-
variant="ghost"
|
|
305
|
-
size="sm"
|
|
306
|
-
icon={Trash2}
|
|
307
|
-
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
308
|
-
onClick={() => handleDelete(tag.id)}
|
|
309
|
-
title={t('common.delete')}
|
|
310
|
-
className="h-8 w-8 p-0 flex-shrink-0 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
311
|
-
/>
|
|
232
|
+
<div className="flex-1 min-w-0 min-h-0 overflow-hidden">
|
|
233
|
+
<h3 className="text-sm font-semibold text-foreground line-clamp-2 break-words leading-snug tracking-tight min-h-0">
|
|
234
|
+
{tag.name}
|
|
235
|
+
</h3>
|
|
312
236
|
</div>
|
|
313
|
-
|
|
237
|
+
</header>
|
|
238
|
+
<footer className="flex-shrink-0 flex items-center justify-end gap-0.5 h-6 min-h-[24px] pt-2.5 relative z-10 opacity-0 group-hover:opacity-100 focus-within:opacity-100 transition-opacity duration-150 w-[52px] ml-auto">
|
|
239
|
+
<Button
|
|
240
|
+
variant="ghost"
|
|
241
|
+
size="sm"
|
|
242
|
+
icon={Edit}
|
|
243
|
+
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
244
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleEdit(tag); }}
|
|
245
|
+
className="h-6 w-6 p-0 min-w-6 text-muted-foreground hover:text-foreground"
|
|
246
|
+
title={t('common.edit')}
|
|
247
|
+
/>
|
|
248
|
+
<Button
|
|
249
|
+
variant="ghost"
|
|
250
|
+
size="sm"
|
|
251
|
+
icon={Trash2}
|
|
252
|
+
iconClassName="h-3.5 w-3.5 stroke-[1.5]"
|
|
253
|
+
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleDelete(tag.id); }}
|
|
254
|
+
className="h-6 w-6 p-0 min-w-6 text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
|
|
255
|
+
title={t('common.delete')}
|
|
256
|
+
/>
|
|
257
|
+
</footer>
|
|
258
|
+
</Card>
|
|
314
259
|
))}
|
|
315
260
|
</div>
|
|
316
261
|
) : (
|
|
@@ -318,10 +263,10 @@ export default function Tags() {
|
|
|
318
263
|
<table className="w-full">
|
|
319
264
|
<thead className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-700">
|
|
320
265
|
<tr>
|
|
321
|
-
<th className=
|
|
266
|
+
<th className="px-4 py-3 text-left text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
|
|
322
267
|
{t('tags.name')}
|
|
323
268
|
</th>
|
|
324
|
-
<th className=
|
|
269
|
+
<th className="px-4 py-3 text-right text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">
|
|
325
270
|
{t('common.actions')}
|
|
326
271
|
</th>
|
|
327
272
|
</tr>
|
|
@@ -330,32 +275,29 @@ export default function Tags() {
|
|
|
330
275
|
{sortedTags.map((tag) => (
|
|
331
276
|
<tr
|
|
332
277
|
key={tag.id}
|
|
333
|
-
className=
|
|
278
|
+
className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors"
|
|
334
279
|
>
|
|
335
|
-
<td className=
|
|
280
|
+
<td className="px-4 py-3">
|
|
336
281
|
<Link
|
|
337
282
|
to={`${prefix}/bookmarks?tag_id=${tag.id}`}
|
|
338
|
-
className=
|
|
283
|
+
className="flex items-center gap-3 hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 rounded"
|
|
339
284
|
>
|
|
340
|
-
<div className=
|
|
341
|
-
<TagIcon className=
|
|
285
|
+
<div className="flex-shrink-0 w-8 h-8 rounded-lg bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/30 dark:to-purple-800/20 flex items-center justify-center border border-purple-100 dark:border-purple-800/50">
|
|
286
|
+
<TagIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
|
342
287
|
</div>
|
|
343
|
-
<div className=
|
|
288
|
+
<div className="font-medium text-gray-900 dark:text-white text-[15px]">
|
|
344
289
|
{tag.name}
|
|
345
290
|
</div>
|
|
346
291
|
</Link>
|
|
347
292
|
</td>
|
|
348
|
-
|
|
349
|
-
<
|
|
350
|
-
)}
|
|
351
|
-
<td className={`${compactMode ? 'px-2 py-1.5' : 'px-4 py-3'}`}>
|
|
352
|
-
<div className={`flex items-center justify-end ${compactMode ? 'gap-1' : 'gap-2'}`}>
|
|
293
|
+
<td className="px-4 py-3">
|
|
294
|
+
<div className="flex items-center justify-end gap-2">
|
|
353
295
|
<Button
|
|
354
296
|
variant="ghost"
|
|
355
297
|
size="sm"
|
|
356
298
|
icon={Edit}
|
|
357
299
|
onClick={() => handleEdit(tag)}
|
|
358
|
-
className=
|
|
300
|
+
className="px-2"
|
|
359
301
|
/>
|
|
360
302
|
<Button
|
|
361
303
|
variant="ghost"
|
|
@@ -363,7 +305,7 @@ export default function Tags() {
|
|
|
363
305
|
icon={Trash2}
|
|
364
306
|
onClick={() => handleDelete(tag.id)}
|
|
365
307
|
title={t('common.delete')}
|
|
366
|
-
className=
|
|
308
|
+
className="text-red-600 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 px-2"
|
|
367
309
|
/>
|
|
368
310
|
</div>
|
|
369
311
|
</td>
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useLocation, Link } from 'react-router-dom';
|
|
4
|
+
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
5
|
+
import api from '../api/client';
|
|
6
|
+
import { Mail } from 'lucide-react';
|
|
7
|
+
import Button from '../components/ui/Button';
|
|
8
|
+
|
|
9
|
+
export default function VerifyEmailRequired() {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const location = useLocation();
|
|
12
|
+
const { pathPrefixForLinks } = useAppConfig();
|
|
13
|
+
const stateEmail = (location.state as { email?: string } | null)?.email?.trim() || '';
|
|
14
|
+
const [email, setEmail] = useState(stateEmail);
|
|
15
|
+
const [newEmail, setNewEmail] = useState('');
|
|
16
|
+
const [loading, setLoading] = useState(false);
|
|
17
|
+
const [error, setError] = useState('');
|
|
18
|
+
const [success, setSuccess] = useState(false);
|
|
19
|
+
const [emailChanged, setEmailChanged] = useState(false);
|
|
20
|
+
|
|
21
|
+
const prefix = pathPrefixForLinks || '';
|
|
22
|
+
const loginPath = `${prefix}/login`.replace(/\/+/g, '/') || '/login';
|
|
23
|
+
const currentEmail = stateEmail || email.trim();
|
|
24
|
+
|
|
25
|
+
const handleResend = async (e: React.FormEvent) => {
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
const toSend = currentEmail || email.trim();
|
|
28
|
+
if (!toSend) {
|
|
29
|
+
setError(t('auth.verifyEmailRequiredPageEmailLabel') + ' is required');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
setLoading(true);
|
|
33
|
+
setError('');
|
|
34
|
+
setSuccess(false);
|
|
35
|
+
setEmailChanged(false);
|
|
36
|
+
try {
|
|
37
|
+
const payload: { email: string; newEmail?: string } = { email: toSend };
|
|
38
|
+
const newEmailTrimmed = newEmail.trim();
|
|
39
|
+
if (newEmailTrimmed && newEmailTrimmed !== toSend) {
|
|
40
|
+
payload.newEmail = newEmailTrimmed;
|
|
41
|
+
}
|
|
42
|
+
const res = await api.post('/auth/request-signup-resend', payload);
|
|
43
|
+
setSuccess(true);
|
|
44
|
+
setEmailChanged(Boolean((res.data as { emailChanged?: boolean })?.emailChanged));
|
|
45
|
+
} catch (err: unknown) {
|
|
46
|
+
const msg = (err as { response?: { data?: { error?: string } } })?.response?.data?.error;
|
|
47
|
+
setError(msg || t('common.error'));
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8">
|
|
55
|
+
<div className="max-w-md w-full space-y-8">
|
|
56
|
+
<div className="text-center">
|
|
57
|
+
<div className="flex justify-center mb-6">
|
|
58
|
+
<img
|
|
59
|
+
src={`/slugbase_icon_blue.svg${(import.meta as any).env?.VITE_ASSET_VERSION ? `?v=${(import.meta as any).env.VITE_ASSET_VERSION}` : ''}`}
|
|
60
|
+
alt="SlugBase"
|
|
61
|
+
className="h-16 w-16 dark:hidden"
|
|
62
|
+
/>
|
|
63
|
+
<img
|
|
64
|
+
src={`/slugbase_icon_white.svg${(import.meta as any).env?.VITE_ASSET_VERSION ? `?v=${(import.meta as any).env.VITE_ASSET_VERSION}` : ''}`}
|
|
65
|
+
alt="SlugBase"
|
|
66
|
+
className="h-16 w-16 hidden dark:block"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
70
|
+
{t('auth.verifyEmailRequiredPageTitle')}
|
|
71
|
+
</h2>
|
|
72
|
+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
73
|
+
{t('auth.verifyEmailRequiredPageDescription')}
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-lg p-4 space-y-6">
|
|
78
|
+
{success ? (
|
|
79
|
+
<div className="space-y-4 text-center">
|
|
80
|
+
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
|
81
|
+
<Mail className="h-8 w-8 text-green-600 dark:text-green-400" />
|
|
82
|
+
</div>
|
|
83
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
84
|
+
{emailChanged ? t('auth.emailChangedResendMessage') : t('auth.resendVerificationSuccess')}
|
|
85
|
+
</p>
|
|
86
|
+
<Link
|
|
87
|
+
to={loginPath}
|
|
88
|
+
className="inline-block text-sm font-medium text-primary hover:underline"
|
|
89
|
+
>
|
|
90
|
+
{t('signup.backToLogin')}
|
|
91
|
+
</Link>
|
|
92
|
+
</div>
|
|
93
|
+
) : (
|
|
94
|
+
<form onSubmit={handleResend} className="space-y-5">
|
|
95
|
+
{stateEmail ? (
|
|
96
|
+
<>
|
|
97
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
98
|
+
{t('auth.verifyEmailRequiredPageEmailLabel')}: <span className="font-medium text-gray-900 dark:text-white">{stateEmail}</span>
|
|
99
|
+
</p>
|
|
100
|
+
<div>
|
|
101
|
+
<label htmlFor="verify-email-required-new-email" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
102
|
+
{t('auth.newEmailLabel')}
|
|
103
|
+
</label>
|
|
104
|
+
<input
|
|
105
|
+
id="verify-email-required-new-email"
|
|
106
|
+
name="newEmail"
|
|
107
|
+
type="email"
|
|
108
|
+
className="w-full px-4 h-9 text-sm text-gray-900 dark:text-white bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
|
109
|
+
placeholder={t('auth.newEmailPlaceholder')}
|
|
110
|
+
value={newEmail}
|
|
111
|
+
onChange={(e) => setNewEmail(e.target.value)}
|
|
112
|
+
/>
|
|
113
|
+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
|
114
|
+
{t('auth.newEmailHint')}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
</>
|
|
118
|
+
) : (
|
|
119
|
+
<div>
|
|
120
|
+
<label htmlFor="verify-email-required-email" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
121
|
+
{t('auth.verifyEmailRequiredPageEmailLabel')}
|
|
122
|
+
</label>
|
|
123
|
+
<input
|
|
124
|
+
id="verify-email-required-email"
|
|
125
|
+
name="email"
|
|
126
|
+
type="email"
|
|
127
|
+
required
|
|
128
|
+
className="w-full px-4 h-9 text-sm text-gray-900 dark:text-white bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
|
129
|
+
placeholder={t('auth.emailPlaceholder')}
|
|
130
|
+
value={email}
|
|
131
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
{error && (
|
|
136
|
+
<div className="px-4 py-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
137
|
+
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
<Button
|
|
141
|
+
type="submit"
|
|
142
|
+
variant="primary"
|
|
143
|
+
disabled={loading}
|
|
144
|
+
icon={Mail}
|
|
145
|
+
className="w-full"
|
|
146
|
+
>
|
|
147
|
+
{loading ? t('common.loading') : t('auth.resendVerificationEmail')}
|
|
148
|
+
</Button>
|
|
149
|
+
<div className="text-center">
|
|
150
|
+
<Link
|
|
151
|
+
to={loginPath}
|
|
152
|
+
className="text-sm font-medium text-primary hover:text-primary/90"
|
|
153
|
+
>
|
|
154
|
+
{t('signup.backToLogin')}
|
|
155
|
+
</Link>
|
|
156
|
+
</div>
|
|
157
|
+
</form>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|