@mdguggenbichler/slugbase-core 0.0.4 → 0.0.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/frontend/index.tsx +3 -3
- package/frontend/public/favicon.svg +1 -0
- package/frontend/public/slugbase_icon_blue.svg +1 -0
- package/frontend/public/slugbase_icon_white.png +0 -0
- package/frontend/public/slugbase_icon_white.svg +1 -0
- package/frontend/src/App.tsx +179 -0
- package/frontend/src/api/client.ts +134 -0
- package/frontend/src/components/AppSidebar.tsx +214 -0
- package/frontend/src/components/EmptyState.tsx +33 -0
- package/frontend/src/components/Favicon.tsx +76 -0
- package/frontend/src/components/FilterChips.tsx +60 -0
- package/frontend/src/components/FolderIcon.tsx +207 -0
- package/frontend/src/components/GlobalSearch.tsx +275 -0
- package/frontend/src/components/Layout.tsx +60 -0
- package/frontend/src/components/PageHeader.tsx +31 -0
- package/frontend/src/components/ScopeSegmentedControl.tsx +42 -0
- package/frontend/src/components/SentryDebug.tsx +32 -0
- package/frontend/src/components/StatCard.tsx +66 -0
- package/frontend/src/components/TopBar.tsx +63 -0
- package/frontend/src/components/UserDropdown.tsx +86 -0
- package/frontend/src/components/admin/AdminAI.tsx +207 -0
- package/frontend/src/components/admin/AdminOIDCProviders.tsx +183 -0
- package/frontend/src/components/admin/AdminSettings.tsx +413 -0
- package/frontend/src/components/admin/AdminTeams.tsx +177 -0
- package/frontend/src/components/admin/AdminUsers.tsx +225 -0
- package/frontend/src/components/bookmarks/BookmarkCard.tsx +312 -0
- package/frontend/src/components/bookmarks/BookmarkListItem.tsx +159 -0
- package/frontend/src/components/bookmarks/BookmarkTableView.tsx +419 -0
- package/frontend/src/components/bookmarks/BulkActionModals.tsx +162 -0
- package/frontend/src/components/bookmarks/FilterChips.tsx +5 -0
- package/frontend/src/components/modals/BookmarkModal.tsx +493 -0
- package/frontend/src/components/modals/FolderModal.tsx +306 -0
- package/frontend/src/components/modals/ImportModal.tsx +232 -0
- package/frontend/src/components/modals/OIDCProviderModal.tsx +284 -0
- package/frontend/src/components/modals/SharingModal.tsx +96 -0
- package/frontend/src/components/modals/TagModal.tsx +101 -0
- package/frontend/src/components/modals/TeamAssignmentModal.tsx +354 -0
- package/frontend/src/components/modals/TeamModal.tsx +117 -0
- package/frontend/src/components/modals/UserModal.tsx +225 -0
- package/frontend/src/components/profile/CreateTokenModal.tsx +172 -0
- package/frontend/src/components/sharing/ShareResourceDialog.tsx +422 -0
- package/frontend/src/components/ui/Autocomplete.tsx +155 -0
- package/frontend/src/components/ui/Button.tsx +68 -0
- package/frontend/src/components/ui/ConfirmDialog.tsx +79 -0
- package/frontend/src/components/ui/FormFieldWrapper.tsx +36 -0
- package/frontend/src/components/ui/ModalFooterActions.tsx +49 -0
- package/frontend/src/components/ui/ModalSection.tsx +34 -0
- package/frontend/src/components/ui/PageLoadingSkeleton.tsx +24 -0
- package/frontend/src/components/ui/Select.tsx +61 -0
- package/frontend/src/components/ui/SharingField.tsx +298 -0
- package/frontend/src/components/ui/Toast.tsx +47 -0
- package/frontend/src/components/ui/Tooltip.tsx +21 -0
- package/frontend/src/components/ui/alert-dialog.tsx +139 -0
- package/frontend/src/components/ui/badge.tsx +36 -0
- package/frontend/src/components/ui/button-base.tsx +57 -0
- package/frontend/src/components/ui/card.tsx +76 -0
- package/frontend/src/components/ui/command.tsx +161 -0
- package/frontend/src/components/ui/dialog.tsx +120 -0
- package/frontend/src/components/ui/dropdown-menu.tsx +199 -0
- package/frontend/src/components/ui/input.tsx +22 -0
- package/frontend/src/components/ui/label.tsx +24 -0
- package/frontend/src/components/ui/popover.tsx +33 -0
- package/frontend/src/components/ui/progress.tsx +26 -0
- package/frontend/src/components/ui/scroll-area.tsx +48 -0
- package/frontend/src/components/ui/select-base.tsx +159 -0
- package/frontend/src/components/ui/separator.tsx +29 -0
- package/frontend/src/components/ui/sheet.tsx +140 -0
- package/frontend/src/components/ui/sidebar.tsx +783 -0
- package/frontend/src/components/ui/skeleton.tsx +15 -0
- package/frontend/src/components/ui/sonner.tsx +46 -0
- package/frontend/src/components/ui/switch.tsx +28 -0
- package/frontend/src/components/ui/table.tsx +120 -0
- package/frontend/src/components/ui/tooltip-base.tsx +30 -0
- package/frontend/src/config/api.ts +16 -0
- package/frontend/src/config/mode.ts +6 -0
- package/frontend/src/contexts/AppConfigContext.tsx +39 -0
- package/frontend/src/contexts/AuthContext.tsx +137 -0
- package/frontend/src/contexts/SearchCommandContext.tsx +28 -0
- package/frontend/src/hooks/use-mobile.tsx +19 -0
- package/frontend/src/hooks/useConfirmDialog.ts +63 -0
- package/frontend/src/hooks/useMarketingTheme.ts +47 -0
- package/frontend/src/i18n.ts +39 -0
- package/frontend/src/index.css +117 -0
- package/frontend/src/instrument.ts +20 -0
- package/frontend/src/lib/utils.ts +6 -0
- package/frontend/src/locales/de.json +899 -0
- package/frontend/src/locales/en.json +937 -0
- package/frontend/src/locales/es.json +884 -0
- package/frontend/src/locales/fr.json +550 -0
- package/frontend/src/locales/it.json +535 -0
- package/frontend/src/locales/ja.json +535 -0
- package/frontend/src/locales/nl.json +550 -0
- package/frontend/src/locales/pl.json +535 -0
- package/frontend/src/locales/pt.json +535 -0
- package/frontend/src/locales/ru.json +535 -0
- package/frontend/src/locales/zh.json +535 -0
- package/frontend/src/main.tsx +44 -0
- package/frontend/src/pages/Bookmarks.tsx +1004 -0
- package/frontend/src/pages/Dashboard.tsx +427 -0
- package/frontend/src/pages/Folders.tsx +578 -0
- package/frontend/src/pages/GoPreferences.tsx +134 -0
- package/frontend/src/pages/Login.tsx +196 -0
- package/frontend/src/pages/PasswordReset.tsx +242 -0
- package/frontend/src/pages/Profile.tsx +593 -0
- package/frontend/src/pages/SearchEngineGuide.tsx +135 -0
- package/frontend/src/pages/Setup.tsx +210 -0
- package/frontend/src/pages/Signup.tsx +199 -0
- package/frontend/src/pages/Tags.tsx +421 -0
- package/frontend/src/pages/VerifyEmail.tsx +254 -0
- package/frontend/src/pages/admin/AdminAIPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminLayout.tsx +40 -0
- package/frontend/src/pages/admin/AdminMembersPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminOIDCPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminSettingsPage.tsx +5 -0
- package/frontend/src/pages/admin/AdminTeamsPage.tsx +5 -0
- package/frontend/src/utils/favicon.ts +36 -0
- package/frontend/src/utils/formatRelativeTime.ts +37 -0
- package/frontend/src/utils/safeHref.ts +31 -0
- package/frontend/src/vite-env.d.ts +10 -0
- package/package.json +9 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { ArrowLeft, Search, Code, CheckCircle } from 'lucide-react';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import Button from '../components/ui/Button';
|
|
5
|
+
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
6
|
+
|
|
7
|
+
export default function SearchEngineGuide() {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const { appBasePath } = useAppConfig();
|
|
10
|
+
|
|
11
|
+
const baseUrl = window.location.origin;
|
|
12
|
+
const goPath = '/go/%s';
|
|
13
|
+
const searchUrl = `${baseUrl}${goPath}`;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="space-y-6 max-w-4xl mx-auto">
|
|
17
|
+
{/* Header */}
|
|
18
|
+
<div className="flex items-center gap-4">
|
|
19
|
+
<Link to={`${appBasePath}/bookmarks`}>
|
|
20
|
+
<Button variant="ghost" size="sm" icon={ArrowLeft}>
|
|
21
|
+
{t('common.back')}
|
|
22
|
+
</Button>
|
|
23
|
+
</Link>
|
|
24
|
+
<div>
|
|
25
|
+
<h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
|
|
26
|
+
<Search className="h-8 w-8" />
|
|
27
|
+
{t('searchEngineGuide.title')}
|
|
28
|
+
</h1>
|
|
29
|
+
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
30
|
+
{t('searchEngineGuide.description')}
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
{/* How it works */}
|
|
36
|
+
<div className="bg-primary/10 border border-primary/30 rounded-xl p-6">
|
|
37
|
+
<h2 className="text-xl font-semibold text-foreground mb-3">
|
|
38
|
+
{t('searchEngineGuide.howItWorks')}
|
|
39
|
+
</h2>
|
|
40
|
+
<p className="text-muted-foreground mb-4">
|
|
41
|
+
{t('searchEngineGuide.howItWorksDescription')}
|
|
42
|
+
</p>
|
|
43
|
+
<div className="bg-card rounded-lg p-4 border border-primary/30">
|
|
44
|
+
<div className="flex items-center gap-2 text-sm font-mono text-foreground">
|
|
45
|
+
<Code className="h-4 w-4 text-primary" />
|
|
46
|
+
<span className="text-primary">go</span>
|
|
47
|
+
<span className="text-gray-400">your-slug</span>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
{/* Your search URL */}
|
|
53
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
54
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-3">
|
|
55
|
+
{t('searchEngineGuide.yourSearchUrl')}
|
|
56
|
+
</h2>
|
|
57
|
+
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
|
|
58
|
+
<code className="text-sm font-mono text-gray-900 dark:text-white break-all">
|
|
59
|
+
{searchUrl}
|
|
60
|
+
</code>
|
|
61
|
+
</div>
|
|
62
|
+
<p className="mt-3 text-sm text-gray-600 dark:text-gray-400">
|
|
63
|
+
{t('searchEngineGuide.urlNote')}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{/* Chromium-based browsers */}
|
|
68
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
69
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
|
70
|
+
<CheckCircle className="h-6 w-6 text-green-600 dark:text-green-400" />
|
|
71
|
+
{t('searchEngineGuide.chromiumTitle')}
|
|
72
|
+
</h2>
|
|
73
|
+
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
|
74
|
+
{t('searchEngineGuide.chromiumDescription')}
|
|
75
|
+
</p>
|
|
76
|
+
<ol className="list-decimal list-inside space-y-3 text-gray-700 dark:text-gray-300">
|
|
77
|
+
<li>{t('searchEngineGuide.chromiumStep1')}</li>
|
|
78
|
+
<li>{t('searchEngineGuide.chromiumStep2')}</li>
|
|
79
|
+
<li>{t('searchEngineGuide.chromiumStep3')}</li>
|
|
80
|
+
<li>
|
|
81
|
+
{t('searchEngineGuide.chromiumStep4')}
|
|
82
|
+
<ul className="list-disc list-inside ml-6 mt-2 space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
|
83
|
+
<li>{t('searchEngineGuide.chromiumStep4a')}</li>
|
|
84
|
+
<li>{t('searchEngineGuide.chromiumStep4b')}</li>
|
|
85
|
+
<li>{t('searchEngineGuide.chromiumStep4c')}</li>
|
|
86
|
+
</ul>
|
|
87
|
+
</li>
|
|
88
|
+
<li>{t('searchEngineGuide.chromiumStep5')}</li>
|
|
89
|
+
</ol>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
{/* Firefox */}
|
|
93
|
+
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
|
|
94
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
|
95
|
+
<CheckCircle className="h-6 w-6 text-orange-600 dark:text-orange-400" />
|
|
96
|
+
{t('searchEngineGuide.firefoxTitle')}
|
|
97
|
+
</h2>
|
|
98
|
+
<p className="text-gray-700 dark:text-gray-300 mb-4">
|
|
99
|
+
{t('searchEngineGuide.firefoxDescription')}
|
|
100
|
+
</p>
|
|
101
|
+
<ol className="list-decimal list-inside space-y-3 text-gray-700 dark:text-gray-300">
|
|
102
|
+
<li>{t('searchEngineGuide.firefoxStep1')}</li>
|
|
103
|
+
<li>{t('searchEngineGuide.firefoxStep2')}</li>
|
|
104
|
+
<li>{t('searchEngineGuide.firefoxStep3')}</li>
|
|
105
|
+
<li>
|
|
106
|
+
{t('searchEngineGuide.firefoxStep4')}
|
|
107
|
+
<ul className="list-disc list-inside ml-6 mt-2 space-y-1 text-sm text-gray-600 dark:text-gray-400">
|
|
108
|
+
<li>{t('searchEngineGuide.firefoxStep4a')}</li>
|
|
109
|
+
<li>{t('searchEngineGuide.firefoxStep4b')}</li>
|
|
110
|
+
<li>{t('searchEngineGuide.firefoxStep4c')}</li>
|
|
111
|
+
</ul>
|
|
112
|
+
</li>
|
|
113
|
+
<li>{t('searchEngineGuide.firefoxStep5')}</li>
|
|
114
|
+
</ol>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
{/* Usage example */}
|
|
118
|
+
<div className="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-xl p-6">
|
|
119
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-3">
|
|
120
|
+
{t('searchEngineGuide.usageExample')}
|
|
121
|
+
</h2>
|
|
122
|
+
<div className="space-y-2 text-gray-700 dark:text-gray-300">
|
|
123
|
+
<p>{t('searchEngineGuide.usageStep1')}</p>
|
|
124
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-green-200 dark:border-green-700">
|
|
125
|
+
<code className="text-sm font-mono text-gray-900 dark:text-white">go test</code>
|
|
126
|
+
</div>
|
|
127
|
+
<p className="mt-3">{t('searchEngineGuide.usageStep2')}</p>
|
|
128
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
129
|
+
{t('searchEngineGuide.usageNote')}
|
|
130
|
+
</p>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useNavigate } from 'react-router-dom';
|
|
4
|
+
import api from '../api/client';
|
|
5
|
+
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
6
|
+
import { useAuth } from '../contexts/AuthContext';
|
|
7
|
+
import { CheckCircle, UserPlus, Shield } from 'lucide-react';
|
|
8
|
+
import Button from '../components/ui/Button';
|
|
9
|
+
|
|
10
|
+
export default function Setup() {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const navigate = useNavigate();
|
|
13
|
+
const { appRootPath } = useAppConfig();
|
|
14
|
+
const { user, checkAuth } = useAuth();
|
|
15
|
+
const [formData, setFormData] = useState({
|
|
16
|
+
email: '',
|
|
17
|
+
name: '',
|
|
18
|
+
password: '',
|
|
19
|
+
confirmPassword: '',
|
|
20
|
+
});
|
|
21
|
+
const [loading, setLoading] = useState(false);
|
|
22
|
+
const [error, setError] = useState('');
|
|
23
|
+
const [success, setSuccess] = useState(false);
|
|
24
|
+
|
|
25
|
+
// If user is already authenticated, redirect to dashboard
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (user) {
|
|
28
|
+
navigate(appRootPath);
|
|
29
|
+
}
|
|
30
|
+
}, [user, navigate]);
|
|
31
|
+
|
|
32
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setLoading(true);
|
|
35
|
+
setError('');
|
|
36
|
+
|
|
37
|
+
if (formData.password !== formData.confirmPassword) {
|
|
38
|
+
setError(t('setup.passwordMismatch'));
|
|
39
|
+
setLoading(false);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (formData.password.length < 8) {
|
|
44
|
+
setError(t('setup.passwordTooShort'));
|
|
45
|
+
setLoading(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const { confirmPassword, ...dataToSend } = formData;
|
|
51
|
+
await api.post('/auth/setup', dataToSend);
|
|
52
|
+
|
|
53
|
+
// User is automatically logged in by the backend (cookie is set)
|
|
54
|
+
// Check auth status to update AuthContext
|
|
55
|
+
await checkAuth();
|
|
56
|
+
|
|
57
|
+
setSuccess(true);
|
|
58
|
+
// Redirect to dashboard after a brief delay
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
navigate(appRootPath);
|
|
61
|
+
}, 1500);
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
setError(err.response?.data?.error || t('common.error'));
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (success) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900">
|
|
72
|
+
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-lg p-4">
|
|
73
|
+
<div className="text-center space-y-4">
|
|
74
|
+
<div className="mx-auto w-16 h-16 bg-green-100 dark:bg-green-900/30 rounded-full flex items-center justify-center">
|
|
75
|
+
<CheckCircle className="h-8 w-8 text-green-600 dark:text-green-400" />
|
|
76
|
+
</div>
|
|
77
|
+
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
78
|
+
{t('setup.success')}
|
|
79
|
+
</h2>
|
|
80
|
+
<p className="text-gray-600 dark:text-gray-400">
|
|
81
|
+
{t('setup.redirectingToDashboard')}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<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">
|
|
91
|
+
<div className="max-w-md w-full space-y-8">
|
|
92
|
+
<div className="text-center">
|
|
93
|
+
<div className="flex justify-center mb-6">
|
|
94
|
+
<img
|
|
95
|
+
src="/slugbase_icon_blue.svg"
|
|
96
|
+
alt="SlugBase"
|
|
97
|
+
className="h-16 w-16 dark:hidden"
|
|
98
|
+
/>
|
|
99
|
+
<img
|
|
100
|
+
src="/slugbase_icon_white.svg"
|
|
101
|
+
alt="SlugBase"
|
|
102
|
+
className="h-16 w-16 hidden dark:block"
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
106
|
+
{t('setup.title')}
|
|
107
|
+
</h2>
|
|
108
|
+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
109
|
+
{t('setup.description')}
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-lg p-4">
|
|
114
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
115
|
+
<div>
|
|
116
|
+
<label htmlFor="email" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
117
|
+
{t('setup.email')}
|
|
118
|
+
</label>
|
|
119
|
+
<input
|
|
120
|
+
id="email"
|
|
121
|
+
name="email"
|
|
122
|
+
type="email"
|
|
123
|
+
required
|
|
124
|
+
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"
|
|
125
|
+
placeholder={t('setup.emailPlaceholder')}
|
|
126
|
+
value={formData.email}
|
|
127
|
+
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<div>
|
|
132
|
+
<label htmlFor="name" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
133
|
+
{t('setup.name')}
|
|
134
|
+
</label>
|
|
135
|
+
<input
|
|
136
|
+
id="name"
|
|
137
|
+
name="name"
|
|
138
|
+
type="text"
|
|
139
|
+
required
|
|
140
|
+
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"
|
|
141
|
+
placeholder={t('setup.namePlaceholder')}
|
|
142
|
+
value={formData.name}
|
|
143
|
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
|
144
|
+
/>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<div>
|
|
148
|
+
<label htmlFor="password" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
149
|
+
{t('setup.password')}
|
|
150
|
+
</label>
|
|
151
|
+
<input
|
|
152
|
+
id="password"
|
|
153
|
+
name="password"
|
|
154
|
+
type="password"
|
|
155
|
+
required
|
|
156
|
+
minLength={8}
|
|
157
|
+
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"
|
|
158
|
+
placeholder={t('setup.passwordPlaceholder')}
|
|
159
|
+
value={formData.password}
|
|
160
|
+
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div>
|
|
165
|
+
<label htmlFor="confirmPassword" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
166
|
+
{t('setup.confirmPassword')}
|
|
167
|
+
</label>
|
|
168
|
+
<input
|
|
169
|
+
id="confirmPassword"
|
|
170
|
+
name="confirmPassword"
|
|
171
|
+
type="password"
|
|
172
|
+
required
|
|
173
|
+
minLength={8}
|
|
174
|
+
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"
|
|
175
|
+
placeholder={t('setup.confirmPasswordPlaceholder')}
|
|
176
|
+
value={formData.confirmPassword}
|
|
177
|
+
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="px-4 py-3 bg-primary/10 border border-primary/30 rounded-lg">
|
|
182
|
+
<div className="flex items-start gap-2">
|
|
183
|
+
<Shield className="h-5 w-5 text-primary flex-shrink-0 mt-0.5" />
|
|
184
|
+
<p className="text-sm text-foreground">
|
|
185
|
+
{t('setup.adminNote')}
|
|
186
|
+
</p>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
{error && (
|
|
191
|
+
<div className="px-4 py-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
192
|
+
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
|
|
196
|
+
<Button
|
|
197
|
+
type="submit"
|
|
198
|
+
variant="primary"
|
|
199
|
+
disabled={loading}
|
|
200
|
+
icon={UserPlus}
|
|
201
|
+
className="w-full"
|
|
202
|
+
>
|
|
203
|
+
{loading ? t('common.loading') : t('setup.submit')}
|
|
204
|
+
</Button>
|
|
205
|
+
</form>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
);
|
|
210
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import api from '../api/client';
|
|
5
|
+
import { useAppConfig } from '../contexts/AppConfigContext';
|
|
6
|
+
import Button from '../components/ui/Button';
|
|
7
|
+
|
|
8
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
9
|
+
|
|
10
|
+
export default function Signup() {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const { appBasePath } = useAppConfig();
|
|
13
|
+
const [email, setEmail] = useState('');
|
|
14
|
+
const [name, setName] = useState('');
|
|
15
|
+
const [password, setPassword] = useState('');
|
|
16
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
17
|
+
const [acceptTerms, setAcceptTerms] = useState(false);
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
const [success, setSuccess] = useState(false);
|
|
20
|
+
const [error, setError] = useState('');
|
|
21
|
+
|
|
22
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
setError('');
|
|
25
|
+
if (password !== confirmPassword) {
|
|
26
|
+
setError(t('setup.passwordMismatch'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
30
|
+
setError(t('setup.passwordTooShort'));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!acceptTerms) {
|
|
34
|
+
setError(t('signup.acceptTermsRequired'));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
setLoading(true);
|
|
38
|
+
try {
|
|
39
|
+
await api.post('/auth/register', { email, name, password });
|
|
40
|
+
setSuccess(true);
|
|
41
|
+
} catch (err: any) {
|
|
42
|
+
setError(err.response?.data?.error || t('common.error'));
|
|
43
|
+
} finally {
|
|
44
|
+
setLoading(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (success) {
|
|
49
|
+
return (
|
|
50
|
+
<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">
|
|
51
|
+
<div className="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-lg p-6 text-center space-y-4">
|
|
52
|
+
<div className="flex justify-center mb-4">
|
|
53
|
+
<img src="/slugbase_icon_blue.svg" alt="SlugBase" className="h-16 w-16 dark:hidden" />
|
|
54
|
+
<img src="/slugbase_icon_white.svg" alt="SlugBase" className="h-16 w-16 hidden dark:block" />
|
|
55
|
+
</div>
|
|
56
|
+
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
|
57
|
+
{t('signup.successTitle')}
|
|
58
|
+
</h2>
|
|
59
|
+
<p className="text-gray-600 dark:text-gray-400 text-sm">
|
|
60
|
+
{t('signup.successMessage')}
|
|
61
|
+
</p>
|
|
62
|
+
<Link
|
|
63
|
+
to={`${appBasePath}/login`}
|
|
64
|
+
className="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg"
|
|
65
|
+
>
|
|
66
|
+
{t('signup.backToLogin')}
|
|
67
|
+
</Link>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<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">
|
|
75
|
+
<div className="max-w-md w-full space-y-8">
|
|
76
|
+
<div className="text-center">
|
|
77
|
+
<div className="flex justify-center mb-6">
|
|
78
|
+
<img src="/slugbase_icon_blue.svg" alt="SlugBase" className="h-16 w-16 dark:hidden" />
|
|
79
|
+
<img src="/slugbase_icon_white.svg" alt="SlugBase" className="h-16 w-16 hidden dark:block" />
|
|
80
|
+
</div>
|
|
81
|
+
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white">
|
|
82
|
+
{t('signup.title')}
|
|
83
|
+
</h2>
|
|
84
|
+
<p className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
85
|
+
{t('signup.subtitle')}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-lg p-4">
|
|
90
|
+
<form onSubmit={handleSubmit} className="space-y-5">
|
|
91
|
+
<div>
|
|
92
|
+
<label htmlFor="signup-email" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
93
|
+
{t('signup.email')}
|
|
94
|
+
</label>
|
|
95
|
+
<input
|
|
96
|
+
id="signup-email"
|
|
97
|
+
type="email"
|
|
98
|
+
required
|
|
99
|
+
autoComplete="email"
|
|
100
|
+
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-blue-500"
|
|
101
|
+
placeholder={t('auth.emailPlaceholder')}
|
|
102
|
+
value={email}
|
|
103
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
<div>
|
|
107
|
+
<label htmlFor="signup-name" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
108
|
+
{t('signup.name')}
|
|
109
|
+
</label>
|
|
110
|
+
<input
|
|
111
|
+
id="signup-name"
|
|
112
|
+
type="text"
|
|
113
|
+
required
|
|
114
|
+
autoComplete="name"
|
|
115
|
+
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-blue-500"
|
|
116
|
+
placeholder={t('signup.namePlaceholder')}
|
|
117
|
+
value={name}
|
|
118
|
+
onChange={(e) => setName(e.target.value)}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
<div>
|
|
122
|
+
<label htmlFor="signup-password" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
123
|
+
{t('signup.password')}
|
|
124
|
+
</label>
|
|
125
|
+
<input
|
|
126
|
+
id="signup-password"
|
|
127
|
+
type="password"
|
|
128
|
+
required
|
|
129
|
+
minLength={MIN_PASSWORD_LENGTH}
|
|
130
|
+
autoComplete="new-password"
|
|
131
|
+
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-blue-500"
|
|
132
|
+
placeholder={t('setup.passwordPlaceholder')}
|
|
133
|
+
value={password}
|
|
134
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
135
|
+
/>
|
|
136
|
+
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">{t('signup.passwordHint')}</p>
|
|
137
|
+
</div>
|
|
138
|
+
<div>
|
|
139
|
+
<label htmlFor="signup-confirm" className="block text-sm font-semibold text-gray-900 dark:text-white mb-2">
|
|
140
|
+
{t('signup.confirmPassword')}
|
|
141
|
+
</label>
|
|
142
|
+
<input
|
|
143
|
+
id="signup-confirm"
|
|
144
|
+
type="password"
|
|
145
|
+
required
|
|
146
|
+
minLength={MIN_PASSWORD_LENGTH}
|
|
147
|
+
autoComplete="new-password"
|
|
148
|
+
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-blue-500"
|
|
149
|
+
placeholder={t('setup.confirmPasswordPlaceholder')}
|
|
150
|
+
value={confirmPassword}
|
|
151
|
+
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
<div className="flex items-start gap-3">
|
|
155
|
+
<input
|
|
156
|
+
id="signup-accept-terms"
|
|
157
|
+
type="checkbox"
|
|
158
|
+
required
|
|
159
|
+
checked={acceptTerms}
|
|
160
|
+
onChange={(e) => setAcceptTerms(e.target.checked)}
|
|
161
|
+
className="mt-1 h-4 w-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500"
|
|
162
|
+
/>
|
|
163
|
+
<label htmlFor="signup-accept-terms" className="text-sm text-gray-700 dark:text-gray-300">
|
|
164
|
+
{t('signup.acceptTermsPrefix')}
|
|
165
|
+
<a href="https://docs.slugbase.app" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">
|
|
166
|
+
{t('signup.acceptTermsTerms')}
|
|
167
|
+
</a>
|
|
168
|
+
{t('signup.acceptTermsAnd')}
|
|
169
|
+
<a href="https://docs.slugbase.app" target="_blank" rel="noopener noreferrer" className="text-blue-600 dark:text-blue-400 hover:underline">
|
|
170
|
+
{t('signup.acceptTermsPrivacy')}
|
|
171
|
+
</a>
|
|
172
|
+
{t('signup.acceptTermsSuffix')}
|
|
173
|
+
</label>
|
|
174
|
+
</div>
|
|
175
|
+
{error && (
|
|
176
|
+
<div className="px-4 py-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
|
|
177
|
+
<p className="text-sm text-red-800 dark:text-red-200">{error}</p>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
<Button
|
|
181
|
+
type="submit"
|
|
182
|
+
variant="primary"
|
|
183
|
+
disabled={loading}
|
|
184
|
+
className="w-full"
|
|
185
|
+
>
|
|
186
|
+
{loading ? t('common.loading') : t('signup.submit')}
|
|
187
|
+
</Button>
|
|
188
|
+
</form>
|
|
189
|
+
<p className="mt-4 text-center text-sm text-gray-600 dark:text-gray-400">
|
|
190
|
+
{t('signup.alreadyHaveAccount')}{' '}
|
|
191
|
+
<Link to={`${appBasePath}/login`} className="font-medium text-blue-600 dark:text-blue-400 hover:underline">
|
|
192
|
+
{t('signup.logIn')}
|
|
193
|
+
</Link>
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|