@svadmin/ui 0.42.3 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.css +462 -825
- package/dist/app.theme.css +462 -825
- package/dist/components/ErrorPage.svelte +4 -56
- package/dist/components/TwoFactorAuthPage.svelte +44 -232
- package/dist/components/account/CompanyProfilePage.svelte +21 -142
- package/dist/components/account/GetStartedPage.svelte +33 -115
- package/dist/components/account/GetStartedPage.svelte.d.ts +1 -2
- package/dist/components/account/SecurityLogPage.svelte +16 -131
- package/dist/components/account/SettingsEnterprisePage.svelte +15 -275
- package/dist/components/account/SettingsPlainPage.svelte +10 -175
- package/dist/components/account/SettingsSidebarPage.svelte +11 -304
- package/dist/components/account/TeamMembersPage.svelte +18 -187
- package/dist/components/account/UserProfilePage.svelte +19 -196
- package/dist/components/account/UserProfilePage.svelte.d.ts +1 -2
- package/dist/components/content/ApiKeyList.svelte +13 -0
- package/dist/components/content/ApiKeyList.svelte.d.ts +16 -0
- package/dist/components/content/ContentPageHeader.svelte +31 -0
- package/dist/components/content/ContentPageHeader.svelte.d.ts +12 -0
- package/dist/components/content/ContentPageShell.svelte +39 -0
- package/dist/components/content/ContentPageShell.svelte.d.ts +14 -0
- package/dist/components/content/DataState.svelte +44 -0
- package/dist/components/content/DataState.svelte.d.ts +13 -0
- package/dist/components/content/DescriptionList.svelte +27 -0
- package/dist/components/content/DescriptionList.svelte.d.ts +14 -0
- package/dist/components/content/FileList.svelte +13 -0
- package/dist/components/content/FileList.svelte.d.ts +16 -0
- package/dist/components/content/FilterToolbar.svelte +24 -0
- package/dist/components/content/FilterToolbar.svelte.d.ts +11 -0
- package/dist/components/content/ImportDropzone.svelte +19 -0
- package/dist/components/content/ImportDropzone.svelte.d.ts +10 -0
- package/dist/components/content/IntegrationCard.svelte +16 -0
- package/dist/components/content/IntegrationCard.svelte.d.ts +15 -0
- package/dist/components/content/MemberList.svelte +16 -0
- package/dist/components/content/MemberList.svelte.d.ts +17 -0
- package/dist/components/content/MetricBlock.svelte +27 -0
- package/dist/components/content/MetricBlock.svelte.d.ts +13 -0
- package/dist/components/content/NetworkTable.svelte +18 -0
- package/dist/components/content/NetworkTable.svelte.d.ts +31 -0
- package/dist/components/content/NetworkTable.types.d.ts +4 -0
- package/dist/components/content/NetworkTable.types.js +1 -0
- package/dist/components/content/NetworkUserCard.svelte +17 -0
- package/dist/components/content/NetworkUserCard.svelte.d.ts +17 -0
- package/dist/components/content/OtpInput.svelte +22 -0
- package/dist/components/content/OtpInput.svelte.d.ts +10 -0
- package/dist/components/content/PageToolbar.svelte +18 -0
- package/dist/components/content/PageToolbar.svelte.d.ts +10 -0
- package/dist/components/content/ProjectCard.svelte +39 -0
- package/dist/components/content/ProjectCard.svelte.d.ts +20 -0
- package/dist/components/content/SectionHeader.svelte +18 -0
- package/dist/components/content/SectionHeader.svelte.d.ts +10 -0
- package/dist/components/content/SecurityEventTable.svelte +18 -0
- package/dist/components/content/SecurityEventTable.svelte.d.ts +15 -0
- package/dist/components/content/StatusBadge.svelte +19 -0
- package/dist/components/content/StatusBadge.svelte.d.ts +9 -0
- package/dist/components/content/SystemErrorState.svelte +18 -0
- package/dist/components/content/SystemErrorState.svelte.d.ts +10 -0
- package/dist/components/content/TeamCard.svelte +26 -0
- package/dist/components/content/TeamCard.svelte.d.ts +23 -0
- package/dist/components/content/TwoFactorStepper.svelte +11 -0
- package/dist/components/content/TwoFactorStepper.svelte.d.ts +8 -0
- package/dist/components/content/index.d.ts +32 -0
- package/dist/components/content/index.js +22 -0
- package/dist/components/network/TeamCrewTablePage.svelte +29 -181
- package/dist/components/network/UserCardsNFTPage.svelte +24 -137
- package/dist/components/profile/ActivityTimeline.svelte +17 -91
- package/dist/components/profile/ProfileCard.svelte +4 -4
- package/dist/components/profile/ProjectsGrid.svelte +28 -106
- package/dist/components/profile/ProjectsGrid.svelte.d.ts +2 -12
- package/dist/components/profile/PublicProfilePage.svelte +31 -181
- package/dist/components/profile/TeamsShowcase.svelte +12 -114
- package/dist/components/profile/TeamsShowcase.svelte.d.ts +2 -12
- package/dist/index.d.ts +36 -0
- package/dist/index.js +24 -0
- package/dist/reference-data.d.ts +171 -0
- package/dist/reference-data.js +39 -0
- package/package.json +1 -1
|
@@ -1,101 +1,27 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import
|
|
3
|
+
import { MessageSquare, Plus, Star, UserPlus } from '@lucide/svelte';
|
|
4
4
|
import { Avatar } from '../ui/avatar/index.js';
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
<
|
|
39
|
-
<div class="flex items-
|
|
40
|
-
|
|
41
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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}
|
|
@@ -1,123 +1,45 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
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
3
|
import { Button } from '../ui/button/index.js';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import Progress from '../ui/progress/progress.svelte';
|
|
9
|
-
|
|
10
|
-
const i18n = useTranslation();
|
|
11
|
-
|
|
12
|
-
interface Project {
|
|
13
|
-
id: string;
|
|
14
|
-
name: string;
|
|
15
|
-
description: string;
|
|
16
|
-
members: number;
|
|
17
|
-
tasks: number;
|
|
18
|
-
status: 'active' | 'completed' | 'on-hold';
|
|
19
|
-
tags?: string[];
|
|
20
|
-
image?: string;
|
|
21
|
-
progress?: number;
|
|
22
|
-
}
|
|
4
|
+
import { DataState, FilterToolbar, ProjectCard } from '../content/index.js';
|
|
5
|
+
import type { ProjectSummary } from '../content/ProjectCard.svelte';
|
|
23
6
|
|
|
24
7
|
interface Props {
|
|
25
8
|
columns?: 2 | 3;
|
|
26
|
-
projects?:
|
|
9
|
+
projects?: ProjectSummary[];
|
|
27
10
|
}
|
|
28
11
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const statusConfig = {
|
|
42
|
-
'active': { color: 'bg-green-500/10 text-green-600', label: 'Active' },
|
|
43
|
-
'completed': { color: 'bg-blue-500/10 text-blue-600', label: 'Completed' },
|
|
44
|
-
'on-hold': { color: 'bg-amber-500/10 text-amber-600', label: 'On Hold' },
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const gridCols = $derived(columns === 3 ? 'lg:grid-cols-3' : 'lg:grid-cols-2');
|
|
12
|
+
const i18n = useTranslation();
|
|
13
|
+
let { columns = 2, projects = [] }: Props = $props();
|
|
14
|
+
let query = $state('');
|
|
15
|
+
const filtered = $derived.by(() => {
|
|
16
|
+
const needle = query.trim().toLowerCase();
|
|
17
|
+
if (!needle) return projects;
|
|
18
|
+
return projects.filter((project) => [project.name, project.description, ...(project.tags ?? [])]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.some((value) => String(value).toLowerCase().includes(needle)));
|
|
21
|
+
});
|
|
22
|
+
const gridClass = $derived(columns === 3 ? 'md:grid-cols-2 xl:grid-cols-3' : 'md:grid-cols-2');
|
|
48
23
|
</script>
|
|
49
24
|
|
|
50
|
-
<
|
|
51
|
-
<div class="flex flex-
|
|
52
|
-
<
|
|
53
|
-
{i18n.t('publicProfile.projectsCount', { count: filtered.length })}
|
|
54
|
-
|
|
55
|
-
<div class="flex w-full items-center gap-2 sm:w-auto">
|
|
56
|
-
<div class="relative min-w-0 flex-1 sm:flex-none">
|
|
57
|
-
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
58
|
-
<Input placeholder={i18n.t('publicProfile.searchProjects')} bind:value={searchQuery} class="w-full pl-9 sm:w-48" />
|
|
59
|
-
</div>
|
|
60
|
-
<Button size="sm"><Plus class="h-3.5 w-3.5 mr-1" />{i18n.t('common.create')}</Button>
|
|
25
|
+
<section class="space-y-4" data-svadmin-profile-projects>
|
|
26
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
27
|
+
<div>
|
|
28
|
+
<h2 class="text-base font-semibold text-foreground">{i18n.t('publicProfile.projectsCount', { count: filtered.length })}</h2>
|
|
29
|
+
<p class="mt-1 text-sm text-muted-foreground">{i18n.t('publicProfile.searchProjects')}</p>
|
|
61
30
|
</div>
|
|
31
|
+
<Button size="sm">{i18n.t('common.create')}</Button>
|
|
62
32
|
</div>
|
|
63
|
-
|
|
64
|
-
{#if
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
33
|
+
<FilterToolbar bind:query placeholder={i18n.t('publicProfile.searchProjects')} />
|
|
34
|
+
{#if projects.length === 0}
|
|
35
|
+
<DataState state="empty" title={i18n.t('publicProfile.noProjects')} />
|
|
36
|
+
{:else if filtered.length === 0}
|
|
37
|
+
<DataState state="empty" title={i18n.t('publicProfile.noProjects')} description={i18n.t('publicProfile.searchProjects')} />
|
|
68
38
|
{:else}
|
|
69
|
-
<div class=
|
|
39
|
+
<div class={'grid gap-4 ' + gridClass}>
|
|
70
40
|
{#each filtered as project (project.id)}
|
|
71
|
-
{
|
|
72
|
-
<Card.Card class="group overflow-hidden border-border/60 hover:border-primary/30 transition-colors">
|
|
73
|
-
{#if project.image}
|
|
74
|
-
<div class="h-32 overflow-hidden">
|
|
75
|
-
<img src={project.image} alt={project.name} class="h-full w-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
|
76
|
-
</div>
|
|
77
|
-
{/if}
|
|
78
|
-
<Card.CardContent class="p-4 space-y-3">
|
|
79
|
-
<div class="flex items-start justify-between gap-2">
|
|
80
|
-
<div class="flex min-w-0 items-center gap-3">
|
|
81
|
-
<span class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md border bg-muted/40 text-muted-foreground">
|
|
82
|
-
<FolderKanban class="h-4 w-4" />
|
|
83
|
-
</span>
|
|
84
|
-
<h4 class="font-semibold text-foreground">{project.name}</h4>
|
|
85
|
-
</div>
|
|
86
|
-
<span class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold {statusConfig[project.status].color}">
|
|
87
|
-
{statusConfig[project.status].label}
|
|
88
|
-
</span>
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
<p class="text-sm text-muted-foreground line-clamp-2">{project.description}</p>
|
|
92
|
-
|
|
93
|
-
{#if project.tags && project.tags.length > 0}
|
|
94
|
-
<div class="flex flex-wrap gap-1">
|
|
95
|
-
{#each project.tags as tag (tag)}
|
|
96
|
-
<Badge variant="outline" class="text-[10px]">{tag}</Badge>
|
|
97
|
-
{/each}
|
|
98
|
-
</div>
|
|
99
|
-
{/if}
|
|
100
|
-
|
|
101
|
-
<div class="space-y-1.5">
|
|
102
|
-
<div class="flex items-center justify-between text-[11px] text-muted-foreground">
|
|
103
|
-
<span>{i18n.t('common.progress')}</span>
|
|
104
|
-
<span class="font-medium text-foreground">{progress}%</span>
|
|
105
|
-
</div>
|
|
106
|
-
<Progress value={progress} class="h-1.5" />
|
|
107
|
-
</div>
|
|
108
|
-
|
|
109
|
-
<div class="flex items-center justify-between pt-2 border-t">
|
|
110
|
-
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
|
111
|
-
<span class="flex items-center gap-1"><Users class="h-3 w-3" />{i18n.t('publicProfile.projectMembers', { count: project.members })}</span>
|
|
112
|
-
<span class="flex items-center gap-1"><CheckCircle2 class="h-3 w-3" />{i18n.t('publicProfile.projectTasks', { count: project.tasks })}</span>
|
|
113
|
-
</div>
|
|
114
|
-
<Button variant="ghost" size="icon-sm" aria-label={project.name} title={project.name}>
|
|
115
|
-
<ArrowRight class="h-3.5 w-3.5" />
|
|
116
|
-
</Button>
|
|
117
|
-
</div>
|
|
118
|
-
</Card.CardContent>
|
|
119
|
-
</Card.Card>
|
|
41
|
+
<ProjectCard {project} />
|
|
120
42
|
{/each}
|
|
121
43
|
</div>
|
|
122
44
|
{/if}
|
|
123
|
-
</
|
|
45
|
+
</section>
|
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
id: string;
|
|
3
|
-
name: string;
|
|
4
|
-
description: string;
|
|
5
|
-
members: number;
|
|
6
|
-
tasks: number;
|
|
7
|
-
status: 'active' | 'completed' | 'on-hold';
|
|
8
|
-
tags?: string[];
|
|
9
|
-
image?: string;
|
|
10
|
-
progress?: number;
|
|
11
|
-
}
|
|
1
|
+
import type { ProjectSummary } from '../content/ProjectCard.svelte';
|
|
12
2
|
interface Props {
|
|
13
3
|
columns?: 2 | 3;
|
|
14
|
-
projects?:
|
|
4
|
+
projects?: ProjectSummary[];
|
|
15
5
|
}
|
|
16
6
|
declare const ProjectsGrid: import("svelte").Component<Props, {}, "">;
|
|
17
7
|
type ProjectsGrid = ReturnType<typeof ProjectsGrid>;
|
|
@@ -1,202 +1,52 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useTranslation } from '@svadmin/core/i18n';
|
|
3
|
-
import * as Card from '../ui/card/index.js';
|
|
4
|
-
import { Button } from '../ui/button/index.js';
|
|
5
|
-
import { Badge } from '../ui/badge/index.js';
|
|
6
3
|
import * as Tabs from '../ui/tabs/index.js';
|
|
4
|
+
import ProfileCard from './ProfileCard.svelte';
|
|
7
5
|
import ProjectsGrid from './ProjectsGrid.svelte';
|
|
8
6
|
import ActivityTimeline from './ActivityTimeline.svelte';
|
|
9
7
|
import TeamsShowcase from './TeamsShowcase.svelte';
|
|
10
8
|
import ProfileVariantSections from './ProfileVariantSections.svelte';
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
import ContentPageShell from '../content/ContentPageShell.svelte';
|
|
10
|
+
import ContentPageHeader from '../content/ContentPageHeader.svelte';
|
|
11
|
+
import { referenceDemoData } from '../../reference-data.js';
|
|
14
12
|
|
|
15
13
|
type ProfileVariant = 'default' | 'company' | 'gamer';
|
|
16
14
|
type ProfileTab = 'projects' | 'activity' | 'teams';
|
|
17
|
-
|
|
18
|
-
interface Props {
|
|
19
|
-
variant?: ProfileVariant;
|
|
20
|
-
initialTab?: ProfileTab;
|
|
21
|
-
columns?: 2 | 3;
|
|
22
|
-
showSections?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
15
|
+
interface Props { variant?: ProfileVariant; initialTab?: ProfileTab; columns?: 2 | 3; showSections?: boolean; }
|
|
25
16
|
let { variant = 'default', initialTab = 'projects', columns = 2, showSections = false }: Props = $props();
|
|
26
17
|
let activeTab = $derived<ProfileTab>(initialTab);
|
|
18
|
+
const i18n = useTranslation();
|
|
27
19
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
stats: [
|
|
42
|
-
{ label: i18n.t('profile.employees'), value: '1,250' },
|
|
43
|
-
{ label: i18n.t('publicProfile.projects'), value: '48' },
|
|
44
|
-
{ label: i18n.t('publicProfile.teams'), value: '12' },
|
|
45
|
-
],
|
|
46
|
-
tags: ['SaaS', 'Enterprise', 'Cloud', 'AI'],
|
|
47
|
-
};
|
|
48
|
-
} else if (variant === 'gamer') {
|
|
49
|
-
return {
|
|
50
|
-
name: 'ShadowFox',
|
|
51
|
-
tagline: 'Pro gamer & content creator',
|
|
52
|
-
gamerTag: 'ShadowFox#7742',
|
|
53
|
-
level: 42,
|
|
54
|
-
rank: 'Diamond II',
|
|
55
|
-
location: 'Seoul, KR',
|
|
56
|
-
followers: 24600,
|
|
57
|
-
following: 180,
|
|
58
|
-
stats: [
|
|
59
|
-
{ label: i18n.t('profile.gamesPlayed'), value: '1,284' },
|
|
60
|
-
{ label: i18n.t('profile.winRate'), value: '68%' },
|
|
61
|
-
{ label: i18n.t('profile.rank'), value: 'Diamond II' },
|
|
62
|
-
],
|
|
63
|
-
tags: ['FPS', 'RPG', 'Streaming', 'Esports'],
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
return {
|
|
67
|
-
name: 'Alex Chen',
|
|
68
|
-
tagline: 'Full-stack developer & open source enthusiast',
|
|
69
|
-
location: 'Shanghai, CN',
|
|
70
|
-
website: 'alexchen.dev',
|
|
71
|
-
joinedDate: '2023',
|
|
72
|
-
followers: 1234,
|
|
73
|
-
following: 567,
|
|
74
|
-
stats: [
|
|
75
|
-
{ label: i18n.t('publicProfile.projects'), value: '24' },
|
|
76
|
-
{ label: i18n.t('publicProfile.followers'), value: '1.2K' },
|
|
77
|
-
{ label: i18n.t('publicProfile.following'), value: '567' },
|
|
78
|
-
],
|
|
79
|
-
tags: ['TypeScript', 'Svelte', 'Node.js', 'Open Source'],
|
|
80
|
-
};
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Demo data
|
|
84
|
-
const demoProjects = [
|
|
85
|
-
{ id: '1', name: 'Dashboard Redesign', description: 'Modern admin dashboard with real-time analytics and customizable widgets.', members: 8, tasks: 34, status: 'active' as const, tags: ['UI', 'Analytics'] },
|
|
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'] },
|
|
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'] },
|
|
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'] },
|
|
97
|
-
];
|
|
98
|
-
|
|
99
|
-
const demoActivities = [
|
|
100
|
-
{ id: '1', type: 'posted' as const, user: 'Alex Chen', content: 'Just shipped the new dashboard redesign with real-time analytics support.', timestamp: '2 hours ago' },
|
|
101
|
-
{ id: '2', type: 'commented' as const, user: 'Alex Chen', target: 'API Gateway PR #42', content: 'Looks great, just a few minor suggestions on error handling.', timestamp: '5 hours ago' },
|
|
102
|
-
{ id: '3', type: 'created' as const, user: 'Alex Chen', target: 'Mobile App project', timestamp: '1 day ago' },
|
|
103
|
-
{ id: '4', type: 'joined' as const, user: 'Alex Chen', target: 'Design Systems Guild', timestamp: '3 days ago' },
|
|
104
|
-
{ id: '5', type: 'starred' as const, user: 'Alex Chen', target: 'svadmin/svadmin', timestamp: '1 week ago' },
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
const demoTeams = [
|
|
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' },
|
|
117
|
-
];
|
|
20
|
+
const profileData = $derived.by(() => variant === 'company'
|
|
21
|
+
? { name: 'Acme Corporation', tagline: 'Building the future of enterprise software', industry: 'Technology', employees: 1250, founded: '2015', website: 'acme.com', location: 'San Francisco, CA', followers: 8420, following: 312, tags: ['SaaS', 'Enterprise', 'Cloud'], stats: [{ label: i18n.t('profile.employees'), value: '1,250' }, { label: i18n.t('publicProfile.projects'), value: '48' }, { label: i18n.t('publicProfile.teams'), value: '12' }] }
|
|
22
|
+
: variant === 'gamer'
|
|
23
|
+
? { name: 'ShadowFox', tagline: 'Pro gamer & content creator', gamerTag: 'ShadowFox#7742', level: 42, rank: 'Diamond II', location: 'Seoul, KR', followers: 24600, following: 180, tags: ['FPS', 'RPG', 'Streaming'], stats: [{ label: i18n.t('profile.gamesPlayed'), value: '1,284' }, { label: i18n.t('profile.winRate'), value: '68%' }, { label: i18n.t('profile.rank'), value: 'Diamond II' }] }
|
|
24
|
+
: { name: 'Alex Chen', tagline: 'Full-stack developer & open source enthusiast', location: 'Shanghai, CN', website: 'alexchen.dev', joinedDate: '2023', followers: 1234, following: 567, tags: ['TypeScript', 'Svelte', 'Open Source'], stats: [{ label: i18n.t('publicProfile.projects'), value: '24' }, { label: i18n.t('publicProfile.followers'), value: '1.2K' }, { label: i18n.t('publicProfile.following'), value: '567' }] });
|
|
25
|
+
|
|
26
|
+
const projects = referenceDemoData.projects;
|
|
27
|
+
const activities = $derived.by(() => [
|
|
28
|
+
{ id: '1', type: 'posted' as const, user: profileData.name, content: 'Shipped the new dashboard redesign with real-time analytics support.', timestamp: '2 hours ago' },
|
|
29
|
+
{ id: '2', type: 'commented' as const, user: profileData.name, target: 'API Gateway PR #42', content: 'Looks great, with a few minor suggestions on error handling.', timestamp: '5 hours ago' },
|
|
30
|
+
{ id: '3', type: 'created' as const, user: profileData.name, target: 'Mobile App project', timestamp: '1 day ago' },
|
|
31
|
+
]);
|
|
32
|
+
const teams = referenceDemoData.teams;
|
|
118
33
|
</script>
|
|
119
34
|
|
|
120
|
-
<
|
|
121
|
-
|
|
122
|
-
<
|
|
123
|
-
<div class="h-28 bg-gradient-to-r from-primary/25 via-primary/15 to-accent/20 relative sm:h-40">
|
|
124
|
-
<div class="absolute inset-0 opacity-[0.06]" style="background-image: radial-gradient(circle, currentColor 1px, transparent 1px); background-size: 24px 24px;"></div>
|
|
125
|
-
</div>
|
|
126
|
-
<Card.CardContent class="relative px-6 pb-5">
|
|
127
|
-
<div class="-mt-10 flex flex-col gap-3 sm:-mt-12 sm:flex-row sm:items-end sm:gap-4">
|
|
128
|
-
<div class="ring-4 ring-card rounded-2xl overflow-hidden shrink-0">
|
|
129
|
-
<div class="flex h-20 w-20 items-center justify-center bg-primary/10 text-2xl font-bold text-primary sm:h-24 sm:w-24 sm:text-3xl">
|
|
130
|
-
{profileData.name.split(' ').map(n => n[0]).join('').slice(0, 2)}
|
|
131
|
-
</div>
|
|
132
|
-
</div>
|
|
133
|
-
<div class="flex-1 min-w-0">
|
|
134
|
-
<h2 class="text-xl font-bold text-foreground sm:text-2xl">{profileData.name}</h2>
|
|
135
|
-
{#if profileData.tagline}
|
|
136
|
-
<p class="text-muted-foreground">{profileData.tagline}</p>
|
|
137
|
-
{/if}
|
|
138
|
-
<div class="mt-2 flex flex-wrap gap-3 text-sm text-muted-foreground">
|
|
139
|
-
{#if profileData.location}
|
|
140
|
-
<span class="flex items-center gap-1"><MapPin class="h-3.5 w-3.5" />{profileData.location}</span>
|
|
141
|
-
{/if}
|
|
142
|
-
{#if profileData.website}
|
|
143
|
-
<span class="flex items-center gap-1"><Link class="h-3.5 w-3.5" />{profileData.website}</span>
|
|
144
|
-
{/if}
|
|
145
|
-
{#if 'joinedDate' in profileData && profileData.joinedDate}
|
|
146
|
-
<span class="flex items-center gap-1"><Calendar class="h-3.5 w-3.5" />{i18n.t('publicProfile.joinedDate', { date: profileData.joinedDate })}</span>
|
|
147
|
-
{/if}
|
|
148
|
-
</div>
|
|
149
|
-
</div>
|
|
150
|
-
<div class="flex shrink-0 flex-wrap gap-2">
|
|
151
|
-
<Button size="sm">{i18n.t('publicProfile.editProfile')}</Button>
|
|
152
|
-
<Button size="sm" variant="outline">{i18n.t('publicProfile.sendMessage')}</Button>
|
|
153
|
-
<Button size="sm" variant="ghost"><MoreHorizontal class="h-4 w-4" /></Button>
|
|
154
|
-
</div>
|
|
155
|
-
</div>
|
|
156
|
-
|
|
157
|
-
<!-- Stats -->
|
|
158
|
-
{#if profileData.stats.length > 0}
|
|
159
|
-
<div class="mt-4 grid grid-cols-3 gap-4 border-t pt-4">
|
|
160
|
-
{#each profileData.stats as stat (stat.label)}
|
|
161
|
-
<div class="text-center">
|
|
162
|
-
<div class="text-xl font-bold text-foreground">{stat.value}</div>
|
|
163
|
-
<div class="text-xs text-muted-foreground">{stat.label}</div>
|
|
164
|
-
</div>
|
|
165
|
-
{/each}
|
|
166
|
-
</div>
|
|
167
|
-
{/if}
|
|
168
|
-
|
|
169
|
-
<!-- Tags -->
|
|
170
|
-
{#if profileData.tags.length > 0}
|
|
171
|
-
<div class="mt-3 flex flex-wrap gap-1.5">
|
|
172
|
-
{#each profileData.tags as tag (tag)}
|
|
173
|
-
<Badge variant="outline" class="text-xs">{tag}</Badge>
|
|
174
|
-
{/each}
|
|
175
|
-
</div>
|
|
176
|
-
{/if}
|
|
177
|
-
</Card.CardContent>
|
|
178
|
-
</Card.Card>
|
|
179
|
-
|
|
180
|
-
<!-- Variant-specific sections (About / Projects table / Jobs / Games ...) -->
|
|
35
|
+
<ContentPageShell pageId="public-profile" width="wide">
|
|
36
|
+
<ContentPageHeader title={profileData.name} eyebrow={i18n.t('publicProfile.title')} description={profileData.tagline} />
|
|
37
|
+
<ProfileCard {...profileData} {variant} />
|
|
181
38
|
{#if showSections}
|
|
182
39
|
<ProfileVariantSections {variant} />
|
|
183
40
|
{:else}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
<Tabs.Trigger value="
|
|
188
|
-
<Tabs.Trigger value="
|
|
189
|
-
<Tabs.Trigger value="teams" active={activeTab === 'teams'} onclick={() => activeTab = 'teams'} class="gap-1.5">{i18n.t('publicProfile.teams')}</Tabs.Trigger>
|
|
41
|
+
<Tabs.Root value={activeTab} onValueChange={(value) => activeTab = value as ProfileTab}>
|
|
42
|
+
<Tabs.List class="w-full justify-start overflow-x-auto">
|
|
43
|
+
<Tabs.Trigger value="projects">{i18n.t('publicProfile.projects')}</Tabs.Trigger>
|
|
44
|
+
<Tabs.Trigger value="activity">{i18n.t('publicProfile.activity')}</Tabs.Trigger>
|
|
45
|
+
<Tabs.Trigger value="teams">{i18n.t('publicProfile.teams')}</Tabs.Trigger>
|
|
190
46
|
</Tabs.List>
|
|
191
|
-
<Tabs.Content value="projects"
|
|
192
|
-
|
|
193
|
-
|
|
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>
|
|
47
|
+
<Tabs.Content value="projects" class="mt-5"><ProjectsGrid {projects} {columns} /></Tabs.Content>
|
|
48
|
+
<Tabs.Content value="activity" class="mt-5"><ActivityTimeline activities={activities} /></Tabs.Content>
|
|
49
|
+
<Tabs.Content value="teams" class="mt-5"><TeamsShowcase {teams} /></Tabs.Content>
|
|
200
50
|
</Tabs.Root>
|
|
201
51
|
{/if}
|
|
202
|
-
</
|
|
52
|
+
</ContentPageShell>
|