mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +00:00
feat: shared components for worlds + p2p instances (#5135)
* feat: base content card component * fix: tooltips + colors * feat: fix orgs * feat: add ContentModpackCard * fix: extract types * feat: selection v-model * add show icon in selected for combobox with stories * feat: add project combobox * clean up project combobox * feat: start install to play modal * fix: events * feat: figma alignments * feat: migrate toggle to tailwind * fix: row borders * feat: disabled state * feat: virtual list impl for card table based on window scroll * fix: lint * feat: virtualization + smaller contentcard items * feat: fix gap + border issues on last elm * fix: use TeleportOverflowMenu * fix: hasUpdate type * fix: fallback to svg if src is invalid on avatar component * fix: storybook * feat: start on updater modal * feat: finish content updater modal * feat: i18n pass * remove install to play modal from ui package * pnpm prepr * feat: reusable table component * feat: add column width prop for table and fix stories * feat: add table overflow menu story example * feat: add surface-1.5 and use in table * chore: export table in index * fix: allow more loose typing on columns * feat: update table component to derive key from column instead of data * feat: surface 1.5 for oled + refactor story for contentcardtable + yeet sorting funcs * fix: lint * feat: add no padding story for new modal --------- Signed-off-by: Calum H. <contact@cal.engineer> Co-authored-by: tdgao <mr.trumgao@gmail.com>
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ClockIcon,
|
||||
DownloadIcon,
|
||||
HeartIcon,
|
||||
MoreVerticalIcon,
|
||||
OrganizationIcon,
|
||||
UnlinkIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { computed, getCurrentInstance } from 'vue'
|
||||
import type { RouteLocationRaw } from 'vue-router'
|
||||
|
||||
import { useRelativeTime } from '../../composables/how-ago'
|
||||
import { defineMessages, useVIntl } from '../../composables/i18n'
|
||||
import { commonMessages } from '../../utils/common-messages'
|
||||
import AutoLink from '../base/AutoLink.vue'
|
||||
import Avatar from '../base/Avatar.vue'
|
||||
import BulletDivider from '../base/BulletDivider.vue'
|
||||
import ButtonStyled from '../base/ButtonStyled.vue'
|
||||
import OverflowMenu, { type Option as OverflowMenuOption } from '../base/OverflowMenu.vue'
|
||||
import TagItem from '../base/TagItem.vue'
|
||||
import type {
|
||||
ContentModpackCardCategory,
|
||||
ContentModpackCardProject,
|
||||
ContentModpackCardVersion,
|
||||
ContentOwner,
|
||||
} from './types'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
unlinkModpack: {
|
||||
id: 'instances.modpack-card.unlink',
|
||||
defaultMessage: 'Unlink modpack',
|
||||
},
|
||||
})
|
||||
|
||||
interface Props {
|
||||
project: ContentModpackCardProject
|
||||
projectLink?: string | RouteLocationRaw
|
||||
version?: ContentModpackCardVersion
|
||||
owner?: ContentOwner
|
||||
categories?: ContentModpackCardCategory[]
|
||||
disabled?: boolean
|
||||
overflowOptions?: OverflowMenuOption[]
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
projectLink: undefined,
|
||||
version: undefined,
|
||||
owner: undefined,
|
||||
categories: undefined,
|
||||
disabled: false,
|
||||
overflowOptions: undefined,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
update: []
|
||||
content: []
|
||||
unlink: []
|
||||
}>()
|
||||
|
||||
const instance = getCurrentInstance()
|
||||
const hasUpdateListener = computed(() => typeof instance?.vnode.props?.onUpdate === 'function')
|
||||
const hasContentListener = computed(() => typeof instance?.vnode.props?.onContent === 'function')
|
||||
const hasUnlinkListener = computed(() => typeof instance?.vnode.props?.onUnlink === 'function')
|
||||
|
||||
const formatTimeAgo = useRelativeTime()
|
||||
|
||||
const formatCompact = (n: number | undefined) => {
|
||||
if (n === undefined) return ''
|
||||
return new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 }).format(n)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-4 rounded-[20px] bg-bg-raised p-6 shadow-md"
|
||||
:class="{ 'opacity-50': disabled }"
|
||||
>
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<Avatar
|
||||
:src="project.icon_url"
|
||||
:alt="project.title"
|
||||
size="5rem"
|
||||
no-shadow
|
||||
raised
|
||||
class="shrink-0"
|
||||
/>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<AutoLink
|
||||
:to="projectLink"
|
||||
class="text-2xl font-semibold leading-8 text-contrast hover:underline"
|
||||
>
|
||||
{{ project.title }}
|
||||
</AutoLink>
|
||||
<div class="flex flex-wrap items-center gap-2 text-secondary">
|
||||
<template v-if="owner">
|
||||
<AutoLink :to="owner.link" class="flex items-center gap-1.5 hover:underline">
|
||||
<Avatar
|
||||
:src="owner.avatar_url"
|
||||
:alt="owner.name"
|
||||
size="2rem"
|
||||
:circle="owner.type === 'user'"
|
||||
no-shadow
|
||||
/>
|
||||
<OrganizationIcon v-if="owner.type === 'organization'" class="size-4" />
|
||||
<span class="font-medium">{{ owner.name }}</span>
|
||||
</AutoLink>
|
||||
</template>
|
||||
<template v-if="owner && version">
|
||||
<BulletDivider />
|
||||
</template>
|
||||
<template v-if="version">
|
||||
<span class="font-medium">v{{ version.version_number }}</span>
|
||||
</template>
|
||||
<template v-if="version?.date_published">
|
||||
<BulletDivider />
|
||||
<div class="flex items-center gap-2">
|
||||
<ClockIcon class="size-5" />
|
||||
<span>{{ formatTimeAgo(new Date(version.date_published)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<ButtonStyled v-if="hasUpdateListener" type="transparent" color="green" color-fill="text">
|
||||
<button class="flex items-center gap-2" @click="emit('update')">
|
||||
<DownloadIcon class="!text-green size-5" />
|
||||
<span class="font-semibold">{{ formatMessage(commonMessages.updateButton) }}</span>
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled v-if="hasContentListener">
|
||||
<button class="!shadow-none" @click="emit('content')">
|
||||
{{ formatMessage(commonMessages.contentLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled v-if="hasUnlinkListener" circular type="outlined">
|
||||
<button
|
||||
v-tooltip="formatMessage(messages.unlinkModpack)"
|
||||
class="!border-surface-4 !border-[1px]"
|
||||
@click="emit('unlink')"
|
||||
>
|
||||
<UnlinkIcon class="size-5" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled v-if="overflowOptions?.length" circular type="transparent">
|
||||
<OverflowMenu :options="overflowOptions">
|
||||
<MoreVerticalIcon class="size-5" />
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span v-if="project.description" class="text-secondary">
|
||||
{{ project.description }}
|
||||
</span>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div v-if="project.downloads !== undefined" class="flex items-center gap-2 text-secondary">
|
||||
<DownloadIcon class="size-5" />
|
||||
<span class="font-medium">{{ formatCompact(project.downloads) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="project.followers !== undefined" class="flex items-center gap-2 text-secondary">
|
||||
<HeartIcon class="size-5" />
|
||||
<span class="font-medium">{{ formatCompact(project.followers) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="categories?.length" class="flex flex-wrap gap-2">
|
||||
<TagItem v-for="cat in categories" :key="cat.name" :action="cat.action">
|
||||
{{ cat.name }}
|
||||
</TagItem>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user