@idevconn/create-icore 0.6.3 → 0.7.0
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/dist/cli.js +384 -276
- package/dist/index.cjs +385 -277
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +382 -274
- package/package.json +1 -1
- package/templates/.yarn/releases/yarn-4.16.0.cjs +944 -0
- package/templates/.yarnrc.yml +1 -1
- package/templates/apps/api/src/app/storage/storage.controller.ts +28 -0
- package/templates/apps/microservices/auth/src/app/app.module.ts +20 -2
- package/templates/apps/microservices/notes/src/app/app.module.ts +17 -2
- package/templates/apps/microservices/upload/src/app/app.module.ts +17 -2
- package/templates/apps/microservices/upload/src/app/storage.controller.ts +7 -0
- package/templates/apps/templates/client-antd/src/components/auth/AuthBrandPanel.tsx +59 -0
- package/templates/apps/templates/client-antd/src/components/auth/CheckEmailScreen.tsx +28 -0
- package/templates/apps/templates/client-antd/src/components/auth/LoginForm.tsx +116 -0
- package/templates/apps/templates/client-antd/src/components/auth/MagicLinkForm.tsx +95 -0
- package/templates/apps/templates/client-antd/src/components/auth/RegisterForm.tsx +98 -0
- package/templates/apps/templates/client-antd/src/globals.less +6 -0
- package/templates/apps/templates/client-antd/src/main.tsx +1 -1
- package/templates/apps/templates/client-antd/src/routes/login.tsx +45 -181
- package/templates/apps/templates/client-mui/src/components/auth/AuthBrandPanel.tsx +59 -0
- package/templates/apps/templates/client-mui/src/components/auth/CheckEmailScreen.tsx +28 -0
- package/templates/apps/templates/client-mui/src/components/auth/LoginForm.tsx +141 -0
- package/templates/apps/templates/client-mui/src/components/auth/MagicLinkForm.tsx +106 -0
- package/templates/apps/templates/client-mui/src/components/auth/RegisterForm.tsx +113 -0
- package/templates/apps/templates/client-mui/src/main.tsx +1 -1
- package/templates/apps/templates/client-mui/src/routes/login.tsx +50 -186
- package/templates/apps/templates/client-shadcn/src/components/auth/AuthBrandPanel.tsx +52 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/CheckEmailScreen.tsx +29 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/LoginForm.tsx +161 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/MagicLinkForm.tsx +110 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/RegisterForm.tsx +107 -0
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutHeader.tsx +31 -10
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +22 -27
- package/templates/apps/templates/client-shadcn/src/components/ui/card.tsx +1 -1
- package/templates/apps/templates/client-shadcn/src/globals.css +39 -13
- package/templates/apps/templates/client-shadcn/src/routes/auth.callback.tsx +1 -1
- package/templates/apps/templates/client-shadcn/src/routes/login.tsx +55 -165
- package/templates/libs/auth-strategies/mongodb/CHANGELOG.md +8 -0
- package/templates/libs/auth-strategies/mongodb/README.md +11 -0
- package/templates/libs/auth-strategies/mongodb/eslint.config.mjs +19 -0
- package/templates/libs/auth-strategies/mongodb/jest.config.cts +10 -0
- package/templates/libs/auth-strategies/mongodb/package.json +16 -0
- package/templates/libs/auth-strategies/mongodb/project.json +19 -0
- package/templates/libs/auth-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/__tests__/mongodb-auth.strategy.unit.test.ts +42 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/auth-mongodb.spec.ts +7 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/auth-mongodb.ts +3 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/mongodb-auth.strategy.ts +188 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.json +23 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.lib.json +10 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.spec.json +16 -0
- package/templates/libs/db-strategies/mongodb/CHANGELOG.md +7 -0
- package/templates/libs/db-strategies/mongodb/README.md +11 -0
- package/templates/libs/db-strategies/mongodb/eslint.config.mjs +19 -0
- package/templates/libs/db-strategies/mongodb/jest.config.cts +10 -0
- package/templates/libs/db-strategies/mongodb/package.json +14 -0
- package/templates/libs/db-strategies/mongodb/project.json +19 -0
- package/templates/libs/db-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/db-strategies/mongodb/src/lib/__tests__/mongodb-db.strategy.unit.test.ts +38 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb-db.strategy.ts +108 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb.spec.ts +7 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb.ts +3 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.json +23 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.lib.json +10 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.spec.json +16 -0
- package/templates/libs/shared/src/strategies/storage.ts +3 -0
- package/templates/libs/storage-strategies/mongodb/CHANGELOG.md +8 -0
- package/templates/libs/storage-strategies/mongodb/README.md +11 -0
- package/templates/libs/storage-strategies/mongodb/eslint.config.mjs +19 -0
- package/templates/libs/storage-strategies/mongodb/jest.config.cts +10 -0
- package/templates/libs/storage-strategies/mongodb/package.json +14 -0
- package/templates/libs/storage-strategies/mongodb/project.json +19 -0
- package/templates/libs/storage-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/__tests__/mongodb-storage.strategy.unit.test.ts +38 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/mongodb-storage.strategy.ts +93 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/storage-mongodb.spec.ts +7 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/storage-mongodb.ts +3 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.json +23 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.lib.json +10 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.spec.json +16 -0
- package/templates/libs/template-shared/src/lib/i18n/keys.ts +216 -56
- package/templates/libs/template-shared/src/lib/stores/theme.store.ts +1 -6
- package/templates/libs/upload-client/src/lib/upload-client.service.ts +7 -0
- package/templates/tsconfig.base.json +4 -1
- package/templates/.yarn/releases/yarn-4.15.0.cjs +0 -940
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { SyntheticEvent, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Loader2, MailCheck } from 'lucide-react';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
import { Input } from '../ui/input';
|
|
6
|
+
import { Label } from '../ui/label';
|
|
7
|
+
|
|
8
|
+
interface MagicLinkFormProps {
|
|
9
|
+
onError: (msg: string) => void;
|
|
10
|
+
onSwitchToLogin: () => void;
|
|
11
|
+
api: <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function MagicLinkForm({ onError, onSwitchToLogin, api }: MagicLinkFormProps) {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const [email, setEmail] = useState('');
|
|
17
|
+
const [submitting, setSubmitting] = useState(false);
|
|
18
|
+
const [sent, setSent] = useState(false);
|
|
19
|
+
|
|
20
|
+
async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setSubmitting(true);
|
|
23
|
+
try {
|
|
24
|
+
await api('/auth/magic-link', {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: { 'Content-Type': 'application/json' },
|
|
27
|
+
body: JSON.stringify({ email }),
|
|
28
|
+
});
|
|
29
|
+
setSent(true);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
onError(err instanceof Error ? err.message : t('error.unknown'));
|
|
32
|
+
} finally {
|
|
33
|
+
setSubmitting(false);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (sent) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex flex-col items-center text-center space-y-4 py-4">
|
|
40
|
+
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-[--color-primary]/15">
|
|
41
|
+
<MailCheck size={24} className="text-[--color-primary]" />
|
|
42
|
+
</div>
|
|
43
|
+
<div className="space-y-1">
|
|
44
|
+
<h3 className="text-lg font-semibold">{t('auth.magicLinkSent')}</h3>
|
|
45
|
+
<p className="text-sm text-[--color-muted-foreground] max-w-xs">
|
|
46
|
+
{t('auth.magicLinkSentDescription', { email })}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
<Button
|
|
50
|
+
type="button"
|
|
51
|
+
variant="outline"
|
|
52
|
+
className="w-full cursor-pointer"
|
|
53
|
+
onClick={() => {
|
|
54
|
+
setEmail('');
|
|
55
|
+
setSent(false);
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{t('auth.magicLinkUseDifferentEmail')}
|
|
59
|
+
</Button>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={onSwitchToLogin}
|
|
63
|
+
className="text-sm text-[--color-muted-foreground] hover:underline cursor-pointer"
|
|
64
|
+
>
|
|
65
|
+
{t('auth.backToLogin')}
|
|
66
|
+
</button>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="space-y-5">
|
|
73
|
+
<div className="space-y-1">
|
|
74
|
+
<h1 className="text-2xl font-bold">{t('auth.withMagicLink')}</h1>
|
|
75
|
+
<p className="text-sm text-[--color-muted-foreground]">
|
|
76
|
+
{t('auth.magicLinkSentDescription', { email: 'your email' }).replace(
|
|
77
|
+
'your email',
|
|
78
|
+
t('auth.email').toLowerCase(),
|
|
79
|
+
)}
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
84
|
+
<div className="space-y-2">
|
|
85
|
+
<Label htmlFor="ml-email">{t('auth.email')}</Label>
|
|
86
|
+
<Input
|
|
87
|
+
id="ml-email"
|
|
88
|
+
type="email"
|
|
89
|
+
value={email}
|
|
90
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
91
|
+
placeholder="you@example.com"
|
|
92
|
+
required
|
|
93
|
+
autoComplete="email"
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
<Button type="submit" className="w-full cursor-pointer" disabled={submitting}>
|
|
97
|
+
{submitting ? <Loader2 size={16} className="animate-spin" /> : t('auth.sendMagicLink')}
|
|
98
|
+
</Button>
|
|
99
|
+
</form>
|
|
100
|
+
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
onClick={onSwitchToLogin}
|
|
104
|
+
className="block w-full text-center text-sm text-[--color-muted-foreground] hover:underline cursor-pointer"
|
|
105
|
+
>
|
|
106
|
+
{t('auth.backToLogin')}
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { SyntheticEvent, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Loader2 } from 'lucide-react';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
import { Input } from '../ui/input';
|
|
6
|
+
import { Label } from '../ui/label';
|
|
7
|
+
|
|
8
|
+
interface RegisterFormProps {
|
|
9
|
+
onSuccess: (email: string) => void;
|
|
10
|
+
onError: (msg: string) => void;
|
|
11
|
+
onSwitchToLogin: () => void;
|
|
12
|
+
api: <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function RegisterForm({ onSuccess, onError, onSwitchToLogin, api }: RegisterFormProps) {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const [email, setEmail] = useState('');
|
|
18
|
+
const [password, setPassword] = useState('');
|
|
19
|
+
const [confirm, setConfirm] = useState('');
|
|
20
|
+
const [submitting, setSubmitting] = useState(false);
|
|
21
|
+
const [validationError, setValidationError] = useState('');
|
|
22
|
+
|
|
23
|
+
async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setValidationError('');
|
|
26
|
+
if (password !== confirm) {
|
|
27
|
+
setValidationError(t('auth.passwordMismatch'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
setSubmitting(true);
|
|
31
|
+
try {
|
|
32
|
+
await api('/auth/register', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ email, password }),
|
|
36
|
+
});
|
|
37
|
+
onSuccess(email);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
onError(err instanceof Error ? err.message : t('error.unknown'));
|
|
40
|
+
} finally {
|
|
41
|
+
setSubmitting(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="space-y-5">
|
|
47
|
+
<div className="space-y-1">
|
|
48
|
+
<h1 className="text-2xl font-bold">{t('auth.registerTitle')}</h1>
|
|
49
|
+
<p className="text-sm text-[--color-muted-foreground]">{t('auth.registerSubtitle')}</p>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
53
|
+
<div className="space-y-2">
|
|
54
|
+
<Label htmlFor="reg-email">{t('auth.email')}</Label>
|
|
55
|
+
<Input
|
|
56
|
+
id="reg-email"
|
|
57
|
+
type="email"
|
|
58
|
+
value={email}
|
|
59
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
60
|
+
placeholder="you@example.com"
|
|
61
|
+
required
|
|
62
|
+
autoComplete="email"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<Label htmlFor="reg-password">{t('auth.password')}</Label>
|
|
67
|
+
<Input
|
|
68
|
+
id="reg-password"
|
|
69
|
+
type="password"
|
|
70
|
+
value={password}
|
|
71
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
72
|
+
required
|
|
73
|
+
autoComplete="new-password"
|
|
74
|
+
/>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<Label htmlFor="reg-confirm">{t('auth.confirmPassword')}</Label>
|
|
78
|
+
<Input
|
|
79
|
+
id="reg-confirm"
|
|
80
|
+
type="password"
|
|
81
|
+
value={confirm}
|
|
82
|
+
onChange={(e) => setConfirm(e.target.value)}
|
|
83
|
+
required
|
|
84
|
+
autoComplete="new-password"
|
|
85
|
+
/>
|
|
86
|
+
{validationError && (
|
|
87
|
+
<p className="text-xs text-[--color-destructive]">{validationError}</p>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<Button type="submit" className="w-full cursor-pointer" disabled={submitting}>
|
|
91
|
+
{submitting ? <Loader2 size={16} className="animate-spin" /> : t('auth.register')}
|
|
92
|
+
</Button>
|
|
93
|
+
</form>
|
|
94
|
+
|
|
95
|
+
<p className="text-center text-sm text-[--color-muted-foreground]">
|
|
96
|
+
{t('auth.switchToLogin')}{' '}
|
|
97
|
+
<button
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={onSwitchToLogin}
|
|
100
|
+
className="text-[--color-primary] font-medium hover:underline cursor-pointer"
|
|
101
|
+
>
|
|
102
|
+
{t('auth.switchToLoginLink')}
|
|
103
|
+
</button>
|
|
104
|
+
</p>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useTranslation } from 'react-i18next';
|
|
2
2
|
import { useNavigate } from '@tanstack/react-router';
|
|
3
|
+
import { LogOut } from 'lucide-react';
|
|
3
4
|
import { useAuthStore, setStoredLocale, type IcoreLocale } from '@icore/template-shared';
|
|
4
5
|
import { Button } from '../ui/button';
|
|
5
6
|
import { ThemeToggle } from '../ThemeToggle';
|
|
@@ -27,17 +28,22 @@ export function LayoutHeader() {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
return (
|
|
30
|
-
<header className="
|
|
31
|
-
<
|
|
31
|
+
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-[--color-border] bg-[--color-background]/80 px-4 backdrop-blur-sm">
|
|
32
|
+
<div className="flex items-center gap-2">
|
|
33
|
+
<div className="flex h-6 w-6 items-center justify-center rounded bg-[--color-primary]">
|
|
34
|
+
<span className="text-xs font-bold text-[--color-primary-foreground]">i</span>
|
|
35
|
+
</div>
|
|
36
|
+
<span className="text-sm font-semibold tracking-tight">iCore</span>
|
|
37
|
+
</div>
|
|
32
38
|
|
|
33
|
-
<div className="flex items-center gap-
|
|
34
|
-
<div className="flex items-center
|
|
39
|
+
<div className="flex items-center gap-1">
|
|
40
|
+
<div className="flex items-center rounded-md border border-[--color-border] overflow-hidden mr-2">
|
|
35
41
|
{LOCALES.map(({ code, label }) => (
|
|
36
42
|
<button
|
|
37
43
|
key={code}
|
|
38
|
-
onClick={() => handleLocale(code)}
|
|
39
|
-
className="text-xs px-2 py-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors"
|
|
40
44
|
type="button"
|
|
45
|
+
onClick={() => handleLocale(code)}
|
|
46
|
+
className="px-2.5 py-1 text-xs text-[--color-muted-foreground] hover:bg-[--color-muted] hover:text-[--color-foreground] transition-colors cursor-pointer"
|
|
41
47
|
>
|
|
42
48
|
{label}
|
|
43
49
|
</button>
|
|
@@ -46,14 +52,29 @@ export function LayoutHeader() {
|
|
|
46
52
|
|
|
47
53
|
<ThemeToggle />
|
|
48
54
|
|
|
49
|
-
<div className="flex items-center gap-2">
|
|
50
|
-
<span className="text-
|
|
55
|
+
<div className="hidden sm:flex items-center gap-2 ml-1 pl-2 border-l border-[--color-border]">
|
|
56
|
+
<span className="text-xs text-[--color-muted-foreground] max-w-[140px] truncate">
|
|
51
57
|
{user?.email ?? ''}
|
|
52
58
|
</span>
|
|
53
|
-
<Button
|
|
54
|
-
|
|
59
|
+
<Button
|
|
60
|
+
variant="ghost"
|
|
61
|
+
size="icon"
|
|
62
|
+
onClick={handleLogout}
|
|
63
|
+
aria-label={t('common.logout')}
|
|
64
|
+
className="h-8 w-8 cursor-pointer"
|
|
65
|
+
>
|
|
66
|
+
<LogOut size={15} />
|
|
55
67
|
</Button>
|
|
56
68
|
</div>
|
|
69
|
+
|
|
70
|
+
<Button
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="sm"
|
|
73
|
+
onClick={handleLogout}
|
|
74
|
+
className="sm:hidden cursor-pointer"
|
|
75
|
+
>
|
|
76
|
+
<LogOut size={15} />
|
|
77
|
+
</Button>
|
|
57
78
|
</div>
|
|
58
79
|
</header>
|
|
59
80
|
);
|
|
@@ -3,46 +3,41 @@ import { Link } from '@tanstack/react-router';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { ChevronLeft, ChevronRight, LayoutDashboard, StickyNote, User } from 'lucide-react';
|
|
5
5
|
|
|
6
|
+
const NAV_ITEMS = [
|
|
7
|
+
{ to: '/dashboard' as const, icon: LayoutDashboard, labelKey: 'nav.dashboard', exact: true },
|
|
8
|
+
{ to: '/notes' as const, icon: StickyNote, labelKey: 'nav.notes', exact: false },
|
|
9
|
+
{ to: '/profile' as const, icon: User, labelKey: 'nav.profile', exact: false },
|
|
10
|
+
];
|
|
11
|
+
|
|
6
12
|
export function LayoutSider() {
|
|
7
13
|
const { t } = useTranslation();
|
|
8
14
|
const [collapsed, setCollapsed] = useState(false);
|
|
9
15
|
|
|
10
16
|
return (
|
|
11
17
|
<aside
|
|
12
|
-
className={`relative flex flex-col border-r border-border bg-
|
|
13
|
-
collapsed ? 'w-
|
|
18
|
+
className={`relative flex flex-col border-r border-[--color-border] bg-[--color-card] transition-all duration-200 ${
|
|
19
|
+
collapsed ? 'w-14' : 'w-52'
|
|
14
20
|
}`}
|
|
15
21
|
>
|
|
16
|
-
<nav className="flex flex-col gap-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
>
|
|
29
|
-
<StickyNote size={16} className="shrink-0" />
|
|
30
|
-
{!collapsed && <span>{t('notes.title')}</span>}
|
|
31
|
-
</Link>
|
|
32
|
-
<Link
|
|
33
|
-
to="/profile"
|
|
34
|
-
className="flex items-center gap-2 px-2 py-2 rounded hover:bg-muted text-sm text-foreground transition-colors [&.active]:bg-muted [&.active]:font-medium"
|
|
35
|
-
>
|
|
36
|
-
<User size={16} className="shrink-0" />
|
|
37
|
-
{!collapsed && <span>{t('nav.profile')}</span>}
|
|
38
|
-
</Link>
|
|
22
|
+
<nav className="flex flex-col gap-0.5 p-2 flex-1 pt-3">
|
|
23
|
+
{NAV_ITEMS.map(({ to, icon: Icon, labelKey, exact }) => (
|
|
24
|
+
<Link
|
|
25
|
+
key={to}
|
|
26
|
+
to={to}
|
|
27
|
+
activeOptions={{ exact }}
|
|
28
|
+
className="group flex items-center gap-3 rounded-md px-2.5 py-2 text-sm text-[--color-muted-foreground] transition-colors hover:bg-[--color-muted] hover:text-[--color-foreground] [&.active]:bg-[--color-primary]/10 [&.active]:text-[--color-primary] [&.active]:font-medium cursor-pointer"
|
|
29
|
+
>
|
|
30
|
+
<Icon size={16} className="shrink-0" />
|
|
31
|
+
{!collapsed && <span className="truncate">{t(labelKey)}</span>}
|
|
32
|
+
</Link>
|
|
33
|
+
))}
|
|
39
34
|
</nav>
|
|
40
35
|
|
|
41
36
|
<button
|
|
42
|
-
onClick={() => setCollapsed((c) => !c)}
|
|
43
|
-
className="absolute -right-3 top-4 z-10 flex h-6 w-6 items-center justify-center rounded-full border border-border bg-background text-muted-foreground hover:text-foreground shadow-sm"
|
|
44
37
|
type="button"
|
|
38
|
+
onClick={() => setCollapsed((c) => !c)}
|
|
45
39
|
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
40
|
+
className="absolute -right-3 top-5 z-10 flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border border-[--color-border] bg-[--color-card] text-[--color-muted-foreground] shadow-sm hover:text-[--color-foreground] transition-colors"
|
|
46
41
|
>
|
|
47
42
|
{collapsed ? <ChevronRight size={12} /> : <ChevronLeft size={12} />}
|
|
48
43
|
</button>
|
|
@@ -11,7 +11,7 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
|
|
|
11
11
|
<div
|
|
12
12
|
ref={ref}
|
|
13
13
|
className={cn(
|
|
14
|
-
'rounded-lg border border-[--color-border] bg-[--color-
|
|
14
|
+
'rounded-lg border border-[--color-border] bg-[--color-card] text-[--color-card-foreground] shadow-sm',
|
|
15
15
|
className,
|
|
16
16
|
)}
|
|
17
17
|
{...props}
|
|
@@ -1,27 +1,53 @@
|
|
|
1
1
|
@import 'tailwindcss';
|
|
2
|
+
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap');
|
|
2
3
|
|
|
3
4
|
@theme {
|
|
4
|
-
--
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
--color-
|
|
8
|
-
--color-
|
|
9
|
-
--color-
|
|
10
|
-
--color-
|
|
11
|
-
--color-
|
|
5
|
+
--font-sans: 'Plus Jakarta Sans', system-ui, sans-serif;
|
|
6
|
+
|
|
7
|
+
/* Light mode defaults */
|
|
8
|
+
--color-background: #ffffff;
|
|
9
|
+
--color-foreground: #0f172a;
|
|
10
|
+
--color-card: #f8fafc;
|
|
11
|
+
--color-card-foreground: #0f172a;
|
|
12
|
+
--color-primary: #16a34a;
|
|
13
|
+
--color-primary-foreground: #ffffff;
|
|
14
|
+
--color-secondary: #f1f5f9;
|
|
15
|
+
--color-secondary-foreground: #0f172a;
|
|
16
|
+
--color-muted: #f1f5f9;
|
|
17
|
+
--color-muted-foreground: #475569;
|
|
18
|
+
--color-border: #e2e8f0;
|
|
19
|
+
--color-input: #e2e8f0;
|
|
20
|
+
--color-ring: #16a34a;
|
|
21
|
+
--color-destructive: #ef4444;
|
|
12
22
|
--radius-default: 0.5rem;
|
|
13
23
|
}
|
|
14
24
|
|
|
15
25
|
@layer base {
|
|
26
|
+
* {
|
|
27
|
+
font-family: var(--font-sans);
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
body {
|
|
17
31
|
background-color: var(--color-background);
|
|
18
32
|
color: var(--color-foreground);
|
|
33
|
+
-webkit-font-smoothing: antialiased;
|
|
19
34
|
}
|
|
35
|
+
|
|
36
|
+
/* OLED Dark mode */
|
|
20
37
|
html.dark {
|
|
21
|
-
--color-background:
|
|
22
|
-
--color-foreground:
|
|
23
|
-
--color-
|
|
24
|
-
--color-
|
|
25
|
-
--color-
|
|
38
|
+
--color-background: #020617;
|
|
39
|
+
--color-foreground: #f8fafc;
|
|
40
|
+
--color-card: #0f172a;
|
|
41
|
+
--color-card-foreground: #f8fafc;
|
|
42
|
+
--color-primary: #22c55e;
|
|
43
|
+
--color-primary-foreground: #020617;
|
|
44
|
+
--color-secondary: #1e293b;
|
|
45
|
+
--color-secondary-foreground: #f8fafc;
|
|
46
|
+
--color-muted: #1e293b;
|
|
47
|
+
--color-muted-foreground: #94a3b8;
|
|
48
|
+
--color-border: #1e293b;
|
|
49
|
+
--color-input: #1e293b;
|
|
50
|
+
--color-ring: #22c55e;
|
|
51
|
+
--color-destructive: #ef4444;
|
|
26
52
|
}
|
|
27
53
|
}
|
|
@@ -3,7 +3,7 @@ import { useEffect, useState } from 'react';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { useAuthStore, useNotify } from '@icore/template-shared';
|
|
5
5
|
import { Loader2 } from 'lucide-react';
|
|
6
|
-
import { api } from '
|
|
6
|
+
import { api } from '@/main';
|
|
7
7
|
|
|
8
8
|
type Status = 'verifying' | 'done' | 'error';
|
|
9
9
|
|