You've already forked AstralRinth
forked from didirus/AstralRinth
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:
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -430,220 +430,208 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<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 }}
|
</template>
|
||||||
</h1>
|
<template #title-suffix>
|
||||||
<Badge
|
<Badge v-if="auth.user && currentMember" :type="project.status" class="status-badge" />
|
||||||
v-if="auth.user && currentMember"
|
</template>
|
||||||
:type="project.status"
|
<template #summary>
|
||||||
class="status-badge"
|
{{ project.description }}
|
||||||
/>
|
</template>
|
||||||
</div>
|
<template #stats>
|
||||||
<p class="m-0 line-clamp-2 max-w-[40rem]">
|
<div
|
||||||
{{ project.description }}
|
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
|
||||||
</p>
|
>
|
||||||
<div class="mt-auto flex flex-wrap gap-4">
|
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||||
|
{{ $formatNumber(project.downloads) }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
|
||||||
|
>
|
||||||
|
<HeartIcon class="h-6 w-6 text-secondary" />
|
||||||
|
<span class="font-semibold">
|
||||||
|
{{ $formatNumber(project.followers) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="hidden items-center gap-2 md:flex">
|
||||||
|
<TagsIcon class="h-6 w-6 text-secondary" />
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4"
|
v-for="(category, index) in project.categories"
|
||||||
|
:key="index"
|
||||||
|
class="tag-list__item"
|
||||||
>
|
>
|
||||||
<DownloadIcon class="h-6 w-6 text-secondary" />
|
{{ formatCategory(category) }}
|
||||||
<span class="font-semibold">
|
|
||||||
{{ $formatNumber(project.downloads) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex items-center gap-2 border-0 border-solid border-button-bg pr-4 md:border-r"
|
|
||||||
>
|
|
||||||
<HeartIcon class="h-6 w-6 text-secondary" />
|
|
||||||
<span class="font-semibold">
|
|
||||||
{{ $formatNumber(project.followers) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="hidden items-center gap-2 md:flex">
|
|
||||||
<TagsIcon class="h-6 w-6 text-secondary" />
|
|
||||||
<div class="flex flex-wrap gap-2">
|
|
||||||
<div
|
|
||||||
v-for="(category, index) in project.categories"
|
|
||||||
:key="index"
|
|
||||||
class="tag-list__item"
|
|
||||||
>
|
|
||||||
{{ formatCategory(category) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<div class="flex flex-col justify-center gap-4">
|
<template #actions>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="hidden sm:contents">
|
||||||
<div class="hidden sm:contents">
|
<ButtonStyled
|
||||||
<ButtonStyled
|
size="large"
|
||||||
size="large"
|
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
||||||
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
>
|
||||||
>
|
<button @click="(event) => downloadModal.show(event)">
|
||||||
<button @click="(event) => downloadModal.show(event)">
|
<DownloadIcon aria-hidden="true" />
|
||||||
<DownloadIcon aria-hidden="true" />
|
Download
|
||||||
Download
|
</button>
|
||||||
</button>
|
</ButtonStyled>
|
||||||
</ButtonStyled>
|
</div>
|
||||||
</div>
|
<div class="contents sm:hidden">
|
||||||
<div class="contents sm:hidden">
|
|
||||||
<ButtonStyled
|
|
||||||
size="large"
|
|
||||||
circular
|
|
||||||
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
aria-label="Download"
|
|
||||||
class="flex sm:hidden"
|
|
||||||
@click="(event) => downloadModal.show(event)"
|
|
||||||
>
|
|
||||||
<DownloadIcon aria-hidden="true" />
|
|
||||||
</button>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
|
||||||
<ButtonStyled
|
<ButtonStyled
|
||||||
size="large"
|
size="large"
|
||||||
circular
|
circular
|
||||||
:color="following ? 'red' : 'standard'"
|
:color="route.name === 'type-id-version-version' ? `standard` : `brand`"
|
||||||
color-fill="none"
|
|
||||||
hover-color-fill="background"
|
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="auth.user"
|
aria-label="Download"
|
||||||
v-tooltip="following ? `Unfollow` : `Follow`"
|
class="flex sm:hidden"
|
||||||
:aria-label="following ? `Unfollow` : `Follow`"
|
@click="(event) => downloadModal.show(event)"
|
||||||
@click="userFollowProject(project)"
|
|
||||||
>
|
>
|
||||||
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
|
<DownloadIcon aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<nuxt-link v-else v-tooltip="'Follow'" to="/auth/sign-in" aria-label="Follow">
|
|
||||||
<HeartIcon aria-hidden="true" />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled size="large" circular>
|
|
||||||
<PopoutMenu v-if="auth.user" v-tooltip="'Save'" from="top-right" aria-label="Save">
|
|
||||||
<BookmarkIcon
|
|
||||||
aria-hidden="true"
|
|
||||||
:fill="
|
|
||||||
collections.some((x) => x.projects.includes(project.id))
|
|
||||||
? 'currentColor'
|
|
||||||
: 'none'
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<template #menu>
|
|
||||||
<input
|
|
||||||
v-model="displayCollectionsSearch"
|
|
||||||
type="text"
|
|
||||||
placeholder="Search collections..."
|
|
||||||
class="search-input menu-search"
|
|
||||||
/>
|
|
||||||
<div v-if="collections.length > 0" class="collections-list">
|
|
||||||
<Checkbox
|
|
||||||
v-for="option in collections
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => a.name.localeCompare(b.name))"
|
|
||||||
:key="option.id"
|
|
||||||
:model-value="option.projects.includes(project.id)"
|
|
||||||
class="popout-checkbox"
|
|
||||||
@update:model-value="() => onUserCollectProject(option, project.id)"
|
|
||||||
>
|
|
||||||
{{ option.name }}
|
|
||||||
</Checkbox>
|
|
||||||
</div>
|
|
||||||
<div v-else class="menu-text">
|
|
||||||
<p class="popout-text">No collections found.</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
class="btn collection-button"
|
|
||||||
@click="(event) => $refs.modal_collection.show(event)"
|
|
||||||
>
|
|
||||||
<PlusIcon aria-hidden="true" />
|
|
||||||
Create new collection
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</PopoutMenu>
|
|
||||||
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
|
|
||||||
<BookmarkIcon aria-hidden="true" />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
|
|
||||||
<nuxt-link
|
|
||||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
|
||||||
>
|
|
||||||
<SettingsIcon aria-hidden="true" />
|
|
||||||
</nuxt-link>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled size="large" circular type="transparent">
|
|
||||||
<OverflowMenu
|
|
||||||
:options="[
|
|
||||||
{
|
|
||||||
id: 'analytics',
|
|
||||||
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
|
|
||||||
hoverOnly: true,
|
|
||||||
shown: auth.user && !!currentMember,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
divider: true,
|
|
||||||
shown: auth.user && !!currentMember,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'moderation-checklist',
|
|
||||||
action: () => (showModerationChecklist = true),
|
|
||||||
color: 'orange',
|
|
||||||
hoverOnly: true,
|
|
||||||
shown:
|
|
||||||
auth.user &&
|
|
||||||
tags.staffRoles.includes(auth.user.role) &&
|
|
||||||
!showModerationChecklist,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
divider: true,
|
|
||||||
shown:
|
|
||||||
auth.user &&
|
|
||||||
tags.staffRoles.includes(auth.user.role) &&
|
|
||||||
!showModerationChecklist,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'report',
|
|
||||||
action: () =>
|
|
||||||
auth.user ? reportProject(project.id) : navigateTo('/auth/sign-in'),
|
|
||||||
color: 'red',
|
|
||||||
hoverOnly: true,
|
|
||||||
},
|
|
||||||
{ id: 'copy-id', action: () => copyId() },
|
|
||||||
]"
|
|
||||||
aria-label="More options"
|
|
||||||
>
|
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
|
||||||
<template #analytics>
|
|
||||||
<ChartIcon aria-hidden="true" />
|
|
||||||
Analytics
|
|
||||||
</template>
|
|
||||||
<template #moderation-checklist>
|
|
||||||
<ScaleIcon aria-hidden="true" />
|
|
||||||
Review project
|
|
||||||
</template>
|
|
||||||
<template #report>
|
|
||||||
<ReportIcon aria-hidden="true" />
|
|
||||||
Report
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
|
||||||
<ClipboardCopyIcon aria-hidden="true" />
|
|
||||||
Copy ID
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<ButtonStyled
|
||||||
</div>
|
size="large"
|
||||||
|
circular
|
||||||
|
:color="following ? 'red' : 'standard'"
|
||||||
|
color-fill="none"
|
||||||
|
hover-color-fill="background"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-if="auth.user"
|
||||||
|
v-tooltip="following ? `Unfollow` : `Follow`"
|
||||||
|
:aria-label="following ? `Unfollow` : `Follow`"
|
||||||
|
@click="userFollowProject(project)"
|
||||||
|
>
|
||||||
|
<HeartIcon :fill="following ? 'currentColor' : 'none'" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
<nuxt-link v-else v-tooltip="'Follow'" to="/auth/sign-in" aria-label="Follow">
|
||||||
|
<HeartIcon aria-hidden="true" />
|
||||||
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled size="large" circular>
|
||||||
|
<PopoutMenu v-if="auth.user" v-tooltip="'Save'" from="top-right" aria-label="Save">
|
||||||
|
<BookmarkIcon
|
||||||
|
aria-hidden="true"
|
||||||
|
:fill="
|
||||||
|
collections.some((x) => x.projects.includes(project.id))
|
||||||
|
? 'currentColor'
|
||||||
|
: 'none'
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<template #menu>
|
||||||
|
<input
|
||||||
|
v-model="displayCollectionsSearch"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search collections..."
|
||||||
|
class="search-input menu-search"
|
||||||
|
/>
|
||||||
|
<div v-if="collections.length > 0" class="collections-list">
|
||||||
|
<Checkbox
|
||||||
|
v-for="option in collections
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name))"
|
||||||
|
:key="option.id"
|
||||||
|
:model-value="option.projects.includes(project.id)"
|
||||||
|
class="popout-checkbox"
|
||||||
|
@update:model-value="() => onUserCollectProject(option, project.id)"
|
||||||
|
>
|
||||||
|
{{ option.name }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<div v-else class="menu-text">
|
||||||
|
<p class="popout-text">No collections found.</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn collection-button"
|
||||||
|
@click="(event) => $refs.modal_collection.show(event)"
|
||||||
|
>
|
||||||
|
<PlusIcon aria-hidden="true" />
|
||||||
|
Create new collection
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</PopoutMenu>
|
||||||
|
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
|
||||||
|
<BookmarkIcon aria-hidden="true" />
|
||||||
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
|
||||||
|
<nuxt-link
|
||||||
|
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
|
||||||
|
>
|
||||||
|
<SettingsIcon aria-hidden="true" />
|
||||||
|
</nuxt-link>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled size="large" circular type="transparent">
|
||||||
|
<OverflowMenu
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'analytics',
|
||||||
|
link: `/${project.project_type}/${project.slug ? project.slug : project.id}/settings/analytics`,
|
||||||
|
hoverOnly: true,
|
||||||
|
shown: auth.user && !!currentMember,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
divider: true,
|
||||||
|
shown: auth.user && !!currentMember,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'moderation-checklist',
|
||||||
|
action: () => (showModerationChecklist = true),
|
||||||
|
color: 'orange',
|
||||||
|
hoverOnly: true,
|
||||||
|
shown:
|
||||||
|
auth.user &&
|
||||||
|
tags.staffRoles.includes(auth.user.role) &&
|
||||||
|
!showModerationChecklist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
divider: true,
|
||||||
|
shown:
|
||||||
|
auth.user &&
|
||||||
|
tags.staffRoles.includes(auth.user.role) &&
|
||||||
|
!showModerationChecklist,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'report',
|
||||||
|
action: () =>
|
||||||
|
auth.user ? reportProject(project.id) : navigateTo('/auth/sign-in'),
|
||||||
|
color: 'red',
|
||||||
|
hoverOnly: true,
|
||||||
|
},
|
||||||
|
{ id: 'copy-id', action: () => copyId() },
|
||||||
|
]"
|
||||||
|
aria-label="More options"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
|
<template #analytics>
|
||||||
|
<ChartIcon aria-hidden="true" />
|
||||||
|
Analytics
|
||||||
|
</template>
|
||||||
|
<template #moderation-checklist>
|
||||||
|
<ScaleIcon aria-hidden="true" />
|
||||||
|
Review project
|
||||||
|
</template>
|
||||||
|
<template #report>
|
||||||
|
<ReportIcon aria-hidden="true" />
|
||||||
|
Report
|
||||||
|
</template>
|
||||||
|
<template #copy-id>
|
||||||
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
Copy ID
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</ContentPageHeader>
|
||||||
<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,34 +705,29 @@
|
|||||||
(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') ||
|
||||||
|
project.client_side === 'optional' ||
|
||||||
|
(project.client_side === 'required' && project.server_side === 'optional') ||
|
||||||
|
project.server_side === 'optional' ||
|
||||||
|
(project.server_side === 'required' && project.client_side === 'optional'))
|
||||||
|
"
|
||||||
|
class="tag-list__item"
|
||||||
>
|
>
|
||||||
<MonitorSmartphoneIcon aria-hidden="true" />
|
<MonitorSmartphoneIcon aria-hidden="true" />
|
||||||
Client and server
|
Client and server
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-else-if="
|
|
||||||
project.client_side === 'optional' ||
|
|
||||||
(project.client_side === 'required' && project.server_side === 'optional') ||
|
|
||||||
project.server_side === 'optional' ||
|
|
||||||
(project.server_side === 'required' && project.client_side === 'optional')
|
|
||||||
"
|
|
||||||
class="status-list__item"
|
|
||||||
>
|
|
||||||
<MonitorSmartphoneIcon aria-hidden="true" />
|
|
||||||
Client and server <span class="text-sm">(optional)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,240 +1,260 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="organization" class="normal-page">
|
<div
|
||||||
<div class="normal-page__sidebar">
|
v-if="organization"
|
||||||
<div v-if="routeHasSettings" class="universal-card">
|
class="experimental-styles-within new-page sidebar"
|
||||||
<Breadcrumbs
|
:class="{ 'alt-layout': cosmetics.leftContentLayout || routeHasSettings }"
|
||||||
current-title="Settings"
|
>
|
||||||
:link-stack="[
|
<ModalCreation ref="modal_creation" :organization-id="organization.id" />
|
||||||
{ href: `/dashboard/organizations`, label: 'Organizations' },
|
<template v-if="routeHasSettings">
|
||||||
{
|
<div class="normal-page__sidebar">
|
||||||
href: `/organization/${organization.slug}`,
|
|
||||||
label: organization.name,
|
|
||||||
allowTrimming: true,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="page-header__settings">
|
|
||||||
<Avatar size="sm" :src="organization.icon_url" />
|
|
||||||
<div class="title-section">
|
|
||||||
<h2 class="settings-title">
|
|
||||||
<nuxt-link :to="`/organization/${organization.slug}/settings`">
|
|
||||||
{{ organization.name }}
|
|
||||||
</nuxt-link>
|
|
||||||
</h2>
|
|
||||||
<span>
|
|
||||||
{{ $formatNumber(acceptedMembers?.length || 0) }}
|
|
||||||
member<template v-if="acceptedMembers?.length !== 1">s</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Organization settings</h2>
|
|
||||||
|
|
||||||
<NavStack>
|
|
||||||
<NavStackItem :link="`/organization/${organization.slug}/settings`" label="Overview">
|
|
||||||
<SettingsIcon />
|
|
||||||
</NavStackItem>
|
|
||||||
<NavStackItem
|
|
||||||
:link="`/organization/${organization.slug}/settings/members`"
|
|
||||||
label="Members"
|
|
||||||
>
|
|
||||||
<UsersIcon />
|
|
||||||
</NavStackItem>
|
|
||||||
<NavStackItem
|
|
||||||
:link="`/organization/${organization.slug}/settings/projects`"
|
|
||||||
label="Projects"
|
|
||||||
>
|
|
||||||
<BoxIcon />
|
|
||||||
</NavStackItem>
|
|
||||||
<NavStackItem
|
|
||||||
:link="`/organization/${organization.slug}/settings/analytics`"
|
|
||||||
label="Analytics"
|
|
||||||
>
|
|
||||||
<ChartIcon />
|
|
||||||
</NavStackItem>
|
|
||||||
</NavStack>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<div class="universal-card">
|
<div class="universal-card">
|
||||||
<div class="page-header__icon">
|
<Breadcrumbs
|
||||||
<Avatar size="md" :src="organization.icon_url" />
|
current-title="Settings"
|
||||||
</div>
|
:link-stack="[
|
||||||
|
{ href: `/dashboard/organizations`, label: 'Organizations' },
|
||||||
<div class="page-header__text">
|
{
|
||||||
<h1 class="title">{{ organization.name }}</h1>
|
href: `/organization/${organization.slug}`,
|
||||||
|
label: organization.name,
|
||||||
<div>
|
allowTrimming: true,
|
||||||
<span class="organization-label"><OrganizationIcon /> Organization</span>
|
},
|
||||||
</div>
|
]"
|
||||||
|
/>
|
||||||
<div class="organization-description">
|
<div class="page-header__settings">
|
||||||
<div class="metadata-item markdown-body collection-description">
|
<Avatar size="sm" :src="organization.icon_url" />
|
||||||
<p>{{ organization.description }}</p>
|
<div class="title-section">
|
||||||
</div>
|
<h2 class="settings-title">
|
||||||
|
<nuxt-link :to="`/organization/${organization.slug}/settings`">
|
||||||
<hr class="card-divider" />
|
{{ organization.name }}
|
||||||
|
</nuxt-link>
|
||||||
<div class="primary-stat">
|
</h2>
|
||||||
<UserIcon class="primary-stat__icon" aria-hidden="true" />
|
<span>
|
||||||
<div class="primary-stat__text">
|
{{ $formatNumber(acceptedMembers?.length || 0) }}
|
||||||
<span class="primary-stat__counter">
|
member<template v-if="acceptedMembers?.length !== 1">s</template>
|
||||||
{{ $formatNumber(acceptedMembers?.length || 0) }}
|
</span>
|
||||||
</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) }}
|
|
||||||
</span>
|
|
||||||
download<span v-if="sumDownloads !== 1">s</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h2>Organization settings</h2>
|
||||||
|
|
||||||
|
<NavStack>
|
||||||
|
<NavStackItem :link="`/organization/${organization.slug}/settings`" label="Overview">
|
||||||
|
<SettingsIcon />
|
||||||
|
</NavStackItem>
|
||||||
|
<NavStackItem
|
||||||
|
:link="`/organization/${organization.slug}/settings/members`"
|
||||||
|
label="Members"
|
||||||
|
>
|
||||||
|
<UsersIcon />
|
||||||
|
</NavStackItem>
|
||||||
|
<NavStackItem
|
||||||
|
:link="`/organization/${organization.slug}/settings/projects`"
|
||||||
|
label="Projects"
|
||||||
|
>
|
||||||
|
<BoxIcon />
|
||||||
|
</NavStackItem>
|
||||||
|
<NavStackItem
|
||||||
|
:link="`/organization/${organization.slug}/settings/analytics`"
|
||||||
|
label="Analytics"
|
||||||
|
>
|
||||||
|
<ChartIcon />
|
||||||
|
</NavStackItem>
|
||||||
|
</NavStack>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="normal-page__content">
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="normal-page__header py-4">
|
||||||
|
<ContentPageHeader>
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
<template #summary>
|
||||||
|
{{ organization.description }}
|
||||||
|
</template>
|
||||||
|
<template #stats>
|
||||||
|
<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
|
||||||
|
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 class="flex items-center gap-2 font-semibold">
|
||||||
|
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||||
|
{{ formatCompactNumber(sumDownloads) }}
|
||||||
|
downloads
|
||||||
|
</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 class="normal-page__sidebar">
|
||||||
<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">
|
||||||
|
<template v-for="member in acceptedMembers" :key="member.user.id">
|
||||||
|
<nuxt-link
|
||||||
|
class="details-list__item details-list__item--type-large"
|
||||||
|
:to="`/user/${member.user.username}`"
|
||||||
|
>
|
||||||
|
<Avatar :src="member.user.avatar_url" circle />
|
||||||
|
<div class="rows">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
{{ member.user.username }}
|
||||||
|
<CrownIcon
|
||||||
|
v-if="member.is_owner"
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-for="member in acceptedMembers" :key="member.user.id">
|
|
||||||
<nuxt-link class="creator button-base" :to="`/user/${member.user.username}`">
|
|
||||||
<Avatar :src="member.user.avatar_url" circle />
|
|
||||||
<p class="name">
|
|
||||||
{{ member.user.username }}
|
|
||||||
<CrownIcon v-if="member.is_owner" v-tooltip="'Organization owner'" />
|
|
||||||
</p>
|
|
||||||
<p class="role">{{ member.role }}</p>
|
|
||||||
</nuxt-link>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div v-if="!routeHasSettings" class="normal-page__content">
|
|
||||||
<ModalCreation ref="modal_creation" :organization-id="organization.id" />
|
|
||||||
<div v-if="isInvited" class="universal-card information invited">
|
|
||||||
<h2>Invitation to join {{ organization.name }}</h2>
|
|
||||||
<p>You have been invited to join {{ organization.name }}.</p>
|
|
||||||
<div class="input-group">
|
|
||||||
<button class="iconified-button brand-button" @click="onAcceptInvite">
|
|
||||||
<CheckIcon />Accept
|
|
||||||
</button>
|
|
||||||
<button class="iconified-button danger-button" @click="onDeclineInvite">
|
|
||||||
<XIcon />Decline
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<nav class="navigation-card">
|
<div class="normal-page__content">
|
||||||
<NavRow
|
<div v-if="isInvited" class="universal-card information invited">
|
||||||
:links="[
|
<h2>Invitation to join {{ organization.name }}</h2>
|
||||||
{
|
<p>You have been invited to join {{ organization.name }}.</p>
|
||||||
label: formatMessage(commonMessages.allProjectType),
|
<div class="input-group">
|
||||||
href: `/organization/${organization.slug}`,
|
<button class="iconified-button brand-button" @click="onAcceptInvite">
|
||||||
},
|
<CheckIcon />Accept
|
||||||
...projectTypes.map((x) => {
|
</button>
|
||||||
return {
|
<button class="iconified-button danger-button" @click="onDeclineInvite">
|
||||||
label: formatMessage(getProjectTypeMessage(x, true)),
|
<XIcon />Decline
|
||||||
href: `/organization/${organization.slug}/${x}s`,
|
</button>
|
||||||
};
|
</div>
|
||||||
}),
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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>
|
<div v-if="navLinks.length > 2" class="mb-4 max-w-full overflow-x-auto">
|
||||||
<template v-if="projects?.length > 0">
|
<NavTabs :links="navLinks" />
|
||||||
<div class="project-list display-mode--list">
|
|
||||||
<ProjectCard
|
|
||||||
v-for="project in (route.params.projectType !== undefined
|
|
||||||
? projects.filter((x) =>
|
|
||||||
x.project_types.includes(
|
|
||||||
route.params.projectType.substr(0, route.params.projectType.length - 1),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: projects
|
|
||||||
)
|
|
||||||
.slice()
|
|
||||||
.sort((a, b) => b.downloads - a.downloads)"
|
|
||||||
:id="project.slug || project.id"
|
|
||||||
:key="project.id"
|
|
||||||
:name="project.name"
|
|
||||||
:display="cosmetics.searchDisplayMode.user"
|
|
||||||
:featured-image="project.gallery.find((element) => element.featured)?.url"
|
|
||||||
project-type-url="project"
|
|
||||||
:description="project.summary"
|
|
||||||
:created-at="project.published"
|
|
||||||
:updated-at="project.updated"
|
|
||||||
:downloads="project.downloads.toString()"
|
|
||||||
:follows="project.followers.toString()"
|
|
||||||
:icon-url="project.icon_url"
|
|
||||||
:categories="project.categories"
|
|
||||||
:client-side="project.client_side"
|
|
||||||
:server-side="project.server_side"
|
|
||||||
:status="
|
|
||||||
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
|
|
||||||
? project.status
|
|
||||||
: null
|
|
||||||
"
|
|
||||||
:type="project.project_types[0] ?? 'project'"
|
|
||||||
:color="project.color"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<template v-if="projects?.length > 0">
|
||||||
|
<div class="project-list display-mode--list">
|
||||||
|
<ProjectCard
|
||||||
|
v-for="project in (route.params.projectType !== undefined
|
||||||
|
? projects.filter((x) =>
|
||||||
|
x.project_types.includes(
|
||||||
|
route.params.projectType.substr(0, route.params.projectType.length - 1),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: projects
|
||||||
|
)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => b.downloads - a.downloads)"
|
||||||
|
:id="project.slug || project.id"
|
||||||
|
:key="project.id"
|
||||||
|
:name="project.name"
|
||||||
|
:display="cosmetics.searchDisplayMode.user"
|
||||||
|
:featured-image="project.gallery.find((element) => element.featured)?.url"
|
||||||
|
project-type-url="project"
|
||||||
|
:description="project.summary"
|
||||||
|
:created-at="project.published"
|
||||||
|
:updated-at="project.updated"
|
||||||
|
:downloads="project.downloads.toString()"
|
||||||
|
:follows="project.followers.toString()"
|
||||||
|
:icon-url="project.icon_url"
|
||||||
|
:categories="project.categories"
|
||||||
|
:client-side="project.client_side"
|
||||||
|
:server-side="project.server_side"
|
||||||
|
:status="
|
||||||
|
auth.user && (auth.user.id === user.id || tags.staffRoles.includes(auth.user.role))
|
||||||
|
? project.status
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
:type="project.project_types[0] ?? 'project'"
|
||||||
|
:color="project.color"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div v-else-if="true" class="error">
|
<div v-else-if="true" class="error">
|
||||||
<UpToDate class="icon" /><br />
|
<UpToDate class="icon" /><br />
|
||||||
<span class="preserve-lines text">
|
<span class="preserve-lines text">
|
||||||
This organization doesn't have any projects yet.
|
This organization doesn't have any projects yet.
|
||||||
<template v-if="isPermission(currentMember?.organization_permissions, 1 << 4)">
|
<template v-if="isPermission(currentMember?.organization_permissions, 1 << 4)">
|
||||||
Would you like to
|
Would you like to
|
||||||
<a class="link" @click="$refs.modal_creation.show()">create one</a>?
|
<a class="link" @click="$refs.modal_creation.show()">create one</a>?
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
<NuxtPage />
|
|
||||||
</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">
|
||||||
|
|||||||
@@ -3,75 +3,88 @@
|
|||||||
<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 }}
|
</template>
|
||||||
</h1>
|
<template #summary>
|
||||||
</div>
|
{{
|
||||||
<p class="m-0 line-clamp-2 max-w-[40rem]">
|
user.bio
|
||||||
{{
|
? user.bio
|
||||||
user.bio
|
: projects.length === 0
|
||||||
? user.bio
|
? "A Modrinth user."
|
||||||
: projects.length === 0
|
: "A Modrinth creator."
|
||||||
? "A Modrinth user."
|
}}
|
||||||
: "A Modrinth creator."
|
</template>
|
||||||
}}
|
<template #stats>
|
||||||
</p>
|
<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>
|
<div
|
||||||
<div class="flex flex-col justify-center gap-4">
|
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
|
||||||
<div class="flex flex-wrap gap-2">
|
>
|
||||||
<ButtonStyled size="large">
|
<DownloadIcon class="h-6 w-6 text-secondary" />
|
||||||
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
|
{{ formatCompactNumber(sumDownloads) }}
|
||||||
<EditIcon aria-hidden="true" />
|
downloads
|
||||||
{{ formatMessage(commonMessages.editButton) }}
|
|
||||||
</NuxtLink>
|
|
||||||
</ButtonStyled>
|
|
||||||
<ButtonStyled size="large" circular type="transparent">
|
|
||||||
<OverflowMenu
|
|
||||||
:options="[
|
|
||||||
{
|
|
||||||
id: 'manage-projects',
|
|
||||||
action: () => navigateTo('/dashboard/projects'),
|
|
||||||
hoverOnly: true,
|
|
||||||
shown: auth.user && auth.user.id === user.id,
|
|
||||||
},
|
|
||||||
{ divider: true, shown: auth.user && auth.user.id === user.id },
|
|
||||||
{
|
|
||||||
id: 'report',
|
|
||||||
action: () => reportUser(user.id),
|
|
||||||
color: 'red',
|
|
||||||
hoverOnly: true,
|
|
||||||
},
|
|
||||||
{ id: 'copy-id', action: () => copyId() },
|
|
||||||
]"
|
|
||||||
aria-label="More options"
|
|
||||||
>
|
|
||||||
<MoreVerticalIcon aria-hidden="true" />
|
|
||||||
<template #manage-projects>
|
|
||||||
<BoxIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(messages.profileManageProjectsButton) }}
|
|
||||||
</template>
|
|
||||||
<template #report>
|
|
||||||
<ReportIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(commonMessages.reportButton) }}
|
|
||||||
</template>
|
|
||||||
<template #copy-id>
|
|
||||||
<ClipboardCopyIcon aria-hidden="true" />
|
|
||||||
{{ formatMessage(commonMessages.copyIdButton) }}
|
|
||||||
</template>
|
|
||||||
</OverflowMenu>
|
|
||||||
</ButtonStyled>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex items-center gap-2 font-semibold">
|
||||||
</div>
|
<CalendarIcon class="h-6 w-6 text-secondary" />
|
||||||
|
Joined
|
||||||
|
{{ formatRelativeTime(user.created) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions>
|
||||||
|
<ButtonStyled size="large">
|
||||||
|
<NuxtLink v-if="auth.user && auth.user.id === user.id" to="/settings/profile">
|
||||||
|
<EditIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(commonMessages.editButton) }}
|
||||||
|
</NuxtLink>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled size="large" circular type="transparent">
|
||||||
|
<OverflowMenu
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
id: 'manage-projects',
|
||||||
|
action: () => navigateTo('/dashboard/projects'),
|
||||||
|
hoverOnly: true,
|
||||||
|
shown: auth.user && auth.user.id === user.id,
|
||||||
|
},
|
||||||
|
{ divider: true, shown: auth.user && auth.user.id === user.id },
|
||||||
|
{
|
||||||
|
id: 'report',
|
||||||
|
action: () => reportUser(user.id),
|
||||||
|
color: 'red',
|
||||||
|
hoverOnly: true,
|
||||||
|
},
|
||||||
|
{ id: 'copy-id', action: () => copyId() },
|
||||||
|
]"
|
||||||
|
aria-label="More options"
|
||||||
|
>
|
||||||
|
<MoreVerticalIcon aria-hidden="true" />
|
||||||
|
<template #manage-projects>
|
||||||
|
<BoxIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.profileManageProjectsButton) }}
|
||||||
|
</template>
|
||||||
|
<template #report>
|
||||||
|
<ReportIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(commonMessages.reportButton) }}
|
||||||
|
</template>
|
||||||
|
<template #copy-id>
|
||||||
|
<ClipboardCopyIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(commonMessages.copyIdButton) }}
|
||||||
|
</template>
|
||||||
|
</OverflowMenu>
|
||||||
|
</ButtonStyled>
|
||||||
|
</template>
|
||||||
|
</ContentPageHeader>
|
||||||
</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>
|
||||||
|
|||||||
26
packages/ui/src/components/base/ContentPageHeader.vue
Normal file
26
packages/ui/src/components/base/ContentPageHeader.vue
Normal 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>
|
||||||
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user