@svadmin/ui 0.42.2 → 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 +482 -840
- package/dist/app.theme.css +482 -840
- package/dist/components/ErrorPage.svelte +4 -56
- package/dist/components/Layout.svelte +1 -1
- package/dist/components/Sidebar.svelte +1 -0
- 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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
status?: '404' | '500';
|
|
4
|
+
title?: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
action?: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const SystemErrorState: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type SystemErrorState = ReturnType<typeof SystemErrorState>;
|
|
10
|
+
export default SystemErrorState;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ArrowRight, Users } from '@lucide/svelte';
|
|
3
|
+
import { Avatar } from '../ui/avatar/index.js';
|
|
4
|
+
import { Button } from '../ui/button/index.js';
|
|
5
|
+
import * as Card from '../ui/card/index.js';
|
|
6
|
+
export interface TeamSummary {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
totalMembers?: number;
|
|
11
|
+
members?: Array<{ id?: string; name: string; avatar?: string; role?: string }>;
|
|
12
|
+
color?: string;
|
|
13
|
+
rating?: number;
|
|
14
|
+
}
|
|
15
|
+
interface Props { team: TeamSummary; onjoin?: () => void; onclick?: () => void; class?: string; }
|
|
16
|
+
let { team, onjoin, onclick, class: className = '' }: Props = $props();
|
|
17
|
+
const initials = (name: string) => name.split(' ').map((part) => part[0]).join('').slice(0, 2).toUpperCase();
|
|
18
|
+
</script>
|
|
19
|
+
<Card.Card data-interactive={onclick ? 'true' : undefined} class={'h-full ' + className}>
|
|
20
|
+
<Card.CardContent class="flex h-full flex-col gap-3 p-4">
|
|
21
|
+
<div class="flex items-start gap-3"><span class="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary" style:background-color={team.color ? `${team.color}20` : undefined}><Users class="size-4" style={team.color ? `color: ${team.color}` : undefined} /></span><div class="min-w-0 flex-1"><h3 class="truncate text-sm font-semibold text-foreground">{team.name}</h3><p class="text-xs text-muted-foreground">{team.totalMembers ?? team.members?.length ?? 0} members</p></div>{#if team.rating}<span class="text-xs font-semibold text-warning">{team.rating}</span>{/if}</div>
|
|
22
|
+
{#if team.description}<p class="line-clamp-2 text-sm text-muted-foreground">{team.description}</p>{/if}
|
|
23
|
+
<div class="flex items-center gap-1">{#each (team.members ?? []).slice(0, 5) as member, index (member.id ?? member.name + '-' + index)}<Avatar src={member.avatar} alt={member.name} fallback={initials(member.name)} size="sm" class="-mr-1 ring-2 ring-card" />{/each}</div>
|
|
24
|
+
<div class="mt-auto flex gap-2 border-t border-border pt-3">{#if onjoin}<Button variant="outline" size="sm" class="flex-1" onclick={onjoin}>Join</Button>{/if}{#if onclick}<Button variant="ghost" size="sm" class="flex-1" onclick={onclick}>View <ArrowRight class="size-3.5" /></Button>{/if}</div>
|
|
25
|
+
</Card.CardContent>
|
|
26
|
+
</Card.Card>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface TeamSummary {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
totalMembers?: number;
|
|
6
|
+
members?: Array<{
|
|
7
|
+
id?: string;
|
|
8
|
+
name: string;
|
|
9
|
+
avatar?: string;
|
|
10
|
+
role?: string;
|
|
11
|
+
}>;
|
|
12
|
+
color?: string;
|
|
13
|
+
rating?: number;
|
|
14
|
+
}
|
|
15
|
+
interface Props {
|
|
16
|
+
team: TeamSummary;
|
|
17
|
+
onjoin?: () => void;
|
|
18
|
+
onclick?: () => void;
|
|
19
|
+
class?: string;
|
|
20
|
+
}
|
|
21
|
+
declare const TeamCard: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type TeamCard = ReturnType<typeof TeamCard>;
|
|
23
|
+
export default TeamCard;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props { current: number; steps?: string[]; class?: string; }
|
|
3
|
+
let { current, steps = ['Setup', 'Verify', 'Recovery'], class: className = '' }: Props = $props();
|
|
4
|
+
</script>
|
|
5
|
+
<ol class={'grid gap-2 sm:grid-cols-3 ' + className} aria-label="Progress">
|
|
6
|
+
{#each steps as step, index (step)}
|
|
7
|
+
<li class={'flex items-center gap-2 rounded-md border px-3 py-2 text-xs ' + (index < current ? 'border-success/30 bg-success/10 text-success' : index === current ? 'border-primary/30 bg-primary/10 font-medium text-primary' : 'border-border text-muted-foreground')}>
|
|
8
|
+
<span class="flex size-5 shrink-0 items-center justify-center rounded-full border text-[11px]">{index + 1}</span><span class="truncate">{step}</span>
|
|
9
|
+
</li>
|
|
10
|
+
{/each}
|
|
11
|
+
</ol>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export { default as ContentPageShell } from './ContentPageShell.svelte';
|
|
2
|
+
export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
3
|
+
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
|
+
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
|
+
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
6
|
+
export { default as DescriptionList } from './DescriptionList.svelte';
|
|
7
|
+
export { default as StatusBadge } from './StatusBadge.svelte';
|
|
8
|
+
export { default as FilterToolbar } from './FilterToolbar.svelte';
|
|
9
|
+
export { default as DataState } from './DataState.svelte';
|
|
10
|
+
export type { DataStateKind } from './DataState.svelte';
|
|
11
|
+
export { default as ProjectCard } from './ProjectCard.svelte';
|
|
12
|
+
export type { ProjectSummary, ProjectStatus } from './ProjectCard.svelte';
|
|
13
|
+
export { default as TeamCard } from './TeamCard.svelte';
|
|
14
|
+
export type { TeamSummary } from './TeamCard.svelte';
|
|
15
|
+
export { default as FileList } from './FileList.svelte';
|
|
16
|
+
export type { FileItem } from './FileList.svelte';
|
|
17
|
+
export { default as IntegrationCard } from './IntegrationCard.svelte';
|
|
18
|
+
export type { IntegrationSummary } from './IntegrationCard.svelte';
|
|
19
|
+
export { default as ApiKeyList } from './ApiKeyList.svelte';
|
|
20
|
+
export type { ApiKeySummary } from './ApiKeyList.svelte';
|
|
21
|
+
export { default as SecurityEventTable } from './SecurityEventTable.svelte';
|
|
22
|
+
export type { SecurityEvent } from './SecurityEventTable.svelte';
|
|
23
|
+
export { default as MemberList } from './MemberList.svelte';
|
|
24
|
+
export type { MemberSummary } from './MemberList.svelte';
|
|
25
|
+
export { default as ImportDropzone } from './ImportDropzone.svelte';
|
|
26
|
+
export { default as NetworkUserCard } from './NetworkUserCard.svelte';
|
|
27
|
+
export type { NetworkUser } from './NetworkUserCard.svelte';
|
|
28
|
+
export { default as NetworkTable } from './NetworkTable.svelte';
|
|
29
|
+
export type { NetworkColumn } from './NetworkTable.types.js';
|
|
30
|
+
export { default as OtpInput } from './OtpInput.svelte';
|
|
31
|
+
export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
32
|
+
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { default as ContentPageShell } from './ContentPageShell.svelte';
|
|
2
|
+
export { default as ContentPageHeader } from './ContentPageHeader.svelte';
|
|
3
|
+
export { default as SectionHeader } from './SectionHeader.svelte';
|
|
4
|
+
export { default as PageToolbar } from './PageToolbar.svelte';
|
|
5
|
+
export { default as MetricBlock } from './MetricBlock.svelte';
|
|
6
|
+
export { default as DescriptionList } from './DescriptionList.svelte';
|
|
7
|
+
export { default as StatusBadge } from './StatusBadge.svelte';
|
|
8
|
+
export { default as FilterToolbar } from './FilterToolbar.svelte';
|
|
9
|
+
export { default as DataState } from './DataState.svelte';
|
|
10
|
+
export { default as ProjectCard } from './ProjectCard.svelte';
|
|
11
|
+
export { default as TeamCard } from './TeamCard.svelte';
|
|
12
|
+
export { default as FileList } from './FileList.svelte';
|
|
13
|
+
export { default as IntegrationCard } from './IntegrationCard.svelte';
|
|
14
|
+
export { default as ApiKeyList } from './ApiKeyList.svelte';
|
|
15
|
+
export { default as SecurityEventTable } from './SecurityEventTable.svelte';
|
|
16
|
+
export { default as MemberList } from './MemberList.svelte';
|
|
17
|
+
export { default as ImportDropzone } from './ImportDropzone.svelte';
|
|
18
|
+
export { default as NetworkUserCard } from './NetworkUserCard.svelte';
|
|
19
|
+
export { default as NetworkTable } from './NetworkTable.svelte';
|
|
20
|
+
export { default as OtpInput } from './OtpInput.svelte';
|
|
21
|
+
export { default as TwoFactorStepper } from './TwoFactorStepper.svelte';
|
|
22
|
+
export { default as SystemErrorState } from './SystemErrorState.svelte';
|
|
@@ -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
|
|
3
|
+
import { Download, UserPlus } from '@lucide/svelte';
|
|
5
4
|
import { Button } from '../ui/button/index.js';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
id:
|
|
14
|
-
name:
|
|
15
|
-
email:
|
|
16
|
-
role:
|
|
17
|
-
department:
|
|
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
|
|
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
|
-
|
|
81
|
-
<div class="flex items-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
collectionCount:
|
|
27
|
-
floorPrice:
|
|
28
|
-
volume:
|
|
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
|
-
|
|
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
|
-
<
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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>
|