Update public-facing orgs page, componetize page headers (#2307)

* Update public-facing orgs page, componetize page headers

* Improve supported environments

* Move user page stats to top and remove details card

* Fix padding on orgs page when no navlinks

* fix lint

---------

Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Prospector
2024-08-28 10:12:25 -07:00
committed by GitHub
parent 4b75cb8357
commit 8311451420
6 changed files with 567 additions and 575 deletions

View File

@@ -62,6 +62,10 @@
.normal-page__content { .normal-page__content {
grid-area: content; grid-area: content;
} }
.normal-page__header {
grid-area: header;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
@@ -161,4 +165,8 @@
max-width: calc(80rem - 18.75rem - 0.75rem); max-width: calc(80rem - 18.75rem - 0.75rem);
//overflow-x: hidden; //overflow-x: hidden;
} }
.normal-page__header {
grid-area: header;
}
} }

View File

@@ -430,33 +430,25 @@
}" }"
> >
<div class="normal-page__header relative my-4"> <div class="normal-page__header relative my-4">
<div <ContentPageHeader>
class="grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]" <template #icon>
>
<div class="flex gap-4">
<Avatar :src="project.icon_url" :alt="project.title" size="96px" /> <Avatar :src="project.icon_url" :alt="project.title" size="96px" />
<div class="flex flex-col gap-1"> </template>
<div class="flex flex-wrap items-center gap-2"> <template #title>
<h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
{{ project.title }} {{ project.title }}
</h1> </template>
<Badge <template #title-suffix>
v-if="auth.user && currentMember" <Badge v-if="auth.user && currentMember" :type="project.status" class="status-badge" />
:type="project.status" </template>
class="status-badge" <template #summary>
/>
</div>
<p class="m-0 line-clamp-2 max-w-[40rem]">
{{ project.description }} {{ project.description }}
</p> </template>
<div class="mt-auto flex flex-wrap gap-4"> <template #stats>
<div <div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4" class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
> >
<DownloadIcon class="h-6 w-6 text-secondary" /> <DownloadIcon class="h-6 w-6 text-secondary" />
<span class="font-semibold">
{{ $formatNumber(project.downloads) }} {{ $formatNumber(project.downloads) }}
</span>
</div> </div>
<div <div
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r" class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
@@ -478,11 +470,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> </template>
</div> <template #actions>
</div>
<div class="flex flex-col justify-center gap-4">
<div class="flex flex-wrap gap-2">
<div class="hidden sm:contents"> <div class="hidden sm:contents">
<ButtonStyled <ButtonStyled
size="large" size="large"
@@ -641,9 +630,8 @@
</template> </template>
</OverflowMenu> </OverflowMenu>
</ButtonStyled> </ButtonStyled>
</div> </template>
</div> </ContentPageHeader>
</div>
<ProjectMemberHeader <ProjectMemberHeader
v-if="currentMember" v-if="currentMember"
:project="project" :project="project"
@@ -701,13 +689,13 @@
" "
> >
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3> <h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="status-list"> <div class="tag-list">
<div <div
v-if=" v-if="
(project.client_side === 'required' && project.server_side !== 'required') || (project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional') (project.client_side === 'optional' && project.server_side === 'optional')
" "
class="status-list__item" class="tag-list__item"
> >
<ClientIcon aria-hidden="true" /> <ClientIcon aria-hidden="true" />
Client-side Client-side
@@ -717,33 +705,28 @@
(project.server_side === 'required' && project.client_side !== 'required') || (project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional') (project.client_side === 'optional' && project.server_side === 'optional')
" "
class="status-list__item" class="tag-list__item"
> >
<ServerIcon aria-hidden="true" /> <ServerIcon aria-hidden="true" />
Server-side Server-side
</div> </div>
<div v-if="false" class="status-list__item"> <div v-if="false" class="tag-list__item">
<UserIcon aria-hidden="true" /> <UserIcon aria-hidden="true" />
Singleplayer Singleplayer
</div> </div>
<div <div
v-if="project.client_side === 'required' && project.server_side === 'required'" v-if="
class="status-list__item" project.project_type !== 'datapack' &&
> ((project.client_side === 'required' && project.server_side === 'required') ||
<MonitorSmartphoneIcon aria-hidden="true" />
Client and server
</div>
<div
v-else-if="
project.client_side === 'optional' || project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional') || (project.client_side === 'required' && project.server_side === 'optional') ||
project.server_side === 'optional' || project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional') (project.server_side === 'required' && project.client_side === 'optional'))
" "
class="status-list__item" class="tag-list__item"
> >
<MonitorSmartphoneIcon aria-hidden="true" /> <MonitorSmartphoneIcon aria-hidden="true" />
Client and server <span class="text-sm">(optional)</span> Client and server
</div> </div>
</div> </div>
</section> </section>
@@ -1044,6 +1027,7 @@ import {
OverflowMenu, OverflowMenu,
PopoutMenu, PopoutMenu,
ScrollablePanel, ScrollablePanel,
ContentPageHeader,
} from "@modrinth/ui"; } from "@modrinth/ui";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils"; import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -1734,10 +1718,6 @@ const navLinks = computed(() => {
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.normal-page__header {
grid-area: header;
}
.settings-header { .settings-header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;

View File

@@ -1,7 +1,13 @@
<template> <template>
<div v-if="organization" class="normal-page"> <div
v-if="organization"
class="experimental-styles-within new-page sidebar"
:class="{ 'alt-layout': cosmetics.leftContentLayout || routeHasSettings }"
>
<ModalCreation ref="modal_creation" :organization-id="organization.id" />
<template v-if="routeHasSettings">
<div class="normal-page__sidebar"> <div class="normal-page__sidebar">
<div v-if="routeHasSettings" class="universal-card"> <div class="universal-card">
<Breadcrumbs <Breadcrumbs
current-title="Settings" current-title="Settings"
:link-stack="[ :link-stack="[
@@ -13,7 +19,6 @@
}, },
]" ]"
/> />
<div class="page-header__settings"> <div class="page-header__settings">
<Avatar size="sm" :src="organization.icon_url" /> <Avatar size="sm" :src="organization.icon_url" />
<div class="title-section"> <div class="title-section">
@@ -55,84 +60,118 @@
</NavStackItem> </NavStackItem>
</NavStack> </NavStack>
</div> </div>
</div>
<div class="normal-page__content">
<NuxtPage />
</div>
</template>
<template v-else> <template v-else>
<div class="universal-card"> <div class="normal-page__header py-4">
<div class="page-header__icon"> <ContentPageHeader>
<Avatar size="md" :src="organization.icon_url" /> <template #icon>
<Avatar :src="organization.icon_url" :alt="organization.name" size="96px" />
</template>
<template #title>
{{ organization.name }}
</template>
<template #title-suffix>
<div class="ml-1 flex items-center gap-2 font-semibold">
<OrganizationIcon /> Organization
</div> </div>
</template>
<div class="page-header__text"> <template #summary>
<h1 class="title">{{ organization.name }}</h1> {{ organization.description }}
</template>
<div> <template #stats>
<span class="organization-label"><OrganizationIcon /> Organization</span> <div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<UsersIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(acceptedMembers?.length || 0) }}
members
</div> </div>
<div
<div class="organization-description"> class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
<div class="metadata-item markdown-body collection-description"> >
<p>{{ organization.description }}</p> <BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
</div> </div>
<div class="flex items-center gap-2 font-semibold">
<hr class="card-divider" /> <DownloadIcon class="h-6 w-6 text-secondary" />
<div class="primary-stat">
<UserIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ $formatNumber(acceptedMembers?.length || 0) }}
</span>
member<template v-if="acceptedMembers?.length !== 1">s</template>
</div>
</div>
<div class="primary-stat">
<BoxIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ $formatNumber(projects?.length || 0) }}
</span>
project<span v-if="projects?.length !== 1">s</span>
</div>
</div>
<div class="primary-stat no-margin">
<DownloadIcon class="primary-stat__icon" aria-hidden="true" />
<div class="primary-stat__text">
<span class="primary-stat__counter">
{{ formatCompactNumber(sumDownloads) }} {{ formatCompactNumber(sumDownloads) }}
</span> downloads
download<span v-if="sumDownloads !== 1">s</span>
</div> </div>
</template>
<template #actions>
<ButtonStyled v-if="auth.user && currentMember" size="large">
<NuxtLink :to="`/organization/${organization.slug}/settings`">
<SettingsIcon aria-hidden="true" />
Manage
</NuxtLink>
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:options="[
{
id: 'manage-projects',
action: () =>
navigateTo('/organization/' + organization.slug + '/settings/projects'),
hoverOnly: true,
shown: auth.user && currentMember,
},
{ divider: true, shown: auth.user && currentMember },
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #manage-projects>
<BoxIcon aria-hidden="true" />
Manage projects
</template>
<template #copy-id>
<ClipboardCopyIcon aria-hidden="true" />
{{ formatMessage(commonMessages.copyIdButton) }}
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
</div> </div>
</div> <div class="normal-page__sidebar">
</div>
</div>
<AdPlaceholder <AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus" v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/> />
<div class="creator-list universal-card"> <div class="card flex-card">
<div class="title-and-link"> <h2>Members</h2>
<h3>Members</h3> <div class="details-list">
</div>
<template v-for="member in acceptedMembers" :key="member.user.id"> <template v-for="member in acceptedMembers" :key="member.user.id">
<nuxt-link class="creator button-base" :to="`/user/${member.user.username}`"> <nuxt-link
class="details-list__item details-list__item--type-large"
:to="`/user/${member.user.username}`"
>
<Avatar :src="member.user.avatar_url" circle /> <Avatar :src="member.user.avatar_url" circle />
<p class="name"> <div class="rows">
<span class="flex items-center gap-1">
{{ member.user.username }} {{ member.user.username }}
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" /> <CrownIcon
</p> v-if="member.is_owner"
<p class="role">{{ member.role }}</p> v-tooltip="'Organization owner'"
class="text-brand-orange"
/>
</span>
<span class="details-list__item__text--style-secondary">
{{ member.role ? member.role : "Member" }}
</span>
</div>
</nuxt-link> </nuxt-link>
</template> </template>
</div> </div>
</template>
</div> </div>
<div v-if="!routeHasSettings" class="normal-page__content"> </div>
<ModalCreation ref="modal_creation" :organization-id="organization.id" /> <div class="normal-page__content">
<div v-if="isInvited" class="universal-card information invited"> <div v-if="isInvited" class="universal-card information invited">
<h2>Invitation to join {{ organization.name }}</h2> <h2>Invitation to join {{ organization.name }}</h2>
<p>You have been invited to join {{ organization.name }}.</p> <p>You have been invited to join {{ organization.name }}.</p>
@@ -145,28 +184,9 @@
</button> </button>
</div> </div>
</div> </div>
<nav class="navigation-card"> <div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
<NavRow <NavTabs :links="navLinks" />
:links="[
{
label: formatMessage(commonMessages.allProjectType),
href: `/organization/${organization.slug}`,
},
...projectTypes.map((x) => {
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/organization/${organization.slug}/${x}s`,
};
}),
]"
/>
<div v-if="auth.user && currentMember" class="input-group">
<nuxt-link :to="`/organization/${organization.slug}/settings`" class="iconified-button">
<SettingsIcon /> Manage
</nuxt-link>
</div> </div>
</nav>
<template v-if="projects?.length > 0"> <template v-if="projects?.length > 0">
<div class="project-list display-mode--list"> <div class="project-list display-mode--list">
<ProjectCard <ProjectCard
@@ -217,24 +237,24 @@
</span> </span>
</div> </div>
</div> </div>
<NuxtPage /> </template>
</div> </div>
</template> </template>
<script setup> <script setup>
import { import {
BoxIcon, BoxIcon,
UserIcon, MoreVerticalIcon,
UsersIcon, UsersIcon,
SettingsIcon, SettingsIcon,
ChartIcon, ChartIcon,
CheckIcon, CheckIcon,
XIcon, XIcon,
ClipboardCopyIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { Avatar, Breadcrumbs } from "@modrinth/ui"; import { Avatar, ButtonStyled, Breadcrumbs, ContentPageHeader, OverflowMenu } from "@modrinth/ui";
import NavStack from "~/components/ui/NavStack.vue"; import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue"; import NavStackItem from "~/components/ui/NavStackItem.vue";
import NavRow from "~/components/ui/NavRow.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component"; import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
@@ -244,6 +264,7 @@ import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
import DownloadIcon from "~/assets/images/utils/download.svg?component"; import DownloadIcon from "~/assets/images/utils/download.svg?component";
import CrownIcon from "~/assets/images/utils/crown.svg?component"; import CrownIcon from "~/assets/images/utils/crown.svg?component";
import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js"; import { acceptTeamInvite, removeTeamMember } from "~/helpers/teams.js";
import NavTabs from "~/components/ui/NavTabs.vue";
const vintl = useVIntl(); const vintl = useVIntl();
const { formatMessage } = vintl; const { formatMessage } = vintl;
@@ -451,6 +472,26 @@ useSeoMeta({
ogDescription: organization.value.description, ogDescription: organization.value.description,
ogImage: organization.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png", ogImage: organization.value.icon_url ?? "https://cdn.modrinth.com/placeholder.png",
}); });
const navLinks = computed(() => [
{
label: formatMessage(commonMessages.allProjectType),
href: `/organization/${organization.value.slug}`,
},
...projectTypes.value
.map((x) => {
return {
label: formatMessage(getProjectTypeMessage(x, true)),
href: `/organization/${organization.value.slug}/${x}s`,
};
})
.slice()
.sort((a, b) => a.label.localeCompare(b.label)),
]);
async function copyId() {
await navigator.clipboard.writeText(organization.value.id);
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -3,19 +3,15 @@
<ModalCreation ref="modal_creation" /> <ModalCreation ref="modal_creation" />
<CollectionCreateModal ref="modal_collection_creation" /> <CollectionCreateModal ref="modal_collection_creation" />
<div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }"> <div class="new-page sidebar" :class="{ 'alt-layout': cosmetics.leftContentLayout }">
<div class="normal-page__header pt-4"> <div class="normal-page__header py-4">
<div <ContentPageHeader>
class="mb-4 grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]" <template #icon>
>
<div class="flex gap-4">
<Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle /> <Avatar :src="user.avatar_url" :alt="user.username" size="96px" circle />
<div class="flex flex-col gap-2"> </template>
<div class="flex flex-wrap items-center gap-2"> <template #title>
<h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
{{ user.username }} {{ user.username }}
</h1> </template>
</div> <template #summary>
<p class="m-0 line-clamp-2 max-w-[40rem]">
{{ {{
user.bio user.bio
? user.bio ? user.bio
@@ -23,11 +19,29 @@
? "A Modrinth user." ? "A Modrinth user."
: "A Modrinth creator." : "A Modrinth creator."
}} }}
</p> </template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
projects
</div> </div>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(sumDownloads) }}
downloads
</div> </div>
<div class="flex flex-col justify-center gap-4"> <div class="flex items-center gap-2 font-semibold">
<div class="flex flex-wrap gap-2"> <CalendarIcon class="h-6 w-6 text-secondary" />
Joined
{{ formatRelativeTime(user.created) }}
</div>
</template>
<template #actions>
<ButtonStyled size="large"> <ButtonStyled size="large">
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile"> <NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
<EditIcon aria-hidden="true" /> <EditIcon aria-hidden="true" />
@@ -69,9 +83,8 @@
</template> </template>
</OverflowMenu> </OverflowMenu>
</ButtonStyled> </ButtonStyled>
</div> </template>
</div> </ContentPageHeader>
</div>
</div> </div>
<div class="normal-page__content"> <div class="normal-page__content">
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto"> <div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
@@ -194,72 +207,6 @@
</div> </div>
</div> </div>
<div class="normal-page__sidebar"> <div class="normal-page__sidebar">
<div class="card flex-card">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileDetails) }}</h2>
<div class="flex items-center gap-2">
<BoxIcon aria-hidden="true" class="stroke-[3] text-secondary" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileProjectsStats"
:values="{ count: formatCompactNumber(projects.length) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<DownloadIcon aria-hidden="true" class="stroke-[3] text-secondary" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileDownloadsStats"
:values="{ count: formatCompactNumber(sumDownloads) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<HeartIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileProjectsFollowersStats"
:values="{ count: formatCompactNumber(sumFollows) }"
>
<template #stat="{ children }">
<span class="font-bold text-primary">
<component :is="() => normalizeChildren(children)" />
</span>
</template>
</IntlFormatted>
</div>
</div>
<div class="flex items-center gap-2">
<CalendarIcon aria-hidden="true" class="text-secondary *:stroke-[3]" />
<div class="text-secondary">
<IntlFormatted
:message-id="messages.profileJoinedAt"
:values="{ ago: formatRelativeTime(user.created) }"
>
<template #date="{ children }">
<span class="font-bold text-primary">
<component :is="() => children" />
</span>
</template>
</IntlFormatted>
</div>
</div>
</div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
<div v-if="organizations.length > 0" class="card flex-card"> <div v-if="organizations.length > 0" class="card flex-card">
<h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2> <h2 class="text-lg text-contrast">{{ formatMessage(messages.profileOrganizations) }}</h2>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
@@ -288,6 +235,9 @@
</div> </div>
</div> </div>
</div> </div>
<AdPlaceholder
v-if="!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -304,7 +254,7 @@ import {
ClipboardCopyIcon, ClipboardCopyIcon,
MoreVerticalIcon, MoreVerticalIcon,
} from "@modrinth/assets"; } from "@modrinth/assets";
import { OverflowMenu, ButtonStyled } from "@modrinth/ui"; import { OverflowMenu, ButtonStyled, ContentPageHeader } from "@modrinth/ui";
import NavTabs from "~/components/ui/NavTabs.vue"; import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue"; import ProjectCard from "~/components/ui/ProjectCard.vue";
import { reportUser } from "~/utils/report-helpers.ts"; import { reportUser } from "~/utils/report-helpers.ts";
@@ -318,7 +268,6 @@ import EarlyAdopterBadge from "~/assets/images/badges/early-adopter.svg?componen
import ReportIcon from "~/assets/images/utils/report.svg?component"; import ReportIcon from "~/assets/images/utils/report.svg?component";
import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component"; import UpToDate from "~/assets/images/illustrations/up_to_date.svg?component";
import EditIcon from "~/assets/images/utils/edit.svg?component"; import EditIcon from "~/assets/images/utils/edit.svg?component";
import HeartIcon from "~/assets/images/utils/heart.svg?component";
import WorldIcon from "~/assets/images/utils/world.svg?component"; import WorldIcon from "~/assets/images/utils/world.svg?component";
import ModalCreation from "~/components/ui/ModalCreation.vue"; import ModalCreation from "~/components/ui/ModalCreation.vue";
import Avatar from "~/components/ui/Avatar.vue"; import Avatar from "~/components/ui/Avatar.vue";
@@ -505,15 +454,6 @@ const sumDownloads = computed(() => {
return sum; return sum;
}); });
const sumFollows = computed(() => {
let sum = 0;
for (const project of projects.value) {
sum += project.followers;
}
return sum;
});
const badges = computed(() => { const badges = computed(() => {
const badges = []; const badges = [];
@@ -653,8 +593,4 @@ export default defineNuxtComponent({
} }
} }
} }
.normal-page__header {
grid-area: header;
}
</style> </style>

View File

@@ -0,0 +1,26 @@
<template>
<div
class="grid grid-cols-1 gap-x-8 gap-y-6 border-0 border-b border-solid border-button-bg pb-6 lg:grid-cols-[1fr_auto]"
>
<div class="flex gap-4">
<slot name="icon" />
<div class="flex flex-col gap-1">
<div class="flex flex-wrap items-center gap-2">
<h1 class="m-0 text-2xl font-extrabold leading-none text-contrast">
<slot name="title" />
</h1>
<slot name="title-suffix" />
</div>
<p class="m-0 line-clamp-2 max-w-[40rem]">
<slot name="summary" />
</p>
<div class="mt-auto flex flex-wrap gap-4">
<slot name="stats" />
</div>
</div>
</div>
<div class="flex flex-wrap gap-2 items-center">
<slot name="actions" />
</div>
</div>
</template>

View File

@@ -7,6 +7,7 @@ export { default as Card } from './base/Card.vue'
export { default as Checkbox } from './base/Checkbox.vue' export { default as Checkbox } from './base/Checkbox.vue'
export { default as Chips } from './base/Chips.vue' export { default as Chips } from './base/Chips.vue'
export { default as ConditionalNuxtLink } from './base/ConditionalNuxtLink.vue' export { default as ConditionalNuxtLink } from './base/ConditionalNuxtLink.vue'
export { default as ContentPageHeader } from './base/ContentPageHeader.vue'
export { default as CopyCode } from './base/CopyCode.vue' export { default as CopyCode } from './base/CopyCode.vue'
export { default as DoubleIcon } from './base/DoubleIcon.vue' export { default as DoubleIcon } from './base/DoubleIcon.vue'
export { default as DropArea } from './base/DropArea.vue' export { default as DropArea } from './base/DropArea.vue'