From 8b8887f8f3b2fa3b64aed3c5654151a0fec3bbbe Mon Sep 17 00:00:00 2001 From: eti Date: Wed, 9 Sep 2026 17:17:10 +0200 Subject: [PATCH] web/org: add org profile page Signed-off-by: eti --- .../profile/ProfileChrome.stories.svelte | 36 +++- .../components/profile/ProfileChrome.svelte | 51 ++++-- .../lib/components/profile/ProfileTabs.svelte | 99 ++++++++--- .../organization/OrganizationChrome.svelte | 155 ++++++++++++++++++ .../OrganizationMembersTab.svelte | 13 ++ .../OrganizationOverviewTab.svelte | 91 ++++++++++ .../OrganizationSettingsTab.svelte | 10 ++ .../components/profile/organization/mock.ts | 27 +++ web/src/lib/components/ui/Tag.svelte | 2 +- web/src/routes/[handle]/+layout.svelte | 4 +- web/src/routes/[handle]/+layout.ts | 16 +- web/src/routes/[handle]/+page.svelte | 26 ++- web/src/routes/[handle]/+page.ts | 8 +- 13 files changed, 480 insertions(+), 58 deletions(-) create mode 100644 web/src/lib/components/profile/organization/OrganizationChrome.svelte create mode 100644 web/src/lib/components/profile/organization/OrganizationMembersTab.svelte create mode 100644 web/src/lib/components/profile/organization/OrganizationOverviewTab.svelte create mode 100644 web/src/lib/components/profile/organization/OrganizationSettingsTab.svelte create mode 100644 web/src/lib/components/profile/organization/mock.ts diff --git a/web/src/lib/components/profile/ProfileChrome.stories.svelte b/web/src/lib/components/profile/ProfileChrome.stories.svelte index 4cbf9cc60..eaec066f8 100644 --- a/web/src/lib/components/profile/ProfileChrome.stories.svelte +++ b/web/src/lib/components/profile/ProfileChrome.stories.svelte @@ -1,6 +1,8 @@ @@ -39,6 +55,10 @@ {/snippet} +{#snippet organizationContent()} + +{/snippet} + + + + + diff --git a/web/src/lib/components/profile/ProfileChrome.svelte b/web/src/lib/components/profile/ProfileChrome.svelte index 0e6ffa375..b7b68cadb 100644 --- a/web/src/lib/components/profile/ProfileChrome.svelte +++ b/web/src/lib/components/profile/ProfileChrome.svelte @@ -11,6 +11,7 @@ import { fetchVouchesPage } from "$lib/components/profile/pages"; import ProfileCard from "$lib/components/profile/ProfileCard.svelte"; import ProfileTabs from "$lib/components/profile/ProfileTabs.svelte"; + import OrganizationChrome from "$lib/components/profile/organization/OrganizationChrome.svelte"; import type { DirectVouch, NetworkVouch, ProfileCounts } from "$lib/components/profile/types"; interface Props { @@ -50,6 +51,7 @@ let networkVouches = $state([]); $effect(() => { + if (profile?.isOrganization) return; const agent = auth.agent; const did = identity.did; const viewerDid = auth.currentDid; @@ -78,21 +80,36 @@ {#if notJoined} {:else} - - - -
- {@render content()} -
-
+ {#if profile?.isOrganization} + + {:else} + + + +
+ {@render content()} +
+
+ {/if} {/if} diff --git a/web/src/lib/components/profile/ProfileTabs.svelte b/web/src/lib/components/profile/ProfileTabs.svelte index 226bce26e..7fd67dbcb 100644 --- a/web/src/lib/components/profile/ProfileTabs.svelte +++ b/web/src/lib/components/profile/ProfileTabs.svelte @@ -1,47 +1,92 @@ diff --git a/web/src/lib/components/profile/organization/OrganizationChrome.svelte b/web/src/lib/components/profile/organization/OrganizationChrome.svelte new file mode 100644 index 000000000..1d51c342e --- /dev/null +++ b/web/src/lib/components/profile/organization/OrganizationChrome.svelte @@ -0,0 +1,155 @@ + + + + + {#if activeTab === "overview"} +
+
+ + +
+
+ +
+ + + Atom feed + + + +
+ + +
+ +
+

+

+ + {#if profile.description} +

{profile.description}

+ {/if} + +
+ + + +
+
+
+ {@render content()} + {:else} +
{@render content()}
+ {/if} +
diff --git a/web/src/lib/components/profile/organization/OrganizationMembersTab.svelte b/web/src/lib/components/profile/organization/OrganizationMembersTab.svelte new file mode 100644 index 000000000..aad36fd4b --- /dev/null +++ b/web/src/lib/components/profile/organization/OrganizationMembersTab.svelte @@ -0,0 +1,13 @@ + + +
+

Members

+
+ {#each organizationMembers as person (person.did)} + + {/each} +
+
diff --git a/web/src/lib/components/profile/organization/OrganizationOverviewTab.svelte b/web/src/lib/components/profile/organization/OrganizationOverviewTab.svelte new file mode 100644 index 000000000..0adfdf5e2 --- /dev/null +++ b/web/src/lib/components/profile/organization/OrganizationOverviewTab.svelte @@ -0,0 +1,91 @@ + + +
+
+

Languages

+
+ {#each organizationLanguages as language (language)} + + + {language} + + {/each} +
+
+ +
+

Topics

+
+ {#each organizationTopics as topic (topic)} + {topic} + {/each} +
+
+ +
+

Pinned repositories

+ {#if repositories.length > 0} +
+ {#each repositories as repo (repo.repoDid || repo.rkey || repo.name)} + + {/each} +
+
+ +
+ {:else} + + {/if} +
+
diff --git a/web/src/lib/components/profile/organization/OrganizationSettingsTab.svelte b/web/src/lib/components/profile/organization/OrganizationSettingsTab.svelte new file mode 100644 index 000000000..6df8f51a7 --- /dev/null +++ b/web/src/lib/components/profile/organization/OrganizationSettingsTab.svelte @@ -0,0 +1,10 @@ + + + diff --git a/web/src/lib/components/profile/organization/mock.ts b/web/src/lib/components/profile/organization/mock.ts new file mode 100644 index 000000000..59b0dc398 --- /dev/null +++ b/web/src/lib/components/profile/organization/mock.ts @@ -0,0 +1,27 @@ +import type { PersonData } from "$lib/components/profile/types"; + +export const organizationLanguages = ["Go", "Rust", "JavaScript"]; + +export const organizationTopics = ["climate-change", "biology", "astronomy", "design"]; + +export const organizationManager = { + did: "did:plc:organization-manager", + handle: "user.tld" +}; + +export const organizationMembers: PersonData[] = Array.from({ length: 9 }, (_, index) => ({ + did: `did:plc:organization-member-${index + 1}`, + handle: index === 0 ? "user.tld" : `member-${index + 1}.tld`, + description: "Organization member", + followers: 0, + following: 0, + viewerFollowRkey: null, + isSelf: false +})); + +export const organizationMemberCount = 86; + +export const organizationRepoStats = { + issues: 94, + pulls: 76 +}; diff --git a/web/src/lib/components/ui/Tag.svelte b/web/src/lib/components/ui/Tag.svelte index 26ac74d68..bcb228ed0 100644 --- a/web/src/lib/components/ui/Tag.svelte +++ b/web/src/lib/components/ui/Tag.svelte @@ -20,7 +20,7 @@ size: { small: "px-1 typography-paragraph-mini", regular: "px-1.5 py-0.5 typography-paragraph-small", - large: "gap-2 px-2 py-1 typography-paragraph-regular font-normal" + large: "gap-2 px-2 py-1.5 typography-paragraph-regular font-normal" } }, defaultVariants: { diff --git a/web/src/routes/[handle]/+layout.svelte b/web/src/routes/[handle]/+layout.svelte index dfbeab382..fcc488be7 100644 --- a/web/src/routes/[handle]/+layout.svelte +++ b/web/src/routes/[handle]/+layout.svelte @@ -14,7 +14,9 @@ const tabParam = $derived(page.url.searchParams.get("tab") ?? "overview"); const activeTab = $derived( - ["repos", "starred", "strings", "vouches"].includes(tabParam) ? tabParam : "overview" + ["repos", "starred", "strings", "vouches", "members", "settings"].includes(tabParam) + ? tabParam + : "overview" ); // bare data accesses can see mid-commit state across child reads, derived stays coherent diff --git a/web/src/routes/[handle]/+layout.ts b/web/src/routes/[handle]/+layout.ts index 0b0ef5691..c794b144c 100644 --- a/web/src/routes/[handle]/+layout.ts +++ b/web/src/routes/[handle]/+layout.ts @@ -1,4 +1,4 @@ -import { browser } from "$app/environment"; +import { browser, dev } from "$app/environment"; import { error, redirect } from "@sveltejs/kit"; import { createBobbinClient } from "$lib/api/client"; import type { MiniDoc } from "$lib/api/identity"; @@ -44,6 +44,7 @@ export const load: LayoutLoad = async (event) => { fetch: event.fetch }); const viewerDid = parent.auth?.did; + const previewOrganization = dev && event.url.searchParams.get("preview") === "organization"; // server awaits for 404s and redirects, client navigation streams for instant commit const resolved = (async () => { @@ -90,15 +91,20 @@ export const load: LayoutLoad = async (event) => { const followPromise = stream(resolved.then(({ viewerFollowRkey }) => viewerFollowRkey)); const profilePromise = stream( - resolved.then(({ identity }): Promise => - getProfile(ctx, identity.did) + resolved.then(async ({ identity }): Promise => { + const profile = await getProfile(ctx, identity.did) .then((view) => view.value) .catch((cause) => { if (cause instanceof ClientResponseError && httpStatusFor(cause) === 404) return null; return toHttpError(cause, "Could not load profile"); - }) - ) + }); + if (!previewOrganization) return profile; + return { + ...(profile ?? { $type: "sh.tangled.actor.profile", bluesky: false }), + isOrganization: true + }; + }) ); // direct vouch is public so it loads here, network vouches require viewer auth client-side diff --git a/web/src/routes/[handle]/+page.svelte b/web/src/routes/[handle]/+page.svelte index 3ccc1ecd9..2fa4790f2 100644 --- a/web/src/routes/[handle]/+page.svelte +++ b/web/src/routes/[handle]/+page.svelte @@ -5,6 +5,9 @@ import StarredTab from "$lib/components/profile/tabs/StarredTab.svelte"; import StringListTab from "$lib/components/profile/tabs/StringListTab.svelte"; import VouchTab from "$lib/components/profile/tabs/VouchTab.svelte"; + import OrganizationMembersTab from "$lib/components/profile/organization/OrganizationMembersTab.svelte"; + import OrganizationOverviewTab from "$lib/components/profile/organization/OrganizationOverviewTab.svelte"; + import OrganizationSettingsTab from "$lib/components/profile/organization/OrganizationSettingsTab.svelte"; import Pending from "$lib/components/ui/Pending.svelte"; import Bones from "$lib/components/ui/Bones.svelte"; import Section from "$lib/components/ui/Section.svelte"; @@ -30,11 +33,16 @@ {/snippet} {@const identity = await data.identity} - + {@const profile = await data.profile} + {#if profile?.isOrganization} + + {:else} + + {/if} {:else if data.tab === "repos"} @@ -113,4 +121,12 @@ {@const identity = await data.identity} +{:else if data.tab === "members"} + {#if (await data.profile)?.isOrganization} + + {/if} +{:else if data.tab === "settings"} + {#if (await data.profile)?.isOrganization} + + {/if} {/if} diff --git a/web/src/routes/[handle]/+page.ts b/web/src/routes/[handle]/+page.ts index d6583a8b5..f9bbbf786 100644 --- a/web/src/routes/[handle]/+page.ts +++ b/web/src/routes/[handle]/+page.ts @@ -19,7 +19,9 @@ const TABS = [ "strings", "followers", "following", - "vouches" + "vouches", + "members", + "settings" ] as const; type Tab = (typeof TABS)[number]; @@ -80,6 +82,10 @@ export const load: PageLoad = async (event) => { isSelf: stream(identity.then(({ did }) => viewerDid === did)) }); } + case "members": + return { tab: "members" as const }; + case "settings": + return { tab: "settings" as const }; case "starred": { const stars = identity.then(({ did }) => cached(`profile:${viewerDid}:${did}:starred`, 30_000, () => -- 2.51.2