@svadmin/ui 0.38.5 → 0.39.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/components/AdminApp.svelte +3 -2
- package/dist/components/ApiSettings.svelte +86 -1
- package/dist/components/AutoTable.svelte +108 -96
- package/dist/components/DevTools.svelte +0 -5
- package/dist/components/ForgotPasswordPage.svelte +5 -8
- package/dist/components/LoginPage.svelte +5 -8
- package/dist/components/RegisterPage.svelte +5 -8
- package/dist/components/Sidebar.svelte +10 -4
- package/dist/components/SvadminLogo.svelte +16 -0
- package/dist/components/SvadminLogo.svelte.d.ts +7 -0
- package/dist/components/UpdatePasswordPage.svelte +5 -8
- package/dist/components/account/CompanyProfilePage.svelte +56 -1
- package/dist/components/account/ImportMembersPage.svelte +31 -0
- package/dist/components/account/SettingsEnterprisePage.svelte +172 -19
- package/dist/components/account/SettingsPlainPage.svelte +121 -17
- package/dist/components/account/SettingsSidebarPage.svelte +282 -70
- package/dist/components/account/TeamMembersPage.svelte +68 -2
- package/dist/components/account/UserProfilePage.svelte +206 -0
- package/dist/components/account/UserProfilePage.svelte.d.ts +4 -0
- package/dist/components/network/TeamCrewTablePage.svelte +17 -1
- package/dist/components/network/UserCardsNFTPage.svelte +5 -0
- package/dist/components/profile/ActivityTimeline.svelte +24 -2
- package/dist/components/profile/ActivityTimeline.svelte.d.ts +1 -0
- package/dist/components/profile/ProfileVariantSections.svelte +452 -0
- package/dist/components/profile/ProfileVariantSections.svelte.d.ts +8 -0
- package/dist/components/profile/ProjectsGrid.svelte +20 -6
- package/dist/components/profile/PublicProfilePage.svelte +42 -21
- package/dist/components/profile/PublicProfilePage.svelte.d.ts +1 -0
- package/dist/components/profile/TeamsShowcase.svelte +31 -12
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +1 -0
- package/dist/components/ui/alert/alert.svelte.d.ts +14 -14
- package/dist/components/ui/avatar/avatar.svelte.d.ts +14 -14
- package/dist/components/ui/badge/badge.svelte.d.ts +20 -20
- package/dist/components/ui/button/button.svelte.d.ts +44 -44
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/package.json +14 -13
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
+
import * as Card from '../ui/card/index.js';
|
|
4
|
+
import * as Table from '../ui/table/index.js';
|
|
5
|
+
import { Badge } from '../ui/badge/index.js';
|
|
6
|
+
import { Button } from '../ui/button/index.js';
|
|
7
|
+
import { Progress } from '../ui/progress/index.js';
|
|
8
|
+
import ActivityTimeline from './ActivityTimeline.svelte';
|
|
9
|
+
import {
|
|
10
|
+
Briefcase, MapPin, Users, TrendingUp, Gamepad2, Trophy, Play,
|
|
11
|
+
FileText, Image, FileArchive, Star, Award, Medal, Crown, Building2, Globe,
|
|
12
|
+
} from '@lucide/svelte';
|
|
13
|
+
import type { Component } from 'svelte';
|
|
14
|
+
|
|
15
|
+
const i18n = useTranslation();
|
|
16
|
+
|
|
17
|
+
type ProfileVariant = 'default' | 'company' | 'gamer';
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
variant?: ProfileVariant;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let { variant = 'default' }: Props = $props();
|
|
24
|
+
|
|
25
|
+
const initials = (name: string) => name.split(' ').map((n) => n[0]).join('').toUpperCase().slice(0, 2);
|
|
26
|
+
|
|
27
|
+
// Default variant demo data
|
|
28
|
+
const workExperience = [
|
|
29
|
+
{ role: 'Senior Frontend Engineer', company: 'Nebula Labs', period: '2023 - ', current: true },
|
|
30
|
+
{ role: 'Frontend Engineer', company: 'Craftworks Studio', period: '2020 - 2023', current: false },
|
|
31
|
+
{ role: 'UI Developer', company: 'PixelForge', period: '2018 - 2020', current: false },
|
|
32
|
+
];
|
|
33
|
+
const skills = ['TypeScript', 'Svelte', 'Node.js', 'GraphQL', 'Design Systems', 'Accessibility'];
|
|
34
|
+
const badges: { name: string; Icon: Component; tone: string }[] = [
|
|
35
|
+
{ name: 'Top Contributor', Icon: Medal, tone: 'bg-amber-500/10 text-amber-600' },
|
|
36
|
+
{ name: 'Early Adopter', Icon: Star, tone: 'bg-blue-500/10 text-blue-600' },
|
|
37
|
+
{ name: 'Community Mentor', Icon: Award, tone: 'bg-violet-500/10 text-violet-600' },
|
|
38
|
+
{ name: 'Launch Partner', Icon: Crown, tone: 'bg-emerald-500/10 text-emerald-600' },
|
|
39
|
+
];
|
|
40
|
+
const recentUploads: { name: string; size: string; Icon: Component; tone: string }[] = [
|
|
41
|
+
{ name: 'dashboard-spec.fig', size: '4.2 MB', Icon: Image, tone: 'bg-pink-500/10 text-pink-600' },
|
|
42
|
+
{ name: 'release-notes.md', size: '18 KB', Icon: FileText, tone: 'bg-blue-500/10 text-blue-600' },
|
|
43
|
+
{ name: 'assets-bundle.zip', size: '24 MB', Icon: FileArchive, tone: 'bg-amber-500/10 text-amber-600' },
|
|
44
|
+
];
|
|
45
|
+
const contributors = ['Mia Torres', 'Chen Wei', 'Ava Novak', 'Liam Ortiz', 'Sofia Petrova', '+8'];
|
|
46
|
+
|
|
47
|
+
// Company variant demo data
|
|
48
|
+
const highlights = [
|
|
49
|
+
{ label: 'Total revenue', value: '$4.8M', delta: '+18.2%' },
|
|
50
|
+
{ label: 'Active customers', value: '2,340', delta: '+9.4%' },
|
|
51
|
+
{ label: 'Open positions', value: '17', delta: '+3' },
|
|
52
|
+
];
|
|
53
|
+
const openJobs = [
|
|
54
|
+
{ title: 'Senior Product Designer', location: 'Remote', type: 'Full-time' },
|
|
55
|
+
{ title: 'Staff Engineer, Platform', location: 'San Francisco', type: 'Full-time' },
|
|
56
|
+
{ title: 'Developer Advocate', location: 'Berlin', type: 'Contract' },
|
|
57
|
+
];
|
|
58
|
+
const locations = ['San Francisco, US', 'Berlin, DE', 'Singapore, SG'];
|
|
59
|
+
const companyMembers = ['Elena Marsh', 'David Kim', 'Priya Nair', 'Oscar Lund', 'Hana Sato', '+27'];
|
|
60
|
+
const investments = [
|
|
61
|
+
{ round: 'Series B', amount: '$24M', date: '2025-11' },
|
|
62
|
+
{ round: 'Series A', amount: '$9M', date: '2023-06' },
|
|
63
|
+
{ round: 'Seed', amount: '$1.8M', date: '2021-03' },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Gamer variant demo data
|
|
67
|
+
const favoriteGames = [
|
|
68
|
+
{ name: 'Starfall Odyssey', hours: '412h', rank: 'Diamond II' },
|
|
69
|
+
{ name: 'Neon Drift', hours: '268h', rank: 'Master' },
|
|
70
|
+
{ name: 'Iron Bastion', hours: '190h', rank: 'Platinum I' },
|
|
71
|
+
];
|
|
72
|
+
const tournaments = [
|
|
73
|
+
{ name: 'Winter Clash 2026', result: 'Top 8', prize: '$1,200' },
|
|
74
|
+
{ name: 'City Invitational', result: '1st', prize: '$3,500' },
|
|
75
|
+
{ name: 'Open Qualifier #14', result: '2nd', prize: '$800' },
|
|
76
|
+
];
|
|
77
|
+
const gamerNetwork = ['VortexQueen', 'NoScopeNina', 'PixelPunk', 'GrimReaperX', '+42'];
|
|
78
|
+
const gamerActivities = [
|
|
79
|
+
{ id: 'g1', type: 'posted' as const, user: 'ShadowFox', content: 'Just hit Diamond II in Starfall Odyssey ranked. Clutch final round!', timestamp: '1 hour ago' },
|
|
80
|
+
{ id: 'g2', type: 'created' as const, user: 'ShadowFox', target: 'Winter Clash 2026 highlights', timestamp: '6 hours ago' },
|
|
81
|
+
{ id: 'g3', type: 'joined' as const, user: 'ShadowFox', target: 'City Invitational', timestamp: '2 days ago' },
|
|
82
|
+
{ id: 'g4', type: 'commented' as const, user: 'ShadowFox', target: "VortexQueen's stream", content: 'That flank was insane, gg!', timestamp: '3 days ago' },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Shared projects table demo data
|
|
86
|
+
const projectRows = [
|
|
87
|
+
{ name: 'Dashboard Redesign', progress: 72, people: ['AC', 'SK', 'MJ'], due: '2026-09-15' },
|
|
88
|
+
{ name: 'API Gateway v2', progress: 45, people: ['LW', 'TB'], due: '2026-10-02' },
|
|
89
|
+
{ name: 'Mobile App', progress: 100, people: ['ED', 'AC', 'MK', 'RS'], due: '2026-06-30' },
|
|
90
|
+
{ name: 'Data Pipeline', progress: 20, people: ['NK'], due: '2026-12-01' },
|
|
91
|
+
];
|
|
92
|
+
</script>
|
|
93
|
+
|
|
94
|
+
{#snippet projectsTable()}
|
|
95
|
+
<Card.Card class="border-border/60">
|
|
96
|
+
<Card.CardHeader class="pb-3">
|
|
97
|
+
<div class="flex items-center justify-between">
|
|
98
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.projects')}</Card.CardTitle>
|
|
99
|
+
<Button variant="ghost" size="sm">{i18n.t('profileSections.viewAll')}</Button>
|
|
100
|
+
</div>
|
|
101
|
+
</Card.CardHeader>
|
|
102
|
+
<Card.CardContent class="p-0">
|
|
103
|
+
<Table.Root>
|
|
104
|
+
<Table.Header>
|
|
105
|
+
<Table.Row>
|
|
106
|
+
<Table.Head>{i18n.t('profileSections.projectName')}</Table.Head>
|
|
107
|
+
<Table.Head class="w-40">{i18n.t('profileSections.progress')}</Table.Head>
|
|
108
|
+
<Table.Head>{i18n.t('profileSections.people')}</Table.Head>
|
|
109
|
+
<Table.Head class="text-right">{i18n.t('profileSections.dueDate')}</Table.Head>
|
|
110
|
+
</Table.Row>
|
|
111
|
+
</Table.Header>
|
|
112
|
+
<Table.Body>
|
|
113
|
+
{#each projectRows as row (row.name)}
|
|
114
|
+
<Table.Row>
|
|
115
|
+
<Table.Cell class="font-medium">{row.name}</Table.Cell>
|
|
116
|
+
<Table.Cell>
|
|
117
|
+
<div class="flex items-center gap-2">
|
|
118
|
+
<Progress value={row.progress} class="h-1.5 flex-1" />
|
|
119
|
+
<span class="w-9 text-right text-xs text-muted-foreground">{row.progress}%</span>
|
|
120
|
+
</div>
|
|
121
|
+
</Table.Cell>
|
|
122
|
+
<Table.Cell>
|
|
123
|
+
<div class="flex -space-x-1.5">
|
|
124
|
+
{#each row.people as person (person)}
|
|
125
|
+
<div class="flex h-6 w-6 items-center justify-center rounded-full bg-muted text-[9px] font-semibold text-muted-foreground ring-2 ring-card">{person}</div>
|
|
126
|
+
{/each}
|
|
127
|
+
</div>
|
|
128
|
+
</Table.Cell>
|
|
129
|
+
<Table.Cell class="text-right text-muted-foreground">{row.due}</Table.Cell>
|
|
130
|
+
</Table.Row>
|
|
131
|
+
{/each}
|
|
132
|
+
</Table.Body>
|
|
133
|
+
</Table.Root>
|
|
134
|
+
</Card.CardContent>
|
|
135
|
+
</Card.Card>
|
|
136
|
+
{/snippet}
|
|
137
|
+
|
|
138
|
+
{#snippet badgesCard()}
|
|
139
|
+
<Card.Card class="border-border/60">
|
|
140
|
+
<Card.CardHeader class="pb-3">
|
|
141
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.communityBadges')}</Card.CardTitle>
|
|
142
|
+
</Card.CardHeader>
|
|
143
|
+
<Card.CardContent>
|
|
144
|
+
<div class="grid grid-cols-2 gap-2">
|
|
145
|
+
{#each badges as badge (badge.name)}
|
|
146
|
+
<div class="flex items-center gap-2 rounded-lg border border-border/60 p-2.5">
|
|
147
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg {badge.tone}">
|
|
148
|
+
<badge.Icon class="h-4 w-4" />
|
|
149
|
+
</div>
|
|
150
|
+
<span class="text-xs font-medium text-foreground">{badge.name}</span>
|
|
151
|
+
</div>
|
|
152
|
+
{/each}
|
|
153
|
+
</div>
|
|
154
|
+
</Card.CardContent>
|
|
155
|
+
</Card.Card>
|
|
156
|
+
{/snippet}
|
|
157
|
+
|
|
158
|
+
{#if variant === 'default'}
|
|
159
|
+
<div class="grid gap-4 lg:grid-cols-3">
|
|
160
|
+
<div class="space-y-4">
|
|
161
|
+
<Card.Card class="border-border/60">
|
|
162
|
+
<Card.CardHeader class="pb-3">
|
|
163
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.about')}</Card.CardTitle>
|
|
164
|
+
</Card.CardHeader>
|
|
165
|
+
<Card.CardContent class="space-y-2.5 text-sm">
|
|
166
|
+
<p class="text-muted-foreground">Full-stack developer focused on design systems and developer tooling. Building in the open.</p>
|
|
167
|
+
<div class="flex items-center gap-2 text-muted-foreground"><Briefcase class="h-3.5 w-3.5" />Nebula Labs</div>
|
|
168
|
+
<div class="flex items-center gap-2 text-muted-foreground"><MapPin class="h-3.5 w-3.5" />Shanghai, CN</div>
|
|
169
|
+
<div class="flex items-center gap-2 text-muted-foreground"><Globe class="h-3.5 w-3.5" />alexchen.dev</div>
|
|
170
|
+
</Card.CardContent>
|
|
171
|
+
</Card.Card>
|
|
172
|
+
|
|
173
|
+
<Card.Card class="border-border/60">
|
|
174
|
+
<Card.CardHeader class="pb-3">
|
|
175
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.workExperience')}</Card.CardTitle>
|
|
176
|
+
</Card.CardHeader>
|
|
177
|
+
<Card.CardContent class="space-y-3">
|
|
178
|
+
{#each workExperience as job (job.company)}
|
|
179
|
+
<div class="flex items-start justify-between gap-2">
|
|
180
|
+
<div>
|
|
181
|
+
<p class="text-sm font-medium text-foreground">{job.role}</p>
|
|
182
|
+
<p class="text-xs text-muted-foreground">{job.company}</p>
|
|
183
|
+
</div>
|
|
184
|
+
<span class="shrink-0 text-xs text-muted-foreground">{job.period}{#if job.current}{i18n.t('profileSections.present')}{/if}</span>
|
|
185
|
+
</div>
|
|
186
|
+
{/each}
|
|
187
|
+
</Card.CardContent>
|
|
188
|
+
</Card.Card>
|
|
189
|
+
|
|
190
|
+
<Card.Card class="border-border/60">
|
|
191
|
+
<Card.CardHeader class="pb-3">
|
|
192
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.skills')}</Card.CardTitle>
|
|
193
|
+
</Card.CardHeader>
|
|
194
|
+
<Card.CardContent>
|
|
195
|
+
<div class="flex flex-wrap gap-1.5">
|
|
196
|
+
{#each skills as skill (skill)}
|
|
197
|
+
<Badge variant="secondary" class="text-xs">{skill}</Badge>
|
|
198
|
+
{/each}
|
|
199
|
+
</div>
|
|
200
|
+
</Card.CardContent>
|
|
201
|
+
</Card.Card>
|
|
202
|
+
|
|
203
|
+
{@render badgesCard()}
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div class="space-y-4 lg:col-span-2">
|
|
207
|
+
{@render projectsTable()}
|
|
208
|
+
|
|
209
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
210
|
+
<Card.Card class="border-border/60">
|
|
211
|
+
<Card.CardHeader class="pb-3">
|
|
212
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.recentUploads')}</Card.CardTitle>
|
|
213
|
+
</Card.CardHeader>
|
|
214
|
+
<Card.CardContent class="space-y-2.5">
|
|
215
|
+
{#each recentUploads as file (file.name)}
|
|
216
|
+
<div class="flex items-center gap-3">
|
|
217
|
+
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg {file.tone}">
|
|
218
|
+
<file.Icon class="h-4 w-4" />
|
|
219
|
+
</div>
|
|
220
|
+
<div class="min-w-0 flex-1">
|
|
221
|
+
<p class="truncate text-sm font-medium text-foreground">{file.name}</p>
|
|
222
|
+
<p class="text-xs text-muted-foreground">{file.size}</p>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
{/each}
|
|
226
|
+
</Card.CardContent>
|
|
227
|
+
</Card.Card>
|
|
228
|
+
|
|
229
|
+
<Card.Card class="border-border/60">
|
|
230
|
+
<Card.CardHeader class="pb-3">
|
|
231
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.contributors')}</Card.CardTitle>
|
|
232
|
+
</Card.CardHeader>
|
|
233
|
+
<Card.CardContent>
|
|
234
|
+
<div class="flex flex-wrap gap-1.5">
|
|
235
|
+
{#each contributors as person (person)}
|
|
236
|
+
<div class="flex h-9 w-9 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-muted-foreground ring-2 ring-card" title={person}>
|
|
237
|
+
{person.startsWith('+') ? person : initials(person)}
|
|
238
|
+
</div>
|
|
239
|
+
{/each}
|
|
240
|
+
</div>
|
|
241
|
+
</Card.CardContent>
|
|
242
|
+
</Card.Card>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
{:else if variant === 'company'}
|
|
247
|
+
<div class="space-y-4">
|
|
248
|
+
<div class="grid gap-4 sm:grid-cols-3">
|
|
249
|
+
{#each highlights as item (item.label)}
|
|
250
|
+
<Card.Card class="border-border/60">
|
|
251
|
+
<Card.CardContent class="p-4">
|
|
252
|
+
<p class="text-xs text-muted-foreground">{item.label}</p>
|
|
253
|
+
<div class="mt-1 flex items-baseline gap-2">
|
|
254
|
+
<span class="text-xl font-bold text-foreground">{item.value}</span>
|
|
255
|
+
<span class="flex items-center gap-0.5 text-xs font-medium text-green-600"><TrendingUp class="h-3 w-3" />{item.delta}</span>
|
|
256
|
+
</div>
|
|
257
|
+
</Card.CardContent>
|
|
258
|
+
</Card.Card>
|
|
259
|
+
{/each}
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
<div class="grid gap-4 lg:grid-cols-3">
|
|
263
|
+
<div class="lg:col-span-2">
|
|
264
|
+
{@render projectsTable()}
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<div class="space-y-4">
|
|
268
|
+
<Card.Card class="border-border/60">
|
|
269
|
+
<Card.CardHeader class="pb-3">
|
|
270
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.openJobs')}</Card.CardTitle>
|
|
271
|
+
</Card.CardHeader>
|
|
272
|
+
<Card.CardContent class="space-y-3">
|
|
273
|
+
{#each openJobs as job (job.title)}
|
|
274
|
+
<div>
|
|
275
|
+
<p class="text-sm font-medium text-foreground">{job.title}</p>
|
|
276
|
+
<p class="text-xs text-muted-foreground">{job.location} · {job.type}</p>
|
|
277
|
+
</div>
|
|
278
|
+
{/each}
|
|
279
|
+
</Card.CardContent>
|
|
280
|
+
</Card.Card>
|
|
281
|
+
|
|
282
|
+
<Card.Card class="border-border/60">
|
|
283
|
+
<Card.CardHeader class="pb-3">
|
|
284
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.locations')}</Card.CardTitle>
|
|
285
|
+
</Card.CardHeader>
|
|
286
|
+
<Card.CardContent class="space-y-2">
|
|
287
|
+
{#each locations as loc (loc)}
|
|
288
|
+
<div class="flex items-center gap-2 text-sm text-muted-foreground">
|
|
289
|
+
<Building2 class="h-3.5 w-3.5" />{loc}
|
|
290
|
+
</div>
|
|
291
|
+
{/each}
|
|
292
|
+
</Card.CardContent>
|
|
293
|
+
</Card.Card>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
298
|
+
<Card.Card class="border-border/60">
|
|
299
|
+
<Card.CardHeader class="pb-3">
|
|
300
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.members')}</Card.CardTitle>
|
|
301
|
+
</Card.CardHeader>
|
|
302
|
+
<Card.CardContent>
|
|
303
|
+
<div class="flex flex-wrap gap-1.5">
|
|
304
|
+
{#each companyMembers as person (person)}
|
|
305
|
+
<div class="flex h-9 w-9 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-muted-foreground ring-2 ring-card" title={person}>
|
|
306
|
+
{person.startsWith('+') ? person : initials(person)}
|
|
307
|
+
</div>
|
|
308
|
+
{/each}
|
|
309
|
+
</div>
|
|
310
|
+
</Card.CardContent>
|
|
311
|
+
</Card.Card>
|
|
312
|
+
|
|
313
|
+
<Card.Card class="border-border/60">
|
|
314
|
+
<Card.CardHeader class="pb-3">
|
|
315
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.investments')}</Card.CardTitle>
|
|
316
|
+
</Card.CardHeader>
|
|
317
|
+
<Card.CardContent class="space-y-2.5">
|
|
318
|
+
{#each investments as inv (inv.round)}
|
|
319
|
+
<div class="flex items-center justify-between text-sm">
|
|
320
|
+
<span class="font-medium text-foreground">{inv.round}</span>
|
|
321
|
+
<span class="text-muted-foreground">{inv.amount} · {inv.date}</span>
|
|
322
|
+
</div>
|
|
323
|
+
{/each}
|
|
324
|
+
</Card.CardContent>
|
|
325
|
+
</Card.Card>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
{:else}
|
|
329
|
+
<div class="grid gap-4 lg:grid-cols-3">
|
|
330
|
+
<div class="space-y-4">
|
|
331
|
+
<Card.Card class="border-border/60">
|
|
332
|
+
<Card.CardHeader class="pb-3">
|
|
333
|
+
<Card.CardTitle class="text-base">{i18n.t('profileSections.about')}</Card.CardTitle>
|
|
334
|
+
</Card.CardHeader>
|
|
335
|
+
<Card.CardContent class="space-y-2.5 text-sm">
|
|
336
|
+
<p class="text-muted-foreground">Pro gamer & content creator. Competing in FPS tournaments and streaming daily.</p>
|
|
337
|
+
<div class="flex items-center gap-2 text-muted-foreground"><Gamepad2 class="h-3.5 w-3.5" />ShadowFox#7742</div>
|
|
338
|
+
<div class="flex items-center gap-2 text-muted-foreground"><MapPin class="h-3.5 w-3.5" />Seoul, KR</div>
|
|
339
|
+
</Card.CardContent>
|
|
340
|
+
</Card.Card>
|
|
341
|
+
|
|
342
|
+
<Card.Card class="border-border/60">
|
|
343
|
+
<Card.CardHeader class="pb-3">
|
|
344
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
345
|
+
<Gamepad2 class="h-4 w-4 text-muted-foreground" />{i18n.t('profileSections.favoriteGames')}
|
|
346
|
+
</Card.CardTitle>
|
|
347
|
+
</Card.CardHeader>
|
|
348
|
+
<Card.CardContent class="space-y-3">
|
|
349
|
+
{#each favoriteGames as game (game.name)}
|
|
350
|
+
<div class="flex items-center justify-between gap-2">
|
|
351
|
+
<div>
|
|
352
|
+
<p class="text-sm font-medium text-foreground">{game.name}</p>
|
|
353
|
+
<p class="text-xs text-muted-foreground">{game.hours}</p>
|
|
354
|
+
</div>
|
|
355
|
+
<Badge variant="secondary" class="text-[10px]">{game.rank}</Badge>
|
|
356
|
+
</div>
|
|
357
|
+
{/each}
|
|
358
|
+
</Card.CardContent>
|
|
359
|
+
</Card.Card>
|
|
360
|
+
|
|
361
|
+
<Card.Card class="border-primary/30 bg-primary/5">
|
|
362
|
+
<Card.CardHeader class="pb-3">
|
|
363
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
364
|
+
<Play class="h-4 w-4 text-primary" />{i18n.t('profileSections.nowPlaying')}
|
|
365
|
+
</Card.CardTitle>
|
|
366
|
+
</Card.CardHeader>
|
|
367
|
+
<Card.CardContent>
|
|
368
|
+
<p class="text-sm font-medium text-foreground">Starfall Odyssey — Ranked</p>
|
|
369
|
+
<p class="mt-1 text-xs text-muted-foreground">Streaming live for 2.4K viewers</p>
|
|
370
|
+
<div class="mt-3 h-1.5 overflow-hidden rounded-full bg-primary/15">
|
|
371
|
+
<div class="h-full w-2/3 rounded-full bg-primary"></div>
|
|
372
|
+
</div>
|
|
373
|
+
</Card.CardContent>
|
|
374
|
+
</Card.Card>
|
|
375
|
+
|
|
376
|
+
{@render badgesCard()}
|
|
377
|
+
|
|
378
|
+
<Card.Card class="border-border/60">
|
|
379
|
+
<Card.CardHeader class="pb-3">
|
|
380
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
381
|
+
<Users class="h-4 w-4 text-muted-foreground" />{i18n.t('profileSections.team')}
|
|
382
|
+
</Card.CardTitle>
|
|
383
|
+
</Card.CardHeader>
|
|
384
|
+
<Card.CardContent class="space-y-2.5">
|
|
385
|
+
{#each [{ name: 'VortexQueen', role: 'IGL' }, { name: 'NoScopeNina', role: 'AWPer' }, { name: 'PixelPunk', role: 'Support' }] as mate (mate.name)}
|
|
386
|
+
<div class="flex items-center gap-2.5">
|
|
387
|
+
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-muted-foreground">
|
|
388
|
+
{initials(mate.name)}
|
|
389
|
+
</div>
|
|
390
|
+
<div class="min-w-0">
|
|
391
|
+
<p class="truncate text-sm font-medium text-foreground">{mate.name}</p>
|
|
392
|
+
<p class="text-xs text-muted-foreground">{mate.role}</p>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
{/each}
|
|
396
|
+
</Card.CardContent>
|
|
397
|
+
</Card.Card>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div class="space-y-4 lg:col-span-2">
|
|
401
|
+
<Card.Card class="border-border/60">
|
|
402
|
+
<Card.CardHeader class="pb-3">
|
|
403
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
404
|
+
<Trophy class="h-4 w-4 text-muted-foreground" />{i18n.t('profileSections.tournaments')}
|
|
405
|
+
</Card.CardTitle>
|
|
406
|
+
</Card.CardHeader>
|
|
407
|
+
<Card.CardContent class="p-0">
|
|
408
|
+
<Table.Root>
|
|
409
|
+
<Table.Header>
|
|
410
|
+
<Table.Row>
|
|
411
|
+
<Table.Head>{i18n.t('profileSections.tournaments')}</Table.Head>
|
|
412
|
+
<Table.Head>{i18n.t('profileSections.progress')}</Table.Head>
|
|
413
|
+
<Table.Head class="text-right">{i18n.t('account.amount')}</Table.Head>
|
|
414
|
+
</Table.Row>
|
|
415
|
+
</Table.Header>
|
|
416
|
+
<Table.Body>
|
|
417
|
+
{#each tournaments as t (t.name)}
|
|
418
|
+
<Table.Row>
|
|
419
|
+
<Table.Cell class="font-medium">{t.name}</Table.Cell>
|
|
420
|
+
<Table.Cell><Badge variant="secondary" class="text-[10px]">{t.result}</Badge></Table.Cell>
|
|
421
|
+
<Table.Cell class="text-right text-muted-foreground">{t.prize}</Table.Cell>
|
|
422
|
+
</Table.Row>
|
|
423
|
+
{/each}
|
|
424
|
+
</Table.Body>
|
|
425
|
+
</Table.Root>
|
|
426
|
+
</Card.CardContent>
|
|
427
|
+
</Card.Card>
|
|
428
|
+
|
|
429
|
+
<Card.Card class="border-border/60">
|
|
430
|
+
<Card.CardHeader class="pb-3">
|
|
431
|
+
<Card.CardTitle class="flex items-center gap-2 text-base">
|
|
432
|
+
<Users class="h-4 w-4 text-muted-foreground" />{i18n.t('profileSections.network')}
|
|
433
|
+
</Card.CardTitle>
|
|
434
|
+
</Card.CardHeader>
|
|
435
|
+
<Card.CardContent>
|
|
436
|
+
<div class="flex flex-wrap gap-1.5">
|
|
437
|
+
{#each gamerNetwork as person (person)}
|
|
438
|
+
<div class="flex h-9 w-9 items-center justify-center rounded-full bg-muted text-[10px] font-semibold text-muted-foreground ring-2 ring-card" title={person}>
|
|
439
|
+
{person.startsWith('+') ? person : initials(person)}
|
|
440
|
+
</div>
|
|
441
|
+
{/each}
|
|
442
|
+
</div>
|
|
443
|
+
</Card.CardContent>
|
|
444
|
+
</Card.Card>
|
|
445
|
+
|
|
446
|
+
<div>
|
|
447
|
+
<h4 class="mb-3 text-base font-semibold text-foreground">{i18n.t('profileSections.recentActivity')}</h4>
|
|
448
|
+
<ActivityTimeline activities={gamerActivities} showAutoRefresh={false} />
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
{/if}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Component } from 'svelte';
|
|
2
|
+
type ProfileVariant = 'default' | 'company' | 'gamer';
|
|
3
|
+
interface Props {
|
|
4
|
+
variant?: ProfileVariant;
|
|
5
|
+
}
|
|
6
|
+
declare const ProfileVariantSections: Component<Props, {}, "">;
|
|
7
|
+
type ProfileVariantSections = ReturnType<typeof ProfileVariantSections>;
|
|
8
|
+
export default ProfileVariantSections;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
import * as Card from '../ui/card/index.js';
|
|
4
4
|
import { Badge } from '../ui/badge/index.js';
|
|
5
5
|
import { Button } from '../ui/button/index.js';
|
|
6
|
-
import {
|
|
6
|
+
import { Input } from '../ui/input/index.js';
|
|
7
|
+
import { Users, CheckCircle2, ArrowRight, Search, Plus } from '@lucide/svelte';
|
|
7
8
|
|
|
8
9
|
const i18n = useTranslation();
|
|
9
10
|
|
|
@@ -28,6 +29,13 @@
|
|
|
28
29
|
projects = [],
|
|
29
30
|
}: Props = $props();
|
|
30
31
|
|
|
32
|
+
let searchQuery = $state('');
|
|
33
|
+
const filtered = $derived(
|
|
34
|
+
searchQuery
|
|
35
|
+
? projects.filter(p => p.name.toLowerCase().includes(searchQuery.toLowerCase()) || p.description.toLowerCase().includes(searchQuery.toLowerCase()) || p.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())))
|
|
36
|
+
: projects
|
|
37
|
+
);
|
|
38
|
+
|
|
31
39
|
const statusConfig = {
|
|
32
40
|
'active': { color: 'bg-green-500/10 text-green-600', label: 'Active' },
|
|
33
41
|
'completed': { color: 'bg-blue-500/10 text-blue-600', label: 'Completed' },
|
|
@@ -38,20 +46,26 @@
|
|
|
38
46
|
</script>
|
|
39
47
|
|
|
40
48
|
<div class="space-y-4">
|
|
41
|
-
<div class="flex items-center justify-between">
|
|
49
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
42
50
|
<h3 class="text-lg font-semibold text-foreground">
|
|
43
|
-
{
|
|
51
|
+
{i18n.t('publicProfile.projectsCount', { count: filtered.length })}
|
|
44
52
|
</h3>
|
|
45
|
-
<
|
|
53
|
+
<div class="flex items-center gap-2">
|
|
54
|
+
<div class="relative">
|
|
55
|
+
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
56
|
+
<Input placeholder={i18n.t('publicProfile.searchProjects')} bind:value={searchQuery} class="w-48 pl-9" />
|
|
57
|
+
</div>
|
|
58
|
+
<Button size="sm"><Plus class="h-3.5 w-3.5 mr-1" />{i18n.t('common.create')}</Button>
|
|
59
|
+
</div>
|
|
46
60
|
</div>
|
|
47
61
|
|
|
48
|
-
{#if
|
|
62
|
+
{#if filtered.length === 0}
|
|
49
63
|
<div class="rounded-xl border border-dashed border-border/60 p-8 text-center">
|
|
50
64
|
<p class="text-sm text-muted-foreground">{i18n.t('publicProfile.noProjects')}</p>
|
|
51
65
|
</div>
|
|
52
66
|
{:else}
|
|
53
67
|
<div class="grid gap-4 sm:grid-cols-1 md:grid-cols-2 {gridCols}">
|
|
54
|
-
{#each
|
|
68
|
+
{#each filtered as project (project.id)}
|
|
55
69
|
<Card.Card class="group overflow-hidden border-border/60 hover:border-primary/30 transition-colors">
|
|
56
70
|
{#if project.image}
|
|
57
71
|
<div class="h-32 overflow-hidden">
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import ProjectsGrid from './ProjectsGrid.svelte';
|
|
8
8
|
import ActivityTimeline from './ActivityTimeline.svelte';
|
|
9
9
|
import TeamsShowcase from './TeamsShowcase.svelte';
|
|
10
|
+
import ProfileVariantSections from './ProfileVariantSections.svelte';
|
|
10
11
|
import { MapPin, Link, Calendar, MoreHorizontal } from '@lucide/svelte';
|
|
11
12
|
|
|
12
13
|
const i18n = useTranslation();
|
|
@@ -18,9 +19,10 @@
|
|
|
18
19
|
variant?: ProfileVariant;
|
|
19
20
|
initialTab?: ProfileTab;
|
|
20
21
|
columns?: 2 | 3;
|
|
22
|
+
showSections?: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
let { variant = 'default', initialTab = 'projects', columns = 2 }: Props = $props();
|
|
25
|
+
let { variant = 'default', initialTab = 'projects', columns = 2, showSections = false }: Props = $props();
|
|
24
26
|
let activeTab = $derived<ProfileTab>(initialTab);
|
|
25
27
|
|
|
26
28
|
// Demo profile data based on variant
|
|
@@ -84,6 +86,14 @@
|
|
|
84
86
|
{ id: '2', name: 'API Gateway v2', description: 'High-performance API gateway with rate limiting and circuit breaker patterns.', members: 5, tasks: 21, status: 'active' as const, tags: ['Backend', 'Infra'] },
|
|
85
87
|
{ id: '3', name: 'Mobile App', description: 'Cross-platform mobile application built with React Native.', members: 12, tasks: 56, status: 'completed' as const, tags: ['Mobile', 'React'] },
|
|
86
88
|
{ id: '4', name: 'Data Pipeline', description: 'Real-time data processing pipeline for event streaming.', members: 3, tasks: 18, status: 'on-hold' as const, tags: ['Data', 'Streaming'] },
|
|
89
|
+
{ id: '5', name: 'Design System 2.0', description: 'Unified component library with tokens, theming, and accessibility built in.', members: 6, tasks: 42, status: 'active' as const, tags: ['Design', 'UI'] },
|
|
90
|
+
{ id: '6', name: 'Billing Platform', description: 'Subscription billing with usage-based metering and invoicing.', members: 4, tasks: 27, status: 'active' as const, tags: ['Fintech', 'Backend'] },
|
|
91
|
+
{ id: '7', name: 'Search Relevance', description: 'Ranking improvements and vector search for the product catalog.', members: 5, tasks: 19, status: 'on-hold' as const, tags: ['Search', 'ML'] },
|
|
92
|
+
{ id: '8', name: 'Customer Portal', description: 'Self-service portal for account management and support tickets.', members: 9, tasks: 63, status: 'active' as const, tags: ['Web', 'Support'] },
|
|
93
|
+
{ id: '9', name: 'Notification Hub', description: 'Multi-channel notification delivery with user preference controls.', members: 3, tasks: 15, status: 'completed' as const, tags: ['Infra', 'Messaging'] },
|
|
94
|
+
{ id: '10', name: 'Analytics SDK', description: 'Lightweight event tracking SDK for web and mobile clients.', members: 4, tasks: 22, status: 'active' as const, tags: ['Analytics', 'SDK'] },
|
|
95
|
+
{ id: '11', name: 'Docs Revamp', description: 'New documentation site with interactive examples and search.', members: 2, tasks: 31, status: 'completed' as const, tags: ['Docs', 'Content'] },
|
|
96
|
+
{ id: '12', name: 'Edge Cache Layer', description: 'Distributed caching layer to cut origin latency for global users.', members: 5, tasks: 17, status: 'on-hold' as const, tags: ['Infra', 'Performance'] },
|
|
87
97
|
];
|
|
88
98
|
|
|
89
99
|
const demoActivities = [
|
|
@@ -95,9 +105,15 @@
|
|
|
95
105
|
];
|
|
96
106
|
|
|
97
107
|
const demoTeams = [
|
|
98
|
-
{ id: '1', name: 'Frontend Platform', description: 'Building the core UI component library and design system.', members: [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Carol' }], totalMembers: 8, color: '#3b82f6' },
|
|
99
|
-
{ id: '2', name: 'Backend Services', description: 'API development, microservices architecture, and infrastructure.', members: [{ name: 'Dave' }, { name: 'Eve' }], totalMembers: 5, color: '#10b981' },
|
|
100
|
-
{ id: '3', name: 'Design', description: 'User experience research and visual design.', members: [{ name: 'Frank' }, { name: 'Grace' }, { name: 'Hank' }, { name: 'Ivy' }], totalMembers: 6, color: '#f59e0b' },
|
|
108
|
+
{ id: '1', name: 'Frontend Platform', description: 'Building the core UI component library and design system.', members: [{ name: 'Alice' }, { name: 'Bob' }, { name: 'Carol' }], totalMembers: 8, rating: 4.8, color: '#3b82f6' },
|
|
109
|
+
{ id: '2', name: 'Backend Services', description: 'API development, microservices architecture, and infrastructure.', members: [{ name: 'Dave' }, { name: 'Eve' }], totalMembers: 5, rating: 4.5, color: '#10b981' },
|
|
110
|
+
{ id: '3', name: 'Design', description: 'User experience research and visual design.', members: [{ name: 'Frank' }, { name: 'Grace' }, { name: 'Hank' }, { name: 'Ivy' }], totalMembers: 6, rating: 4.9, color: '#f59e0b' },
|
|
111
|
+
{ id: '4', name: 'DevOps', description: 'CI/CD pipelines, cloud infrastructure, and reliability engineering.', members: [{ name: 'Jack' }, { name: 'Karen' }], totalMembers: 4, rating: 4.3, color: '#8b5cf6' },
|
|
112
|
+
{ id: '5', name: 'Data Engineering', description: 'Data pipelines, warehousing, and analytics infrastructure.', members: [{ name: 'Leo' }, { name: 'Mona' }, { name: 'Nina' }], totalMembers: 7, rating: 4.6, color: '#ec4899' },
|
|
113
|
+
{ id: '6', name: 'Mobile', description: 'iOS and Android apps with shared native modules.', members: [{ name: 'Oscar' }, { name: 'Paula' }], totalMembers: 5, rating: 4.4, color: '#14b8a6' },
|
|
114
|
+
{ id: '7', name: 'QA & Release', description: 'Test automation, release management, and quality gates.', members: [{ name: 'Quinn' }, { name: 'Rosa' }, { name: 'Sam' }], totalMembers: 4, rating: 4.2, color: '#f97316' },
|
|
115
|
+
{ id: '8', name: 'Security', description: 'Application security, compliance, and incident response.', members: [{ name: 'Tina' }, { name: 'Umar' }], totalMembers: 3, rating: 4.7, color: '#ef4444' },
|
|
116
|
+
{ id: '9', name: 'Developer Relations', description: 'Documentation, community programs, and developer outreach.', members: [{ name: 'Vera' }, { name: 'Will' }, { name: 'Xu' }], totalMembers: 6, rating: 4.5, color: '#0ea5e9' },
|
|
101
117
|
];
|
|
102
118
|
</script>
|
|
103
119
|
|
|
@@ -161,21 +177,26 @@
|
|
|
161
177
|
</Card.CardContent>
|
|
162
178
|
</Card.Card>
|
|
163
179
|
|
|
164
|
-
<!--
|
|
165
|
-
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
<
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
<!-- Variant-specific sections (About / Projects table / Jobs / Games ...) -->
|
|
181
|
+
{#if showSections}
|
|
182
|
+
<ProfileVariantSections {variant} />
|
|
183
|
+
{:else}
|
|
184
|
+
<!-- Tabs: Projects / Activity / Teams -->
|
|
185
|
+
<Tabs.Root value={activeTab}>
|
|
186
|
+
<Tabs.List class="w-full justify-start border-b">
|
|
187
|
+
<Tabs.Trigger value="projects" active={activeTab === 'projects'} onclick={() => activeTab = 'projects'} class="gap-1.5">{i18n.t('publicProfile.projects')}</Tabs.Trigger>
|
|
188
|
+
<Tabs.Trigger value="activity" active={activeTab === 'activity'} onclick={() => activeTab = 'activity'} class="gap-1.5">{i18n.t('publicProfile.activity')}</Tabs.Trigger>
|
|
189
|
+
<Tabs.Trigger value="teams" active={activeTab === 'teams'} onclick={() => activeTab = 'teams'} class="gap-1.5">{i18n.t('publicProfile.teams')}</Tabs.Trigger>
|
|
190
|
+
</Tabs.List>
|
|
191
|
+
<Tabs.Content value="projects" active={activeTab === 'projects'} class="mt-4">
|
|
192
|
+
<ProjectsGrid {columns} projects={columns === 3 ? demoProjects : demoProjects.slice(0, 6)} />
|
|
193
|
+
</Tabs.Content>
|
|
194
|
+
<Tabs.Content value="activity" active={activeTab === 'activity'} class="mt-4">
|
|
195
|
+
<ActivityTimeline activities={demoActivities} />
|
|
196
|
+
</Tabs.Content>
|
|
197
|
+
<Tabs.Content value="teams" active={activeTab === 'teams'} class="mt-4">
|
|
198
|
+
<TeamsShowcase teams={demoTeams} />
|
|
199
|
+
</Tabs.Content>
|
|
200
|
+
</Tabs.Root>
|
|
201
|
+
{/if}
|
|
181
202
|
</div>
|