@svadmin/ui 0.34.10 → 0.35.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/app.css +372 -0
- package/dist/component-registry.svelte.d.ts +1 -0
- package/dist/components/AdminApp.svelte +94 -15
- package/dist/components/AdminApp.svelte.d.ts +32 -2
- package/dist/components/ApiSettings.svelte +191 -0
- package/dist/components/ApiSettings.svelte.d.ts +3 -0
- package/dist/components/AutoForm.svelte +48 -43
- package/dist/components/AutoTable.svelte +27 -22
- package/dist/components/ErrorPage.svelte +58 -0
- package/dist/components/ErrorPage.svelte.d.ts +8 -0
- package/dist/components/FieldRenderer.svelte +2 -2
- package/dist/components/ForgotPasswordPage.svelte +111 -68
- package/dist/components/Header.svelte +6 -7
- package/dist/components/IntegrationsSettings.svelte +70 -0
- package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
- package/dist/components/Layout.svelte +8 -8
- package/dist/components/LoginPage.svelte +207 -105
- package/dist/components/LoginPage.svelte.d.ts +3 -15
- package/dist/components/NotificationsSettings.svelte +115 -0
- package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
- package/dist/components/PageHeader.svelte +9 -7
- package/dist/components/RegisterPage.svelte +103 -65
- package/dist/components/ResourceOperationsPage.svelte +153 -0
- package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
- package/dist/components/SecuritySettings.svelte +179 -0
- package/dist/components/SecuritySettings.svelte.d.ts +4 -0
- package/dist/components/SettingsPage.svelte +121 -54
- package/dist/components/SettingsPage.svelte.d.ts +25 -1
- package/dist/components/ShowPage.svelte +9 -9
- package/dist/components/Sidebar.svelte +96 -48
- package/dist/components/SidebarItem.svelte +37 -12
- package/dist/components/TwoFactorAuthPage.svelte +247 -0
- package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
- package/dist/components/UpdatePasswordPage.svelte +86 -48
- package/dist/components/account/CompanyProfilePage.svelte +99 -0
- package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
- package/dist/components/account/GetStartedPage.svelte +123 -0
- package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
- package/dist/components/account/ImportMembersPage.svelte +143 -0
- package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
- package/dist/components/account/MembersStarterPage.svelte +104 -0
- package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
- package/dist/components/account/SecurityLogPage.svelte +108 -0
- package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
- package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsPlainPage.svelte +81 -0
- package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
- package/dist/components/account/SettingsSidebarPage.svelte +98 -0
- package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
- package/dist/components/account/TeamMembersPage.svelte +98 -0
- package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
- package/dist/components/network/TeamCrewTablePage.svelte +122 -0
- package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
- package/dist/components/network/UserCardsNFTPage.svelte +134 -0
- package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
- package/dist/components/profile/ActivityTimeline.svelte +77 -0
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
- package/dist/components/profile/ProfileCard.svelte +216 -0
- package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
- package/dist/components/profile/ProjectsGrid.svelte +91 -0
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
- package/dist/components/profile/PublicProfilePage.svelte +179 -0
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
- package/dist/components/profile/TeamsShowcase.svelte +98 -0
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +27 -0
- package/dist/router-state.svelte.js +17 -0
- package/package.json +2 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import * as Card from '../ui/card/index.js';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import { Input } from '../ui/input/index.js';
|
|
6
|
+
import { Label } from '../ui/label/index.js';
|
|
7
|
+
import { Switch } from '../ui/switch/index.js';
|
|
8
|
+
import { Separator } from '../ui/separator/index.js';
|
|
9
|
+
import { Loader2 } from '@lucide/svelte';
|
|
10
|
+
|
|
11
|
+
let displayName = $state('Alex Chen');
|
|
12
|
+
let email = $state('alex@example.com');
|
|
13
|
+
let emailNotifications = $state(true);
|
|
14
|
+
let pushNotifications = $state(true);
|
|
15
|
+
let twoFactor = $state(false);
|
|
16
|
+
let saving = $state(false);
|
|
17
|
+
|
|
18
|
+
async function handleSave() {
|
|
19
|
+
saving = true;
|
|
20
|
+
await new Promise(r => setTimeout(r, 800));
|
|
21
|
+
saving = false;
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<div class="space-y-6">
|
|
26
|
+
<div>
|
|
27
|
+
<h2 class="text-xl font-semibold text-foreground">{t('account.settingsPlain')}</h2>
|
|
28
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('account.settingsPlainDescription')}</p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<Card.Card class="border-border/60">
|
|
32
|
+
<Card.CardHeader>
|
|
33
|
+
<Card.CardTitle class="text-base">{t('settings.general')}</Card.CardTitle>
|
|
34
|
+
</Card.CardHeader>
|
|
35
|
+
<Card.CardContent class="space-y-4">
|
|
36
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
37
|
+
<div class="space-y-2">
|
|
38
|
+
<Label for="display-name">{t('profile.name')}</Label>
|
|
39
|
+
<Input id="display-name" bind:value={displayName} />
|
|
40
|
+
</div>
|
|
41
|
+
<div class="space-y-2">
|
|
42
|
+
<Label for="email">Email</Label>
|
|
43
|
+
<Input id="email" bind:value={email} type="email" />
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<Separator />
|
|
48
|
+
|
|
49
|
+
<div class="flex items-center justify-between gap-4">
|
|
50
|
+
<div class="space-y-1">
|
|
51
|
+
<Label for="email-notif">{t('notifications.emailTitle')}</Label>
|
|
52
|
+
<p class="text-xs text-muted-foreground">{t('notifications.emailDescription')}</p>
|
|
53
|
+
</div>
|
|
54
|
+
<Switch id="email-notif" bind:checked={emailNotifications} />
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="flex items-center justify-between gap-4">
|
|
58
|
+
<div class="space-y-1">
|
|
59
|
+
<Label for="push-notif">{t('notifications.pushTitle')}</Label>
|
|
60
|
+
<p class="text-xs text-muted-foreground">{t('notifications.pushDescription')}</p>
|
|
61
|
+
</div>
|
|
62
|
+
<Switch id="push-notif" bind:checked={pushNotifications} />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="flex items-center justify-between gap-4">
|
|
66
|
+
<div class="space-y-1">
|
|
67
|
+
<Label for="two-factor">{t('security.twoFactorAuth')}</Label>
|
|
68
|
+
<p class="text-xs text-muted-foreground">{t('security.twoFactorDescription')}</p>
|
|
69
|
+
</div>
|
|
70
|
+
<Switch id="two-factor" bind:checked={twoFactor} />
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<div class="flex justify-end">
|
|
74
|
+
<Button onclick={handleSave} disabled={saving}>
|
|
75
|
+
{#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
|
|
76
|
+
{t('common.save')}
|
|
77
|
+
</Button>
|
|
78
|
+
</div>
|
|
79
|
+
</Card.CardContent>
|
|
80
|
+
</Card.Card>
|
|
81
|
+
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import * as Card from '../ui/card/index.js';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import { Label } from '../ui/label/index.js';
|
|
6
|
+
import { Switch } from '../ui/switch/index.js';
|
|
7
|
+
import { Separator } from '../ui/separator/index.js';
|
|
8
|
+
import { Loader2, PanelLeft } from '@lucide/svelte';
|
|
9
|
+
|
|
10
|
+
let defaultCollapsed = $state(false);
|
|
11
|
+
let pinSidebar = $state(true);
|
|
12
|
+
let showLabels = $state(true);
|
|
13
|
+
let showIcons = $state(true);
|
|
14
|
+
let density = $state<'compact' | 'standard'>('standard');
|
|
15
|
+
let saving = $state(false);
|
|
16
|
+
|
|
17
|
+
async function handleSave() {
|
|
18
|
+
saving = true;
|
|
19
|
+
await new Promise(r => setTimeout(r, 800));
|
|
20
|
+
saving = false;
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="space-y-6">
|
|
25
|
+
<div>
|
|
26
|
+
<h2 class="text-xl font-semibold text-foreground">{t('account.settingsSidebar')}</h2>
|
|
27
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('account.settingsSidebarDescription')}</p>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<Card.Card class="border-border/60">
|
|
31
|
+
<Card.CardHeader>
|
|
32
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
33
|
+
<PanelLeft class="h-4 w-4 text-muted-foreground" />
|
|
34
|
+
{t('account.settingsSidebar')}
|
|
35
|
+
</Card.CardTitle>
|
|
36
|
+
</Card.CardHeader>
|
|
37
|
+
<Card.CardContent class="space-y-4">
|
|
38
|
+
<div class="flex items-center justify-between gap-4">
|
|
39
|
+
<div class="space-y-1">
|
|
40
|
+
<Label for="default-collapsed">默认折叠</Label>
|
|
41
|
+
<p class="text-xs text-muted-foreground">启动时自动折叠侧边栏</p>
|
|
42
|
+
</div>
|
|
43
|
+
<Switch id="default-collapsed" bind:checked={defaultCollapsed} />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<Separator />
|
|
47
|
+
|
|
48
|
+
<div class="flex items-center justify-between gap-4">
|
|
49
|
+
<div class="space-y-1">
|
|
50
|
+
<Label for="pin-sidebar">{t('settings.general')}</Label>
|
|
51
|
+
<p class="text-xs text-muted-foreground">固定侧边栏,防止自动收起</p>
|
|
52
|
+
</div>
|
|
53
|
+
<Switch id="pin-sidebar" bind:checked={pinSidebar} />
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="flex items-center justify-between gap-4">
|
|
57
|
+
<div class="space-y-1">
|
|
58
|
+
<Label for="show-labels">显示标签文字</Label>
|
|
59
|
+
<p class="text-xs text-muted-foreground">在图标旁显示菜单项名称</p>
|
|
60
|
+
</div>
|
|
61
|
+
<Switch id="show-labels" bind:checked={showLabels} />
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="flex items-center justify-between gap-4">
|
|
65
|
+
<div class="space-y-1">
|
|
66
|
+
<Label for="show-icons">显示图标</Label>
|
|
67
|
+
<p class="text-xs text-muted-foreground">在标签旁显示菜单图标</p>
|
|
68
|
+
</div>
|
|
69
|
+
<Switch id="show-icons" bind:checked={showIcons} />
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<Separator />
|
|
73
|
+
|
|
74
|
+
<div class="space-y-2">
|
|
75
|
+
<Label>{t('settings.sidebarDensity')}</Label>
|
|
76
|
+
<div class="flex rounded-lg border border-input overflow-hidden">
|
|
77
|
+
<button
|
|
78
|
+
class="flex-1 px-4 py-2 text-sm font-medium transition-colors
|
|
79
|
+
{density === 'compact' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
|
|
80
|
+
onclick={() => density = 'compact'}
|
|
81
|
+
>{t('settings.compact')}</button>
|
|
82
|
+
<button
|
|
83
|
+
class="flex-1 px-4 py-2 text-sm font-medium transition-colors
|
|
84
|
+
{density === 'standard' ? 'bg-primary text-primary-foreground' : 'bg-background text-muted-foreground hover:bg-accent'}"
|
|
85
|
+
onclick={() => density = 'standard'}
|
|
86
|
+
>{t('settings.standard')}</button>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="flex justify-end">
|
|
91
|
+
<Button onclick={handleSave} disabled={saving}>
|
|
92
|
+
{#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
|
|
93
|
+
{t('common.save')}
|
|
94
|
+
</Button>
|
|
95
|
+
</div>
|
|
96
|
+
</Card.CardContent>
|
|
97
|
+
</Card.Card>
|
|
98
|
+
</div>
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import * as Card from '../ui/card/index.js';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import { Input } from '../ui/input/index.js';
|
|
6
|
+
import { Search, UserPlus, MoreHorizontal } from '@lucide/svelte';
|
|
7
|
+
|
|
8
|
+
interface TeamMember {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
role: 'Admin' | 'Editor' | 'Viewer';
|
|
13
|
+
avatar?: string;
|
|
14
|
+
department: string;
|
|
15
|
+
status: 'active' | 'invited' | 'inactive';
|
|
16
|
+
lastActive: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let searchQuery = $state('');
|
|
20
|
+
|
|
21
|
+
let members = $state<TeamMember[]>([
|
|
22
|
+
{ id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: 'Just now' },
|
|
23
|
+
{ id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Editor', department: 'Design', status: 'active', lastActive: '5 min ago' },
|
|
24
|
+
{ id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Viewer', department: 'Marketing', status: 'invited', lastActive: 'Pending' },
|
|
25
|
+
{ id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Editor', department: 'Engineering', status: 'active', lastActive: '1 hour ago' },
|
|
26
|
+
{ id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'Viewer', department: 'Sales', status: 'inactive', lastActive: '2 weeks ago' },
|
|
27
|
+
{ id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Admin', department: 'Engineering', status: 'active', lastActive: '10 min ago' },
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const filtered = $derived(
|
|
31
|
+
searchQuery
|
|
32
|
+
? members.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
33
|
+
: members
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const roleColors = {
|
|
37
|
+
'Admin': 'bg-red-500/10 text-red-600',
|
|
38
|
+
'Editor': 'bg-blue-500/10 text-blue-600',
|
|
39
|
+
'Viewer': 'bg-green-500/10 text-green-600',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const statusColors = {
|
|
43
|
+
'active': 'bg-green-500/10 text-green-600',
|
|
44
|
+
'invited': 'bg-amber-500/10 text-amber-600',
|
|
45
|
+
'inactive': 'bg-muted text-muted-foreground',
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<div class="space-y-6">
|
|
52
|
+
<div class="flex items-start justify-between gap-4">
|
|
53
|
+
<div>
|
|
54
|
+
<h2 class="text-xl font-semibold text-foreground">{t('account.membersStarter')}</h2>
|
|
55
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('account.membersStarterDescription')}</p>
|
|
56
|
+
</div>
|
|
57
|
+
<Button size="sm">
|
|
58
|
+
<UserPlus class="h-4 w-4 mr-1" />{t('account.inviteMember')}
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="relative">
|
|
63
|
+
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
64
|
+
<Input placeholder={t('common.search')} bind:value={searchQuery} class="pl-9" />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<!-- Members grid -->
|
|
68
|
+
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
69
|
+
{#each filtered as member (member.id)}
|
|
70
|
+
<Card.Card class="border-border/60 hover:border-primary/30 transition-colors">
|
|
71
|
+
<Card.CardContent class="p-4">
|
|
72
|
+
<div class="flex items-start gap-3">
|
|
73
|
+
<div class="shrink-0">
|
|
74
|
+
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-primary/10 text-primary text-sm font-semibold">
|
|
75
|
+
{initials(member.name)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex-1 min-w-0">
|
|
79
|
+
<div class="flex items-center gap-2">
|
|
80
|
+
<h4 class="text-sm font-semibold text-foreground truncate">{member.name}</h4>
|
|
81
|
+
</div>
|
|
82
|
+
<p class="text-xs text-muted-foreground truncate">{member.email}</p>
|
|
83
|
+
<div class="mt-2 flex flex-wrap gap-1.5">
|
|
84
|
+
<span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {roleColors[member.role]}">{member.role}</span>
|
|
85
|
+
<span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {statusColors[member.status]}">{member.status}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="mt-2 flex items-center justify-between">
|
|
88
|
+
<span class="text-xs text-muted-foreground">{member.department}</span>
|
|
89
|
+
<span class="text-xs text-muted-foreground">{member.lastActive}</span>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<Button variant="ghost" size="icon-sm"><MoreHorizontal class="h-4 w-4" /></Button>
|
|
93
|
+
</div>
|
|
94
|
+
</Card.CardContent>
|
|
95
|
+
</Card.Card>
|
|
96
|
+
{/each}
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import * as Card from '../ui/card/index.js';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import { Input } from '../ui/input/index.js';
|
|
6
|
+
import { Search, Download, UserPlus, MoreHorizontal, CheckCircle2, Clock, XCircle } from '@lucide/svelte';
|
|
7
|
+
|
|
8
|
+
interface CrewMember {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
email: string;
|
|
12
|
+
role: string;
|
|
13
|
+
department: string;
|
|
14
|
+
status: 'active' | 'away' | 'offline';
|
|
15
|
+
avatar?: string;
|
|
16
|
+
joinedAt: string;
|
|
17
|
+
projects: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let searchQuery = $state('');
|
|
21
|
+
|
|
22
|
+
const crew: CrewMember[] = [
|
|
23
|
+
{ id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Tech Lead', department: 'Engineering', status: 'active', joinedAt: '2022-03-15', projects: 12 },
|
|
24
|
+
{ id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Senior Designer', department: 'Design', status: 'active', joinedAt: '2022-06-22', projects: 8 },
|
|
25
|
+
{ id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Product Manager', department: 'Product', status: 'away', joinedAt: '2023-01-10', projects: 6 },
|
|
26
|
+
{ id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Backend Engineer', department: 'Engineering', status: 'active', joinedAt: '2023-04-05', projects: 10 },
|
|
27
|
+
{ id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'QA Engineer', department: 'Engineering', status: 'offline', joinedAt: '2022-11-20', projects: 4 },
|
|
28
|
+
{ id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Engineering Manager', department: 'Engineering', status: 'active', joinedAt: '2022-02-14', projects: 15 },
|
|
29
|
+
{ id: '7', name: 'James Wilson', email: 'james@acme.com', role: 'DevOps Engineer', department: 'Infrastructure', status: 'active', joinedAt: '2023-08-01', projects: 7 },
|
|
30
|
+
{ id: '8', name: 'Yuki Tanaka', email: 'yuki@acme.com', role: 'Frontend Engineer', department: 'Engineering', status: 'away', joinedAt: '2023-05-12', projects: 9 },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const filtered = $derived(
|
|
34
|
+
searchQuery
|
|
35
|
+
? crew.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
|
|
36
|
+
: crew
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const statusConfig = {
|
|
40
|
+
'active': { icon: CheckCircle2, color: 'text-green-500', dot: 'bg-green-500', label: t('account.active') },
|
|
41
|
+
'away': { icon: Clock, color: 'text-amber-500', dot: 'bg-amber-500', label: t('account.invited') },
|
|
42
|
+
'offline': { icon: XCircle, color: 'text-muted-foreground', dot: 'bg-muted-foreground', label: t('account.inactive') },
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<div class="space-y-6">
|
|
49
|
+
<div class="flex items-start justify-between gap-4">
|
|
50
|
+
<div>
|
|
51
|
+
<h2 class="text-xl font-semibold text-foreground">{t('network.teamCrew')}</h2>
|
|
52
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('network.teamCrewDescription')}</p>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="flex gap-2">
|
|
55
|
+
<Button variant="outline" size="sm">
|
|
56
|
+
<Download class="h-3.5 w-3.5 mr-1" />{t('common.export')}
|
|
57
|
+
</Button>
|
|
58
|
+
<Button size="sm">
|
|
59
|
+
<UserPlus class="h-3.5 w-3.5 mr-1" />{t('account.inviteMember')}
|
|
60
|
+
</Button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="relative">
|
|
65
|
+
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
66
|
+
<Input placeholder={t('common.search')} bind:value={searchQuery} class="pl-9" />
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<!-- Table -->
|
|
70
|
+
<Card.Card class="border-border/60">
|
|
71
|
+
<Card.CardContent class="p-0">
|
|
72
|
+
<div class="overflow-x-auto">
|
|
73
|
+
<table class="w-full">
|
|
74
|
+
<thead>
|
|
75
|
+
<tr class="border-b bg-muted/30">
|
|
76
|
+
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.member')}</th>
|
|
77
|
+
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.role')}</th>
|
|
78
|
+
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">Department</th>
|
|
79
|
+
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.status')}</th>
|
|
80
|
+
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('publicProfile.projects')}</th>
|
|
81
|
+
<th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{t('account.joined')}</th>
|
|
82
|
+
<th class="px-4 py-3 w-12"></th>
|
|
83
|
+
</tr>
|
|
84
|
+
</thead>
|
|
85
|
+
<tbody class="divide-y">
|
|
86
|
+
{#each filtered as member (member.id)}
|
|
87
|
+
{@const sc = statusConfig[member.status]}
|
|
88
|
+
<tr class="hover:bg-accent/30 transition-colors">
|
|
89
|
+
<td class="px-4 py-3">
|
|
90
|
+
<div class="flex items-center gap-3">
|
|
91
|
+
<div class="relative shrink-0">
|
|
92
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-semibold">
|
|
93
|
+
{initials(member.name)}
|
|
94
|
+
</div>
|
|
95
|
+
<span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full {sc.dot} ring-2 ring-card"></span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="min-w-0">
|
|
98
|
+
<p class="text-sm font-medium text-foreground truncate">{member.name}</p>
|
|
99
|
+
<p class="text-xs text-muted-foreground truncate">{member.email}</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</td>
|
|
103
|
+
<td class="px-4 py-3 text-sm text-foreground">{member.role}</td>
|
|
104
|
+
<td class="px-4 py-3 text-sm text-muted-foreground">{member.department}</td>
|
|
105
|
+
<td class="px-4 py-3">
|
|
106
|
+
<span class="flex items-center gap-1 text-xs {sc.color}">
|
|
107
|
+
<sc.icon class="h-3 w-3" />{sc.label}
|
|
108
|
+
</span>
|
|
109
|
+
</td>
|
|
110
|
+
<td class="px-4 py-3 text-sm text-foreground">{member.projects}</td>
|
|
111
|
+
<td class="px-4 py-3 text-sm text-muted-foreground whitespace-nowrap">{member.joinedAt}</td>
|
|
112
|
+
<td class="px-4 py-3">
|
|
113
|
+
<Button variant="ghost" size="icon-sm"><MoreHorizontal class="h-4 w-4" /></Button>
|
|
114
|
+
</td>
|
|
115
|
+
</tr>
|
|
116
|
+
{/each}
|
|
117
|
+
</tbody>
|
|
118
|
+
</table>
|
|
119
|
+
</div>
|
|
120
|
+
</Card.CardContent>
|
|
121
|
+
</Card.Card>
|
|
122
|
+
</div>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import * as Card from '../ui/card/index.js';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import { Badge } from '../ui/badge/index.js';
|
|
6
|
+
import { Input } from '../ui/input/index.js';
|
|
7
|
+
import { Search, ExternalLink, Check, Plus } from '@lucide/svelte';
|
|
8
|
+
|
|
9
|
+
interface NFTUser {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
username: string;
|
|
13
|
+
avatar?: string;
|
|
14
|
+
bio: string;
|
|
15
|
+
collectionCount: number;
|
|
16
|
+
floorPrice: string;
|
|
17
|
+
volume: string;
|
|
18
|
+
verified: boolean;
|
|
19
|
+
tags: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let searchQuery = $state('');
|
|
23
|
+
|
|
24
|
+
const users: NFTUser[] = [
|
|
25
|
+
{ id: '1', name: 'CryptoArt Studio', username: '@cryptoart', bio: 'Digital art collection featuring generative abstracts', collectionCount: 42, floorPrice: '0.5 ETH', volume: '128.3 ETH', verified: true, tags: ['Art', 'Generative'] },
|
|
26
|
+
{ id: '2', name: 'PixelPunk Collective', username: '@pixelpunk', bio: 'On-chain pixel art characters with unique traits', collectionCount: 18, floorPrice: '1.2 ETH', volume: '342.7 ETH', verified: true, tags: ['PFP', 'Pixel'] },
|
|
27
|
+
{ id: '3', name: 'MetaVerse Builders', username: '@metabuild', bio: '3D architectural models for virtual worlds', collectionCount: 7, floorPrice: '0.8 ETH', volume: '56.1 ETH', verified: false, tags: ['3D', 'Virtual'] },
|
|
28
|
+
{ id: '4', name: 'SoundWave NFT', username: '@soundwave', bio: 'Music-backed NFTs with on-chain audio', collectionCount: 23, floorPrice: '0.3 ETH', volume: '89.4 ETH', verified: true, tags: ['Music', 'Audio'] },
|
|
29
|
+
{ id: '5', name: 'GameAssets DAO', username: '@gameassets', bio: 'In-game items and collectibles for Web3 gaming', collectionCount: 156, floorPrice: '0.05 ETH', volume: '412.8 ETH', verified: true, tags: ['Gaming', 'Items'] },
|
|
30
|
+
{ id: '6', name: 'PhotoVerse', username: '@photoverse', bio: 'Curated photography NFTs from world artists', collectionCount: 31, floorPrice: '0.15 ETH', volume: '67.2 ETH', verified: false, tags: ['Photo', 'Curated'] },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const filtered = $derived(
|
|
34
|
+
searchQuery
|
|
35
|
+
? users.filter(u => u.name.toLowerCase().includes(searchQuery.toLowerCase()) || u.username.toLowerCase().includes(searchQuery.toLowerCase()) || u.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
|
|
36
|
+
: users
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
|
|
40
|
+
|
|
41
|
+
let connectedIds = $state<string[]>([]);
|
|
42
|
+
|
|
43
|
+
function toggleConnect(id: string) {
|
|
44
|
+
connectedIds = connectedIds.includes(id)
|
|
45
|
+
? connectedIds.filter(connectedId => connectedId !== id)
|
|
46
|
+
: [...connectedIds, id];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isConnected(id: string): boolean {
|
|
50
|
+
return connectedIds.includes(id);
|
|
51
|
+
}
|
|
52
|
+
</script>
|
|
53
|
+
|
|
54
|
+
<div class="space-y-6">
|
|
55
|
+
<div>
|
|
56
|
+
<h2 class="text-xl font-semibold text-foreground">{t('network.userCards')}</h2>
|
|
57
|
+
<p class="mt-1 text-sm text-muted-foreground">{t('network.userCardsDescription')}</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div class="relative">
|
|
61
|
+
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
62
|
+
<Input placeholder={t('common.search')} bind:value={searchQuery} class="pl-9" />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
|
66
|
+
{#each filtered as user (user.id)}
|
|
67
|
+
<Card.Card class="overflow-hidden border-border/60 hover:border-primary/30 transition-colors">
|
|
68
|
+
<div class="h-20 bg-gradient-to-r from-violet-500/20 via-purple-500/15 to-fuchsia-500/20 relative">
|
|
69
|
+
<div class="absolute inset-0 opacity-[0.08]" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 16px 16px;"></div>
|
|
70
|
+
</div>
|
|
71
|
+
<Card.CardContent class="relative px-4 pb-4">
|
|
72
|
+
<div class="-mt-8 mb-2 flex items-end gap-3">
|
|
73
|
+
<div class="ring-3 ring-card rounded-xl overflow-hidden shrink-0">
|
|
74
|
+
<div class="flex h-14 w-14 items-center justify-center bg-primary/10 text-primary font-bold">
|
|
75
|
+
{initials(user.name)}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="flex-1 min-w-0 pb-0.5">
|
|
79
|
+
<div class="flex items-center gap-1.5">
|
|
80
|
+
<h4 class="text-sm font-semibold text-foreground truncate">{user.name}</h4>
|
|
81
|
+
{#if user.verified}
|
|
82
|
+
<Check class="h-3.5 w-3.5 text-blue-500" />
|
|
83
|
+
{/if}
|
|
84
|
+
</div>
|
|
85
|
+
<p class="text-xs text-muted-foreground">{user.username}</p>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<p class="text-xs text-muted-foreground line-clamp-2 mb-3">{user.bio}</p>
|
|
90
|
+
|
|
91
|
+
<div class="flex flex-wrap gap-1 mb-3">
|
|
92
|
+
{#each user.tags as tag (tag)}
|
|
93
|
+
<Badge variant="outline" class="text-[10px]">{tag}</Badge>
|
|
94
|
+
{/each}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- NFT stats -->
|
|
98
|
+
<div class="grid grid-cols-3 gap-2 border-t pt-3">
|
|
99
|
+
<div class="text-center">
|
|
100
|
+
<div class="text-sm font-semibold text-foreground">{user.collectionCount}</div>
|
|
101
|
+
<div class="text-[10px] text-muted-foreground">{t('network.nftCollection')}</div>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="text-center">
|
|
104
|
+
<div class="text-sm font-semibold text-foreground">{user.floorPrice}</div>
|
|
105
|
+
<div class="text-[10px] text-muted-foreground">{t('network.floorPrice')}</div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="text-center">
|
|
108
|
+
<div class="text-xs font-semibold text-foreground">{user.volume}</div>
|
|
109
|
+
<div class="text-[10px] text-muted-foreground">{t('network.volume')}</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
113
|
+
<div class="mt-3 flex gap-2">
|
|
114
|
+
<Button
|
|
115
|
+
size="sm"
|
|
116
|
+
variant={isConnected(user.id) ? 'outline' : 'default'}
|
|
117
|
+
class="flex-1"
|
|
118
|
+
onclick={() => toggleConnect(user.id)}
|
|
119
|
+
>
|
|
120
|
+
{#if isConnected(user.id)}
|
|
121
|
+
<Check class="h-3.5 w-3.5 mr-1" />{t('network.connected')}
|
|
122
|
+
{:else}
|
|
123
|
+
<Plus class="h-3.5 w-3.5 mr-1" />{t('network.connect')}
|
|
124
|
+
{/if}
|
|
125
|
+
</Button>
|
|
126
|
+
<Button size="sm" variant="ghost">
|
|
127
|
+
<ExternalLink class="h-3.5 w-3.5" />
|
|
128
|
+
</Button>
|
|
129
|
+
</div>
|
|
130
|
+
</Card.CardContent>
|
|
131
|
+
</Card.Card>
|
|
132
|
+
{/each}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '@svadmin/core/i18n';
|
|
3
|
+
import * as Card from '../ui/card/index.js';
|
|
4
|
+
import { Avatar } from '../ui/avatar/index.js';
|
|
5
|
+
import { MessageSquare, Plus, UserPlus, Star } from '@lucide/svelte';
|
|
6
|
+
import type { Component } from 'svelte';
|
|
7
|
+
|
|
8
|
+
interface ActivityItem {
|
|
9
|
+
id: string;
|
|
10
|
+
type: 'posted' | 'commented' | 'joined' | 'created' | 'starred';
|
|
11
|
+
user: string;
|
|
12
|
+
avatar?: string;
|
|
13
|
+
content?: string;
|
|
14
|
+
target?: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
activities?: ActivityItem[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { activities = [] }: Props = $props();
|
|
23
|
+
|
|
24
|
+
const typeConfig: Record<string, { icon: Component; color: string; action: string }> = {
|
|
25
|
+
'posted': { icon: MessageSquare, color: 'bg-blue-500/10 text-blue-500', action: t('publicProfile.activityPosted') },
|
|
26
|
+
'commented': { icon: MessageSquare, color: 'bg-green-500/10 text-green-500', action: t('publicProfile.activityCommented') },
|
|
27
|
+
'joined': { icon: UserPlus, color: 'bg-purple-500/10 text-purple-500', action: t('publicProfile.activityJoined') },
|
|
28
|
+
'created': { icon: Plus, color: 'bg-amber-500/10 text-amber-500', action: t('publicProfile.activityCreated') },
|
|
29
|
+
'starred': { icon: Star, color: 'bg-yellow-500/10 text-yellow-500', action: t('publicProfile.activityCreated') },
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<div class="space-y-4">
|
|
34
|
+
<h3 class="text-lg font-semibold text-foreground">{t('publicProfile.activity')}</h3>
|
|
35
|
+
|
|
36
|
+
{#if activities.length === 0}
|
|
37
|
+
<div class="rounded-xl border border-dashed border-border/60 p-8 text-center">
|
|
38
|
+
<p class="text-sm text-muted-foreground">{t('publicProfile.noActivity')}</p>
|
|
39
|
+
</div>
|
|
40
|
+
{:else}
|
|
41
|
+
<Card.Card class="border-border/60">
|
|
42
|
+
<Card.CardContent class="p-0">
|
|
43
|
+
<div class="divide-y">
|
|
44
|
+
{#each activities as activity (activity.id)}
|
|
45
|
+
{@const config = typeConfig[activity.type] ?? typeConfig['posted']}
|
|
46
|
+
<div class="flex items-start gap-3 p-4">
|
|
47
|
+
<div class="shrink-0 mt-0.5">
|
|
48
|
+
{#if activity.avatar}
|
|
49
|
+
<Avatar class="h-8 w-8">
|
|
50
|
+
<img src={activity.avatar} alt={activity.user} />
|
|
51
|
+
</Avatar>
|
|
52
|
+
{:else}
|
|
53
|
+
<div class="flex h-8 w-8 items-center justify-center rounded-full {config.color}">
|
|
54
|
+
<config.icon class="h-4 w-4" />
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
<div class="flex-1 min-w-0">
|
|
59
|
+
<p class="text-sm">
|
|
60
|
+
<strong class="text-foreground">{activity.user}</strong>
|
|
61
|
+
<span class="text-muted-foreground"> {config.action} </span>
|
|
62
|
+
{#if activity.target}
|
|
63
|
+
<strong class="text-foreground">{activity.target}</strong>
|
|
64
|
+
{/if}
|
|
65
|
+
</p>
|
|
66
|
+
{#if activity.content}
|
|
67
|
+
<p class="mt-1 text-sm text-muted-foreground line-clamp-2">{activity.content}</p>
|
|
68
|
+
{/if}
|
|
69
|
+
<p class="mt-1 text-xs text-muted-foreground/60">{activity.timestamp}</p>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
{/each}
|
|
73
|
+
</div>
|
|
74
|
+
</Card.CardContent>
|
|
75
|
+
</Card.Card>
|
|
76
|
+
{/if}
|
|
77
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
interface ActivityItem {
|
|
3
|
+
id: string;
|
|
4
|
+
type: 'posted' | 'commented' | 'joined' | 'created' | 'starred';
|
|
5
|
+
user: string;
|
|
6
|
+
avatar?: string;
|
|
7
|
+
content?: string;
|
|
8
|
+
target?: string;
|
|
9
|
+
timestamp: string;
|
|
10
|
+
}
|
|
11
|
+
interface Props {
|
|
12
|
+
activities?: ActivityItem[];
|
|
13
|
+
}
|
|
14
|
+
declare const ActivityTimeline: Component<Props, {}, "">;
|
|
15
|
+
type ActivityTimeline = ReturnType<typeof ActivityTimeline>;
|
|
16
|
+
export default ActivityTimeline;
|