@svadmin/ui 0.42.3 → 0.44.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 (79) hide show
  1. package/dist/app.css +462 -825
  2. package/dist/app.theme.css +462 -825
  3. package/dist/components/ErrorPage.svelte +4 -56
  4. package/dist/components/ResourceOperationsPage.svelte +84 -223
  5. package/dist/components/ResourceOperationsPage.svelte.d.ts +1 -1
  6. package/dist/components/TwoFactorAuthPage.svelte +44 -232
  7. package/dist/components/account/CompanyProfilePage.svelte +21 -142
  8. package/dist/components/account/GetStartedPage.svelte +45 -106
  9. package/dist/components/account/GetStartedPage.svelte.d.ts +1 -2
  10. package/dist/components/account/MembersStarterPage.svelte +49 -92
  11. package/dist/components/account/SecurityLogPage.svelte +16 -131
  12. package/dist/components/account/SettingsEnterprisePage.svelte +15 -275
  13. package/dist/components/account/SettingsPlainPage.svelte +10 -175
  14. package/dist/components/account/SettingsSidebarPage.svelte +11 -304
  15. package/dist/components/account/TeamMembersPage.svelte +18 -187
  16. package/dist/components/account/UserProfilePage.svelte +19 -196
  17. package/dist/components/account/UserProfilePage.svelte.d.ts +1 -2
  18. package/dist/components/content/ApiKeyList.svelte +13 -0
  19. package/dist/components/content/ApiKeyList.svelte.d.ts +16 -0
  20. package/dist/components/content/ContentPageHeader.svelte +31 -0
  21. package/dist/components/content/ContentPageHeader.svelte.d.ts +12 -0
  22. package/dist/components/content/ContentPageShell.svelte +39 -0
  23. package/dist/components/content/ContentPageShell.svelte.d.ts +14 -0
  24. package/dist/components/content/DataState.svelte +44 -0
  25. package/dist/components/content/DataState.svelte.d.ts +13 -0
  26. package/dist/components/content/DescriptionList.svelte +27 -0
  27. package/dist/components/content/DescriptionList.svelte.d.ts +14 -0
  28. package/dist/components/content/FileList.svelte +13 -0
  29. package/dist/components/content/FileList.svelte.d.ts +16 -0
  30. package/dist/components/content/FilterToolbar.svelte +24 -0
  31. package/dist/components/content/FilterToolbar.svelte.d.ts +11 -0
  32. package/dist/components/content/ImportDropzone.svelte +19 -0
  33. package/dist/components/content/ImportDropzone.svelte.d.ts +10 -0
  34. package/dist/components/content/IntegrationCard.svelte +16 -0
  35. package/dist/components/content/IntegrationCard.svelte.d.ts +15 -0
  36. package/dist/components/content/MemberList.svelte +16 -0
  37. package/dist/components/content/MemberList.svelte.d.ts +17 -0
  38. package/dist/components/content/MetricBlock.svelte +27 -0
  39. package/dist/components/content/MetricBlock.svelte.d.ts +13 -0
  40. package/dist/components/content/NetworkTable.svelte +18 -0
  41. package/dist/components/content/NetworkTable.svelte.d.ts +31 -0
  42. package/dist/components/content/NetworkTable.types.d.ts +4 -0
  43. package/dist/components/content/NetworkTable.types.js +1 -0
  44. package/dist/components/content/NetworkUserCard.svelte +17 -0
  45. package/dist/components/content/NetworkUserCard.svelte.d.ts +17 -0
  46. package/dist/components/content/OtpInput.svelte +22 -0
  47. package/dist/components/content/OtpInput.svelte.d.ts +10 -0
  48. package/dist/components/content/PageToolbar.svelte +18 -0
  49. package/dist/components/content/PageToolbar.svelte.d.ts +10 -0
  50. package/dist/components/content/ProjectCard.svelte +39 -0
  51. package/dist/components/content/ProjectCard.svelte.d.ts +20 -0
  52. package/dist/components/content/SectionHeader.svelte +18 -0
  53. package/dist/components/content/SectionHeader.svelte.d.ts +10 -0
  54. package/dist/components/content/SecurityEventTable.svelte +18 -0
  55. package/dist/components/content/SecurityEventTable.svelte.d.ts +15 -0
  56. package/dist/components/content/StatusBadge.svelte +19 -0
  57. package/dist/components/content/StatusBadge.svelte.d.ts +9 -0
  58. package/dist/components/content/SystemErrorState.svelte +18 -0
  59. package/dist/components/content/SystemErrorState.svelte.d.ts +10 -0
  60. package/dist/components/content/TeamCard.svelte +26 -0
  61. package/dist/components/content/TeamCard.svelte.d.ts +23 -0
  62. package/dist/components/content/TwoFactorStepper.svelte +11 -0
  63. package/dist/components/content/TwoFactorStepper.svelte.d.ts +8 -0
  64. package/dist/components/content/index.d.ts +32 -0
  65. package/dist/components/content/index.js +22 -0
  66. package/dist/components/network/TeamCrewTablePage.svelte +29 -181
  67. package/dist/components/network/UserCardsNFTPage.svelte +24 -137
  68. package/dist/components/profile/ActivityTimeline.svelte +17 -91
  69. package/dist/components/profile/ProfileCard.svelte +4 -4
  70. package/dist/components/profile/ProjectsGrid.svelte +28 -106
  71. package/dist/components/profile/ProjectsGrid.svelte.d.ts +2 -12
  72. package/dist/components/profile/PublicProfilePage.svelte +34 -186
  73. package/dist/components/profile/TeamsShowcase.svelte +12 -114
  74. package/dist/components/profile/TeamsShowcase.svelte.d.ts +2 -12
  75. package/dist/index.d.ts +36 -0
  76. package/dist/index.js +24 -0
  77. package/dist/reference-data.d.ts +171 -0
  78. package/dist/reference-data.js +47 -0
  79. package/package.json +2 -2
@@ -1,189 +1,37 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext } from '@svadmin/core';
3
2
  import { useTranslation } from '@svadmin/core/i18n';
4
- import * as Card from '../ui/card/index.js';
3
+ import { Download, UserPlus } from '@lucide/svelte';
5
4
  import { Button } from '../ui/button/index.js';
6
- import { Input } from '../ui/input/index.js';
7
- import { Search, Download, UserPlus, MoreHorizontal, CheckCircle2, Clock, XCircle } from '@lucide/svelte';
8
-
5
+ import ContentPageShell from '../content/ContentPageShell.svelte';
6
+ import ContentPageHeader from '../content/ContentPageHeader.svelte';
7
+ import FilterToolbar from '../content/FilterToolbar.svelte';
8
+ import NetworkTable from '../content/NetworkTable.svelte';
9
+ import type { NetworkColumn } from '../content/NetworkTable.types.js';
10
+ import { Avatar } from '../ui/avatar/index.js';
11
+ import { Badge } from '../ui/badge/index.js';
12
+
13
+ interface CrewMember { id: string; name: string; email: string; role: string; department: string; status: 'active' | 'away' | 'offline'; projects: number; }
9
14
  const i18n = useTranslation();
10
- const adminContext = captureAdminContext();
11
-
12
- interface CrewMember {
13
- id: string;
14
- name: string;
15
- email: string;
16
- role: string;
17
- department: string;
18
- status: 'active' | 'away' | 'offline';
19
- avatar?: string;
20
- joinedAt: string;
21
- projects: number;
22
- }
23
-
24
- let searchQuery = $state('');
25
-
26
- const crew: CrewMember[] = [
27
- { id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Tech Lead', department: 'Engineering', status: 'active', joinedAt: '2022-03-15', projects: 12 },
28
- { id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Senior Designer', department: 'Design', status: 'active', joinedAt: '2022-06-22', projects: 8 },
29
- { id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Product Manager', department: 'Product', status: 'away', joinedAt: '2023-01-10', projects: 6 },
30
- { id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Backend Engineer', department: 'Engineering', status: 'active', joinedAt: '2023-04-05', projects: 10 },
31
- { id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'QA Engineer', department: 'Engineering', status: 'offline', joinedAt: '2022-11-20', projects: 4 },
32
- { id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Engineering Manager', department: 'Engineering', status: 'active', joinedAt: '2022-02-14', projects: 15 },
33
- { id: '7', name: 'James Wilson', email: 'james@acme.com', role: 'DevOps Engineer', department: 'Infrastructure', status: 'active', joinedAt: '2023-08-01', projects: 7 },
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 },
15
+ let query = $state('');
16
+ const rows: CrewMember[] = [
17
+ { id: '1', name: 'Alex Chen', email: 'alex@acme.com', role: 'Tech Lead', department: 'Engineering', status: 'active', projects: 12 },
18
+ { id: '2', name: 'Sarah Kim', email: 'sarah@acme.com', role: 'Senior Designer', department: 'Design', status: 'active', projects: 8 },
19
+ { id: '3', name: 'Mike Johnson', email: 'mike@acme.com', role: 'Product Manager', department: 'Product', status: 'away', projects: 6 },
20
+ { id: '4', name: 'Lisa Wang', email: 'lisa@acme.com', role: 'Backend Engineer', department: 'Engineering', status: 'active', projects: 10 },
21
+ { id: '5', name: 'Tom Brown', email: 'tom@acme.com', role: 'QA Engineer', department: 'Engineering', status: 'offline', projects: 4 },
22
+ { id: '6', name: 'Emma Davis', email: 'emma@acme.com', role: 'Engineering Manager', department: 'Engineering', status: 'active', projects: 15 },
47
23
  ];
48
-
49
- const totalCrew = 34;
50
-
51
- const filtered = $derived(
52
- searchQuery
53
- ? crew.filter(m => m.name.toLowerCase().includes(searchQuery.toLowerCase()) || m.email.toLowerCase().includes(searchQuery.toLowerCase()) || m.department.toLowerCase().includes(searchQuery.toLowerCase()))
54
- : crew
55
- );
56
-
57
- const statusConfig = {
58
- 'active': { icon: CheckCircle2, color: 'text-green-500', dot: 'bg-green-500', label: i18n.t('account.active') },
59
- 'away': { icon: Clock, color: 'text-amber-500', dot: 'bg-amber-500', label: i18n.t('account.invited') },
60
- 'offline': { icon: XCircle, color: 'text-muted-foreground', dot: 'bg-muted-foreground', label: i18n.t('account.inactive') },
61
- };
62
-
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
- }
24
+ const filtered = $derived(query ? rows.filter((row) => `${row.name} ${row.email} ${row.department}`.toLowerCase().includes(query.toLowerCase())) : rows);
25
+ const columns: NetworkColumn<CrewMember>[] = [{ key: 'name', label: i18n.t('account.member') }, { key: 'role', label: i18n.t('account.role') }, { key: 'department', label: i18n.t('account.department') }, { key: 'status', label: i18n.t('account.status') }, { key: 'projects', label: i18n.t('publicProfile.projects') }];
26
+ const initials = (name: string) => name.split(' ').map((part) => part[0]).join('').slice(0, 2).toUpperCase();
78
27
  </script>
79
28
 
80
- <div class="space-y-6" data-svadmin-content-page="network">
81
- <div class="flex items-start justify-between gap-4">
82
- <div>
83
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('network.teamCrew')}</h2>
84
- <p class="mt-1 text-sm text-muted-foreground">{i18n.t('network.teamCrewDescription')}</p>
85
- </div>
86
- <div class="flex gap-2">
87
- <Button variant="outline" size="sm" onclick={exportCrew}>
88
- <Download class="h-3.5 w-3.5 mr-1" />{i18n.t('common.export')}
89
- </Button>
90
- <Button size="sm" onclick={() => adminContext.navigate('/account/members/team-members')}>
91
- <UserPlus class="h-3.5 w-3.5 mr-1" />{i18n.t('account.inviteMember')}
92
- </Button>
93
- </div>
94
- </div>
95
-
96
- <div class="relative">
97
- <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
98
- <Input placeholder={i18n.t('common.search')} bind:value={searchQuery} class="pl-9" />
99
- </div>
100
-
101
- <!-- Table -->
102
- <Card.Card class="border-border/60">
103
- <Card.CardContent class="p-0">
104
- <div class="hidden overflow-x-auto lg:block">
105
- <table class="w-full">
106
- <thead>
107
- <tr class="border-b bg-muted/30">
108
- <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.member')}</th>
109
- <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.role')}</th>
110
- <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.department')}</th>
111
- <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.status')}</th>
112
- <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('publicProfile.projects')}</th>
113
- <th class="px-4 py-3 text-left text-xs font-semibold text-muted-foreground">{i18n.t('account.joined')}</th>
114
- <th class="px-4 py-3 w-12"></th>
115
- </tr>
116
- </thead>
117
- <tbody class="divide-y">
118
- {#each filtered as member (member.id)}
119
- {@const sc = statusConfig[member.status]}
120
- <tr class="hover:bg-accent/30 transition-colors">
121
- <td class="px-4 py-3">
122
- <div class="flex items-center gap-3">
123
- <div class="relative shrink-0">
124
- <div class="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10 text-primary text-xs font-semibold">
125
- {initials(member.name)}
126
- </div>
127
- <span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full {sc.dot} ring-2 ring-card"></span>
128
- </div>
129
- <div class="min-w-0">
130
- <p class="text-sm font-medium text-foreground truncate">{member.name}</p>
131
- <p class="text-xs text-muted-foreground truncate">{member.email}</p>
132
- </div>
133
- </div>
134
- </td>
135
- <td class="px-4 py-3 text-sm text-foreground">{member.role}</td>
136
- <td class="px-4 py-3 text-sm text-muted-foreground">{member.department}</td>
137
- <td class="px-4 py-3">
138
- <span class="flex items-center gap-1 text-xs {sc.color}">
139
- <sc.icon class="h-3 w-3" />{sc.label}
140
- </span>
141
- </td>
142
- <td class="px-4 py-3 text-sm text-foreground">{member.projects}</td>
143
- <td class="px-4 py-3 text-sm text-muted-foreground whitespace-nowrap">{member.joinedAt}</td>
144
- <td class="px-4 py-3">
145
- <Button variant="ghost" size="icon-sm" aria-label={member.name} title={member.name}><MoreHorizontal class="h-4 w-4" /></Button>
146
- </td>
147
- </tr>
148
- {/each}
149
- </tbody>
150
- </table>
151
- </div>
152
- <div class="divide-y lg:hidden">
153
- {#each filtered as member (member.id)}
154
- {@const sc = statusConfig[member.status]}
155
- <div class="space-y-3 p-4">
156
- <div class="flex items-start justify-between gap-3">
157
- <div class="flex min-w-0 items-center gap-3">
158
- <div class="relative shrink-0">
159
- <div class="flex h-9 w-9 items-center justify-center rounded-full bg-primary/10 text-sm font-semibold text-primary">
160
- {initials(member.name)}
161
- </div>
162
- <span class="absolute -bottom-0.5 -right-0.5 h-2.5 w-2.5 rounded-full {sc.dot} ring-2 ring-card"></span>
163
- </div>
164
- <div class="min-w-0">
165
- <p class="truncate text-sm font-medium text-foreground">{member.name}</p>
166
- <p class="truncate text-xs text-muted-foreground">{member.email}</p>
167
- </div>
168
- </div>
169
- <Button variant="ghost" size="icon-sm" class="shrink-0" aria-label={member.name} title={member.name}><MoreHorizontal class="h-4 w-4" /></Button>
170
- </div>
171
- <div class="grid gap-2 text-xs text-muted-foreground sm:grid-cols-2">
172
- <div class="min-w-0">
173
- <span class="block font-medium text-foreground">{member.role}</span>
174
- <span class="truncate">{member.department}</span>
175
- </div>
176
- <div class="flex items-center gap-1 {sc.color}">
177
- <sc.icon class="h-3 w-3" />{sc.label}
178
- </div>
179
- <div>{i18n.t('publicProfile.projects')}: <span class="font-medium text-foreground">{member.projects}</span></div>
180
- <div>{i18n.t('account.joined')}: <span class="font-medium text-foreground">{member.joinedAt}</span></div>
181
- </div>
182
- </div>
183
- {/each}
184
- </div>
185
- </Card.CardContent>
186
- </Card.Card>
29
+ {#snippet row(item: CrewMember)}
30
+ <tr data-svadmin-table-row><td class="px-4 py-3"><div class="flex items-center gap-3"><Avatar fallback={initials(item.name)} alt={item.name} size="sm" /><div><p class="text-sm font-medium text-foreground">{item.name}</p><p class="text-xs text-muted-foreground">{item.email}</p></div></div></td><td class="px-4 py-3 text-sm">{item.role}</td><td class="px-4 py-3 text-sm text-muted-foreground">{item.department}</td><td class="px-4 py-3"><Badge variant="outline">{item.status}</Badge></td><td class="px-4 py-3 text-sm">{item.projects}</td></tr>
31
+ {/snippet}
187
32
 
188
- <p class="text-sm text-muted-foreground">{i18n.t('network.showingUsersOf', { shown: filtered.length, total: totalCrew })}</p>
189
- </div>
33
+ <ContentPageShell pageId="network-team-crew" width="wide">
34
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><ContentPageHeader title={i18n.t('network.teamCrew')} description={i18n.t('network.teamCrewDescription')} /><div class="flex flex-wrap gap-2"><Button variant="outline" size="sm"><Download class="size-3.5" />{i18n.t('common.export')}</Button><Button size="sm"><UserPlus class="size-3.5" />{i18n.t('account.inviteMember')}</Button></div></div>
35
+ <FilterToolbar bind:query placeholder={i18n.t('common.search')} />
36
+ <NetworkTable rows={filtered} {columns} {row} />
37
+ </ContentPageShell>
@@ -1,146 +1,33 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
- import * as Card from '../ui/card/index.js';
3
+ import { Check, Plus } from '@lucide/svelte';
4
4
  import { Button } from '../ui/button/index.js';
5
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
- 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';
6
+ import * as Card from '../ui/card/index.js';
7
+ import ContentPageShell from '../content/ContentPageShell.svelte';
8
+ import ContentPageHeader from '../content/ContentPageHeader.svelte';
9
+ import FilterToolbar from '../content/FilterToolbar.svelte';
10
+ import DataState from '../content/DataState.svelte';
11
+ import type { NetworkUser } from '../content/NetworkUserCard.svelte';
17
12
 
18
13
  const i18n = useTranslation();
19
-
20
- interface NFTUser {
21
- id: string;
22
- name: string;
23
- username: string;
24
- artwork: string;
25
- bio: string;
26
- collectionCount: number;
27
- floorPrice: string;
28
- volume: string;
29
- verified: boolean;
30
- tags: string[];
31
- }
32
-
33
- let searchQuery = $state('');
34
-
35
- const users: NFTUser[] = [
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'] },
14
+ type NftUser = NetworkUser & { tags: string[]; collectionCount: number; floorPrice: string; volume: string; verified: boolean };
15
+ let query = $state('');
16
+ let connected = $state<string[]>([]);
17
+ const users: NftUser[] = [
18
+ { id: '1', name: 'CryptoArt Studio', handle: '@cryptoart', role: 'Generative art', summary: 'Digital art collection featuring generative abstracts', tags: ['Art', 'Generative'], collectionCount: 42, floorPrice: '0.5 ETH', volume: '128.3 ETH', verified: true },
19
+ { id: '2', name: 'PixelPunk Collective', handle: '@pixelpunk', role: 'On-chain studio', summary: 'Pixel art characters with unique traits', tags: ['PFP', 'Pixel'], collectionCount: 18, floorPrice: '1.2 ETH', volume: '342.7 ETH', verified: true },
20
+ { id: '3', name: 'MetaVerse Builders', handle: '@metabuild', role: '3D architecture', summary: 'Architectural models for virtual worlds', tags: ['3D', 'Virtual'], collectionCount: 7, floorPrice: '0.8 ETH', volume: '56.1 ETH', verified: false },
21
+ { id: '4', name: 'SoundWave NFT', handle: '@soundwave', role: 'Music studio', summary: 'Music-backed NFTs with on-chain audio', tags: ['Music', 'Audio'], collectionCount: 23, floorPrice: '0.3 ETH', volume: '89.4 ETH', verified: true },
22
+ { id: '5', name: 'GameAssets DAO', handle: '@gameassets', role: 'Gaming collective', summary: 'In-game items and collectibles for Web3 gaming', tags: ['Gaming', 'Items'], collectionCount: 156, floorPrice: '0.05 ETH', volume: '412.8 ETH', verified: true },
23
+ { id: '6', name: 'PhotoVerse', handle: '@photoverse', role: 'Photography', summary: 'Curated photography from world artists', tags: ['Photo', 'Curated'], collectionCount: 31, floorPrice: '0.15 ETH', volume: '67.2 ETH', verified: false },
45
24
  ];
46
-
47
- const filtered = $derived(
48
- searchQuery
49
- ? users.filter(u => u.name.toLowerCase().includes(searchQuery.toLowerCase()) || u.username.toLowerCase().includes(searchQuery.toLowerCase()) || u.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
50
- : users
51
- );
52
-
53
- let connectedIds = $state<string[]>([]);
54
-
55
- function toggleConnect(id: string) {
56
- connectedIds = connectedIds.includes(id)
57
- ? connectedIds.filter(connectedId => connectedId !== id)
58
- : [...connectedIds, id];
59
- }
60
-
61
- function isConnected(id: string): boolean {
62
- return connectedIds.includes(id);
63
- }
25
+ const filtered = $derived(query ? users.filter((user) => `${user.name} ${user.handle} ${user.tags.join(' ')}`.toLowerCase().includes(query.toLowerCase())) : users);
26
+ function toggle(id: string) { connected = connected.includes(id) ? connected.filter((item) => item !== id) : [...connected, id]; }
64
27
  </script>
65
28
 
66
- <div class="space-y-6" data-svadmin-content-page="network">
67
- <div>
68
- <h2 class="text-xl font-semibold text-foreground">{i18n.t('network.userCards')}</h2>
69
- <p class="mt-1 text-sm text-muted-foreground">{i18n.t('network.userCardsDescription')}</p>
70
- </div>
71
-
72
- <div class="relative">
73
- <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
74
- <Input placeholder={i18n.t('common.search')} bind:value={searchQuery} class="pl-9" />
75
- </div>
76
-
77
- <p class="text-sm font-medium text-muted-foreground">{i18n.t('network.showingUsers', { count: filtered.length })}</p>
78
-
79
- <div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
80
- {#each filtered as user (user.id)}
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]" />
84
- </div>
85
- <Card.CardContent class="relative px-4 pb-4">
86
- <div class="-mt-8 mb-2 flex items-end gap-3">
87
- <div class="ring-3 ring-card rounded-xl overflow-hidden shrink-0">
88
- <img src={user.artwork} alt="" class="h-14 w-14 object-cover" />
89
- </div>
90
- <div class="flex-1 min-w-0 pb-0.5">
91
- <div class="flex items-center gap-1.5">
92
- <h4 class="text-sm font-semibold text-foreground truncate">{user.name}</h4>
93
- {#if user.verified}
94
- <Check class="h-3.5 w-3.5 text-blue-500" />
95
- {/if}
96
- </div>
97
- <p class="text-xs text-muted-foreground">{user.username}</p>
98
- </div>
99
- </div>
100
-
101
- <p class="text-xs text-muted-foreground line-clamp-2 mb-3">{user.bio}</p>
102
-
103
- <div class="flex flex-wrap gap-1 mb-3">
104
- {#each user.tags as tag (tag)}
105
- <Badge variant="outline" class="text-[10px]">{tag}</Badge>
106
- {/each}
107
- </div>
108
-
109
- <!-- NFT stats -->
110
- <div class="grid grid-cols-3 gap-2 border-t pt-3">
111
- <div class="text-center">
112
- <div class="text-sm font-semibold text-foreground">{user.collectionCount}</div>
113
- <div class="text-[10px] text-muted-foreground">{i18n.t('network.nftCollection')}</div>
114
- </div>
115
- <div class="text-center">
116
- <div class="text-sm font-semibold text-foreground">{user.floorPrice}</div>
117
- <div class="text-[10px] text-muted-foreground">{i18n.t('network.floorPrice')}</div>
118
- </div>
119
- <div class="text-center">
120
- <div class="text-xs font-semibold text-foreground">{user.volume}</div>
121
- <div class="text-[10px] text-muted-foreground">{i18n.t('network.volume')}</div>
122
- </div>
123
- </div>
124
-
125
- <div class="mt-3 flex gap-2">
126
- <Button
127
- size="sm"
128
- variant={isConnected(user.id) ? 'outline' : 'default'}
129
- class="flex-1"
130
- onclick={() => toggleConnect(user.id)}
131
- >
132
- {#if isConnected(user.id)}
133
- <Check class="h-3.5 w-3.5 mr-1" />{i18n.t('network.connected')}
134
- {:else}
135
- <Plus class="h-3.5 w-3.5 mr-1" />{i18n.t('network.connect')}
136
- {/if}
137
- </Button>
138
- <Button size="sm" variant="ghost" aria-label={i18n.t('common.detail')} title={i18n.t('common.detail')}>
139
- <ExternalLink class="h-3.5 w-3.5" />
140
- </Button>
141
- </div>
142
- </Card.CardContent>
143
- </Card.Card>
144
- {/each}
145
- </div>
146
- </div>
29
+ <ContentPageShell pageId="network-user-cards" width="wide">
30
+ <ContentPageHeader title={i18n.t('network.userCards')} description={i18n.t('network.userCardsDescription')} />
31
+ <FilterToolbar bind:query placeholder={i18n.t('common.search')} />
32
+ {#if filtered.length === 0}<DataState state="empty" title={i18n.t('network.userCards')} />{:else}<div class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">{#each filtered as user (user.id)}<Card.Card><Card.CardContent class="space-y-4 p-4"><div class="flex items-start gap-3"><span class="flex size-10 items-center justify-center rounded-md bg-muted text-sm font-semibold text-muted-foreground">{user.name.slice(0, 2).toUpperCase()}</span><div class="min-w-0 flex-1"><div class="flex items-center gap-1.5"><h2 class="truncate text-sm font-semibold text-foreground">{user.name}</h2>{#if user.verified}<Check class="size-3.5 text-primary" />{/if}</div><p class="text-xs text-muted-foreground">{user.handle}</p></div></div><p class="text-sm text-muted-foreground">{user.summary}</p><div class="flex flex-wrap gap-1.5">{#each user.tags as tag (tag)}<Badge variant="outline">{tag}</Badge>{/each}</div><div class="grid grid-cols-3 gap-2 border-t border-border pt-3 text-center"><div><p class="text-sm font-semibold text-foreground">{user.collectionCount}</p><p class="text-xs text-muted-foreground">{i18n.t('network.nftCollection')}</p></div><div><p class="text-sm font-semibold text-foreground">{user.floorPrice}</p><p class="text-xs text-muted-foreground">{i18n.t('network.floorPrice')}</p></div><div><p class="text-sm font-semibold text-foreground">{user.volume}</p><p class="text-xs text-muted-foreground">{i18n.t('network.volume')}</p></div></div><Button size="sm" variant={connected.includes(user.id) ? 'outline' : 'default'} class="w-full" onclick={() => toggle(user.id)}>{#if connected.includes(user.id)}<Check class="size-3.5" />{i18n.t('network.connected')}{:else}<Plus class="size-3.5" />{i18n.t('network.connect')}{/if}</Button></Card.CardContent></Card.Card>{/each}</div>{/if}
33
+ </ContentPageShell>
@@ -1,101 +1,27 @@
1
1
  <script lang="ts">
2
2
  import { useTranslation } from '@svadmin/core/i18n';
3
- import * as Card from '../ui/card/index.js';
3
+ import { MessageSquare, Plus, Star, UserPlus } from '@lucide/svelte';
4
4
  import { Avatar } from '../ui/avatar/index.js';
5
- import { MessageSquare, Plus, UserPlus, Star } from '@lucide/svelte';
5
+ import { Button } from '../ui/button/index.js';
6
+ import { DataState, SectionHeader } from '../content/index.js';
7
+ import * as Card from '../ui/card/index.js';
6
8
  import type { Component } from 'svelte';
7
9
 
8
- const i18n = useTranslation();
9
-
10
- interface ActivityItem {
11
- id: string;
12
- type: 'posted' | 'commented' | 'joined' | 'created' | 'starred';
13
- user: string;
14
- avatar?: string;
15
- content?: string;
16
- target?: string;
17
- timestamp: string;
18
- }
19
-
20
- interface Props {
21
- activities?: ActivityItem[];
22
- showAutoRefresh?: boolean;
23
- }
24
-
10
+ interface ActivityItem { id: string; type: 'posted' | 'commented' | 'joined' | 'created' | 'starred'; user: string; avatar?: string; content?: string; target?: string; timestamp: string; }
11
+ interface Props { activities?: ActivityItem[]; showAutoRefresh?: boolean; }
25
12
  let { activities = [], showAutoRefresh = true }: Props = $props();
26
-
27
13
  let autoRefresh = $state(false);
28
-
29
- const typeConfig: Record<string, { icon: Component; color: string; action: string }> = {
30
- 'posted': { icon: MessageSquare, color: 'bg-blue-500/10 text-blue-500', action: i18n.t('publicProfile.activityPosted') },
31
- 'commented': { icon: MessageSquare, color: 'bg-green-500/10 text-green-500', action: i18n.t('publicProfile.activityCommented') },
32
- 'joined': { icon: UserPlus, color: 'bg-purple-500/10 text-purple-500', action: i18n.t('publicProfile.activityJoined') },
33
- 'created': { icon: Plus, color: 'bg-amber-500/10 text-amber-500', action: i18n.t('publicProfile.activityCreated') },
34
- 'starred': { icon: Star, color: 'bg-yellow-500/10 text-yellow-500', action: i18n.t('publicProfile.activityCreated') },
14
+ const i18n = useTranslation();
15
+ const config: Record<string, { icon: Component; tone: string; action: string }> = {
16
+ posted: { icon: MessageSquare, tone: 'bg-primary/10 text-primary', action: i18n.t('publicProfile.activityPosted') },
17
+ commented: { icon: MessageSquare, tone: 'bg-success/10 text-success', action: i18n.t('publicProfile.activityCommented') },
18
+ joined: { icon: UserPlus, tone: 'bg-muted text-muted-foreground', action: i18n.t('publicProfile.activityJoined') },
19
+ created: { icon: Plus, tone: 'bg-warning/10 text-warning-foreground', action: i18n.t('publicProfile.activityCreated') },
20
+ starred: { icon: Star, tone: 'bg-primary/10 text-primary', action: i18n.t('publicProfile.activityCreated') },
35
21
  };
36
22
  </script>
37
23
 
38
- <div class="space-y-4">
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>
59
-
60
- {#if activities.length === 0}
61
- <div class="rounded-xl border border-dashed border-border/60 p-8 text-center">
62
- <p class="text-sm text-muted-foreground">{i18n.t('publicProfile.noActivity')}</p>
63
- </div>
64
- {:else}
65
- <Card.Card class="border-border/60">
66
- <Card.CardContent class="p-0">
67
- <div class="divide-y">
68
- {#each activities as activity (activity.id)}
69
- {@const config = typeConfig[activity.type] ?? typeConfig['posted']}
70
- <div class="flex items-start gap-3 p-4">
71
- <div class="shrink-0 mt-0.5">
72
- {#if activity.avatar}
73
- <Avatar class="h-8 w-8">
74
- <img src={activity.avatar} alt={activity.user} />
75
- </Avatar>
76
- {:else}
77
- <div class="flex h-8 w-8 items-center justify-center rounded-full {config.color}">
78
- <config.icon class="h-4 w-4" />
79
- </div>
80
- {/if}
81
- </div>
82
- <div class="flex-1 min-w-0">
83
- <p class="text-sm">
84
- <strong class="text-foreground">{activity.user}</strong>
85
- <span class="text-muted-foreground"> {config.action} </span>
86
- {#if activity.target}
87
- <strong class="text-foreground">{activity.target}</strong>
88
- {/if}
89
- </p>
90
- {#if activity.content}
91
- <p class="mt-1 text-sm text-muted-foreground line-clamp-2">{activity.content}</p>
92
- {/if}
93
- <p class="mt-1 text-xs text-muted-foreground/60">{activity.timestamp}</p>
94
- </div>
95
- </div>
96
- {/each}
97
- </div>
98
- </Card.CardContent>
99
- </Card.Card>
100
- {/if}
101
- </div>
24
+ <section class="space-y-4">
25
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><SectionHeader title={i18n.t("publicProfile.activity")} />{#if showAutoRefresh}<div class="flex items-center gap-1 rounded-md border border-border p-1"><Button size="sm" variant={autoRefresh ? "default" : "ghost"} onclick={() => autoRefresh = true}>{i18n.t("common.on")}</Button><Button size="sm" variant={!autoRefresh ? "default" : "ghost"} onclick={() => autoRefresh = false}>{i18n.t("common.off")}</Button></div>{/if}</div>
26
+ {#if activities.length === 0}<DataState state="empty" title={i18n.t('publicProfile.noActivity')} />{:else}<Card.Card><Card.CardContent class="p-0"><div class="divide-y divide-border">{#each activities as activity (activity.id)}{@const item = config[activity.type] ?? config.posted}<div class="flex items-start gap-3 p-4">{#if activity.avatar}<Avatar src={activity.avatar} alt={activity.user} size="sm" />{:else}<span class={'flex size-8 shrink-0 items-center justify-center rounded-full ' + item.tone}><item.icon class="size-4" /></span>{/if}<div class="min-w-0 flex-1"><p class="text-sm"><strong class="text-foreground">{activity.user}</strong><span class="text-muted-foreground"> {item.action} </span>{#if activity.target}<strong class="text-foreground">{activity.target}</strong>{/if}</p>{#if activity.content}<p class="mt-1 text-sm text-muted-foreground">{activity.content}</p>{/if}<p class="mt-1 text-xs text-muted-foreground">{activity.timestamp}</p></div></div>{/each}</div></Card.CardContent></Card.Card>{/if}
27
+ </section>
@@ -69,24 +69,24 @@
69
69
 
70
70
  <Card.Card class="overflow-hidden border-border/60">
71
71
  {#if coverImage}
72
- <div class="h-32 bg-gradient-to-r from-primary/20 via-primary/10 to-accent/20 relative">
72
+ <div class="h-32 bg-muted relative">
73
73
  <img src={coverImage} alt="" class="h-full w-full object-cover" />
74
74
  </div>
75
75
  {:else}
76
- <div class="h-32 bg-gradient-to-r from-primary/20 via-primary/10 to-accent/20 relative">
76
+ <div class="h-32 bg-muted relative">
77
77
  <div class="absolute inset-0 opacity-[0.06]" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 20px 20px;"></div>
78
78
  </div>
79
79
  {/if}
80
80
 
81
81
  <Card.CardContent class="relative px-5 pb-5">
82
82
  <div class="-mt-10 mb-3 flex items-end gap-4">
83
- <div class="ring-4 ring-card rounded-xl overflow-hidden shrink-0">
83
+ <div class="ring-4 ring-card rounded-lg overflow-hidden shrink-0">
84
84
  {#if avatar}
85
85
  <Avatar class="h-20 w-20">
86
86
  <img src={avatar} alt={name} />
87
87
  </Avatar>
88
88
  {:else}
89
- <div class="flex h-20 w-20 items-center justify-center rounded-xl bg-primary/10 text-primary text-2xl font-bold">
89
+ <div class="flex h-20 w-20 items-center justify-center rounded-lg bg-primary/10 text-primary text-2xl font-bold">
90
90
  {initials}
91
91
  </div>
92
92
  {/if}