mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 09:34:50 +00:00
refactor: introduce PageHeader component (#6629)
* refactor: introduce PageHeader component * feat: split up according to component structure guide * fix: lint * fix: label inconsistencies * feat: refactor PageHeader (again) * refactor: old impls * fix: changes * fix: rescan * fix: prepr * fix: dedupe * fix: rev * fix: lint * fix: lint --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<PageHeader :title="organization.name" :summary="organization.description">
|
||||
<template #leading>
|
||||
<Avatar
|
||||
:src="organization.icon_url"
|
||||
:alt="organization.name"
|
||||
:tint-by="organization.id"
|
||||
size="96px"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #badges>
|
||||
<PageHeaderBadgeItem :icon="OrganizationIcon" class="px-0 text-primary">
|
||||
{{ formatMessage(messages.organizationLabel) }}
|
||||
</PageHeaderBadgeItem>
|
||||
</template>
|
||||
|
||||
<template #metadata>
|
||||
<PageHeaderMetadata>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="UsersIcon"
|
||||
:value="membersCount"
|
||||
:label="formatMessage(messages.membersLabel)"
|
||||
/>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="BoxIcon"
|
||||
:value="projectsCount"
|
||||
:label="formatMessage(messages.projectsLabel)"
|
||||
/>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="DownloadIcon"
|
||||
:value="downloads"
|
||||
:label="formatMessage(messages.downloadsLabel)"
|
||||
:tooltip="formatNumber(downloads)"
|
||||
/>
|
||||
</PageHeaderMetadata>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<ButtonStyled v-if="canManage" size="large">
|
||||
<nuxt-link :to="`/organization/${organization.slug}/settings`">
|
||||
<SettingsIcon />
|
||||
{{ formatMessage(messages.manage) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="moreActions"
|
||||
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
||||
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</PageHeaderActions>
|
||||
</template>
|
||||
</PageHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BoxIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
MoreVerticalIcon,
|
||||
OrganizationIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
PageHeader,
|
||||
PageHeaderActions,
|
||||
PageHeaderBadgeItem,
|
||||
PageHeaderMetadata,
|
||||
PageHeaderMetadataNumberItem,
|
||||
TeleportOverflowMenu,
|
||||
type TeleportOverflowMenuItem,
|
||||
useFormatNumber,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const messages = defineMessages({
|
||||
downloadsLabel: {
|
||||
id: 'organization.label.downloads',
|
||||
defaultMessage: 'downloads',
|
||||
},
|
||||
manage: {
|
||||
id: 'organization.button.manage',
|
||||
defaultMessage: 'Manage',
|
||||
},
|
||||
manageProjects: {
|
||||
id: 'organization.button.manage-projects',
|
||||
defaultMessage: 'Manage projects',
|
||||
},
|
||||
membersLabel: {
|
||||
id: 'organization.label.members',
|
||||
defaultMessage: 'members',
|
||||
},
|
||||
organizationLabel: {
|
||||
id: 'organization.label.organization',
|
||||
defaultMessage: 'Organization',
|
||||
},
|
||||
projectsLabel: {
|
||||
id: 'organization.label.projects',
|
||||
defaultMessage: 'projects',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
organization: {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
description?: string | null
|
||||
icon_url?: string | null
|
||||
}
|
||||
membersCount: number
|
||||
projectsCount: number
|
||||
downloads: number
|
||||
canManage?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
manageProjects: []
|
||||
copyId: []
|
||||
copyPermalink: []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatNumber = useFormatNumber()
|
||||
|
||||
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
|
||||
{
|
||||
id: 'manage-projects',
|
||||
label: formatMessage(messages.manageProjects),
|
||||
icon: BoxIcon,
|
||||
action: () => emit('manageProjects'),
|
||||
shown: props.canManage,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: props.canManage,
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyId'),
|
||||
},
|
||||
{
|
||||
id: 'copy-permalink',
|
||||
label: formatMessage(commonMessages.copyPermalinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyPermalink'),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<ButtonStyled size="large" circular>
|
||||
<PopoutMenu
|
||||
v-if="authUser"
|
||||
:tooltip="
|
||||
saved ? formatMessage(commonMessages.savedLabel) : formatMessage(commonMessages.saveButton)
|
||||
"
|
||||
from="top-right"
|
||||
:aria-label="formatMessage(commonMessages.saveButton)"
|
||||
:dropdown-id="`${baseId}-save`"
|
||||
>
|
||||
<BookmarkIcon aria-hidden="true" :fill="saved ? 'currentColor' : 'none'" />
|
||||
<template #menu>
|
||||
<StyledInput
|
||||
v-model="displayCollectionsSearch"
|
||||
:placeholder="formatMessage(commonMessages.searchPlaceholder)"
|
||||
wrapper-class="menu-search"
|
||||
/>
|
||||
<div v-if="filteredCollections.length > 0" class="collections-list text-primary">
|
||||
<Checkbox
|
||||
v-for="option in filteredCollections"
|
||||
:key="option.id"
|
||||
:model-value="option.projects.includes(projectId)"
|
||||
class="popout-checkbox"
|
||||
@update:model-value="() => collectProject(option, projectId)"
|
||||
>
|
||||
{{ option.name }}
|
||||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div v-else class="menu-text">
|
||||
<p class="popout-text">{{ noCollectionsLabel }}</p>
|
||||
</div>
|
||||
<ButtonStyled>
|
||||
<button class="mx-3 mb-3" @click="createCollection">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ createNewCollectionLabel }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PopoutMenu>
|
||||
<nuxt-link
|
||||
v-else
|
||||
v-tooltip="formatMessage(commonMessages.saveButton)"
|
||||
:to="signInRoute"
|
||||
:aria-label="formatMessage(commonMessages.saveButton)"
|
||||
>
|
||||
<BookmarkIcon aria-hidden="true" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BookmarkIcon, PlusIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
Checkbox,
|
||||
commonMessages,
|
||||
PopoutMenu,
|
||||
StyledInput,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
type CollectionOption = {
|
||||
id: string
|
||||
name: string
|
||||
projects: string[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
authUser?: unknown
|
||||
signInRoute: RouteLocationRaw
|
||||
projectId: string
|
||||
collections: CollectionOption[]
|
||||
saved: boolean
|
||||
baseId: string
|
||||
noCollectionsLabel: string
|
||||
createNewCollectionLabel: string
|
||||
collectProject: (option: CollectionOption, projectId: string) => void | Promise<void>
|
||||
createCollection: (event: MouseEvent) => void
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const displayCollectionsSearch = ref('')
|
||||
|
||||
const filteredCollections = computed(() =>
|
||||
props.collections
|
||||
.filter((collection) =>
|
||||
collection.name.toLowerCase().includes(displayCollectionsSearch.value.toLowerCase()),
|
||||
)
|
||||
.slice()
|
||||
.sort((a, b) => a.name.localeCompare(b.name)),
|
||||
)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popout-checkbox {
|
||||
padding: var(--gap-sm) var(--gap-md);
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
padding: 0 var(--gap-md);
|
||||
font-size: var(--font-size-nm);
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.menu-search {
|
||||
margin: var(--gap-sm) var(--gap-md);
|
||||
width: calc(100% - var(--gap-md) * 2);
|
||||
}
|
||||
|
||||
.collections-list {
|
||||
max-height: 40rem;
|
||||
overflow-y: auto;
|
||||
background-color: var(--color-bg);
|
||||
border-radius: var(--radius-md);
|
||||
margin: var(--gap-sm) var(--gap-md);
|
||||
padding: var(--gap-sm);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<PageHeader :title="user.username" :summary="summary">
|
||||
<template #leading>
|
||||
<Avatar
|
||||
:src="user.avatar_url"
|
||||
:alt="user.username"
|
||||
:size="isModrinthUser ? '64px' : '96px'"
|
||||
:tint-by="user.username"
|
||||
circle
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="isOfficialAccount || showAffiliateBadge" #badges>
|
||||
<PageHeaderBadgeItem
|
||||
v-if="isOfficialAccount"
|
||||
:icon="BadgeCheckIcon"
|
||||
:icon-props="{ fill: 'var(--color-brand-highlight)' }"
|
||||
:tooltip="formatMessage(messages.officialAccount)"
|
||||
class="border-brand-highlight bg-brand-highlight text-brand"
|
||||
>
|
||||
{{ formatMessage(messages.officialAccount) }}
|
||||
</PageHeaderBadgeItem>
|
||||
<PageHeaderBadgeItem
|
||||
v-if="showAffiliateBadge"
|
||||
:icon="AffiliateIcon"
|
||||
class="border-brand-highlight bg-brand-highlight text-brand"
|
||||
>
|
||||
{{ formatMessage(messages.affiliateLabel) }}
|
||||
</PageHeaderBadgeItem>
|
||||
</template>
|
||||
|
||||
<template v-if="$slots.summary" #summary>
|
||||
<slot name="summary" />
|
||||
</template>
|
||||
|
||||
<template v-if="!isModrinthUser" #metadata>
|
||||
<PageHeaderMetadata>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="BoxIcon"
|
||||
:value="projectsCount"
|
||||
:label="formatMessage(messages.profileProjectCountLabel, { count: projectsCount })"
|
||||
/>
|
||||
<PageHeaderMetadataNumberItem
|
||||
:icon="DownloadIcon"
|
||||
:value="downloads"
|
||||
:label="formatMessage(messages.profileDownloadCountLabel, { count: downloads })"
|
||||
:tooltip="downloadsTooltip"
|
||||
/>
|
||||
<PageHeaderMetadataTimeItem
|
||||
:icon="CalendarIcon"
|
||||
:date="user.created"
|
||||
:label="formatMessage(messages.profileJoinedLabel)"
|
||||
:tooltip="joinedTooltip"
|
||||
/>
|
||||
</PageHeaderMetadata>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<PageHeaderActions>
|
||||
<ButtonStyled v-if="isSelf" size="large">
|
||||
<nuxt-link to="/settings/profile">
|
||||
<EditIcon />
|
||||
{{ formatMessage(commonMessages.editButton) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular size="large" type="transparent">
|
||||
<TeleportOverflowMenu
|
||||
:options="moreActions"
|
||||
:tooltip="formatMessage(commonMessages.moreOptionsButton)"
|
||||
:aria-label="formatMessage(commonMessages.moreOptionsButton)"
|
||||
>
|
||||
<MoreVerticalIcon />
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</PageHeaderActions>
|
||||
</template>
|
||||
</PageHeader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
import {
|
||||
AffiliateIcon,
|
||||
BadgeCheckIcon,
|
||||
BoxIcon,
|
||||
CalendarIcon,
|
||||
ChartIcon,
|
||||
ClipboardCopyIcon,
|
||||
CurrencyIcon,
|
||||
DownloadIcon,
|
||||
EditIcon,
|
||||
InfoIcon,
|
||||
MoreVerticalIcon,
|
||||
ReportIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
defineMessages,
|
||||
PageHeader,
|
||||
PageHeaderActions,
|
||||
PageHeaderBadgeItem,
|
||||
PageHeaderMetadata,
|
||||
PageHeaderMetadataNumberItem,
|
||||
PageHeaderMetadataTimeItem,
|
||||
TeleportOverflowMenu,
|
||||
type TeleportOverflowMenuItem,
|
||||
useFormatDateTime,
|
||||
useFormatNumber,
|
||||
useVIntl,
|
||||
} from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const messages = defineMessages({
|
||||
affiliateLabel: {
|
||||
id: 'profile.label.affiliate',
|
||||
defaultMessage: 'Affiliate',
|
||||
},
|
||||
analyticsButton: {
|
||||
id: 'profile.button.analytics',
|
||||
defaultMessage: 'View user analytics',
|
||||
},
|
||||
billingButton: {
|
||||
id: 'profile.button.billing',
|
||||
defaultMessage: 'Manage user billing',
|
||||
},
|
||||
editRoleButton: {
|
||||
id: 'profile.button.edit-role',
|
||||
defaultMessage: 'Edit role',
|
||||
},
|
||||
infoButton: {
|
||||
id: 'profile.button.info',
|
||||
defaultMessage: 'View user details',
|
||||
},
|
||||
officialAccount: {
|
||||
id: 'profile.official-account',
|
||||
defaultMessage: 'Official Modrinth account',
|
||||
},
|
||||
profileJoinedLabel: {
|
||||
id: 'profile.label.joined',
|
||||
defaultMessage: 'Joined',
|
||||
},
|
||||
profileProjectCountLabel: {
|
||||
id: 'profile.label.project-count',
|
||||
defaultMessage: '{count, plural, one {project} other {projects}}',
|
||||
},
|
||||
profileDownloadCountLabel: {
|
||||
id: 'profile.label.download-count',
|
||||
defaultMessage: '{count, plural, one {download} other {downloads}}',
|
||||
},
|
||||
profileManageProjectsButton: {
|
||||
id: 'profile.button.manage-projects',
|
||||
defaultMessage: 'Manage projects',
|
||||
},
|
||||
removeAffiliateButton: {
|
||||
id: 'profile.button.remove-affiliate',
|
||||
defaultMessage: 'Remove as affiliate',
|
||||
},
|
||||
setAffiliateButton: {
|
||||
id: 'profile.button.set-affiliate',
|
||||
defaultMessage: 'Set as affiliate',
|
||||
},
|
||||
})
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
user: Labrinth.Users.v3.User
|
||||
summary?: string | null
|
||||
authUser?: Labrinth.Users.v3.User | null
|
||||
isModrinthUser?: boolean
|
||||
isOfficialAccount?: boolean
|
||||
showAffiliateBadge?: boolean
|
||||
isAffiliate?: boolean
|
||||
isSelf?: boolean
|
||||
isAdmin?: boolean
|
||||
isStaff?: boolean
|
||||
projectsCount?: number
|
||||
downloads?: number
|
||||
}>(),
|
||||
{
|
||||
summary: null,
|
||||
authUser: null,
|
||||
isModrinthUser: false,
|
||||
isOfficialAccount: false,
|
||||
showAffiliateBadge: false,
|
||||
isAffiliate: false,
|
||||
isSelf: false,
|
||||
isAdmin: false,
|
||||
isStaff: false,
|
||||
projectsCount: 0,
|
||||
downloads: 0,
|
||||
},
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
manageProjects: []
|
||||
report: []
|
||||
copyId: []
|
||||
copyPermalink: []
|
||||
openBilling: []
|
||||
toggleAffiliate: []
|
||||
openInfo: []
|
||||
openAnalytics: []
|
||||
editRole: []
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const formatNumber = useFormatNumber()
|
||||
const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
const downloadsTooltip = computed(() => formatNumber(props.downloads))
|
||||
const joinedTooltip = computed(() => formatDateTime(props.user.created))
|
||||
|
||||
const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
|
||||
{
|
||||
id: 'manage-projects',
|
||||
label: formatMessage(messages.profileManageProjectsButton),
|
||||
icon: BoxIcon,
|
||||
action: () => emit('manageProjects'),
|
||||
shown: props.isSelf,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: props.isSelf,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
label: formatMessage(commonMessages.reportButton),
|
||||
icon: ReportIcon,
|
||||
action: () => emit('report'),
|
||||
color: 'red',
|
||||
shown: props.authUser?.id !== props.user.id,
|
||||
},
|
||||
{
|
||||
id: 'copy-id',
|
||||
label: formatMessage(commonMessages.copyIdButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyId'),
|
||||
},
|
||||
{
|
||||
id: 'copy-permalink',
|
||||
label: formatMessage(commonMessages.copyPermalinkButton),
|
||||
icon: ClipboardCopyIcon,
|
||||
action: () => emit('copyPermalink'),
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: props.isAdmin,
|
||||
},
|
||||
{
|
||||
id: 'open-billing',
|
||||
label: formatMessage(messages.billingButton),
|
||||
icon: CurrencyIcon,
|
||||
action: () => emit('openBilling'),
|
||||
shown: props.isStaff,
|
||||
},
|
||||
{
|
||||
id: 'toggle-affiliate',
|
||||
label: props.isAffiliate
|
||||
? formatMessage(messages.removeAffiliateButton)
|
||||
: formatMessage(messages.setAffiliateButton),
|
||||
icon: AffiliateIcon,
|
||||
action: () => emit('toggleAffiliate'),
|
||||
shown: props.isAdmin,
|
||||
remainOnClick: true,
|
||||
color: props.isAffiliate ? 'red' : 'orange',
|
||||
},
|
||||
{
|
||||
id: 'open-info',
|
||||
label: formatMessage(messages.infoButton),
|
||||
icon: InfoIcon,
|
||||
action: () => emit('openInfo'),
|
||||
shown: props.isStaff,
|
||||
},
|
||||
{
|
||||
id: 'open-analytics',
|
||||
label: formatMessage(messages.analyticsButton),
|
||||
icon: ChartIcon,
|
||||
action: () => emit('openAnalytics'),
|
||||
shown: props.isAdmin,
|
||||
},
|
||||
{
|
||||
id: 'edit-role',
|
||||
label: formatMessage(messages.editRoleButton),
|
||||
icon: EditIcon,
|
||||
action: () => emit('editRole'),
|
||||
shown: props.isAdmin,
|
||||
},
|
||||
])
|
||||
</script>
|
||||
Reference in New Issue
Block a user