Compare commits

...
Author SHA1 Message Date
Prospector f76378d1c2 Begin project page refactor to NormalPage 2025-12-11 13:02:55 -08:00
14 changed files with 437 additions and 56 deletions
@@ -0,0 +1,6 @@
<script setup lang="ts">
import { ProjectPage } from '@modrinth/ui'
</script>
<template>
<ProjectPage />
</template>
+22 -20
View File
@@ -6,35 +6,35 @@ import _AlignLeftIcon from './icons/align-left.svg?component'
import _ArchiveIcon from './icons/archive.svg?component'
import _ArrowBigRightDashIcon from './icons/arrow-big-right-dash.svg?component'
import _ArrowBigUpDashIcon from './icons/arrow-big-up-dash.svg?component'
import _ArrowDownIcon from './icons/arrow-down.svg?component'
import _ArrowDownLeftIcon from './icons/arrow-down-left.svg?component'
import _ArrowDownIcon from './icons/arrow-down.svg?component'
import _ArrowLeftRightIcon from './icons/arrow-left-right.svg?component'
import _ArrowUpIcon from './icons/arrow-up.svg?component'
import _ArrowUpRightIcon from './icons/arrow-up-right.svg?component'
import _ArrowUpIcon from './icons/arrow-up.svg?component'
import _AsteriskIcon from './icons/asterisk.svg?component'
import _BadgeCheckIcon from './icons/badge-check.svg?component'
import _BadgeDollarSignIcon from './icons/badge-dollar-sign.svg?component'
import _BanIcon from './icons/ban.svg?component'
import _BellIcon from './icons/bell.svg?component'
import _BellRingIcon from './icons/bell-ring.svg?component'
import _BellIcon from './icons/bell.svg?component'
import _BlocksIcon from './icons/blocks.svg?component'
import _BoldIcon from './icons/bold.svg?component'
import _BookIcon from './icons/book.svg?component'
import _BookOpenIcon from './icons/book-open.svg?component'
import _BookTextIcon from './icons/book-text.svg?component'
import _BookIcon from './icons/book.svg?component'
import _BookmarkIcon from './icons/bookmark.svg?component'
import _BotIcon from './icons/bot.svg?component'
import _BoxIcon from './icons/box.svg?component'
import _BoxImportIcon from './icons/box-import.svg?component'
import _BoxIcon from './icons/box.svg?component'
import _BracesIcon from './icons/braces.svg?component'
import _BrushCleaningIcon from './icons/brush-cleaning.svg?component'
import _CalendarIcon from './icons/calendar.svg?component'
import _CardIcon from './icons/card.svg?component'
import _ChangeSkinIcon from './icons/change-skin.svg?component'
import _ChartIcon from './icons/chart.svg?component'
import _CheckIcon from './icons/check.svg?component'
import _CheckCheckIcon from './icons/check-check.svg?component'
import _CheckCircleIcon from './icons/check-circle.svg?component'
import _CheckIcon from './icons/check.svg?component'
import _ChevronLeftIcon from './icons/chevron-left.svg?component'
import _ChevronRightIcon from './icons/chevron-right.svg?component'
import _CircleUserIcon from './icons/circle-user.svg?component'
@@ -65,13 +65,13 @@ import _EditIcon from './icons/edit.svg?component'
import _EllipsisVerticalIcon from './icons/ellipsis-vertical.svg?component'
import _ExpandIcon from './icons/expand.svg?component'
import _ExternalIcon from './icons/external.svg?component'
import _EyeIcon from './icons/eye.svg?component'
import _EyeOffIcon from './icons/eye-off.svg?component'
import _FileIcon from './icons/file.svg?component'
import _EyeIcon from './icons/eye.svg?component'
import _FileArchiveIcon from './icons/file-archive.svg?component'
import _FileTextIcon from './icons/file-text.svg?component'
import _FilterIcon from './icons/filter.svg?component'
import _FileIcon from './icons/file.svg?component'
import _FilterXIcon from './icons/filter-x.svg?component'
import _FilterIcon from './icons/filter.svg?component'
import _FolderArchiveIcon from './icons/folder-archive.svg?component'
import _FolderOpenIcon from './icons/folder-open.svg?component'
import _FolderSearchIcon from './icons/folder-search.svg?component'
@@ -91,12 +91,13 @@ import _HashIcon from './icons/hash.svg?component'
import _Heading1Icon from './icons/heading-1.svg?component'
import _Heading2Icon from './icons/heading-2.svg?component'
import _Heading3Icon from './icons/heading-3.svg?component'
import _HeartIcon from './icons/heart.svg?component'
import _HeartHandshakeIcon from './icons/heart-handshake.svg?component'
import _HeartMinusIcon from './icons/heart-minus.svg?component'
import _HeartIcon from './icons/heart.svg?component'
import _HistoryIcon from './icons/history.svg?component'
import _HomeIcon from './icons/home.svg?component'
import _ImageIcon from './icons/image.svg?component'
import _ImagesIcon from './icons/images.svg?component'
import _ImportIcon from './icons/import.svg?component'
import _InProgressIcon from './icons/in-progress.svg?component'
import _InfoIcon from './icons/info.svg?component'
@@ -110,14 +111,14 @@ import _LeftArrowIcon from './icons/left-arrow.svg?component'
import _LibraryIcon from './icons/library.svg?component'
import _LightBulbIcon from './icons/light-bulb.svg?component'
import _LinkIcon from './icons/link.svg?component'
import _ListIcon from './icons/list.svg?component'
import _ListBulletedIcon from './icons/list-bulleted.svg?component'
import _ListEndIcon from './icons/list-end.svg?component'
import _ListOrderedIcon from './icons/list-ordered.svg?component'
import _LoaderIcon from './icons/loader.svg?component'
import _ListIcon from './icons/list.svg?component'
import _LoaderCircleIcon from './icons/loader-circle.svg?component'
import _LockIcon from './icons/lock.svg?component'
import _LoaderIcon from './icons/loader.svg?component'
import _LockOpenIcon from './icons/lock-open.svg?component'
import _LockIcon from './icons/lock.svg?component'
import _LogInIcon from './icons/log-in.svg?component'
import _LogOutIcon from './icons/log-out.svg?component'
import _MailIcon from './icons/mail.svg?component'
@@ -128,8 +129,8 @@ import _MessageIcon from './icons/message.svg?component'
import _MicrophoneIcon from './icons/microphone.svg?component'
import _MinimizeIcon from './icons/minimize.svg?component'
import _MinusIcon from './icons/minus.svg?component'
import _MonitorIcon from './icons/monitor.svg?component'
import _MonitorSmartphoneIcon from './icons/monitor-smartphone.svg?component'
import _MonitorIcon from './icons/monitor.svg?component'
import _MoonIcon from './icons/moon.svg?component'
import _MoreHorizontalIcon from './icons/more-horizontal.svg?component'
import _MoreVerticalIcon from './icons/more-vertical.svg?component'
@@ -138,16 +139,16 @@ import _NoSignalIcon from './icons/no-signal.svg?component'
import _NotepadTextIcon from './icons/notepad-text.svg?component'
import _OmorphiaIcon from './icons/omorphia.svg?component'
import _OrganizationIcon from './icons/organization.svg?component'
import _PackageIcon from './icons/package.svg?component'
import _PackageClosedIcon from './icons/package-closed.svg?component'
import _PackageOpenIcon from './icons/package-open.svg?component'
import _PackageIcon from './icons/package.svg?component'
import _PaintbrushIcon from './icons/paintbrush.svg?component'
import _PickaxeIcon from './icons/pickaxe.svg?component'
import _PlayIcon from './icons/play.svg?component'
import _PlugIcon from './icons/plug.svg?component'
import _PlusIcon from './icons/plus.svg?component'
import _RadioButtonIcon from './icons/radio-button.svg?component'
import _RadioButtonCheckedIcon from './icons/radio-button-checked.svg?component'
import _RadioButtonIcon from './icons/radio-button.svg?component'
import _ReceiptTextIcon from './icons/receipt-text.svg?component'
import _RedoIcon from './icons/redo.svg?component'
import _RefreshCwIcon from './icons/refresh-cw.svg?component'
@@ -164,8 +165,8 @@ import _ScaleIcon from './icons/scale.svg?component'
import _ScanEyeIcon from './icons/scan-eye.svg?component'
import _SearchIcon from './icons/search.svg?component'
import _SendIcon from './icons/send.svg?component'
import _ServerIcon from './icons/server.svg?component'
import _ServerPlusIcon from './icons/server-plus.svg?component'
import _ServerIcon from './icons/server.svg?component'
import _SettingsIcon from './icons/settings.svg?component'
import _ShareIcon from './icons/share.svg?component'
import _ShieldIcon from './icons/shield.svg?component'
@@ -195,26 +196,26 @@ import _TrashIcon from './icons/trash.svg?component'
import _TriangleAlertIcon from './icons/triangle-alert.svg?component'
import _UnderlineIcon from './icons/underline.svg?component'
import _UndoIcon from './icons/undo.svg?component'
import _UnknownIcon from './icons/unknown.svg?component'
import _UnknownDonationIcon from './icons/unknown-donation.svg?component'
import _UnknownIcon from './icons/unknown.svg?component'
import _UnlinkIcon from './icons/unlink.svg?component'
import _UnplugIcon from './icons/unplug.svg?component'
import _UpdatedIcon from './icons/updated.svg?component'
import _UploadIcon from './icons/upload.svg?component'
import _UserIcon from './icons/user.svg?component'
import _UserCogIcon from './icons/user-cog.svg?component'
import _UserPlusIcon from './icons/user-plus.svg?component'
import _UserRoundIcon from './icons/user-round.svg?component'
import _UserSearchIcon from './icons/user-search.svg?component'
import _UserXIcon from './icons/user-x.svg?component'
import _UserIcon from './icons/user.svg?component'
import _UsersIcon from './icons/users.svg?component'
import _VersionIcon from './icons/version.svg?component'
import _WikiIcon from './icons/wiki.svg?component'
import _WindowIcon from './icons/window.svg?component'
import _WorldIcon from './icons/world.svg?component'
import _WrenchIcon from './icons/wrench.svg?component'
import _XIcon from './icons/x.svg?component'
import _XCircleIcon from './icons/x-circle.svg?component'
import _XIcon from './icons/x.svg?component'
import _ZoomInIcon from './icons/zoom-in.svg?component'
import _ZoomOutIcon from './icons/zoom-out.svg?component'
@@ -314,6 +315,7 @@ export const HeartIcon = _HeartIcon
export const HistoryIcon = _HistoryIcon
export const HomeIcon = _HomeIcon
export const ImageIcon = _ImageIcon
export const ImagesIcon = _ImagesIcon
export const ImportIcon = _ImportIcon
export const InProgressIcon = _InProgressIcon
export const InfoIcon = _InfoIcon
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-images-icon lucide-images"><path d="m22 11-1.296-1.296a2.4 2.4 0 0 0-3.408 0L11 16"/><path d="M4 8a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2"/><circle cx="13" cy="7" r="1" fill="currentColor"/><rect x="8" y="2" width="14" height="14" rx="2"/></svg>

After

Width:  |  Height:  |  Size: 456 B

+31 -31
View File
@@ -1,37 +1,37 @@
// AUTO-GENERATED FILE - DO NOT EDIT
import { article as a_new_chapter_for_modrinth_servers } from "./a_new_chapter_for_modrinth_servers";
import { article as accelerating_development } from "./accelerating_development";
import { article as becoming_sustainable } from "./becoming_sustainable";
import { article as capital_return } from "./capital_return";
import { article as carbon_ads } from "./carbon_ads";
import { article as creator_monetization } from "./creator_monetization";
import { article as creator_update } from "./creator_update";
import { article as creator_updates_july_2025 } from "./creator_updates_july_2025";
import { article as creator_withdrawals_overhaul } from "./creator_withdrawals_overhaul";
import { article as design_refresh } from "./design_refresh";
import { article as download_adjustment } from "./download_adjustment";
import { article as free_server_medal } from "./free_server_medal";
import { article as knossos_v2_1_0 } from "./knossos_v2_1_0";
import { article as licensing_guide } from "./licensing_guide";
import { article as modpack_changes } from "./modpack_changes";
import { article as modpacks_alpha } from "./modpacks_alpha";
import { article as modrinth_app_beta } from "./modrinth_app_beta";
import { article as modrinth_beta } from "./modrinth_beta";
import { article as modrinth_servers_asia } from "./modrinth_servers_asia";
import { article as modrinth_servers_beta } from "./modrinth_servers_beta";
import { article as new_environments } from "./new_environments";
import { article as new_site_beta } from "./new_site_beta";
import { article as plugins_resource_packs } from "./plugins_resource_packs";
import { article as pride_campaign_2025 } from "./pride_campaign_2025";
import { article as redesign } from "./redesign";
import { article as russian_censorship } from "./russian_censorship";
import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
import { article as standing_by_our_values } from "./standing_by_our_values";
import { article as standing_by_our_values_russian } from "./standing_by_our_values_russian";
import { article as windows_borderless_malware_disclosure } from "./windows_borderless_malware_disclosure";
import { article as whats_modrinth } from "./whats_modrinth";
import { article as two_years_of_modrinth } from "./two_years_of_modrinth";
import { article as two_years_of_modrinth_history } from "./two_years_of_modrinth_history";
import { article as whats_modrinth } from "./whats_modrinth";
import { article as windows_borderless_malware_disclosure } from "./windows_borderless_malware_disclosure";
import { article as standing_by_our_values } from "./standing_by_our_values";
import { article as standing_by_our_values_russian } from "./standing_by_our_values_russian";
import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
import { article as russian_censorship } from "./russian_censorship";
import { article as redesign } from "./redesign";
import { article as pride_campaign_2025 } from "./pride_campaign_2025";
import { article as plugins_resource_packs } from "./plugins_resource_packs";
import { article as new_site_beta } from "./new_site_beta";
import { article as new_environments } from "./new_environments";
import { article as modrinth_servers_beta } from "./modrinth_servers_beta";
import { article as modrinth_servers_asia } from "./modrinth_servers_asia";
import { article as modrinth_beta } from "./modrinth_beta";
import { article as modrinth_app_beta } from "./modrinth_app_beta";
import { article as modpacks_alpha } from "./modpacks_alpha";
import { article as modpack_changes } from "./modpack_changes";
import { article as licensing_guide } from "./licensing_guide";
import { article as knossos_v2_1_0 } from "./knossos_v2_1_0";
import { article as free_server_medal } from "./free_server_medal";
import { article as download_adjustment } from "./download_adjustment";
import { article as design_refresh } from "./design_refresh";
import { article as creator_withdrawals_overhaul } from "./creator_withdrawals_overhaul";
import { article as creator_updates_july_2025 } from "./creator_updates_july_2025";
import { article as creator_update } from "./creator_update";
import { article as creator_monetization } from "./creator_monetization";
import { article as carbon_ads } from "./carbon_ads";
import { article as capital_return } from "./capital_return";
import { article as becoming_sustainable } from "./becoming_sustainable";
import { article as accelerating_development } from "./accelerating_development";
import { article as a_new_chapter_for_modrinth_servers } from "./a_new_chapter_for_modrinth_servers";
export const articles = [
windows_borderless_malware_disclosure,
@@ -6,7 +6,7 @@
<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">
<h1 class="m-0 text-2xl font-semibold leading-none text-contrast">
<slot name="title" />
</h1>
<slot name="title-suffix" />
@@ -0,0 +1,72 @@
<script setup lang="ts">
import { CheckIcon, CopyIcon } from '@modrinth/assets';
import { ref } from 'vue';
const props = defineProps<{
text: string
}>()
const copying = ref(false)
function copyText() {
if (!copying.value) {
copying.value = true
setTimeout(() => (copying.value = false), 2000)
}
}
</script>
<template>
<button
class="transition-all appearance-none border-0 rounded-xl text-start p-3 grid grid-cols-[1fr_auto] items-center gap-2"
:class="{
'bg-surface-4 hover:bg-surface-5 active:scale-[0.98]': !copying,
'bg-brand-highlight': copying,
}"
:disabled="copying"
@click="copyText"
>
<span class="text-contrast font-semibold">{{ text }}</span>
<span class="text-primary icon-swapper">
<Transition name="icon-swap-up">
<CheckIcon v-if="copying" />
</Transition>
<Transition name="icon-swap-down">
<CopyIcon v-if="!copying" />
</Transition>
</span>
</button>
</template>
<style scoped>
.icon-swapper {
display: grid;
place-content: center;
> * {
grid-area: 1 / 1;
}
}
.icon-swap-up-enter-active, .icon-swap-up-leave-active, .icon-swap-down-enter-active, .icon-swap-down-leave-active {
transition: opacity 0.2s ease-in-out, translate 0.2s ease-in-out;
}
.icon-swap-up-enter-from {
opacity: 0;
translate: 0 -100%;
}
.icon-swap-up-leave-to {
opacity: 0;
translate: 0 -100%;
}
.icon-swap-down-enter-from {
opacity: 0;
translate: 0 100%;
}
.icon-swap-down-leave-to {
opacity: 0;
translate: 0 100%;
}
</style>
+1
View File
@@ -29,6 +29,7 @@ export { default as HorizontalRule } from './HorizontalRule.vue'
export { default as IconSelect } from './IconSelect.vue'
export type { JoinedButtonAction } from './JoinedButtons.vue'
export { default as JoinedButtons } from './JoinedButtons.vue'
export { default as LargeCopyField } from './LargeCopyField.vue'
export { default as LoadingIndicator } from './LoadingIndicator.vue'
export { default as ManySelect } from './ManySelect.vue'
export { default as MarkdownEditor } from './MarkdownEditor.vue'
+175
View File
@@ -0,0 +1,175 @@
<template>
<nav
ref="scrollContainer"
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
>
<router-link
v-for="(link, index) in filteredLinks"
v-show="link.shown === undefined ? true : link.shown"
:key="index"
ref="tabLinkElements"
:to="query ? (link.href ? `?${query}=${link.href}` : '?') : link.href"
class="button-animation z-[1] flex flex-row items-center gap-2 px-4 py-2 focus:rounded-full"
:class="{
'text-button-textSelected': activeIndex === index && !subpageSelected,
'text-contrast': activeIndex === index && subpageSelected,
}"
>
<component :is="link.icon" v-if="link.icon" class="size-5" />
<span class="text-nowrap">{{ link.label }}</span>
</router-link>
<div
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'
}`"
:style="{
left: sliderLeftPx,
top: sliderTopPx,
right: sliderRightPx,
bottom: sliderBottomPx,
opacity: sliderLeft === 4 && sliderLeft === sliderRight ? 0 : activeIndex === -1 ? 0 : 1,
}"
aria-hidden="true"
></div>
</nav>
</template>
<script setup lang="ts">
import type { Component} from 'vue';
import { computed, onMounted, ref, watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
interface Tab {
label: string
href: string
shown?: boolean
icon?: string | Component
subpages?: string[]
}
const props = defineProps<{
links: Tab[]
query?: string
}>()
const scrollContainer = ref<HTMLElement | null>(null)
const sliderLeft = ref(4)
const sliderTop = ref(4)
const sliderRight = ref(4)
const sliderBottom = ref(4)
const activeIndex = ref(-1)
const subpageSelected = ref(false)
const filteredLinks = computed(() =>
props.links.filter((x) => (x.shown === undefined ? true : x.shown)),
)
const sliderLeftPx = computed(() => `${sliderLeft.value}px`)
const sliderTopPx = computed(() => `${sliderTop.value}px`)
const sliderRightPx = computed(() => `${sliderRight.value}px`)
const sliderBottomPx = computed(() => `${sliderBottom.value}px`)
const tabLinkElements = ref()
function pickLink() {
let index = -1
subpageSelected.value = false
for (let i = filteredLinks.value.length - 1; i >= 0; i--) {
const link = filteredLinks.value[i]
if (props.query) {
if (route.query[props.query] === link.href || (!route.query[props.query] && !link.href)) {
index = i
break
}
} else if (decodeURIComponent(route.path) === link.href) {
index = i
break
} else if (
decodeURIComponent(route.path).includes(link.href) ||
(link.subpages &&
link.subpages.some((subpage) => decodeURIComponent(route.path).includes(subpage)))
) {
index = i
subpageSelected.value = true
break
}
}
activeIndex.value = index
if (activeIndex.value !== -1) {
startAnimation()
} else {
sliderLeft.value = 0
sliderRight.value = 0
}
}
function startAnimation() {
const el = tabLinkElements.value[activeIndex.value]?.$el
if (!el || !el.offsetParent) return
const newValues = {
left: el.offsetLeft,
top: el.offsetTop,
right: el.offsetParent.offsetWidth - el.offsetLeft - el.offsetWidth,
bottom: el.offsetParent.offsetHeight - el.offsetTop - el.offsetHeight,
}
if (sliderLeft.value === 4 && sliderRight.value === 4) {
sliderLeft.value = newValues.left
sliderRight.value = newValues.right
sliderTop.value = newValues.top
sliderBottom.value = newValues.bottom
} else {
const delay = 200
if (newValues.left < sliderLeft.value) {
sliderLeft.value = newValues.left
setTimeout(() => {
sliderRight.value = newValues.right
}, delay)
} else {
sliderRight.value = newValues.right
setTimeout(() => {
sliderLeft.value = newValues.left
}, delay)
}
if (newValues.top < sliderTop.value) {
sliderTop.value = newValues.top
setTimeout(() => {
sliderBottom.value = newValues.bottom
}, delay)
} else {
sliderBottom.value = newValues.bottom
setTimeout(() => {
sliderTop.value = newValues.top
}, delay)
}
}
}
onMounted(() => {
pickLink()
})
watch(
() => [route.path, route.query],
() => pickLink(),
)
</script>
<style scoped>
.navtabs-transition {
transition:
all 150ms cubic-bezier(0.4, 0, 0.2, 1),
opacity 250ms cubic-bezier(0.5, 0, 0.2, 1) 50ms;
}
.card-shadow {
box-shadow: var(--shadow-card);
}
</style>
+1
View File
@@ -1,3 +1,4 @@
export { default as Breadcrumbs } from './Breadcrumbs.vue'
export { default as NavTabs } from './NavTabs.vue'
export { default as NotificationPanel } from './NotificationPanel.vue'
export { default as PagewideBanner } from './PagewideBanner.vue'
@@ -4,7 +4,7 @@ import { injectPageContext } from '../../providers'
const { hierarchicalSidebarAvailable } = injectPageContext()
defineProps<{
title: string
title?: string
}>()
</script>
<template>
@@ -14,7 +14,7 @@ defineProps<{
'card-shadow mb-4 last:mb-0 rounded-2xl bg-bg-raised': !hierarchicalSidebarAvailable,
}"
>
<span class="font-semibold">{{ title }}</span>
<span v-if="title" class="font-semibold">{{ title }}</span>
<slot />
</div>
</template>
+2
View File
@@ -0,0 +1,2 @@
export { default as ServersManageBackupsPage } from './manage/backups.vue'
export { default as ServersManagePageIndex } from './manage/index.vue'
+2 -2
View File
@@ -1,2 +1,2 @@
export { default as ServersManageBackupsPage } from './hosting/manage/backups.vue'
export { default as ServersManagePageIndex } from './hosting/manage/index.vue'
export * from './hosting'
export * from './project'
+1
View File
@@ -0,0 +1 @@
export { default as ProjectPage } from './index.vue'
+120
View File
@@ -0,0 +1,120 @@
<script setup lang="ts">
import { AsteriskIcon,ImageIcon, InfoIcon } from '@modrinth/assets'
import { computed } from 'vue'
import {
Avatar,
ContentPageHeader,
LargeCopyField,
NavTabs,
NormalPage,
SidebarCard,
} from '../../components'
const project = {
id: 'bJuQ1kl5',
slug: 'complex-cobblemon',
project_types: [
'server', // add server type
'modpack',
],
games: ['minecraft-java'],
team_id: 'aUYO8LVq',
organization: null,
name: 'Hypixel',
summary: 'A mini-game network, featuring Bed Wars, SkyBlock, SkyWars, Murder Mystery, and more!',
description: 'project desc',
published: '2025-12-07T20:07:01.351121Z',
updated: '2025-12-07T22:31:02.164985Z', // should track when server IP, mc version, or content changes
approved: '2025-12-08T03:09:10.517396Z',
queued: '2025-12-07T21:50:54.823172Z',
status: 'approved',
requested_status: 'approved',
moderator_message: null,
license: null, // becomes nullable
downloads: 1, // dunno if we should show? could count total server pack downloads or just always 0/null
followers: 0,
categories: ['game-mechanics', 'utility'], // tbd, but will be server-specific categories
additional_categories: [],
loaders: null, // nullable or empty, depending on what is possible
versions: ['dA2xTlv'], // only show the current active release
icon_url: 'https://avatars.githubusercontent.com/u/3840546?s=280&v=4',
link_urls: {}, // same as projects, maybe allow some extra types? can figure out later
gallery: [],
color: 12553338,
thread_id: 'ML721Sj1',
monetization_status: null, // nullable or empty, depending on what is possible
side_types_migration_review_status: null, // nullable or empty, depending on what is possible
game_versions: null, // nullable or empty, depending on what is possible
environment: null, // nullable or empty, depending on what is possible
server_metadata: {
// always exists when project_types contains "server", otherwise does not
java_address: 'mc.hypixel.net',
bedrock_address: null,
players_online: 1234,
max_players: 20000, // nullable, allow creators to show no max
plays: 0, // number of times people have joined the server via Modrinth App
total_players: 2304, // number of unique players who have played via Modrinth App
content: {
// IF vanilla
type: 'vanilla',
suggested_minecraft_version: '1.21.10',
supported_minecraft_versions: ['1.21.9', '1.21.10'],
},
// content: {
// // ELSE IF modpack
// type: 'modpack',
// modpack_version_id: 'dA2xTlv',
// },
},
}
const server = computed(() =>
project.project_types.includes('server') ? project.server_metadata : false,
)
const tabs = computed(() => {})
</script>
<template>
<NormalPage sidebar="right">
<template #header>
<ContentPageHeader>
<template #icon>
<Avatar :src="project.icon_url" size="104px" />
</template>
<template #title>
{{ project.name }}
</template>
<template #summary>
{{ project.summary }}
</template>
</ContentPageHeader>
<NavTabs
:links="[
{
label: 'Description',
href: '',
icon: InfoIcon,
},
{
label: 'Gallery',
href: '',
icon: ImageIcon,
},
{
label: 'Required content',
href: '',
icon: AsteriskIcon,
},
]"
class="mt-6"
/>
</template>
<template v-if="server" #sidebar>
<SidebarCard>
<LargeCopyField :text="server.java_address" />
</SidebarCard>
</template>
Project page content
</NormalPage>
</template>