Add new links card and feature flag system for incremental dev. (#1714)

* Add new links card and feature flag system for incremental dev.

* Switch to env variable for dev flags

* Add members card

* fix order of creators card

* Fix owner icon color and bring org owner to top of list

* lint + other fixes

* Revamp feature flag system, add flag config page

* Add button to flags page in dev mode

* fix env overrides

* make typescript happy with the refs
This commit is contained in:
Prospector
2024-06-11 19:46:07 -07:00
committed by GitHub
parent 5b2d36e976
commit 1d9fe0c03d
32 changed files with 1325 additions and 310 deletions

View File

@@ -62,7 +62,7 @@
}/settings/license`"
label="License"
>
<LicenseIcon />
<CopyrightIcon />
</NavStackItem>
<NavStackItem
:link="`/${project.project_type}/${
@@ -243,7 +243,7 @@
follower<span v-if="project.followers !== 1">s</span>
</div>
</div>
<div class="dates">
<div v-if="!flags.newProjectDetails" class="dates">
<div
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
class="date"
@@ -269,8 +269,8 @@
<span class="label">Submitted</span>
<span class="value">{{ fromNow(project.queued) }}</span>
</div>
<hr class="card-divider" />
</div>
<hr class="card-divider" />
<div class="input-group">
<template v-if="auth.user">
<button
@@ -333,8 +333,8 @@
:direction="cosmetics.projectLayout ? 'left' : 'right'"
>
<MoreHorizontalIcon />
<template #report> <ReportIcon /> Report</template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID</template>
<template #report> <ReportIcon /> Report </template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID </template>
</OverflowMenu>
</template>
<template v-else>
@@ -360,13 +360,103 @@
:direction="cosmetics.projectLayout ? 'left' : 'right'"
>
<MoreHorizontalIcon />
<template #report> <ReportIcon /> Report</template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID</template>
<template #report> <ReportIcon /> Report </template>
<template #copy-id> <ClipboardCopyIcon /> Copy ID </template>
</OverflowMenu>
</template>
</div>
</div>
</div>
<div
v-if="flags.projectCompatibility && versions.length > 0"
class="card flex-card experimental-styles-within"
>
<h2>Compatibility</h2>
<section>
<h3>Minecraft: Java Edition</h3>
<div class="tag-list">
<div
v-for="version in getVersionsToDisplay(project)"
:key="`version-tag-${version}`"
class="tag-list__item"
>
{{ version }}
</div>
</div>
</section>
<section>
<h3>Platforms</h3>
<div class="tag-list">
<div
v-for="platform in project.loaders"
:key="`platform-tag-${platform}`"
:class="`tag-list__item`"
:style="`--_color: var(--color-platform-${platform})`"
>
<svg v-html="tags.loaders.find((x) => x.name === platform).icon"></svg>
{{ $formatCategory(platform) }}
</div>
</div>
</section>
<section>
<h3>Environments</h3>
<div class="status-list">
<div class="status-list__item status-list__item--color-green">
<CheckIcon /> Singleplayer
</div>
<div
v-if="
project.client_side !== 'unsupported' && project.server_side !== 'unsupported'
"
class="status-list__item status-list__item--color-green"
>
<CheckIcon /> Client and server
</div>
<div
v-if="project.client_side === 'required' && project.server_side === 'unsupported'"
class="status-list__item status-list__item--color-green"
>
<CheckIcon /> Client
</div>
<div
v-if="project.server_side === 'required' && project.client_side === 'unsupported'"
class="status-list__item status-list__item--color-green"
>
<CheckIcon /> Server
</div>
<div
v-if="
project.client_side === 'optional' ||
(project.client_side === 'required' && project.server_side === 'optional')
"
class="status-list__item status-list__item--color-orange"
>
<CheckIcon /> Client <span>(Limited functionality)</span>
</div>
<div
v-if="
project.server_side === 'optional' ||
(project.server_side === 'required' && project.client_side === 'optional')
"
class="status-list__item status-list__item--color-orange"
>
<CheckIcon /> Server <span>(Limited functionality)</span>
</div>
<div
v-if="project.client_side === 'unsupported'"
class="status-list__item status-list__item--color-red"
>
<XIcon /> Client
</div>
<div
v-if="project.server_side === 'unsupported'"
class="status-list__item status-list__item--color-red"
>
<XIcon /> Server
</div>
</div>
</section>
</div>
</div>
<section class="normal-page__content">
<ProjectMemberHeader
@@ -467,47 +557,49 @@
:route="route"
/>
</section>
<div class="universal-card normal-page__info">
<template
<div class="normal-page__info">
<div
v-if="
project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url ||
project.donation_urls.length > 0
flags.newProjectLinks &&
(project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url ||
project.donation_urls.length > 0)
"
class="card flex-card experimental-styles-within"
>
<h2 class="card-header">External resources</h2>
<div class="links">
<h2>Links</h2>
<div class="links-list">
<a
v-if="project.issues_url"
:href="project.issues_url"
class="title"
:target="$external()"
rel="noopener nofollow ugc"
>
<IssuesIcon aria-hidden="true" />
<span>Issues</span>
Report issues
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.source_url"
:href="project.source_url"
class="title"
:target="$external()"
rel="noopener nofollow ugc"
>
<CodeIcon aria-hidden="true" />
<span>Source</span>
View source
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.wiki_url"
:href="project.wiki_url"
class="title"
:target="$external()"
rel="noopener nofollow ugc"
>
<WikiIcon aria-hidden="true" />
<span>Wiki</span>
Visit wiki
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.discord_url"
@@ -516,8 +608,18 @@
rel="noopener nofollow ugc"
>
<DiscordIcon class="shrink" aria-hidden="true" />
<span>Discord</span>
Join Discord server
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<hr
v-if="
(project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url) &&
project.donation_urls.length > 0
"
/>
<a
v-for="(donation, index) in project.donation_urls"
:key="index"
@@ -536,195 +638,417 @@
<HeartIcon v-else-if="donation.id === 'github'" />
<UnknownIcon v-else />
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
<span v-else-if="donation.id === 'patreon'">Patreon</span>
<span v-else-if="donation.id === 'paypal'">PayPal</span>
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
<span v-else-if="donation.id === 'patreon'">Donate on Patreon</span>
<span v-else-if="donation.id === 'paypal'">Donate on PayPal</span>
<span v-else-if="donation.id === 'ko-fi'">Donate on Ko-fi</span>
<span v-else-if="donation.id === 'github'">Sponsor on GitHub</span>
<span v-else>Donate</span>
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
</div>
<hr class="card-divider" />
</template>
<template v-if="featuredVersions.length > 0">
<div class="featured-header">
<h2 class="card-header">Featured versions</h2>
<nuxt-link
v-if="route.name !== 'type-id-versions' && (versions.length > 0 || currentMember)"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/versions#all-versions`"
class="goto-link"
>
See all
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
</nuxt-link>
</div>
<div
v-for="version in featuredVersions"
:key="version.id"
class="featured-version button-transparent"
@click="
$router.push(
`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`
)
</div>
<div
v-if="
showFeaturedVersions ||
!flags.newProjectLinks ||
!flags.newProjectDetails ||
!flags.newProjectMembers
"
class="universal-card"
>
<template
v-if="
!flags.newProjectLinks &&
(project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url ||
project.donation_urls.length > 0)
"
>
<a
v-tooltip="
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
"
:href="version.primaryFile.url"
class="download square-button brand-button"
:aria-label="`Download ${version.name}`"
@click.stop="(event) => event.stopPropagation()"
>
<DownloadIcon aria-hidden="true" />
</a>
<div class="info">
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`"
class="top"
>
{{ version.name }}
</nuxt-link>
<div v-if="version.game_versions.length > 0" class="game-version item">
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
{{ $formatVersion(version.game_versions) }}
</div>
<Badge v-if="version.version_type === 'release'" type="release" color="green" />
<Badge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
<Badge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
</div>
</div>
<hr class="card-divider" />
</template>
<h2 class="card-header">Project members</h2>
<nuxt-link
v-if="organization"
class="team-member columns button-transparent"
:to="`/organization/${organization.slug}`"
>
<Avatar :src="organization.icon_url" :alt="organization.name" size="sm" />
<div class="member-info">
<p class="name">
{{ organization.name }}
</p>
<p class="role"><OrganizationIcon /> Organization</p>
</div>
</nuxt-link>
<nuxt-link
v-for="member in members"
:key="member.user.id"
class="team-member columns button-transparent"
:to="'/user/' + member.user.username"
>
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
<div class="member-info">
<p class="name">
{{ member.name }} <CrownIcon v-if="member.is_owner" v-tooltip="'Project owner'" />
</p>
<p class="role">
{{ member.role }}
</p>
</div>
</nuxt-link>
<hr class="card-divider" />
<h2 class="card-header">Technical information</h2>
<div class="infos">
<div class="info">
<div class="key">License</div>
<div class="value lowercase">
<h2 class="card-header">External resources</h2>
<div class="links">
<a
v-if="project.license.url"
class="text-link"
:href="project.license.url"
v-if="project.issues_url"
:href="project.issues_url"
class="title"
:target="$external()"
rel="noopener nofollow ugc"
>
{{ licenseIdDisplay }}
<IssuesIcon aria-hidden="true" />
<span>Issues</span>
</a>
<span
v-else-if="
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
!project.license.id.includes('LicenseRef')
"
class="text-link"
@click="getLicenseData()"
<a
v-if="project.source_url"
:href="project.source_url"
class="title"
:target="$external()"
rel="noopener nofollow ugc"
>
{{ licenseIdDisplay }}
</span>
<span v-else>{{ licenseIdDisplay }}</span>
<CodeIcon aria-hidden="true" />
<span>Source</span>
</a>
<a
v-if="project.wiki_url"
:href="project.wiki_url"
class="title"
:target="$external()"
rel="noopener nofollow ugc"
>
<WikiIcon aria-hidden="true" />
<span>Wiki</span>
</a>
<a
v-if="project.discord_url"
:href="project.discord_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<DiscordIcon class="shrink" aria-hidden="true" />
<span>Discord</span>
</a>
<a
v-for="(donation, index) in project.donation_urls"
:key="index"
:href="donation.url"
:target="$external()"
rel="noopener nofollow ugc"
>
<BuyMeACoffeeLogo v-if="donation.id === 'bmac'" aria-hidden="true" />
<PatreonIcon v-else-if="donation.id === 'patreon'" aria-hidden="true" />
<KoFiIcon v-else-if="donation.id === 'ko-fi'" aria-hidden="true" />
<PayPalIcon v-else-if="donation.id === 'paypal'" aria-hidden="true" />
<OpenCollectiveIcon
v-else-if="donation.id === 'open-collective'"
aria-hidden="true"
/>
<HeartIcon v-else-if="donation.id === 'github'" />
<UnknownIcon v-else />
<span v-if="donation.id === 'bmac'">Buy Me a Coffee</span>
<span v-else-if="donation.id === 'patreon'">Patreon</span>
<span v-else-if="donation.id === 'paypal'">PayPal</span>
<span v-else-if="donation.id === 'ko-fi'">Ko-fi</span>
<span v-else-if="donation.id === 'github'">GitHub Sponsors</span>
<span v-else>Donate</span>
</a>
</div>
</div>
<div
v-if="
project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' &&
project.project_type !== 'shader' &&
project.project_type !== 'datapack'
"
class="info"
>
<div class="key">Client side</div>
<div class="value">
{{ project.client_side }}
<hr
v-if="showFeaturedVersions || !flags.newProjectMembers || !flags.newProjectDetails"
class="card-divider"
/>
</template>
<template v-if="showFeaturedVersions">
<div class="featured-header">
<h2 class="card-header">Featured versions</h2>
<nuxt-link
v-if="route.name !== 'type-id-versions' && (versions.length > 0 || currentMember)"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/versions#all-versions`"
class="goto-link"
>
See all
<ChevronRightIcon class="featured-header-chevron" aria-hidden="true" />
</nuxt-link>
</div>
</div>
<div
v-if="
project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' &&
project.project_type !== 'shader' &&
project.project_type !== 'datapack'
"
class="info"
>
<div class="key">Server side</div>
<div class="value">
{{ project.server_side }}
</div>
</div>
<div class="info">
<div class="key">Project ID</div>
<div class="value lowercase">
<CopyCode :text="project.id" />
</div>
</div>
<div class="input-group">
<a
v-if="
config.public.apiBaseUrl.startsWith('https://api.modrinth.com') &&
config.public.siteUrl !== 'https://modrinth.com'
<div
v-for="version in featuredVersions"
:key="version.id"
class="featured-version button-transparent"
@click="
$router.push(
`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`
)
"
class="iconified-button"
:href="`https://modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}`"
rel="noopener nofollow"
target="_blank"
>
<ExternalIcon aria-hidden="true" />
View on modrinth.com
</a>
<a
v-else-if="
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
config.public.siteUrl !== 'https://staging.modrinth.com'
"
class="iconified-button"
:href="`https://staging.modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}`"
rel="noopener nofollow"
target="_blank"
<a
v-tooltip="
version.primaryFile.filename + ' (' + $formatBytes(version.primaryFile.size) + ')'
"
:href="version.primaryFile.url"
class="download square-button brand-button"
:aria-label="`Download ${version.name}`"
@click.stop="(event) => event.stopPropagation()"
>
<DownloadIcon aria-hidden="true" />
</a>
<div class="info">
<nuxt-link
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`"
class="top"
>
{{ version.name }}
</nuxt-link>
<div v-if="version.game_versions.length > 0" class="game-version item">
{{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
{{ $formatVersion(version.game_versions) }}
</div>
<Badge v-if="version.version_type === 'release'" type="release" color="green" />
<Badge v-else-if="version.version_type === 'beta'" type="beta" color="orange" />
<Badge v-else-if="version.version_type === 'alpha'" type="alpha" color="red" />
</div>
</div>
<hr v-if="!flags.newProjectMembers || !flags.newProjectDetails" class="card-divider" />
</template>
<template v-if="!flags.newProjectMembers">
<h2 class="card-header">Project members</h2>
<nuxt-link
v-if="organization"
class="team-member columns button-transparent"
:to="`/organization/${organization.slug}`"
>
<ExternalIcon aria-hidden="true" />
View on staging.modrinth.com
</a>
<Avatar :src="organization.icon_url" :alt="organization.name" size="sm" />
<div class="member-info">
<p class="name">
{{ organization.name }}
</p>
<p class="role"><OrganizationIcon /> Organization</p>
</div>
</nuxt-link>
<nuxt-link
v-for="member in members"
:key="member.user.id"
class="team-member columns button-transparent"
:to="'/user/' + member.user.username"
>
<Avatar :src="member.avatar_url" :alt="member.username" size="sm" circle />
<div class="member-info">
<p class="name">
{{ member.name }}
<CrownIcon v-if="member.is_owner" v-tooltip="'Project owner'" />
</p>
<p class="role">
{{ member.role }}
</p>
</div>
</nuxt-link>
<hr v-if="!flags.newProjectDetails" class="card-divider" />
</template>
<template v-if="!flags.newProjectDetails">
<h2 class="card-header">Technical information</h2>
<div class="infos">
<div class="info">
<div class="key">License</div>
<div class="value lowercase">
<a
v-if="project.license.url"
class="text-link"
:href="project.license.url"
rel="noopener nofollow ugc"
>
{{ licenseIdDisplay }}
</a>
<span
v-else-if="
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
!project.license.id.includes('LicenseRef')
"
class="text-link"
@click="getLicenseData()"
>
{{ licenseIdDisplay }}
</span>
<span v-else>{{ licenseIdDisplay }}</span>
</div>
</div>
<div
v-if="
project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' &&
project.project_type !== 'shader' &&
project.project_type !== 'datapack'
"
class="info"
>
<div class="key">Client side</div>
<div class="value">
{{ project.client_side }}
</div>
</div>
<div
v-if="
project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' &&
project.project_type !== 'shader' &&
project.project_type !== 'datapack'
"
class="info"
>
<div class="key">Server side</div>
<div class="value">
{{ project.server_side }}
</div>
</div>
<div class="info">
<div class="key">Project ID</div>
<div class="value lowercase">
<CopyCode :text="project.id" />
</div>
</div>
<div class="input-group">
<a
v-if="
config.public.apiBaseUrl.startsWith('https://api.modrinth.com') &&
config.public.siteUrl !== 'https://modrinth.com'
"
class="iconified-button"
:href="`https://modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}`"
rel="noopener nofollow"
target="_blank"
>
<ExternalIcon aria-hidden="true" />
View on modrinth.com
</a>
<a
v-else-if="
config.public.apiBaseUrl.startsWith('https://staging-api.modrinth.com') &&
config.public.siteUrl !== 'https://staging.modrinth.com'
"
class="iconified-button"
:href="`https://staging.modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}`"
rel="noopener nofollow"
target="_blank"
>
<ExternalIcon aria-hidden="true" />
View on staging.modrinth.com
</a>
</div>
</div>
</template>
</div>
<div v-if="flags.newProjectMembers" class="card flex-card experimental-styles-within">
<h2>Creators</h2>
<div class="details-list">
<template v-if="organization">
<nuxt-link
class="details-list__item details-list__item--type-large"
:to="`/organization/${organization.slug}`"
>
<Avatar
:src="organization.icon_url"
:alt="organization.name"
class="icon"
data-size="32"
data-shape="square"
/>
<div class="rows">
<span>
{{ organization.name }}
</span>
<span class="details-list__item__text--style-secondary">Organization</span>
</div>
</nuxt-link>
<hr />
</template>
<nuxt-link
v-for="member in members"
:key="`member-${member.id}`"
class="details-list__item details-list__item--type-large"
:to="'/user/' + member.user.username"
>
<Avatar
:src="member.avatar_url"
:alt="member.name"
class="icon"
data-size="32"
data-shape="circle"
/>
<div class="rows">
<span>
{{ member.name }}
<CrownIcon
v-if="member.is_owner"
v-tooltip="'Project owner'"
class="project-owner-icon"
/>
</span>
<span class="details-list__item__text--style-secondary">{{ member.role }}</span>
</div>
</nuxt-link>
</div>
</div>
<div v-if="flags.newProjectDetails" class="card flex-card experimental-styles-within">
<h2>Details</h2>
<div class="details-list">
<div class="details-list__item">
<LicenseIcon aria-hidden="true" />
<div>
Licensed
<a
v-if="project.license.url"
class="text-link"
:href="project.license.url"
:target="$external()"
rel="noopener nofollow ugc"
>
{{ licenseIdDisplay }} <ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<span
v-else-if="
project.license.id === 'LicenseRef-All-Rights-Reserved' ||
!project.license.id.includes('LicenseRef')
"
class="text-link"
@click="getLicenseData()"
>
{{ licenseIdDisplay }}
</span>
<span v-else>{{ licenseIdDisplay }}</span>
</div>
</div>
<div
v-if="project.approved"
v-tooltip="$dayjs(project.approved).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<CalendarIcon aria-hidden="true" />
<div>
Published
<span>{{ fromNow(project.approved) }}</span>
</div>
</div>
<div
v-else
v-tooltip="$dayjs(project.published).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<CalendarIcon aria-hidden="true" />
<div>
Created
<span>{{ fromNow(project.published) }}</span>
</div>
</div>
<div
v-if="project.status === 'processing' && project.queued"
v-tooltip="$dayjs(project.queued).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<ModeratorIcon aria-hidden="true" />
<div>
Submitted
<span>{{ fromNow(project.queued) }}</span>
</div>
</div>
<div
v-if="versions.length > 0 && project.updated"
v-tooltip="$dayjs(project.updated).format('MMMM D, YYYY [at] h:mm A')"
class="details-list__item"
>
<VersionIcon aria-hidden="true" />
<div>
Updated
<span>{{ fromNow(project.updated) }}</span>
</div>
</div>
</div>
</div>
</div>
@@ -753,6 +1077,8 @@ import {
isRejected,
isUnderReview,
isStaff,
CheckIcon,
XIcon,
} from 'omorphia'
import CrownIcon from '~/assets/images/utils/crown.svg'
import CalendarIcon from '~/assets/images/utils/calendar.svg'
@@ -790,7 +1116,8 @@ import UsersIcon from '~/assets/images/utils/users.svg'
import CategoriesIcon from '~/assets/images/utils/tags.svg'
import DescriptionIcon from '~/assets/images/utils/align-left.svg'
import LinksIcon from '~/assets/images/utils/link.svg'
import LicenseIcon from '~/assets/images/utils/copyright.svg'
import CopyrightIcon from '~/assets/images/utils/copyright.svg'
import LicenseIcon from '~/assets/images/utils/book-text.svg'
import GalleryIcon from '~/assets/images/utils/image.svg'
import VersionIcon from '~/assets/images/utils/version.svg'
import { reportProject } from '~/utils/report-helpers.ts'
@@ -799,6 +1126,8 @@ import { userCollectProject } from '~/composables/user.js'
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
import OrganizationIcon from '~/assets/images/utils/organization.svg'
import ModerationChecklist from '~/components/ui/ModerationChecklist.vue'
import ModeratorIcon from '~/assets/images/sidebar/admin.svg'
import { getVersionsToDisplay } from '~/helpers/projects.js'
const data = useNuxtApp()
const route = useRoute()
@@ -808,6 +1137,7 @@ const auth = await useAuth()
const user = await useUser()
const cosmetics = useCosmetics()
const tags = useTags()
const flags = useFeatureFlags()
const displayCollectionsSearch = ref('')
const collections = computed(() =>
@@ -927,8 +1257,14 @@ if (project.value.project_type !== route.params.type || route.params.id !== proj
// The rest of the members should be sorted by role, then by name
const members = computed(() => {
const acceptedMembers = allMembers.value.filter((x) => x.accepted)
const owner = acceptedMembers.find((x) => x.is_owner)
const rest = acceptedMembers.filter((x) => !x.is_owner) || []
const owner = acceptedMembers.find((x) =>
organization.value
? organization.value.members.some(
(orgMember) => orgMember.user.id === x.user.id && orgMember.is_owner
)
: x.is_owner
)
const rest = acceptedMembers.filter((x) => x.user.id !== owner.user.id) || []
rest.sort((a, b) => {
if (a.role === b.role) {
@@ -1173,10 +1509,15 @@ if (process.client && history && history.state && history.state.showChecklist) {
showModerationChecklist.value = true
futureProjects.value = history.state.projects
}
const showFeaturedVersions = computed(
() => !flags.value.removeFeaturedVersions && featuredVersions.value.length > 0
)
</script>
<style lang="scss" scoped>
.header {
grid-area: header;
.title {
overflow-wrap: break-word;
margin: var(--spacing-card-xs) 0;
@@ -1243,11 +1584,13 @@ if (process.client && history && history.state && history.state.showChecklist) {
.project__gallery {
display: none;
}
&.has-featured-image {
.project__gallery {
display: inline-block;
width: 100%;
height: 10rem;
img {
width: 100%;
height: 10rem;
@@ -1256,6 +1599,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
}
}
.project__icon {
margin-top: calc(-3rem - var(--spacing-card-lg) - 4px);
margin-left: -4px;
@@ -1263,11 +1607,13 @@ if (process.client && history && history.state && history.state.showChecklist) {
box-shadow: -2px -2px 0 2px var(--color-raised-bg), 2px -2px 0 2px var(--color-raised-bg);
}
}
.project__header__content {
margin: 0;
background: none;
border-radius: unset;
}
.input-group {
flex-wrap: nowrap;
}
@@ -1468,6 +1814,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
.modal-license {
padding: var(--spacing-card-bg);
}
.settings-header {
display: flex;
flex-direction: row;
@@ -1495,6 +1842,7 @@ if (process.client && history && history.state && history.state.showChecklist) {
.popout-checkbox {
padding: var(--gap-sm) var(--gap-md);
white-space: nowrap;
&:hover {
filter: brightness(0.95);
}
@@ -1531,4 +1879,12 @@ if (process.client && history && history.state && history.state.showChecklist) {
margin: var(--gap-sm) var(--gap-md);
padding: var(--gap-sm);
}
.normal-page__info:empty {
display: none;
}
.project-owner-icon {
color: var(--color-orange);
}
</style>

View File

@@ -157,18 +157,18 @@ function switchPage(page) {
}
.changelog-bar {
--color: var(--color-special-green);
--color: var(--color-green);
&.alpha {
--color: var(--color-special-red);
--color: var(--color-red);
}
&.release {
--color: var(--color-special-green);
--color: var(--color-green);
}
&.beta {
--color: var(--color-special-orange);
--color: var(--color-orange);
}
left: 0;

View File

@@ -204,12 +204,12 @@ svg {
}
&.bad {
color: var(--color-special-red);
color: var(--color-red);
}
}
.warning {
color: var(--color-special-orange);
color: var(--color-orange);
font-weight: bold;
}
</style>

View File

@@ -421,11 +421,11 @@ svg {
}
&.bad {
color: var(--color-special-red);
color: var(--color-red);
}
&.warn {
color: var(--color-special-orange);
color: var(--color-orange);
}
}

View File

@@ -519,7 +519,6 @@
<script setup>
import { Multiselect } from 'vue-multiselect'
import { Avatar, Badge, Card, Checkbox, TransferIcon, CheckIcon, UsersIcon } from 'omorphia'
import { defineProps, ref, watch } from 'vue'
import ModalConfirm from '~/components/ui/ModalConfirm.vue'
import DropdownIcon from '~/assets/images/utils/dropdown.svg'

View File

@@ -1374,7 +1374,7 @@ useSeoMeta({
display: flex;
align-items: center;
padding: var(--gap-sm) 0;
color: var(--color-special-gray);
color: var(--color-gray);
&.important {
color: var(--color-contrast);

View File

@@ -617,7 +617,7 @@ export default defineNuxtComponent({
gap: var(--spacing-card-xs);
svg {
color: var(--color-special-orange);
color: var(--color-orange);
}
}
@@ -645,7 +645,7 @@ export default defineNuxtComponent({
}
.label-button[data-active='true'] {
--background-color: var(--color-special-red);
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
}

View File

@@ -506,7 +506,7 @@ async function withdraw() {
}
.invalid {
color: var(--color-special-red);
color: var(--color-red);
}
.confirm-text {

63
pages/flags.vue Normal file
View File

@@ -0,0 +1,63 @@
<script setup lang="ts">
import { FeatureFlag, DEFAULT_FEATURE_FLAGS, saveFeatureFlags } from '~/composables/featureFlags.ts'
const flags = shallowReactive(useFeatureFlags().value)
</script>
<template>
<div class="page">
<h1>Feature flags</h1>
<div class="flags">
<div
v-for="flag in Object.keys(flags) as FeatureFlag[]"
:key="`flag-${flag}`"
class="adjacent-input small card"
>
<label :for="`toggle-${flag}`">
<span class="label__title">
{{ flag.replaceAll('_', ' ') }}
</span>
<span class="label__description">
<p>
Default:
<span
:style="`color:var(--color-${
DEFAULT_FEATURE_FLAGS[flag] === false ? 'red' : 'green'
})`"
>{{ DEFAULT_FEATURE_FLAGS[flag] }}</span
>
</p>
</span>
</label>
<input
:id="`toggle-${flag}`"
v-model="flags[flag]"
class="switch stylized-toggle"
type="checkbox"
@change="() => saveFeatureFlags()"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.page {
width: calc(100% - 2 * var(--spacing-card-md));
max-width: 800px;
margin-inline: auto;
box-sizing: border-box;
margin-block: var(--spacing-card-md);
}
.flags {
}
.label__title {
text-transform: capitalize;
}
.label__description p {
margin: 0;
}
</style>

View File

@@ -233,11 +233,11 @@ async function goToProjects() {
}
.warning {
color: var(--color-special-orange);
color: var(--color-orange);
}
.danger {
color: var(--color-special-red);
color: var(--color-red);
font-weight: bold;
}

View File

@@ -630,7 +630,7 @@ const onBulkEditLinks = useClientTry(async () => {
gap: var(--spacing-card-xs);
svg {
color: var(--color-special-orange);
color: var(--color-orange);
}
}
@@ -658,7 +658,7 @@ const onBulkEditLinks = useClientTry(async () => {
}
.label-button[data-active='true'] {
--background-color: var(--color-special-red);
--background-color: var(--color-red);
--text-color: var(--color-brand-inverted);
}

View File

@@ -1,6 +1,6 @@
<template>
<div>
<MessageBanner v-if="cosmetics.developerMode" message-type="warning" class="developer-message">
<MessageBanner v-if="flags.developerMode" message-type="warning" class="developer-message">
<CodeIcon />
<IntlFormatted :message-id="developerModeBanner.description">
<template #strong="{ children }">
@@ -137,15 +137,15 @@
</div>
</section>
<section class="universal-card">
<h2>{{ formatMessage(featureFlags.title) }}</h2>
<p>{{ formatMessage(featureFlags.description) }}</p>
<h2>{{ formatMessage(toggleFeatures.title) }}</h2>
<p>{{ formatMessage(toggleFeatures.description) }}</p>
<div class="adjacent-input small">
<label for="advanced-rendering">
<span class="label__title">
{{ formatMessage(featureFlags.advancedRenderingTitle) }}
{{ formatMessage(toggleFeatures.advancedRenderingTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.advancedRenderingDescription) }}
{{ formatMessage(toggleFeatures.advancedRenderingDescription) }}
</span>
</label>
<input
@@ -159,10 +159,10 @@
<div class="adjacent-input small">
<label for="external-links-new-tab">
<span class="label__title">
{{ formatMessage(featureFlags.externalLinksNewTabTitle) }}
{{ formatMessage(toggleFeatures.externalLinksNewTabTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.externalLinksNewTabDescription) }}
{{ formatMessage(toggleFeatures.externalLinksNewTabDescription) }}
</span>
</label>
<input
@@ -176,10 +176,10 @@
<div class="adjacent-input small">
<label for="modrinth-app-promos">
<span class="label__title">
{{ formatMessage(featureFlags.hideModrinthAppPromosTitle) }}
{{ formatMessage(toggleFeatures.hideModrinthAppPromosTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.hideModrinthAppPromosDescription) }}
{{ formatMessage(toggleFeatures.hideModrinthAppPromosDescription) }}
</span>
</label>
<input
@@ -193,10 +193,10 @@
<div class="adjacent-input small">
<label for="search-layout-toggle">
<span class="label__title">
{{ formatMessage(featureFlags.rightAlignedSearchSidebarTitle) }}
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.rightAlignedSearchSidebarDescription) }}
{{ formatMessage(toggleFeatures.rightAlignedSearchSidebarDescription) }}
</span>
</label>
<input
@@ -210,10 +210,10 @@
<div class="adjacent-input small">
<label for="project-layout-toggle">
<span class="label__title">
{{ formatMessage(featureFlags.rightAlignedProjectSidebarTitle) }}
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarTitle) }}
</span>
<span class="label__description">
{{ formatMessage(featureFlags.rightAlignedProjectSidebarDescription) }}
{{ formatMessage(toggleFeatures.rightAlignedProjectSidebarDescription) }}
</span>
</label>
<input
@@ -331,10 +331,10 @@ const projectListLayouts = defineMessages({
},
})
const featureFlags = defineMessages({
const toggleFeatures = defineMessages({
title: {
id: 'settings.display.flags.title',
defaultMessage: 'Feature flags',
defaultMessage: 'Toggle features',
},
description: {
id: 'settings.display.flags.description',
@@ -386,6 +386,7 @@ const featureFlags = defineMessages({
})
const cosmetics = useCosmetics()
const flags = useFeatureFlags()
const tags = useTags()
const systemTheme = ref('light')
@@ -394,7 +395,7 @@ const theme = useTheme()
const themeOptions = computed(() => {
const options = ['system', 'light', 'dark', 'oled']
if (cosmetics.value.developerMode || theme.value.preference === 'retro') {
if (flags.value.developerMode || theme.value.preference === 'retro') {
options.push('retro')
}
return options
@@ -430,8 +431,8 @@ function updateColorTheme(value) {
}
function disableDeveloperMode() {
cosmetics.value.developerMode = !cosmetics.value.developerMode
saveCosmetics()
flags.value.developerMode = !flags.value.developerMode
saveFeatureFlags()
addNotification({
group: 'main',
title: 'Developer mode deactivated',

View File

@@ -414,10 +414,10 @@ function getItemLabel(locale: Locale) {
}
&.errored {
border-color: var(--color-special-red);
border-color: var(--color-red);
&:hover {
border-color: var(--color-special-red);
border-color: var(--color-red);
}
}
@@ -473,7 +473,7 @@ function getItemLabel(locale: Locale) {
}
.language-load-error {
color: var(--color-special-red);
color: var(--color-red);
font-size: var(--font-size-sm);
margin-left: 0.3rem;
display: flex;

View File

@@ -502,7 +502,7 @@ export default defineNuxtComponent({
// 5 wide
display: flex;
flex-wrap: wrap;
justify-content: start;
justify-content: flex-start;
grid-gap: var(--gap-sm);
margin-top: 0.5rem;