@svadmin/ui 0.38.7 → 0.40.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.
Files changed (61) hide show
  1. package/dist/app.css +0 -4
  2. package/dist/app.theme.css +0 -4
  3. package/dist/assets/nft/nft-cover-01.jpg +0 -0
  4. package/dist/assets/nft/nft-cover-02.jpg +0 -0
  5. package/dist/assets/nft/nft-cover-03.jpg +0 -0
  6. package/dist/assets/nft/nft-cover-04.jpg +0 -0
  7. package/dist/assets/nft/nft-cover-05.jpg +0 -0
  8. package/dist/assets/nft/nft-cover-06.jpg +0 -0
  9. package/dist/assets/nft/nft-cover-07.jpg +0 -0
  10. package/dist/assets/nft/nft-cover-08.jpg +0 -0
  11. package/dist/assets/nft/nft-cover-09.jpg +0 -0
  12. package/dist/components/AdminApp.svelte +29 -12
  13. package/dist/components/ApiSettings.svelte +86 -1
  14. package/dist/components/AutoTable.svelte +40 -13
  15. package/dist/components/Breadcrumbs.svelte +39 -6
  16. package/dist/components/Breadcrumbs.svelte.d.ts +5 -1
  17. package/dist/components/ChatDialog.svelte +4 -2
  18. package/dist/components/ChatDialog.svelte.d.ts +2 -0
  19. package/dist/components/DevTools.svelte +3 -1
  20. package/dist/components/DevTools.svelte.d.ts +4 -1
  21. package/dist/components/ForgotPasswordPage.svelte +5 -8
  22. package/dist/components/Header.svelte +5 -3
  23. package/dist/components/Header.svelte.d.ts +2 -0
  24. package/dist/components/Layout.svelte +10 -1
  25. package/dist/components/LoginPage.svelte +5 -8
  26. package/dist/components/RegisterPage.svelte +5 -8
  27. package/dist/components/SettingsPage.svelte +14 -2
  28. package/dist/components/Sidebar.svelte +10 -4
  29. package/dist/components/SvadminLogo.svelte +16 -0
  30. package/dist/components/SvadminLogo.svelte.d.ts +7 -0
  31. package/dist/components/SystemPageShell.svelte +34 -0
  32. package/dist/components/SystemPageShell.svelte.d.ts +8 -0
  33. package/dist/components/UpdatePasswordPage.svelte +5 -8
  34. package/dist/components/account/CompanyProfilePage.svelte +56 -1
  35. package/dist/components/account/ImportMembersPage.svelte +31 -0
  36. package/dist/components/account/SettingsEnterprisePage.svelte +172 -19
  37. package/dist/components/account/SettingsPlainPage.svelte +121 -17
  38. package/dist/components/account/SettingsSidebarPage.svelte +282 -70
  39. package/dist/components/account/TeamMembersPage.svelte +95 -4
  40. package/dist/components/account/UserProfilePage.svelte +206 -0
  41. package/dist/components/account/UserProfilePage.svelte.d.ts +4 -0
  42. package/dist/components/network/TeamCrewTablePage.svelte +37 -5
  43. package/dist/components/network/UserCardsNFTPage.svelte +26 -16
  44. package/dist/components/profile/ActivityTimeline.svelte +24 -2
  45. package/dist/components/profile/ActivityTimeline.svelte.d.ts +1 -0
  46. package/dist/components/profile/ProfileVariantSections.svelte +452 -0
  47. package/dist/components/profile/ProfileVariantSections.svelte.d.ts +8 -0
  48. package/dist/components/profile/ProjectsGrid.svelte +38 -8
  49. package/dist/components/profile/ProjectsGrid.svelte.d.ts +1 -0
  50. package/dist/components/profile/PublicProfilePage.svelte +47 -26
  51. package/dist/components/profile/PublicProfilePage.svelte.d.ts +1 -0
  52. package/dist/components/profile/TeamsShowcase.svelte +31 -12
  53. package/dist/components/profile/TeamsShowcase.svelte.d.ts +1 -0
  54. package/dist/components/ui/alert/alert.svelte.d.ts +14 -14
  55. package/dist/components/ui/avatar/avatar.svelte.d.ts +14 -14
  56. package/dist/components/ui/badge/badge.svelte.d.ts +20 -20
  57. package/dist/components/ui/button/button.svelte.d.ts +44 -44
  58. package/dist/index.d.ts +3 -0
  59. package/dist/index.js +3 -0
  60. package/dist/router-state.svelte.js +4 -0
  61. package/package.json +15 -15
@@ -0,0 +1,206 @@
1
+ <script lang="ts">
2
+ import { useTranslation } from '@svadmin/core/i18n';
3
+ import * as Card from '../ui/card/index.js';
4
+ import { Badge } from '../ui/badge/index.js';
5
+ import { Button } from '../ui/button/index.js';
6
+ import { Switch } from '../ui/switch/index.js';
7
+ import {
8
+ Briefcase, Calendar, FileText, Image, FileArchive, Link2, Loader2,
9
+ Star, Award, Medal, Crown,
10
+ } from '@lucide/svelte';
11
+ import type { Component } from 'svelte';
12
+
13
+ const i18n = useTranslation();
14
+
15
+ let availableToHire = $state(true);
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
+
24
+ const infoRows = [
25
+ { label: 'profile.name', value: 'Alex Chen' },
26
+ { label: 'account.phone', value: '+86 138 **** 9527' },
27
+ { label: 'account.dateOfBirth', value: '1995-04-18' },
28
+ { label: 'account.address', value: 'Shanghai, CN' },
29
+ ];
30
+
31
+ const badges: { name: string; Icon: Component; tone: string }[] = [
32
+ { name: 'Top Contributor', Icon: Medal, tone: 'bg-amber-500/10 text-amber-600' },
33
+ { name: 'Early Adopter', Icon: Star, tone: 'bg-blue-500/10 text-blue-600' },
34
+ { name: 'Community Mentor', Icon: Award, tone: 'bg-violet-500/10 text-violet-600' },
35
+ { name: 'Launch Partner', Icon: Crown, tone: 'bg-emerald-500/10 text-emerald-600' },
36
+ ];
37
+
38
+ interface Connection {
39
+ id: string;
40
+ name: string;
41
+ detail: string;
42
+ connected: boolean;
43
+ }
44
+ let connections = $state<Connection[]>([
45
+ { id: 'repo', name: 'Source Control', detail: 'alexchen', connected: true },
46
+ { id: 'chat', name: 'ChatOps', detail: '@alex.chen', connected: true },
47
+ { id: 'drive', name: 'Cloud Drive', detail: 'alex@example.com', connected: false },
48
+ ]);
49
+
50
+ const files: { name: string; size: string; Icon: Component; tone: string }[] = [
51
+ { name: 'portfolio-2026.pdf', size: '2.1 MB', Icon: FileText, tone: 'bg-red-500/10 text-red-600' },
52
+ { name: 'avatar-pack.zip', size: '8.4 MB', Icon: FileArchive, tone: 'bg-amber-500/10 text-amber-600' },
53
+ { name: 'cover-photo.png', size: '1.2 MB', Icon: Image, tone: 'bg-pink-500/10 text-pink-600' },
54
+ ];
55
+
56
+ const calendars = [
57
+ { name: 'Work Calendar', detail: 'alex@nebulalabs.io', primary: true },
58
+ { name: 'Personal Calendar', detail: 'alex@example.com', primary: false },
59
+ ];
60
+
61
+ function toggleConnection(id: string, connected: boolean) {
62
+ connections = connections.map((c) => (c.id === id ? { ...c, connected } : c));
63
+ }
64
+ </script>
65
+
66
+ <div class="space-y-6" data-svadmin-content-page="account">
67
+ <div class="flex items-start justify-between gap-4">
68
+ <div>
69
+ <h2 class="text-xl font-semibold text-foreground">{i18n.t('account.userProfile')}</h2>
70
+ <p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.userProfileDescription')}</p>
71
+ </div>
72
+ <Button onclick={handleSave} disabled={saving} size="sm">
73
+ {#if saving}<Loader2 class="h-4 w-4 animate-spin mr-2" />{/if}
74
+ {i18n.t('common.save')}
75
+ </Button>
76
+ </div>
77
+
78
+ <div class="grid gap-4 lg:grid-cols-3">
79
+ <div class="space-y-4 lg:col-span-2">
80
+ <!-- Personal Info -->
81
+ <Card.Card class="border-border/60">
82
+ <Card.CardHeader class="pb-3">
83
+ <Card.CardTitle class="text-base">{i18n.t('account.personalInfo')}</Card.CardTitle>
84
+ </Card.CardHeader>
85
+ <Card.CardContent class="p-0">
86
+ <div class="divide-y">
87
+ {#each infoRows as row (row.label)}
88
+ <div class="flex items-center justify-between px-6 py-3 text-sm">
89
+ <span class="text-muted-foreground">{i18n.t(row.label)}</span>
90
+ <span class="font-medium text-foreground">{row.value}</span>
91
+ </div>
92
+ {/each}
93
+ </div>
94
+ </Card.CardContent>
95
+ </Card.Card>
96
+
97
+ <!-- Work -->
98
+ <Card.Card class="border-border/60">
99
+ <Card.CardHeader class="pb-3">
100
+ <Card.CardTitle class="flex items-center gap-2 text-base">
101
+ <Briefcase class="h-4 w-4 text-muted-foreground" />{i18n.t('account.work')}
102
+ </Card.CardTitle>
103
+ </Card.CardHeader>
104
+ <Card.CardContent class="space-y-4">
105
+ <div class="flex items-center justify-between gap-4">
106
+ <div class="space-y-1">
107
+ <p class="text-sm font-medium text-foreground">{i18n.t('account.availableToHire')}</p>
108
+ <p class="text-xs text-muted-foreground">{i18n.t('account.availableNow')}</p>
109
+ </div>
110
+ <Switch bind:checked={availableToHire} />
111
+ </div>
112
+ <div class="flex flex-wrap gap-1.5">
113
+ {#each ['TypeScript', 'Svelte', 'Node.js', 'Design Systems'] as skill (skill)}
114
+ <Badge variant="secondary" class="text-xs">{skill}</Badge>
115
+ {/each}
116
+ </div>
117
+ </Card.CardContent>
118
+ </Card.Card>
119
+
120
+ <!-- Connections -->
121
+ <Card.Card class="border-border/60">
122
+ <Card.CardHeader class="pb-3">
123
+ <Card.CardTitle class="flex items-center gap-2 text-base">
124
+ <Link2 class="h-4 w-4 text-muted-foreground" />{i18n.t('account.connections')}
125
+ </Card.CardTitle>
126
+ </Card.CardHeader>
127
+ <Card.CardContent class="p-0">
128
+ <div class="divide-y">
129
+ {#each connections as conn (conn.id)}
130
+ <div class="flex items-center justify-between gap-4 px-6 py-3">
131
+ <div class="min-w-0">
132
+ <p class="text-sm font-medium text-foreground">{conn.name}</p>
133
+ <p class="truncate text-xs text-muted-foreground">{conn.detail}</p>
134
+ </div>
135
+ <Switch checked={conn.connected} onCheckedChange={(checked) => toggleConnection(conn.id, checked)} />
136
+ </div>
137
+ {/each}
138
+ </div>
139
+ </Card.CardContent>
140
+ </Card.Card>
141
+ </div>
142
+
143
+ <div class="space-y-4">
144
+ <!-- Badges -->
145
+ <Card.Card class="border-border/60">
146
+ <Card.CardHeader class="pb-3">
147
+ <Card.CardTitle class="text-base">{i18n.t('account.badges')}</Card.CardTitle>
148
+ </Card.CardHeader>
149
+ <Card.CardContent>
150
+ <div class="grid grid-cols-2 gap-2">
151
+ {#each badges as badge (badge.name)}
152
+ <div class="flex items-center gap-2 rounded-lg border border-border/60 p-2.5">
153
+ <div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg {badge.tone}">
154
+ <badge.Icon class="h-4 w-4" />
155
+ </div>
156
+ <span class="text-xs font-medium text-foreground">{badge.name}</span>
157
+ </div>
158
+ {/each}
159
+ </div>
160
+ </Card.CardContent>
161
+ </Card.Card>
162
+
163
+ <!-- Calendar Accounts -->
164
+ <Card.Card class="border-border/60">
165
+ <Card.CardHeader class="pb-3">
166
+ <Card.CardTitle class="flex items-center gap-2 text-base">
167
+ <Calendar class="h-4 w-4 text-muted-foreground" />{i18n.t('account.calendarAccounts')}
168
+ </Card.CardTitle>
169
+ </Card.CardHeader>
170
+ <Card.CardContent class="space-y-3">
171
+ {#each calendars as cal (cal.name)}
172
+ <div class="flex items-center justify-between gap-2">
173
+ <div class="min-w-0">
174
+ <p class="truncate text-sm font-medium text-foreground">{cal.name}</p>
175
+ <p class="truncate text-xs text-muted-foreground">{cal.detail}</p>
176
+ </div>
177
+ {#if cal.primary}
178
+ <Badge variant="secondary" class="shrink-0 text-[10px]">{i18n.t('account.active')}</Badge>
179
+ {/if}
180
+ </div>
181
+ {/each}
182
+ </Card.CardContent>
183
+ </Card.Card>
184
+
185
+ <!-- My Files -->
186
+ <Card.Card class="border-border/60">
187
+ <Card.CardHeader class="pb-3">
188
+ <Card.CardTitle class="text-base">{i18n.t('account.myFiles')}</Card.CardTitle>
189
+ </Card.CardHeader>
190
+ <Card.CardContent class="space-y-2.5">
191
+ {#each files as file (file.name)}
192
+ <div class="flex items-center gap-3">
193
+ <div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg {file.tone}">
194
+ <file.Icon class="h-4 w-4" />
195
+ </div>
196
+ <div class="min-w-0 flex-1">
197
+ <p class="truncate text-sm font-medium text-foreground">{file.name}</p>
198
+ <p class="text-xs text-muted-foreground">{file.size}</p>
199
+ </div>
200
+ </div>
201
+ {/each}
202
+ </Card.CardContent>
203
+ </Card.Card>
204
+ </div>
205
+ </div>
206
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { Component } from 'svelte';
2
+ declare const UserProfilePage: Component<Record<string, never>, {}, "">;
3
+ type UserProfilePage = ReturnType<typeof UserProfilePage>;
4
+ export default UserProfilePage;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { captureAdminContext } from '@svadmin/core';
2
3
  import { useTranslation } from '@svadmin/core/i18n';
3
4
  import * as Card from '../ui/card/index.js';
4
5
  import { Button } from '../ui/button/index.js';
@@ -6,6 +7,7 @@
6
7
  import { Search, Download, UserPlus, MoreHorizontal, CheckCircle2, Clock, XCircle } from '@lucide/svelte';
7
8
 
8
9
  const i18n = useTranslation();
10
+ const adminContext = captureAdminContext();
9
11
 
10
12
  interface CrewMember {
11
13
  id: string;
@@ -30,8 +32,22 @@
30
32
  { id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Engineering Manager', department: 'Engineering', status: 'active', joinedAt: '2022-02-14', projects: 15 },
31
33
  { id: '7', name: 'James Wilson', email: 'james@acme.com', role: 'DevOps Engineer', department: 'Infrastructure', status: 'active', joinedAt: '2023-08-01', projects: 7 },
32
34
  { id: '8', name: 'Yuki Tanaka', email: 'yuki@acme.com', role: 'Frontend Engineer', department: 'Engineering', status: 'away', joinedAt: '2023-05-12', projects: 9 },
35
+ { id: '9', name: 'Priya Nair', email: 'priya@acme.com', role: 'Data Scientist', department: 'Data', status: 'active', joinedAt: '2023-09-18', projects: 5 },
36
+ { id: '10', name: 'Carlos Mendez', email: 'carlos@acme.com', role: 'Mobile Engineer', department: 'Engineering', status: 'active', joinedAt: '2022-07-25', projects: 11 },
37
+ { id: '11', name: 'Hana Sato', email: 'hana@acme.com', role: 'UX Researcher', department: 'Design', status: 'away', joinedAt: '2024-01-15', projects: 3 },
38
+ { id: '12', name: 'Oscar Lund', email: 'oscar@acme.com', role: 'Security Engineer', department: 'Infrastructure', status: 'active', joinedAt: '2023-03-30', projects: 6 },
39
+ { id: '13', name: 'Mia Torres', email: 'mia@acme.com', role: 'Content Strategist', department: 'Marketing', status: 'offline', joinedAt: '2024-02-11', projects: 2 },
40
+ { id: '14', name: 'Chen Wei', email: 'chen@acme.com', role: 'Platform Engineer', department: 'Engineering', status: 'active', joinedAt: '2022-10-08', projects: 13 },
41
+ { id: '15', name: 'Ava Novak', email: 'ava@acme.com', role: 'Product Designer', department: 'Design', status: 'active', joinedAt: '2023-06-19', projects: 7 },
42
+ { id: '16', name: 'Liam Ortiz', email: 'liam@acme.com', role: 'QA Automation', department: 'Engineering', status: 'away', joinedAt: '2024-04-02', projects: 4 },
43
+ { id: '17', name: 'Sofia Petrova', email: 'sofia@acme.com', role: 'Data Engineer', department: 'Data', status: 'active', joinedAt: '2023-11-27', projects: 8 },
44
+ { id: '18', name: 'David Kim', email: 'david@acme.com', role: 'Solutions Architect', department: 'Infrastructure', status: 'active', joinedAt: '2022-05-16', projects: 10 },
45
+ { id: '19', name: 'Elena Marsh', email: 'elena@acme.com', role: 'Engineering Manager', department: 'Engineering', status: 'active', joinedAt: '2021-12-06', projects: 14 },
46
+ { id: '20', name: 'Noah Fischer', email: 'noah@acme.com', role: 'Support Lead', department: 'Support', status: 'offline', joinedAt: '2024-05-20', projects: 1 },
33
47
  ];
34
48
 
49
+ const totalCrew = 34;
50
+
35
51
  const filtered = $derived(
36
52
  searchQuery
37
53
  ? crew.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
@@ -45,6 +61,20 @@
45
61
  };
46
62
 
47
63
  const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
64
+
65
+ function exportCrew() {
66
+ const rows = [
67
+ ['Name', 'Email', 'Role', 'Department', 'Status', 'Projects', 'Joined'],
68
+ ...filtered.map(member => [member.name, member.email, member.role, member.department, member.status, String(member.projects), member.joinedAt]),
69
+ ];
70
+ const csv = rows.map(row => row.map(value => `"${value.replaceAll('"', '""')}"`).join(',')).join('\n');
71
+ const url = URL.createObjectURL(new Blob([csv], { type: 'text/csv;charset=utf-8' }));
72
+ const link = document.createElement('a');
73
+ link.href = url;
74
+ link.download = 'team-crew.csv';
75
+ link.click();
76
+ URL.revokeObjectURL(url);
77
+ }
48
78
  </script>
49
79
 
50
80
  <div class="space-y-6" data-svadmin-content-page="network">
@@ -54,10 +84,10 @@
54
84
  <p class="mt-1 text-sm text-muted-foreground">{i18n.t('network.teamCrewDescription')}</p>
55
85
  </div>
56
86
  <div class="flex gap-2">
57
- <Button variant="outline" size="sm">
87
+ <Button variant="outline" size="sm" onclick={exportCrew}>
58
88
  <Download class="h-3.5 w-3.5 mr-1" />{i18n.t('common.export')}
59
89
  </Button>
60
- <Button size="sm">
90
+ <Button size="sm" onclick={() => adminContext.navigate('/account/members/team-members')}>
61
91
  <UserPlus class="h-3.5 w-3.5 mr-1" />{i18n.t('account.inviteMember')}
62
92
  </Button>
63
93
  </div>
@@ -77,7 +107,7 @@
77
107
  <tr class="border-b bg-muted/30">
78
108
  <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.member')}</th>
79
109
  <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.role')}</th>
80
- <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">Department</th>
110
+ <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.department')}</th>
81
111
  <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.status')}</th>
82
112
  <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('publicProfile.projects')}</th>
83
113
  <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.joined')}</th>
@@ -112,7 +142,7 @@
112
142
  <td class="px-4 py-3 text-sm text-foreground">{member.projects}</td>
113
143
  <td class="px-4 py-3 text-sm text-muted-foreground whitespace-nowrap">{member.joinedAt}</td>
114
144
  <td class="px-4 py-3">
115
- <Button variant="ghost" size="icon-sm"><MoreHorizontal class="h-4 w-4" /></Button>
145
+ <Button variant="ghost" size="icon-sm" aria-label={member.name} title={member.name}><MoreHorizontal class="h-4 w-4" /></Button>
116
146
  </td>
117
147
  </tr>
118
148
  {/each}
@@ -136,7 +166,7 @@
136
166
  <p class="truncate text-xs text-muted-foreground">{member.email}</p>
137
167
  </div>
138
168
  </div>
139
- <Button variant="ghost" size="icon-sm" class="shrink-0"><MoreHorizontal class="h-4 w-4" /></Button>
169
+ <Button variant="ghost" size="icon-sm" class="shrink-0" aria-label={member.name} title={member.name}><MoreHorizontal class="h-4 w-4" /></Button>
140
170
  </div>
141
171
  <div class="grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
142
172
  <div class="min-w-0">
@@ -154,4 +184,6 @@
154
184
  </div>
155
185
  </Card.CardContent>
156
186
  </Card.Card>
187
+
188
+ <p class="text-sm text-muted-foreground">{i18n.t('network.showingUsersOf', { shown: filtered.length, total: totalCrew })}</p>
157
189
  </div>
@@ -5,6 +5,15 @@
5
5
  import { Badge } from '../ui/badge/index.js';
6
6
  import { Input } from '../ui/input/index.js';
7
7
  import { Search, ExternalLink, Check, Plus } from '@lucide/svelte';
8
+ import nftCover01 from '../../assets/nft/nft-cover-01.jpg';
9
+ import nftCover02 from '../../assets/nft/nft-cover-02.jpg';
10
+ import nftCover03 from '../../assets/nft/nft-cover-03.jpg';
11
+ import nftCover04 from '../../assets/nft/nft-cover-04.jpg';
12
+ import nftCover05 from '../../assets/nft/nft-cover-05.jpg';
13
+ import nftCover06 from '../../assets/nft/nft-cover-06.jpg';
14
+ import nftCover07 from '../../assets/nft/nft-cover-07.jpg';
15
+ import nftCover08 from '../../assets/nft/nft-cover-08.jpg';
16
+ import nftCover09 from '../../assets/nft/nft-cover-09.jpg';
8
17
 
9
18
  const i18n = useTranslation();
10
19
 
@@ -12,7 +21,7 @@
12
21
  id: string;
13
22
  name: string;
14
23
  username: string;
15
- avatar?: string;
24
+ artwork: string;
16
25
  bio: string;
17
26
  collectionCount: number;
18
27
  floorPrice: string;
@@ -24,12 +33,15 @@
24
33
  let searchQuery = $state('');
25
34
 
26
35
  const users: NFTUser[] = [
27
- { 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'] },
28
- { 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'] },
29
- { 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'] },
30
- { 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'] },
31
- { 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'] },
32
- { 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'] },
36
+ { id: '1', name: 'CryptoArt Studio', username: '@cryptoart', artwork: nftCover01, bio: 'Digital art collection featuring generative abstracts', collectionCount: 42, floorPrice: '0.5 ETH', volume: '128.3 ETH', verified: true, tags: ['Art', 'Generative'] },
37
+ { id: '2', name: 'PixelPunk Collective', username: '@pixelpunk', artwork: nftCover02, bio: 'On-chain pixel art characters with unique traits', collectionCount: 18, floorPrice: '1.2 ETH', volume: '342.7 ETH', verified: true, tags: ['PFP', 'Pixel'] },
38
+ { id: '3', name: 'MetaVerse Builders', username: '@metabuild', artwork: nftCover03, bio: '3D architectural models for virtual worlds', collectionCount: 7, floorPrice: '0.8 ETH', volume: '56.1 ETH', verified: false, tags: ['3D', 'Virtual'] },
39
+ { id: '4', name: 'SoundWave NFT', username: '@soundwave', artwork: nftCover04, bio: 'Music-backed NFTs with on-chain audio', collectionCount: 23, floorPrice: '0.3 ETH', volume: '89.4 ETH', verified: true, tags: ['Music', 'Audio'] },
40
+ { id: '5', name: 'GameAssets DAO', username: '@gameassets', artwork: nftCover05, bio: 'In-game items and collectibles for Web3 gaming', collectionCount: 156, floorPrice: '0.05 ETH', volume: '412.8 ETH', verified: true, tags: ['Gaming', 'Items'] },
41
+ { id: '6', name: 'PhotoVerse', username: '@photoverse', artwork: nftCover06, bio: 'Curated photography NFTs from world artists', collectionCount: 31, floorPrice: '0.15 ETH', volume: '67.2 ETH', verified: false, tags: ['Photo', 'Curated'] },
42
+ { id: '7', name: 'CryptoKicks', username: '@cryptokicks', artwork: nftCover07, bio: 'Limited-edition digital sneakers and wearables', collectionCount: 64, floorPrice: '0.4 ETH', volume: '156.9 ETH', verified: true, tags: ['Fashion', 'Wearables'] },
43
+ { id: '8', name: 'ChainChronicles', username: '@chainchron', artwork: nftCover08, bio: 'Story-driven collectible cards with on-chain lore', collectionCount: 88, floorPrice: '0.25 ETH', volume: '203.4 ETH', verified: false, tags: ['Cards', 'Story'] },
44
+ { id: '9', name: 'AIArtifacts', username: '@aiartifacts', artwork: nftCover09, bio: 'AI-generated sculptures minted as unique tokens', collectionCount: 12, floorPrice: '2.1 ETH', volume: '512.6 ETH', verified: true, tags: ['AI', '3D'] },
33
45
  ];
34
46
 
35
47
  const filtered = $derived(
@@ -38,8 +50,6 @@
38
50
  : users
39
51
  );
40
52
 
41
- const initials = (name: string) => name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
42
-
43
53
  let connectedIds = $state<string[]>([]);
44
54
 
45
55
  function toggleConnect(id: string) {
@@ -64,18 +74,18 @@
64
74
  <Input placeholder={i18n.t('common.search')} bind:value={searchQuery} class="pl-9" />
65
75
  </div>
66
76
 
77
+ <p class="text-sm font-medium text-muted-foreground">{i18n.t('network.showingUsers', { count: filtered.length })}</p>
78
+
67
79
  <div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
68
80
  {#each filtered as user (user.id)}
69
- <Card.Card class="overflow-hidden border-border/60 hover:border-primary/30 transition-colors">
70
- <div class="h-20 bg-gradient-to-r from-violet-500/20 via-purple-500/15 to-fuchsia-500/20 relative">
71
- <div class="absolute inset-0 opacity-[0.08]" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 16px 16px;"></div>
81
+ <Card.Card class="group overflow-hidden border-border/60 hover:border-primary/30 transition-colors">
82
+ <div class="h-28 overflow-hidden bg-muted">
83
+ <img src={user.artwork} alt="" class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-[1.03]" />
72
84
  </div>
73
85
  <Card.CardContent class="relative px-4 pb-4">
74
86
  <div class="-mt-8 mb-2 flex items-end gap-3">
75
87
  <div class="ring-3 ring-card rounded-xl overflow-hidden shrink-0">
76
- <div class="flex h-14 w-14 items-center justify-center bg-primary/10 text-primary font-bold">
77
- {initials(user.name)}
78
- </div>
88
+ <img src={user.artwork} alt="" class="h-14 w-14 object-cover" />
79
89
  </div>
80
90
  <div class="flex-1 min-w-0 pb-0.5">
81
91
  <div class="flex items-center gap-1.5">
@@ -125,7 +135,7 @@
125
135
  <Plus class="h-3.5 w-3.5 mr-1" />{i18n.t('network.connect')}
126
136
  {/if}
127
137
  </Button>
128
- <Button size="sm" variant="ghost">
138
+ <Button size="sm" variant="ghost" aria-label={i18n.t('common.detail')} title={i18n.t('common.detail')}>
129
139
  <ExternalLink class="h-3.5 w-3.5" />
130
140
  </Button>
131
141
  </div>
@@ -19,9 +19,12 @@
19
19
 
20
20
  interface Props {
21
21
  activities?: ActivityItem[];
22
+ showAutoRefresh?: boolean;
22
23
  }
23
24
 
24
- let { activities = [] }: Props = $props();
25
+ let { activities = [], showAutoRefresh = true }: Props = $props();
26
+
27
+ let autoRefresh = $state(false);
25
28
 
26
29
  const typeConfig: Record<string, { icon: Component; color: string; action: string }> = {
27
30
  'posted': { icon: MessageSquare, color: 'bg-blue-500/10 text-blue-500', action: i18n.t('publicProfile.activityPosted') },
@@ -33,7 +36,26 @@
33
36
  </script>
34
37
 
35
38
  <div class="space-y-4">
36
- <h3 class="text-lg font-semibold text-foreground">{i18n.t('publicProfile.activity')}</h3>
39
+ <div class="flex items-center justify-between gap-3">
40
+ <h3 class="text-lg font-semibold text-foreground">{i18n.t('publicProfile.activity')}</h3>
41
+ {#if showAutoRefresh}
42
+ <div class="flex items-center gap-2 text-xs text-muted-foreground">
43
+ <span>{i18n.t('publicProfile.autoRefresh')}:</span>
44
+ <div class="flex rounded-md border border-border/60 p-0.5">
45
+ <button
46
+ type="button"
47
+ class="rounded px-2 py-0.5 text-xs font-medium transition-colors {autoRefresh ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}"
48
+ onclick={() => autoRefresh = true}
49
+ >{i18n.t('common.on')}</button>
50
+ <button
51
+ type="button"
52
+ class="rounded px-2 py-0.5 text-xs font-medium transition-colors {autoRefresh ? 'text-muted-foreground hover:text-foreground' : 'bg-primary text-primary-foreground'}"
53
+ onclick={() => autoRefresh = false}
54
+ >{i18n.t('common.off')}</button>
55
+ </div>
56
+ </div>
57
+ {/if}
58
+ </div>
37
59
 
38
60
  {#if activities.length === 0}
39
61
  <div class="rounded-xl border border-dashed border-border/60 p-8 text-center">
@@ -10,6 +10,7 @@ interface ActivityItem {
10
10
  }
11
11
  interface Props {
12
12
  activities?: ActivityItem[];
13
+ showAutoRefresh?: boolean;
13
14
  }
14
15
  declare const ActivityTimeline: Component<Props, {}, "">;
15
16
  type ActivityTimeline = ReturnType<typeof ActivityTimeline>;