App redesign (#2946)

* Start of app redesign

* format

* continue progress

* Content page nearly done

* Fix recursion issues with content page

* Fix update all alignment

* Discover page progress

* Settings progress

* Removed unlocked-size hack that breaks web

* Revamp project page, refactor web project page to share code with app, fixed loading bar, misc UI/UX enhancements, update ko-fi logo, update arrow icons, fix web issues caused by floating-vue migration, fix tooltip issues, update web tooltips, clean up web hydration issues

* Ads + run prettier

* Begin auth refactor, move common messages to ui lib, add i18n extraction to all apps, begin Library refactor

* fix ads not hiding when plus log in

* rev lockfile changes/conflicts

* Fix sign in page

* Add generated

* (mostly) Data driven search

* Fix search mobile issue

* profile fixes

* Project versions page, fix typescript on UI lib and misc fixes

* Remove unused gallery component

* Fix linkfunction err

* Search filter controls at top, localization for locked filters

* Fix provided filter names

* Fix navigating from instance browse to main browse

* Friends frontend (#2995)

* Friends system frontend

* (almost) finish frontend

* finish friends, fix lint

* Fix lint

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>

* Refresh macOS app icon

* Update web search UI more

* Fix link opens

* Fix frontend build

---------

Signed-off-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
Co-authored-by: Jai A <jaiagr+gpg@pm.me>
Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
Prospector
2024-12-11 19:54:18 -08:00
committed by GitHub
parent 6ec1dcf088
commit c39bb78e38
257 changed files with 15713 additions and 9475 deletions

View File

@@ -399,22 +399,6 @@
}
}
.v-popper--theme-tooltip {
.v-popper__inner {
background: var(--color-tooltip-bg) !important;
color: var(--color-tooltip-text) !important;
padding: 5px 10px 4px !important;
border-radius: var(--size-rounded-tooltip) !important;
box-shadow: var(--shadow-floating) !important;
font-size: 0.9rem !important;
}
.v-popper__arrow-outer,
.v-popper__arrow-inner {
border-color: var(--color-tooltip-bg) !important;
}
}
.button-base {
@extend .button-animation;
font-weight: 500;
@@ -1232,6 +1216,7 @@ svg.inline-svg {
font-size: var(--text-18);
font-weight: var(--weight-extrabold);
color: var(--color-contrast);
line-height: initial;
margin: 0;
}

View File

@@ -37,92 +37,8 @@ html {
--icon-20: 1.25rem; // used for icons in normal sized buttons
--icon-24: 1.5rem; // used for icons that are used as a primary label or in large buttons
--icon-32: 2rem;
}
.experimental-styles-within {
// Reset deprecated properties
--color-icon: initial !important;
--color-text: initial !important;
--color-text-inactive: initial !important;
--color-text-dark: initial !important;
--color-heading: initial !important;
--color-divider: initial !important;
--color-divider-dark: initial !important;
--color-text-inverted: initial !important;
--color-bg-inverted: initial !important;
--color-brand: var(--color-green) !important;
--color-brand-inverted: initial !important;
--tab-underline-hovered: initial !important;
--color-button-text: initial !important;
--color-button-bg-hover: initial !important;
--color-button-text-hover: initial !important;
--color-button-bg-active: initial !important;
--color-button-text-active: initial !important;
--color-grey-link: inherit !important;
--color-grey-link-hover: inherit !important; // DEPRECATED, use filters in future
--color-grey-link-active: inherit !important; // DEPRECATED, use filters in future
--color-link: var(--color-blue) !important;
--color-link-hover: var(--color-blue) !important; // DEPRECATED, use filters in future
--color-link-active: var(--color-blue) !important; // DEPRECATED, use filters in future
}
.light-mode,
.light {
.experimental-styles-within,
&.experimental-styles-within {
--color-bg: #ebebeb;
--color-raised-bg: #ffffff;
--color-button-bg: #f5f5f5;
--color-base: #2c2e31;
--color-secondary: #484d54;
--color-accent-contrast: #ffffff;
--color-platform-fabric: #8a7b71;
--color-platform-quilt: #8b61b4;
--color-platform-forge: #5b6197;
--color-platform-neoforge: #dc895c;
--color-platform-liteloader: #4c90de;
--color-platform-bukkit: #e78362;
--color-platform-bungeecord: #c69e39;
--color-platform-folia: #6aa54f;
--color-platform-paper: #e67e7e;
--color-platform-purpur: #7763a3;
--color-platform-spigot: #cd7a21;
--color-platform-velocity: #4b98b0;
--color-platform-waterfall: #5f83cb;
--color-platform-sponge: #c49528;
--color-button-border: rgba(161, 161, 161, 0.35);
}
}
.dark-mode,
.dark {
.experimental-styles-within,
&.experimental-styles-within {
--color-button-bg: #33363d;
--color-platform-fabric: #dbb69b;
--color-platform-quilt: #c796f9;
--color-platform-forge: #959eef;
--color-platform-neoforge: #f99e6b;
--color-platform-liteloader: #7ab0ee;
--color-platform-bukkit: #f6af7b;
--color-platform-bungeecord: #d2c080;
--color-platform-folia: #a5e388;
--color-platform-paper: #eeaaaa;
--color-platform-purpur: #c3abf7;
--color-platform-spigot: #f1cc84;
--color-platform-velocity: #83d5ef;
--color-platform-waterfall: #78a4fb;
--color-platform-sponge: #f9e580;
--color-button-border: rgba(193, 190, 209, 0.12);
}
interpolate-size: allow-keywords;
}
.light-mode {
@@ -159,9 +75,6 @@ html {
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);
--color-tooltip-bg: var(--color-text);
--color-tooltip-text: var(--color-bg);
--color-code-bg: var(--color-bg);
--color-code-text: var(--color-text-dark);
@@ -179,12 +92,6 @@ html {
--color-link-hover: #1a76e7;
--color-link-active: #146fd7;
--color-red-bg: rgba(203, 34, 69, 0.1);
--color-orange-bg: rgba(224, 131, 37, 0.1);
--color-green-bg: rgba(0, 175, 92, 0.1);
--color-blue-bg: rgba(31, 104, 192, 0.1);
--color-purple-bg: rgba(142, 50, 243, 0.1);
--color-warning-bg: hsl(355, 70%, 88%);
--color-warning-text: hsl(342, 70%, 35%);
@@ -275,12 +182,6 @@ html {
--color-text-inverted: var(--color-bg);
--color-bg-inverted: var(--color-text);
--color-red-bg: rgba(255, 73, 110, 0.2);
--color-orange-bg: rgba(255, 163, 71, 0.2);
--color-green-bg: rgba(27, 217, 106, 0.2);
--color-blue-bg: rgba(79, 156, 255, 0.2);
--color-purple-bg: rgba(199, 138, 255, 0.2);
--color-brand: var(--color-green);
--color-brand-highlight: rgba(27, 217, 106, 0.25);
--color-brand-shadow: rgba(27, 217, 106, 0.7);
@@ -300,9 +201,6 @@ html {
--color-dropdown-bg: var(--color-button-bg);
--color-dropdown-text: var(--color-button-text);
--color-tooltip-bg: var(--color-button-bg);
--color-tooltip-text: var(--color-text);
--color-code-bg: var(--color-button-bg);
--color-code-text: var(--color-text-dark);

View File

@@ -39,7 +39,7 @@
.normal-page {
display: grid;
padding: 0 0.75rem;
padding: 0 1.5rem;
grid-template:
"sidebar"
@@ -115,7 +115,7 @@
}
.normal-page__content {
max-width: calc(80rem - 18.75rem - 0.75rem);
max-width: calc(80rem - 18.75rem - 1.5rem);
//overflow-x: hidden;
}
}
@@ -125,7 +125,7 @@
margin: 0 auto;
max-width: 80rem;
column-gap: 0.75rem;
padding: 0 0.75rem;
padding: 0 1.5rem;
grid-template:
"header"
@@ -162,7 +162,7 @@
.normal-page__content {
grid-area: content;
max-width: calc(80rem - 18.75rem - 0.75rem);
max-width: calc(80rem - 18.75rem - 1.5rem);
//overflow-x: hidden;
}

View File

@@ -3,7 +3,7 @@
<ButtonStyled v-if="!!slots.title" :type="type">
<button class="!w-full" @click="() => (isOpen ? close() : open())">
<slot name="title" /><DropdownIcon
class="ml-auto size-5 transition-transform duration-300"
class="ml-auto size-5 text-contrast transition-transform duration-300"
:class="{ 'rotate-180': isOpen }"
/>
</button>
@@ -62,6 +62,9 @@ defineOptions({
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out;
animation: height-animate 500ms ease-in-out both;
content-visibility: auto;
animation-composition: replace;
}
@media (prefers-reduced-motion) {
@@ -77,4 +80,13 @@ defineOptions({
.accordion-content > div {
overflow: hidden;
}
@keyframes height-animate {
from {
block-size: initial;
}
to {
block-size: auto;
}
}
</style>

View File

@@ -4,7 +4,7 @@
'badge flex items-center gap-1 font-semibold text-secondary ' + color + ' type--' + type
"
>
<template v-if="color"> <span class="circle" /> {{ $capitalizeString(type) }}</template>
<template v-if="color"> <span class="circle" /> {{ capitalizeString(type) }}</template>
<!-- User roles -->
<template v-else-if="type === 'admin'"> <ModrinthIcon /> Modrinth Team</template>
@@ -36,25 +36,28 @@
<template v-else-if="type === 'closed'"> <CloseIcon /> Closed</template>
<!-- Other -->
<template v-else> <span class="circle" /> {{ $capitalizeString(type) }} </template>
<template v-else> <span class="circle" /> {{ capitalizeString(type) }} </template>
</span>
</template>
<script setup>
import { GlobeIcon, LinkIcon } from "@modrinth/assets";
import ModrinthIcon from "~/assets/images/logo.svg?component";
import PlusIcon from "~/assets/images/utils/plus.svg?component";
import ModeratorIcon from "~/assets/images/sidebar/admin.svg?component";
import CreatorIcon from "~/assets/images/utils/box.svg?component";
import DraftIcon from "~/assets/images/utils/file-text.svg?component";
import CrossIcon from "~/assets/images/utils/x.svg?component";
import ArchiveIcon from "~/assets/images/utils/archive.svg?component";
import ProcessingIcon from "~/assets/images/utils/updated.svg?component";
import CheckIcon from "~/assets/images/utils/check.svg?component";
import LockIcon from "~/assets/images/utils/lock.svg?component";
import CalendarIcon from "~/assets/images/utils/calendar.svg?component";
import CloseIcon from "~/assets/images/utils/check-circle.svg?component";
import {
GlobeIcon,
LinkIcon,
ModrinthIcon,
PlusIcon,
ScaleIcon as ModeratorIcon,
BoxIcon as CreatorIcon,
FileTextIcon as DraftIcon,
XIcon as CrossIcon,
ArchiveIcon,
UpdatedIcon as ProcessingIcon,
CheckIcon,
LockIcon,
CalendarIcon,
XCircleIcon as CloseIcon,
} from "@modrinth/assets";
import { capitalizeString } from "@modrinth/utils";
defineProps({
type: {

View File

@@ -1,7 +1,7 @@
<template>
<nav
ref="scrollContainer"
class="experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
class="card-shadow experimental-styles-within relative flex w-fit overflow-x-auto rounded-full bg-bg-raised p-1 text-sm font-bold"
>
<NuxtLink
v-for="(link, index) in filteredLinks"
@@ -11,7 +11,7 @@
: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-brand': activeIndex === index && !subpageSelected,
'text-button-textSelected': activeIndex === index && !subpageSelected,
'text-contrast': activeIndex === index && subpageSelected,
}"
>
@@ -20,7 +20,7 @@
</NuxtLink>
<div
:class="`navtabs-transition pointer-events-none absolute h-[calc(100%-0.5rem)] overflow-hidden rounded-full p-1 ${
subpageSelected ? 'bg-button-bg' : 'bg-brand-highlight'
subpageSelected ? 'bg-button-bg' : 'bg-button-bgSelected'
}`"
:style="{
left: sliderLeftPx,
@@ -161,4 +161,8 @@ watch(
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>

View File

@@ -1,220 +0,0 @@
<template>
<div class="experimental-styles-within flex flex-col gap-3">
<div class="flex flex-wrap items-center gap-2">
<ManySelect
v-model="selectedPlatforms"
:options="filterOptions.platform"
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Platform
<template #option="{ option }">
{{ formatCategory(option) }}
</template>
</ManySelect>
<ManySelect
v-model="selectedGameVersions"
:options="filterOptions.gameVersion"
search
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Game versions
<template #footer>
<Checkbox v-model="showSnapshots" class="mx-1" :label="`Show all versions`" />
</template>
</ManySelect>
<ManySelect
v-model="selectedChannels"
:options="filterOptions.channel"
@change="updateFilters"
>
<FilterIcon class="h-5 w-5 text-secondary" />
Channels
<template #option="{ option }">
{{ option === "release" ? "Release" : option === "beta" ? "Beta" : "Alpha" }}
</template>
</ManySelect>
</div>
<div class="flex flex-wrap items-center gap-1 empty:hidden">
<button
v-if="selectedChannels.length + selectedGameVersions.length + selectedPlatforms.length > 1"
class="tag-list__item text-contrast transition-transform active:scale-[0.95]"
@click="clearFilters"
>
<XCircleIcon />
Clear all filters
</button>
<button
v-for="channel in selectedChannels"
:key="`remove-filter-${channel}`"
class="tag-list__item transition-transform active:scale-[0.95]"
:style="`--_color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'});--_bg-color: var(--color-${channel === 'alpha' ? 'red' : channel === 'beta' ? 'orange' : 'green'}-highlight)`"
@click="toggleFilter('channel', channel)"
>
<XIcon />
{{ channel.slice(0, 1).toUpperCase() + channel.slice(1) }}
</button>
<button
v-for="version in selectedGameVersions"
:key="`remove-filter-${version}`"
class="tag-list__item transition-transform active:scale-[0.95]"
@click="toggleFilter('gameVersion', version)"
>
<XIcon />
{{ version }}
</button>
<button
v-for="platform in selectedPlatforms"
:key="`remove-filter-${platform}`"
class="tag-list__item transition-transform active:scale-[0.95]"
:style="`--_color: var(--color-platform-${platform})`"
@click="toggleFilter('platform', platform)"
>
<XIcon />
{{ formatCategory(platform) }}
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { FilterIcon, XCircleIcon, XIcon } from "@modrinth/assets";
import { ManySelect, Checkbox } from "@modrinth/ui";
import { formatCategory } from "@modrinth/utils";
import type { ModrinthVersion } from "@modrinth/utils";
const props = defineProps<{ versions: ModrinthVersion[] }>();
const emit = defineEmits(["switch-page"]);
const allChannels = ref(["release", "beta", "alpha"]);
const route = useNativeRoute();
const router = useNativeRouter();
const tags = useTags();
const showSnapshots = ref(false);
type FilterType = "channel" | "gameVersion" | "platform";
type Filter = string;
const filterOptions = computed(() => {
const filters: Record<FilterType, Filter[]> = {
channel: [],
gameVersion: [],
platform: [],
};
const platformSet = new Set();
const gameVersionSet = new Set();
const channelSet = new Set();
for (const version of props.versions) {
for (const loader of version.loaders) {
platformSet.add(loader);
}
for (const gameVersion of version.game_versions) {
gameVersionSet.add(gameVersion);
}
channelSet.add(version.version_type);
}
if (channelSet.size > 0) {
filters.channel = Array.from(channelSet) as Filter[];
filters.channel.sort((a, b) => allChannels.value.indexOf(a) - allChannels.value.indexOf(b));
}
if (gameVersionSet.size > 0) {
const gameVersions = tags.value.gameVersions.filter((x) => gameVersionSet.has(x.version));
filters.gameVersion = gameVersions
.filter((x) => (showSnapshots.value ? true : x.version_type === "release"))
.map((x) => x.version);
}
if (platformSet.size > 0) {
filters.platform = Array.from(platformSet) as Filter[];
}
return filters;
});
const selectedChannels = ref<string[]>([]);
const selectedGameVersions = ref<string[]>([]);
const selectedPlatforms = ref<string[]>([]);
selectedChannels.value = route.query.c ? getArrayOrString(route.query.c) : [];
selectedGameVersions.value = route.query.g ? getArrayOrString(route.query.g) : [];
selectedPlatforms.value = route.query.l ? getArrayOrString(route.query.l) : [];
async function toggleFilters(type: FilterType, filters: Filter[]) {
for (const filter of filters) {
await toggleFilter(type, filter);
}
await router.replace({
query: {
...route.query,
c: selectedChannels.value,
g: selectedGameVersions.value,
l: selectedPlatforms.value,
},
});
emit("switch-page", 1);
}
async function toggleFilter(type: FilterType, filter: Filter, skipRouter = false) {
if (type === "channel") {
selectedChannels.value = selectedChannels.value.includes(filter)
? selectedChannels.value.filter((x) => x !== filter)
: [...selectedChannels.value, filter];
} else if (type === "gameVersion") {
selectedGameVersions.value = selectedGameVersions.value.includes(filter)
? selectedGameVersions.value.filter((x) => x !== filter)
: [...selectedGameVersions.value, filter];
} else if (type === "platform") {
selectedPlatforms.value = selectedPlatforms.value.includes(filter)
? selectedPlatforms.value.filter((x) => x !== filter)
: [...selectedPlatforms.value, filter];
}
if (!skipRouter) {
await updateFilters();
}
}
async function updateFilters() {
await router.replace({
query: {
...route.query,
c: selectedChannels.value,
g: selectedGameVersions.value,
l: selectedPlatforms.value,
},
});
emit("switch-page", 1);
}
async function clearFilters() {
selectedChannels.value = [];
selectedGameVersions.value = [];
selectedPlatforms.value = [];
await router.replace({
query: {
...route.query,
c: undefined,
g: undefined,
l: undefined,
},
});
emit("switch-page", 1);
}
defineExpose({
toggleFilter,
toggleFilters,
});
</script>

View File

@@ -1,46 +0,0 @@
<template>
<div
class="grid grid-cols-[min-content_auto_min-content_min-content] items-center gap-2 rounded-2xl border-[1px] border-button-bg bg-bg p-2"
>
<VersionChannelIndicator :channel="version.version_type" />
<div class="flex min-w-0 flex-col gap-1">
<h1 class="my-0 truncate text-nowrap text-base font-extrabold leading-none text-contrast">
{{ version.version_number }}
</h1>
<p class="m-0 truncate text-nowrap text-xs font-semibold text-secondary">
{{ version.name }}
</p>
</div>
<ButtonStyled color="brand">
<a :href="downloadUrl" class="min-w-0" @click="emit('onDownload')">
<DownloadIcon aria-hidden="true" /> Download
</a>
</ButtonStyled>
<ButtonStyled circular>
<nuxt-link
:to="`/project/${props.version.project_id}/version/${props.version.id}`"
class="min-w-0"
aria-label="Open project page"
@click="emit('onNavigate')"
>
<ExternalIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
</div>
</template>
<script setup lang="ts">
import { ButtonStyled, VersionChannelIndicator } from "@modrinth/ui";
import { DownloadIcon, ExternalIcon } from "@modrinth/assets";
const props = defineProps<{
version: Version;
}>();
const downloadUrl = computed(() => {
const primary: VersionFile = props.version.files.find((x) => x.primary) || props.version.files[0];
return primary.url;
});
const emit = defineEmits(["onDownload", "onNavigate"]);
</script>

View File

@@ -108,7 +108,7 @@
type="search"
name="search"
autocomplete="off"
class="h-8 min-h-[unset] w-full border-[1px] border-solid border-button-bg bg-transparent py-2 pl-9"
class="h-8 min-h-[unset] w-full border-[1px] border-solid border-divider bg-transparent py-2 pl-9"
placeholder="Search..."
@input="$emit('update:searchQuery', ($event.target as HTMLInputElement).value)"
/>

View File

@@ -25,7 +25,7 @@
</div>
<div
class="border-0 border-b border-solid"
:class="danger ? 'border-[#cb2245] dark:border-[#612d38]' : 'border-button-bg'"
:class="danger ? 'border-[#cb2245] dark:border-[#612d38]' : 'border-divider'"
></div>
<div class="mt-2 h-full w-full overflow-auto px-6">
<slot />

View File

@@ -25,7 +25,7 @@
v-if="isOpen"
ref="menuRef"
data-pyro-telepopover-root
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-button-bg bg-bg-raised p-2 shadow-lg"
class="experimental-styles-within fixed isolate z-[9999] flex w-fit flex-col gap-2 overflow-hidden rounded-2xl border-[1px] border-solid border-divider bg-bg-raised p-2 shadow-lg"
:style="menuStyle"
role="menu"
tabindex="-1"
@@ -272,7 +272,7 @@ const handleItemClick = (option: Option, index: number) => {
const handleMouseOver = (index: number) => {
selectedIndex.value = index;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
};
// Scrolling is disabled for keyboard navigation
@@ -295,7 +295,7 @@ const enableBodyScroll = () => {
const focusFirstMenuItem = () => {
if (menuItemsRef.value.length > 0) {
menuItemsRef.value[0].focus();
menuItemsRef.value[0].focus?.();
}
};
@@ -312,26 +312,26 @@ const handleKeydown = (event: KeyboardEvent) => {
case "ArrowDown":
event.preventDefault();
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
break;
case "ArrowUp":
event.preventDefault();
selectedIndex.value =
(selectedIndex.value - 1 + filteredOptions.value.length) % filteredOptions.value.length;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
break;
case "Home":
event.preventDefault();
if (menuItemsRef.value.length > 0) {
selectedIndex.value = 0;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
break;
case "End":
event.preventDefault();
if (menuItemsRef.value.length > 0) {
selectedIndex.value = filteredOptions.value.length - 1;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
break;
case "Enter":
@@ -344,7 +344,7 @@ const handleKeydown = (event: KeyboardEvent) => {
case "Escape":
event.preventDefault();
closeMenu();
triggerRef.value?.focus();
triggerRef.value?.focus?.();
break;
case "Tab":
event.preventDefault();
@@ -355,7 +355,7 @@ const handleKeydown = (event: KeyboardEvent) => {
} else {
selectedIndex.value = (selectedIndex.value + 1) % filteredOptions.value.length;
}
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
break;
default:
@@ -366,7 +366,7 @@ const handleKeydown = (event: KeyboardEvent) => {
);
if (matchIndex !== -1) {
selectedIndex.value = matchIndex;
menuItemsRef.value[selectedIndex.value].focus();
menuItemsRef.value[selectedIndex.value].focus?.();
}
if (typeAheadTimeout.value) {
clearTimeout(typeAheadTimeout.value);

View File

@@ -8,10 +8,9 @@
}"
>
<template v-if="members[message.author_id]">
<ConditionalNuxtLink
<AutoLink
class="message__icon"
:is-link="!noLinks"
:to="`/user/${members[message.author_id].username}`"
:to="noLinks ? '' : `/user/${members[message.author_id].username}`"
tabindex="-1"
aria-hidden="true"
>
@@ -21,19 +20,16 @@
circle
:raised="raised"
/>
</ConditionalNuxtLink>
</AutoLink>
<span :class="`message__author role-${members[message.author_id].role}`">
<LockIcon
v-if="message.body.private"
v-tooltip="'Only visible to moderators'"
class="private-icon"
/>
<ConditionalNuxtLink
:is-link="!noLinks"
:to="`/user/${members[message.author_id].username}`"
>
<AutoLink :to="noLinks ? '' : `/user/${members[message.author_id].username}`">
{{ members[message.author_id].username }}
</ConditionalNuxtLink>
</AutoLink>
<ScaleIcon v-if="members[message.author_id].role === 'moderator'" v-tooltip="'Moderator'" />
<ModrinthIcon
v-else-if="members[message.author_id].role === 'admin'"
@@ -107,7 +103,7 @@ import {
ModrinthIcon,
ScaleIcon,
} from "@modrinth/assets";
import { OverflowMenu, ConditionalNuxtLink } from "@modrinth/ui";
import { AutoLink, OverflowMenu } from "@modrinth/ui";
import { renderString } from "@modrinth/utils";
import Avatar from "~/components/ui/Avatar.vue";
import Badge from "~/components/ui/Badge.vue";
@@ -287,7 +283,7 @@ a:active + .message__author a,
}
.moderation-color,
role-moderator {
.role-moderator {
color: var(--color-orange);
}

View File

@@ -25,6 +25,10 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
// Feature toggles
projectTypesPrimaryNav: false,
hidePlusPromoInUserMenu: false,
oldProjectCards: true,
newProjectCards: false,
projectBackground: false,
searchBackground: false,
// advancedRendering: true,
// externalLinksNewTab: true,
// notUsingBlockers: false,

View File

@@ -1,4 +1,10 @@
<template>
<div class="pointer-events-none fixed inset-0 z-[-1]">
<div id="fixed-background-teleport" class="relative"></div>
</div>
<div class="pointer-events-none absolute inset-0 z-[-1]">
<div id="absolute-background-teleport" class="relative"></div>
</div>
<div ref="main_page" class="layout" :class="{ 'expanded-mobile-nav': isBrowseMenuOpen }">
<div
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
@@ -54,7 +60,7 @@
</div>
</div>
<header
class="experimental-styles-within desktop-only relative z-[5] mx-auto grid max-w-[1280px] grid-cols-[1fr_auto] items-center gap-2 px-3 py-4 lg:grid-cols-[auto_1fr_auto]"
class="experimental-styles-within desktop-only relative z-[5] mx-auto grid max-w-[1280px] grid-cols-[1fr_auto] items-center gap-2 px-6 py-4 lg:grid-cols-[auto_1fr_auto]"
>
<div>
<NuxtLink to="/" aria-label="Modrinth home page">
@@ -203,7 +209,10 @@
<ButtonStyled
type="transparent"
:highlighted="route.name?.startsWith('servers')"
:highlighted="
route.name?.startsWith('servers') ||
(route.name?.startsWith('search-') && route.query.sid)
"
:highlighted-style="
route.name === 'servers' ? 'main-nav-primary' : 'main-nav-secondary'
"
@@ -229,6 +238,7 @@
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
position="bottom"
direction="left"
:dropdown-id="createPopoutId"
aria-label="Create new..."
:options="[
{
@@ -260,6 +270,7 @@
</ButtonStyled>
<OverflowMenu
v-if="auth.user"
:dropdown-id="userPopoutId"
class="btn-dropdown-animation flex items-center gap-1 rounded-xl bg-transparent px-2 py-1"
:options="userMenuOptions"
>
@@ -588,15 +599,14 @@ import {
GlassesIcon,
PaintBrushIcon,
PackageOpenIcon,
XIcon as CrossIcon,
ScaleIcon as ModerationIcon,
BellIcon as NotificationIcon,
} from "@modrinth/assets";
import { Button, ButtonStyled, OverflowMenu, Avatar } from "@modrinth/ui";
import { Button, ButtonStyled, OverflowMenu, Avatar, commonMessages } from "@modrinth/ui";
import CrossIcon from "assets/images/utils/x.svg";
import NotificationIcon from "assets/images/sidebar/notifications.svg";
import ModerationIcon from "assets/images/sidebar/admin.svg";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import { getProjectTypeMessage } from "~/utils/i18n-project-type.ts";
import { commonMessages } from "~/utils/common-messages.ts";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import OrganizationCreateModal from "~/components/ui/OrganizationCreateModal.vue";
import TeleportOverflowMenu from "~/components/ui/servers/TeleportOverflowMenu.vue";
@@ -614,6 +624,9 @@ const config = useRuntimeConfig();
const route = useNativeRoute();
const link = config.public.siteUrl + route.path.replace(/\/+$/, "");
const createPopoutId = useId();
const userPopoutId = useId();
const verifyEmailBannerMessages = defineMessages({
title: {
id: "layout.banner.verify-email.title",
@@ -907,9 +920,13 @@ const userMenuOptions = computed(() => {
return options;
});
const isDiscovering = computed(() => route.name && route.name.startsWith("search-"));
const isDiscovering = computed(
() => route.name && route.name.startsWith("search-") && !route.query.sid,
);
const isDiscoveringSubpage = computed(() => route.name && route.name.startsWith("type-id"));
const isDiscoveringSubpage = computed(
() => route.name && route.name.startsWith("type-id") && !route.query.sid,
);
onMounted(() => {
if (window && import.meta.client) {
@@ -1014,7 +1031,6 @@ function hideStagingBanner() {
.layout {
min-height: 100vh;
background-color: var(--color-bg);
display: block;
@media screen and (min-width: 1024px) {
@@ -1430,7 +1446,7 @@ function hideStagingBanner() {
}
main {
padding-top: 0.75rem;
padding-top: 1.5rem;
}
}
</style>

View File

@@ -1 +1,4 @@
<template><slot id="main" /></template>
<style lang="scss">
@import "~/assets/styles/global.scss";
</style>

View File

@@ -167,39 +167,6 @@
"auth.welcome.title": {
"message": "Welcome"
},
"button.cancel": {
"message": "Cancel"
},
"button.continue": {
"message": "Continue"
},
"button.copy-id": {
"message": "Copy ID"
},
"button.create-a-project": {
"message": "Create a project"
},
"button.edit": {
"message": "Edit"
},
"button.report": {
"message": "Report"
},
"button.save": {
"message": "Save"
},
"button.save-changes": {
"message": "Save changes"
},
"button.sign-in": {
"message": "Sign in"
},
"button.sign-out": {
"message": "Sign out"
},
"button.upload-image": {
"message": "Upload image"
},
"collection.button.delete-icon": {
"message": "Delete icon"
},
@@ -248,9 +215,6 @@
"collection.label.owner": {
"message": "Owner"
},
"collection.label.private": {
"message": "Private"
},
"collection.label.projects-count": {
"message": "{count, plural, one {<stat>{count}</stat> project} other {<stat>{count}</stat> projects}}"
},
@@ -287,72 +251,6 @@
"frog.title": {
"message": "Frog"
},
"input.view.gallery": {
"message": "Gallery view"
},
"input.view.grid": {
"message": "Grid view"
},
"input.view.list": {
"message": "Rows view"
},
"label.changes-saved": {
"message": "Changes saved"
},
"label.collections": {
"message": "Collections"
},
"label.created-ago": {
"message": "Created {ago}"
},
"label.dashboard": {
"message": "Dashboard"
},
"label.delete": {
"message": "Delete"
},
"label.description": {
"message": "Description"
},
"label.error": {
"message": "Error"
},
"label.followed-projects": {
"message": "Followed projects"
},
"label.moderation": {
"message": "Moderation"
},
"label.notifications": {
"message": "Notifications"
},
"label.password": {
"message": "Password"
},
"label.public": {
"message": "Public"
},
"label.rejected": {
"message": "Rejected"
},
"label.scopes": {
"message": "Scopes"
},
"label.settings": {
"message": "Settings"
},
"label.title": {
"message": "Title"
},
"label.unlisted": {
"message": "Unlisted"
},
"label.visibility": {
"message": "Visibility"
},
"label.visit-your-profile": {
"message": "Visit your profile"
},
"layout.action.change-theme": {
"message": "Change theme"
},
@@ -440,9 +338,6 @@
"layout.nav.search": {
"message": "Search"
},
"notification.error.title": {
"message": "An error occurred"
},
"profile.button.manage-projects": {
"message": "Manage projects"
},
@@ -491,9 +386,6 @@
"profile.user-id": {
"message": "User ID: {id}"
},
"project-type.all": {
"message": "All"
},
"project-type.collection.plural": {
"message": "Collections"
},
@@ -542,24 +434,6 @@
"project-type.shader.singular": {
"message": "Shader"
},
"project.about.compatibility.environments": {
"message": "Supported environments"
},
"project.about.compatibility.game.minecraftJava": {
"message": "Minecraft: Java Edition"
},
"project.about.compatibility.platforms": {
"message": "Platforms"
},
"project.about.compatibility.title": {
"message": "Compatibility"
},
"project.about.creators.owner": {
"message": "Project owner"
},
"project.about.creators.title": {
"message": "Creators"
},
"project.about.details.created": {
"message": "Created {date}"
},
@@ -578,39 +452,6 @@
"project.about.details.updated": {
"message": "Updated {date}"
},
"project.about.links.discord": {
"message": "Join Discord server"
},
"project.about.links.donate.bmac": {
"message": "Buy Me a Coffee"
},
"project.about.links.donate.generic": {
"message": "Donate"
},
"project.about.links.donate.github": {
"message": "Sponsor on GitHub"
},
"project.about.links.donate.kofi": {
"message": "Donate on Ko-fi"
},
"project.about.links.donate.patreon": {
"message": "Donate on Patreon"
},
"project.about.links.donate.paypal": {
"message": "Donate on PayPal"
},
"project.about.links.issues": {
"message": "Report issues"
},
"project.about.links.source": {
"message": "View source"
},
"project.about.links.title": {
"message": "Links"
},
"project.about.links.wiki": {
"message": "Visit wiki"
},
"project.description.title": {
"message": "Description"
},
@@ -887,17 +728,17 @@
"scopes.versionWrite.label": {
"message": "Write versions"
},
"settings.account.title": {
"message": "Account and security"
"search.filter.locked.server": {
"message": "Provided by the server"
},
"settings.appearance.title": {
"message": "Appearance"
"search.filter.locked.server-game-version.title": {
"message": "Game version is provided by the server"
},
"settings.applications.title": {
"message": "Your applications"
"search.filter.locked.server-loader.title": {
"message": "Loader is provided by the server"
},
"settings.authorized-apps.title": {
"message": "Authorized apps"
"search.filter.locked.server.sync": {
"message": "Sync with server"
},
"settings.billing.modal.cancel.action": {
"message": "Cancel subscription"
@@ -977,15 +818,18 @@
"settings.billing.payment_method_type.visa": {
"message": "Visa"
},
"settings.billing.pyro_subscription.description": {
"message": "Manage your Modrinth Server subscriptions."
},
"settings.billing.pyro_subscription.title": {
"message": "Modrinth Server Subscriptions"
},
"settings.billing.subscription.description": {
"message": "Manage your Modrinth subscriptions."
},
"settings.billing.subscription.title": {
"message": "Subscriptions"
},
"settings.billing.title": {
"message": "Billing and subscriptions"
},
"settings.display.banner.developer-mode.button": {
"message": "Deactivate developer mode"
},
@@ -1058,30 +902,9 @@
"settings.display.sidebar.right-aligned-filters-sidebar.title": {
"message": "Right-aligned filters sidebar on search pages"
},
"settings.display.theme.dark": {
"message": "Dark"
},
"settings.display.theme.description": {
"message": "Select your preferred color theme for Modrinth on this device."
},
"settings.display.theme.light": {
"message": "Light"
},
"settings.display.theme.oled": {
"message": "OLED"
},
"settings.display.theme.preferred-dark-theme": {
"message": "Preferred dark theme"
},
"settings.display.theme.preferred-light-theme": {
"message": "Preferred light theme"
},
"settings.display.theme.retro": {
"message": "Retro"
},
"settings.display.theme.system": {
"message": "Sync with system"
},
"settings.display.theme.title": {
"message": "Color theme"
},
@@ -1127,9 +950,6 @@
"settings.language.languages.search.no-results": {
"message": "No languages match your search."
},
"settings.language.title": {
"message": "Language"
},
"settings.pats.action.create": {
"message": "Create a PAT"
},
@@ -1163,9 +983,6 @@
"settings.pats.modal.edit.title": {
"message": "Edit personal access token"
},
"settings.pats.title": {
"message": "Personal access tokens"
},
"settings.pats.token.action.edit": {
"message": "Edit token"
},
@@ -1202,9 +1019,6 @@
"settings.profile.profile-picture.title": {
"message": "Profile picture"
},
"settings.profile.title": {
"message": "Public profile"
},
"settings.profile.username.description": {
"message": "A unique case-insensitive name to identify your profile."
},
@@ -1226,16 +1040,10 @@
"settings.sessions.last-accessed-ago": {
"message": "Last accessed {ago}"
},
"settings.sessions.title": {
"message": "Sessions"
},
"settings.sessions.unknown-os": {
"message": "Unknown OS"
},
"settings.sessions.unknown-platform": {
"message": "Unknown platform"
},
"tooltip.date-at-time": {
"message": "{date, date, long} at {time, time, short}"
}
}

View File

@@ -1,4 +1,7 @@
<template>
<Teleport v-if="flags.projectBackground" to="#fixed-background-teleport">
<ProjectBackgroundGradient :project="project" />
</Teleport>
<div v-if="route.name.startsWith('type-id-settings')" class="normal-page">
<div class="normal-page__sidebar">
<aside class="universal-card">
@@ -430,47 +433,7 @@
}"
>
<div class="normal-page__header relative my-4">
<ContentPageHeader>
<template #icon>
<Avatar :src="project.icon_url" :alt="project.title" size="96px" />
</template>
<template #title>
{{ project.title }}
</template>
<template #title-suffix>
<Badge v-if="auth.user && currentMember" :type="project.status" class="status-badge" />
</template>
<template #summary>
{{ project.description }}
</template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
>
<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
v-for="(category, index) in project.categories"
:key="index"
class="tag-list__item"
>
{{ formatCategory(category) }}
</div>
</div>
</div>
</template>
<ProjectHeader :project="project" :member="!!currentMember">
<template #actions>
<div class="hidden sm:contents">
<ButtonStyled
@@ -498,73 +461,104 @@
</button>
</ButtonStyled>
</div>
<ButtonStyled
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)"
<ClientOnly>
<ButtonStyled
size="large"
circular
:color="following ? 'red' : 'standard'"
color-fill="none"
hover-color-fill="background"
>
<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'
<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"
:tooltip="
collections.some((x) => x.projects.includes(project.id)) ? 'Saved' : 'Save'
"
/>
<template #menu>
<input
v-model="displayCollectionsSearch"
type="text"
placeholder="Search collections..."
class="search-input menu-search"
from="top-right"
aria-label="Save"
:dropdown-id="`${baseId}-save`"
>
<BookmarkIcon
aria-hidden="true"
:fill="
collections.some((x) => x.projects.includes(project.id))
? 'currentColor'
: 'none'
"
/>
<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)"
<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)"
>
{{ option.name }}
</Checkbox>
</div>
<div v-else class="menu-text">
<p class="popout-text">No collections found.</p>
</div>
<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>
<template #fallback>
<ButtonStyled size="large" circular>
<button
class="btn collection-button"
@click="(event) => $refs.modal_collection.show(event)"
v-if="auth.user"
v-tooltip="`Follow`"
:aria-label="`Follow`"
@click="userFollowProject(project)"
>
<PlusIcon aria-hidden="true" />
Create new collection
<HeartIcon aria-hidden="true" />
</button>
</template>
</PopoutMenu>
<nuxt-link v-else v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
<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>
<nuxt-link v-tooltip="'Save'" to="/auth/sign-in" aria-label="Save">
<BookmarkIcon aria-hidden="true" />
</nuxt-link>
</ButtonStyled>
</template>
</ClientOnly>
<ButtonStyled v-if="auth.user && currentMember" size="large" circular>
<nuxt-link
v-tooltip="'Settings'"
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/settings`"
>
<SettingsIcon aria-hidden="true" />
@@ -572,6 +566,7 @@
</ButtonStyled>
<ButtonStyled size="large" circular type="transparent">
<OverflowMenu
:tooltip="'More options'"
:options="[
{
id: 'analytics',
@@ -611,6 +606,7 @@
{ id: 'copy-id', action: () => copyId() },
]"
aria-label="More options"
:dropdown-id="`${baseId}-more-options`"
>
<MoreVerticalIcon aria-hidden="true" />
<template #analytics>
@@ -632,7 +628,7 @@
</OverflowMenu>
</ButtonStyled>
</template>
</ContentPageHeader>
</ProjectHeader>
<ProjectMemberHeader
v-if="currentMember"
:project="project"
@@ -654,227 +650,37 @@
</MessageBanner>
</div>
<div class="normal-page__sidebar">
<div v-if="versions.length > 0" class="card flex-card experimental-styles-within">
<h2>{{ formatMessage(compatibilityMessages.title) }}</h2>
<section>
<h3>{{ formatMessage(compatibilityMessages.minecraftJava) }}</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 v-if="project.project_type !== 'resourcepack'">
<h3>{{ formatMessage(compatibilityMessages.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
v-if="
(project.actualProjectType === 'mod' || project.project_type === 'modpack') &&
!(project.client_side === 'unsupported' && project.server_side === 'unsupported') &&
!(project.client_side === 'unknown' && project.server_side === 'unknown')
"
>
<h3>{{ formatMessage(compatibilityMessages.environments) }}</h3>
<div class="tag-list">
<div
v-if="
(project.client_side === 'required' && project.server_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="tag-list__item"
>
<ClientIcon aria-hidden="true" />
Client-side
</div>
<div
v-if="
(project.server_side === 'required' && project.client_side !== 'required') ||
(project.client_side === 'optional' && project.server_side === 'optional')
"
class="tag-list__item"
>
<ServerIcon aria-hidden="true" />
Server-side
</div>
<div v-if="false" class="tag-list__item">
<UserIcon aria-hidden="true" />
Singleplayer
</div>
<div
v-if="
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" />
Client and server
</div>
</div>
</section>
</div>
<ProjectSidebarCompatibility
:project="project"
:tags="tags"
class="card flex-card experimental-styles-within"
/>
<AdPlaceholder
v-if="
(!auth.user || !isPermission(auth.user.badges, 1 << 0) || flags.showAdsWithPlus) &&
tags.approvedStatuses.includes(project.status)
"
/>
<div
v-if="
project.issues_url ||
project.source_url ||
project.wiki_url ||
project.discord_url ||
project.donation_urls.length > 0
"
<ProjectSidebarLinks
:project="project"
:link-target="$external()"
class="card flex-card experimental-styles-within"
>
<h2>{{ formatMessage(linksMessages.title) }}</h2>
<div class="links-list">
<a
v-if="project.issues_url"
:href="project.issues_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<IssuesIcon aria-hidden="true" />
{{ formatMessage(linksMessages.issues) }}
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.source_url"
:href="project.source_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<CodeIcon aria-hidden="true" />
{{ formatMessage(linksMessages.source) }}
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.wiki_url"
:href="project.wiki_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<WikiIcon aria-hidden="true" />
{{ formatMessage(linksMessages.wiki) }}
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
<a
v-if="project.discord_url"
:href="project.discord_url"
:target="$external()"
rel="noopener nofollow ugc"
>
<DiscordIcon class="shrink" aria-hidden="true" />
{{ formatMessage(linksMessages.discord) }}
<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"
:href="donation.url"
:target="$external()"
rel="noopener nofollow ugc"
>
<BuyMeACoffeeIcon 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'" />
<CurrencyIcon v-else />
<span v-if="donation.id === 'bmac'">{{
formatMessage(linksMessages.donateBmac)
}}</span>
<span v-else-if="donation.id === 'patreon'">{{
formatMessage(linksMessages.donatePatreon)
}}</span>
<span v-else-if="donation.id === 'paypal'">{{
formatMessage(linksMessages.donatePayPal)
}}</span>
<span v-else-if="donation.id === 'ko-fi'">{{
formatMessage(linksMessages.donateKoFi)
}}</span>
<span v-else-if="donation.id === 'github'">{{
formatMessage(linksMessages.donateGithub)
}}</span>
<span v-else>{{ formatMessage(linksMessages.donateGeneric) }}</span>
<ExternalIcon aria-hidden="true" class="external-icon" />
</a>
</div>
</div>
<div class="card flex-card experimental-styles-within">
<h2>{{ formatMessage(creatorsMessages.title) }}</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" size="32px" />
<div class="rows">
<span>
{{ organization.name }}
</span>
<span class="details-list__item__text--style-secondary">Organization</span>
</div>
</nuxt-link>
<hr v-if="members.length > 0" />
</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" size="32px" circle />
<div class="rows">
<span class="flex items-center gap-1">
{{ member.name }}
<CrownIcon
v-if="member.is_owner"
v-tooltip="formatMessage(creatorsMessages.owner)"
class="text-brand-orange"
/>
</span>
<span class="details-list__item__text--style-secondary">{{ member.role }}</span>
</div>
</nuxt-link>
</div>
</div>
/>
<ProjectSidebarCreators
:organization="organization"
:members="members"
:org-link="(slug) => `/organization/${slug}`"
:user-link="(username) => `/user/${username}`"
class="card flex-card experimental-styles-within"
/>
<!-- TODO: Finish license modal and enable -->
<ProjectSidebarDetails
v-if="false"
:project="project"
:has-versions="versions.length > 0"
:link-target="$external()"
class="card flex-card experimental-styles-within"
/>
<div class="card flex-card experimental-styles-within">
<h2>{{ formatMessage(detailsMessages.title) }}</h2>
<div class="details-list">
@@ -1002,23 +808,8 @@ import {
UsersIcon,
VersionIcon,
WrenchIcon,
ClientIcon,
BookTextIcon,
MonitorSmartphoneIcon,
WikiIcon,
DiscordIcon,
CalendarIcon,
KoFiIcon,
BuyMeACoffeeIcon,
IssuesIcon,
UserIcon,
PayPalIcon,
ServerIcon,
PatreonIcon,
CrownIcon,
OpenCollectiveIcon,
CodeIcon,
CurrencyIcon,
} from "@modrinth/assets";
import {
Avatar,
@@ -1028,10 +819,16 @@ import {
OverflowMenu,
PopoutMenu,
ScrollablePanel,
ContentPageHeader,
ProjectHeader,
ProjectSidebarCompatibility,
ProjectSidebarCreators,
ProjectSidebarLinks,
ProjectSidebarDetails,
ProjectBackgroundGradient,
} from "@modrinth/ui";
import { formatCategory, isRejected, isStaff, isUnderReview, renderString } from "@modrinth/utils";
import dayjs from "dayjs";
import VersionSummary from "@modrinth/ui/src/components/version/VersionSummary.vue";
import Badge from "~/components/ui/Badge.vue";
import NavTabs from "~/components/ui/NavTabs.vue";
import NavStack from "~/components/ui/NavStack.vue";
@@ -1045,9 +842,7 @@ import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";
import ModerationChecklist from "~/components/ui/ModerationChecklist.vue";
import Accordion from "~/components/ui/Accordion.vue";
import ModrinthIcon from "~/assets/images/utils/modrinth.svg?component";
import VersionSummary from "~/components/ui/VersionSummary.vue";
import AutomaticAccordion from "~/components/ui/AutomaticAccordion.vue";
import { getVersionsToDisplay } from "~/helpers/projects.js";
import AdPlaceholder from "~/components/ui/AdPlaceholder.vue";
const data = useNuxtApp();
@@ -1074,6 +869,8 @@ const gameVersionFilterInput = ref();
const versionFilter = ref("");
const baseId = useId();
const currentGameVersion = computed(() => {
return (
userSelectedGameVersion.value ||
@@ -1111,84 +908,6 @@ const getModrinthAppAccordion = ref();
const formatRelativeTime = useRelativeTime();
const compatibilityMessages = defineMessages({
title: {
id: "project.about.compatibility.title",
defaultMessage: "Compatibility",
},
minecraftJava: {
id: "project.about.compatibility.game.minecraftJava",
defaultMessage: "Minecraft: Java Edition",
},
platforms: {
id: "project.about.compatibility.platforms",
defaultMessage: "Platforms",
},
environments: {
id: "project.about.compatibility.environments",
defaultMessage: "Supported environments",
},
});
const linksMessages = defineMessages({
title: {
id: "project.about.links.title",
defaultMessage: "Links",
},
issues: {
id: "project.about.links.issues",
defaultMessage: "Report issues",
},
source: {
id: "project.about.links.source",
defaultMessage: "View source",
},
wiki: {
id: "project.about.links.wiki",
defaultMessage: "Visit wiki",
},
discord: {
id: "project.about.links.discord",
defaultMessage: "Join Discord server",
},
donateGeneric: {
id: "project.about.links.donate.generic",
defaultMessage: "Donate",
},
donateGitHub: {
id: "project.about.links.donate.github",
defaultMessage: "Sponsor on GitHub",
},
donateBmac: {
id: "project.about.links.donate.bmac",
defaultMessage: "Buy Me a Coffee",
},
donatePatreon: {
id: "project.about.links.donate.patreon",
defaultMessage: "Donate on Patreon",
},
donatePayPal: {
id: "project.about.links.donate.paypal",
defaultMessage: "Donate on PayPal",
},
donateKoFi: {
id: "project.about.links.donate.kofi",
defaultMessage: "Donate on Ko-fi",
},
donateGithub: {
id: "project.about.links.donate.github",
defaultMessage: "Sponsor on GitHub",
},
});
const creatorsMessages = defineMessages({
title: {
id: "project.about.creators.title",
defaultMessage: "Creators",
},
owner: {
id: "project.about.creators.owner",
defaultMessage: "Project owner",
},
});
const detailsMessages = defineMessages({
title: {
id: "project.about.details.title",

View File

@@ -1,7 +1,11 @@
<template>
<div class="content">
<div class="mb-3 flex">
<VersionFilterControl :versions="props.versions" @switch-page="switchPage" />
<VersionFilterControl
:versions="props.versions"
:game-versions="tags.gameVersions"
@update:query="updateQuery"
/>
<Pagination
:page="currentPage"
:count="Math.ceil(filteredVersions.length / 20)"
@@ -72,8 +76,8 @@
import { Pagination } from "@modrinth/ui";
import { DownloadIcon } from "@modrinth/assets";
import VersionFilterControl from "@modrinth/ui/src/components/version/VersionFilterControl.vue";
import { renderHighlightedString } from "~/helpers/highlight.js";
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
const props = defineProps({
project: {
@@ -108,6 +112,7 @@ useSeoMeta({
const router = useNativeRouter();
const route = useNativeRoute();
const tags = useTags();
const currentPage = ref(Number(route.query.page ?? 1));
const filteredVersions = computed(() => {
@@ -138,6 +143,21 @@ function switchPage(page) {
},
});
}
function updateQuery(newQueries) {
if (newQueries.page) {
currentPage.value = Number(newQueries.page);
} else if (newQueries.page === undefined) {
currentPage.value = 1;
}
router.replace({
query: {
...route.query,
...newQueries,
},
});
}
</script>
<style lang="scss">

View File

@@ -1,15 +1,13 @@
<template>
<section class="normal-page__content">
<div
v-if="project.body"
class="markdown-body card"
v-html="renderHighlightedString(project.body || '')"
/>
<div v-if="project.body" class="card">
<ProjectPageDescription :description="project.body" />
</div>
</section>
</template>
<script setup>
import { renderHighlightedString } from "~/helpers/highlight.js";
import { ProjectPageDescription } from "@modrinth/ui";
defineProps({
project: {

View File

@@ -19,283 +19,133 @@
</span>
<DropArea :accept="acceptFileFromProjectType(project.project_type)" @change="handleFiles" />
</div>
<div class="mb-3 flex flex-wrap gap-2">
<VersionFilterControl
ref="versionFilters"
:versions="props.versions"
@switch-page="switchPage"
/>
<Pagination
:page="currentPage"
class="ml-auto mt-auto"
:count="Math.ceil(filteredVersions.length / 20)"
:link-function="(page) => `?page=${currentPage}`"
@switch-page="switchPage"
/>
</div>
<div
v-if="versions.length > 0"
class="flex flex-col gap-4 rounded-2xl bg-bg-raised px-6 pb-8 pt-4 supports-[grid-template-columns:subgrid]:grid supports-[grid-template-columns:subgrid]:grid-cols-[1fr_min-content] sm:px-8 supports-[grid-template-columns:subgrid]:sm:grid-cols-[min-content_auto_auto_auto_min-content] supports-[grid-template-columns:subgrid]:xl:grid-cols-[min-content_auto_auto_auto_auto_auto_min-content]"
<ProjectPageVersions
:project="project"
:versions="versions"
:show-files="flags.showVersionFilesInTable"
:current-member="!!currentMember"
:loaders="tags.loaders"
:game-versions="tags.gameVersions"
:base-id="baseDropdownId"
:version-link="
(version) =>
`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`
"
>
<div class="versions-grid-row">
<div class="w-9 max-sm:hidden"></div>
<div class="text-sm font-bold text-contrast max-sm:hidden">Name</div>
<div
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
>
Game version
</div>
<div
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
>
Platforms
</div>
<div
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
>
Published
</div>
<div
class="text-sm font-bold text-contrast max-sm:hidden sm:max-xl:collapse sm:max-xl:hidden"
>
Downloads
</div>
<div class="text-sm font-bold text-contrast max-sm:hidden xl:collapse xl:hidden">
Compatibility
</div>
<div class="text-sm font-bold text-contrast max-sm:hidden xl:collapse xl:hidden">Stats</div>
<div class="w-9 max-sm:hidden"></div>
</div>
<template
v-for="(version, index) in filteredVersions.slice((currentPage - 1) * 20, currentPage * 20)"
:key="index"
>
<div
:class="`versions-grid-row h-px w-full bg-button-bg ${index === 0 ? `max-sm:!hidden` : ``}`"
></div>
<div class="versions-grid-row group relative">
<nuxt-link
class="absolute inset-[calc(-1rem-2px)_-2rem] before:absolute before:inset-0 before:transition-all before:content-[''] hover:before:backdrop-brightness-110"
:to="`/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`"
></nuxt-link>
<div class="flex flex-col justify-center gap-2 sm:contents">
<div class="flex flex-row items-center gap-2 sm:contents">
<div class="self-center">
<div class="relative z-[1] cursor-pointer">
<VersionChannelIndicator
v-tooltip="`Toggle filter for ${version.version_type}`"
:channel="version.version_type"
@click="versionFilters.toggleFilter('channel', version.version_type)"
/>
</div>
</div>
<div
class="pointer-events-none relative z-[1] flex flex-col justify-center group-hover:underline"
>
<div class="font-bold text-contrast">{{ version.version_number }}</div>
<div class="text-xs font-medium">{{ version.name }}</div>
</div>
</div>
<div class="flex flex-col justify-center gap-2 sm:contents">
<div class="flex flex-row flex-wrap items-center gap-1 xl:contents">
<div class="flex items-center">
<div class="tag-list">
<div
v-for="gameVersion in formatVersionsForDisplay(version.game_versions)"
:key="`version-tag-${gameVersion}`"
v-tooltip="`Toggle filter for ${gameVersion}`"
class="tag-list__item z-[1] cursor-pointer hover:underline"
@click="versionFilters.toggleFilters('gameVersion', version.game_versions)"
>
{{ gameVersion }}
</div>
</div>
</div>
<div class="flex items-center">
<div class="tag-list">
<div
v-for="platform in version.loaders"
:key="`platform-tag-${platform}`"
v-tooltip="`Toggle filter for ${platform}`"
:class="`tag-list__item z-[1] cursor-pointer hover:underline`"
:style="`--_color: var(--color-platform-${platform})`"
@click="versionFilters.toggleFilter('platform', platform)"
>
<svg v-html="tags.loaders.find((x) => x.name === platform).icon"></svg>
{{ formatCategory(platform) }}
</div>
</div>
</div>
</div>
<div
class="flex flex-col justify-center gap-1 max-sm:flex-row max-sm:justify-start max-sm:gap-3 xl:contents"
>
<div
v-tooltip="
formatMessage(commonMessages.dateAtTimeTooltip, {
date: new Date(version.date_published),
time: new Date(version.date_published),
})
"
class="z-[1] flex cursor-help items-center gap-1 text-nowrap font-medium xl:self-center"
>
<CalendarIcon class="xl:hidden" />
{{ formatRelativeTime(version.date_published) }}
</div>
<div
class="pointer-events-none z-[1] flex items-center gap-1 font-medium xl:self-center"
>
<DownloadIcon class="xl:hidden" />
{{ formatCompactNumber(version.downloads) }}
</div>
</div>
</div>
</div>
<div class="flex items-start justify-end gap-1 sm:items-center">
<ButtonStyled circular type="transparent">
<a
v-tooltip="`Download`"
:href="getPrimaryFile(version).url"
class="z-[1] group-hover:!bg-brand group-hover:!text-brand-inverted"
aria-label="Download"
@click="emits('onDownload')"
>
<DownloadIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<OverflowMenu
class="group-hover:!bg-button-bg"
:options="[
{
id: 'download',
color: 'primary',
hoverFilled: true,
link: getPrimaryFile(version).url,
action: () => {
emits('onDownload');
},
},
{
id: 'new-tab',
action: () => {},
link: `/${project.project_type}/${
<template #actions="{ version }">
<ButtonStyled circular type="transparent">
<a
v-tooltip="`Download`"
:href="getPrimaryFile(version).url"
class="group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted"
aria-label="Download"
@click="emits('onDownload')"
>
<DownloadIcon aria-hidden="true" />
</a>
</ButtonStyled>
<ButtonStyled circular type="transparent">
<OverflowMenu
class="group-hover:!bg-button-bg"
:dropdown-id="`${baseDropdownId}-${version.id}`"
:options="[
{
id: 'download',
color: 'primary',
hoverFilled: true,
link: getPrimaryFile(version).url,
action: () => {
emits('onDownload');
},
},
{
id: 'new-tab',
action: () => {},
link: `/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`,
external: true,
},
{
id: 'copy-link',
action: () =>
copyToClipboard(
`https://modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`,
external: true,
},
{
id: 'copy-link',
action: () =>
copyToClipboard(
`https://modrinth.com/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}`,
),
},
{
id: 'share',
action: () => {},
shown: false,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
action: () =>
auth.user ? reportVersion(version.id) : navigateTo('/auth/sign-in'),
shown: !currentMember,
},
{ divider: true, shown: currentMember },
{
id: 'edit',
link: `/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}/edit`,
shown: currentMember,
},
{
id: 'delete',
color: 'red',
hoverFilled: true,
action: () => {},
shown: currentMember && false,
},
]"
aria-label="More options"
>
<MoreVerticalIcon aria-hidden="true" />
<template #download>
<DownloadIcon aria-hidden="true" />
Download
</template>
<template #new-tab>
<ExternalIcon aria-hidden="true" />
Open in new tab
</template>
<template #copy-link>
<LinkIcon aria-hidden="true" />
Copy link
</template>
<template #share>
<ShareIcon aria-hidden="true" />
Share
</template>
<template #report>
<ReportIcon aria-hidden="true" />
Report
</template>
<template #edit>
<EditIcon aria-hidden="true" />
Edit
</template>
<template #delete>
<TrashIcon aria-hidden="true" />
Delete
</template>
</OverflowMenu>
</ButtonStyled>
</div>
<div
v-if="flags.showVersionFilesInTable"
class="tag-list pointer-events-none relative z-[1] col-span-full"
),
},
{
id: 'share',
action: () => {},
shown: false,
},
{
id: 'report',
color: 'red',
hoverFilled: true,
action: () => (auth.user ? reportVersion(version.id) : navigateTo('/auth/sign-in')),
shown: !currentMember,
},
{ divider: true, shown: currentMember },
{
id: 'edit',
link: `/${project.project_type}/${
project.slug ? project.slug : project.id
}/version/${encodeURI(version.displayUrlEnding)}/edit`,
shown: currentMember,
},
{
id: 'delete',
color: 'red',
hoverFilled: true,
action: () => {},
shown: currentMember && false,
},
]"
aria-label="More options"
>
<div
v-for="(file, fileIdx) in version.files"
:key="`platform-tag-${fileIdx}`"
:class="`flex items-center gap-1 text-wrap rounded-full bg-button-bg px-2 py-0.5 text-xs font-medium ${file.primary || fileIdx === 0 ? 'bg-brand-highlight text-contrast' : 'text-primary'}`"
>
<StarIcon v-if="file.primary || fileIdx === 0" class="shrink-0" />
{{ file.filename }} - {{ formatBytes(file.size) }}
</div>
</div>
</div>
<MoreVerticalIcon aria-hidden="true" />
<template #download>
<DownloadIcon aria-hidden="true" />
Download
</template>
<template #new-tab>
<ExternalIcon aria-hidden="true" />
Open in new tab
</template>
<template #copy-link>
<LinkIcon aria-hidden="true" />
Copy link
</template>
<template #share>
<ShareIcon aria-hidden="true" />
Share
</template>
<template #report>
<ReportIcon aria-hidden="true" />
Report
</template>
<template #edit>
<EditIcon aria-hidden="true" />
Edit
</template>
<template #delete>
<TrashIcon aria-hidden="true" />
Delete
</template>
</OverflowMenu>
</ButtonStyled>
</template>
</div>
<div class="my-3 flex justify-end">
<Pagination
:page="currentPage"
:count="Math.ceil(filteredVersions.length / 20)"
:link-function="(page) => `?page=${currentPage}`"
@switch-page="switchPage"
/>
</div>
</ProjectPageVersions>
</section>
</template>
<script setup>
import { ButtonStyled, OverflowMenu, FileInput, ProjectPageVersions } from "@modrinth/ui";
import {
ButtonStyled,
OverflowMenu,
Pagination,
VersionChannelIndicator,
FileInput,
} from "@modrinth/ui";
import {
StarIcon,
CalendarIcon,
DownloadIcon,
MoreVerticalIcon,
TrashIcon,
@@ -307,15 +157,9 @@ import {
UploadIcon,
InfoIcon,
} from "@modrinth/assets";
import { formatBytes, formatCategory } from "@modrinth/utils";
import { formatVersionsForDisplay } from "~/helpers/projects.js";
import VersionFilterControl from "~/components/ui/VersionFilterControl.vue";
import DropArea from "~/components/ui/DropArea.vue";
import { acceptFileFromProjectType } from "~/helpers/fileUtils.js";
const formatCompactNumber = useCompactNumber();
const { formatMessage } = useVIntl();
const props = defineProps({
project: {
type: Object,
@@ -339,58 +183,18 @@ const props = defineProps({
const tags = useTags();
const flags = useFeatureFlags();
const formatRelativeTime = useRelativeTime();
const auth = await useAuth();
const emits = defineEmits(["onDownload"]);
const route = useNativeRoute();
const router = useNativeRouter();
const currentPage = ref(route.query.page ?? 1);
function switchPage(page) {
currentPage.value = page;
router.replace({
query: {
...route.query,
page: currentPage.value !== 1 ? currentPage.value : undefined,
},
});
}
const baseDropdownId = useId();
function getPrimaryFile(version) {
return version.files.find((x) => x.primary) || version.files[0];
}
const selectedGameVersions = computed(() => {
return getArrayOrString(route.query.g) ?? [];
});
const selectedPlatforms = computed(() => {
return getArrayOrString(route.query.l) ?? [];
});
const selectedVersionChannels = computed(() => {
return getArrayOrString(route.query.c) ?? [];
});
const versionFilters = ref(null);
const filteredVersions = computed(() => {
return props.versions.filter(
(projectVersion) =>
(selectedGameVersions.value.length === 0 ||
selectedGameVersions.value.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion),
)) &&
(selectedPlatforms.value.length === 0 ||
selectedPlatforms.value.some((loader) => projectVersion.loaders.includes(loader))) &&
(selectedVersionChannels.value.length === 0 ||
selectedVersionChannels.value.includes(projectVersion.version_type)),
);
});
async function handleFiles(files) {
await router.push({
name: "type-id-version-version",
@@ -409,8 +213,3 @@ async function copyToClipboard(text) {
await navigator.clipboard.writeText(text);
}
</script>
<style scoped>
.versions-grid-row {
@apply grid grid-cols-[1fr_min-content] gap-4 supports-[grid-template-columns:subgrid]:col-span-full supports-[grid-template-columns:subgrid]:!grid-cols-subgrid sm:grid-cols-[min-content_1fr_1fr_1fr_min-content] xl:grid-cols-[min-content_1fr_1fr_1fr_1fr_1fr_min-content];
}
</style>

View File

@@ -83,4 +83,20 @@ definePageMeta({
gap: var(--gap-md);
flex-wrap: wrap;
}
.turnstile {
display: flex;
justify-content: center;
overflow: hidden;
border-radius: var(--radius-md);
border: 2px solid var(--color-button-bg);
height: 65px;
width: 100%;
> div {
position: relative;
top: -2px;
min-width: calc(100% + 4px);
}
}
</style>

View File

@@ -80,7 +80,7 @@
</template>
<script setup>
import { Button, Avatar } from "@modrinth/ui";
import { Button, Avatar, commonMessages } from "@modrinth/ui";
import { XIcon, CheckIcon } from "@modrinth/assets";
import { useBaseFetch } from "@/composables/fetch.js";
import { useAuth } from "@/composables/auth.js";

View File

@@ -68,6 +68,7 @@
</template>
<script setup>
import { SendIcon, MailIcon, KeyIcon } from "@modrinth/assets";
import { commonMessages } from "@modrinth/ui";
import HCaptcha from "@/components/ui/HCaptcha.vue";
const { formatMessage } = useVIntl();

View File

@@ -134,6 +134,7 @@ import {
KeyIcon,
MailIcon,
} from "@modrinth/assets";
import { commonMessages } from "@modrinth/ui";
import HCaptcha from "@/components/ui/HCaptcha.vue";
const { formatMessage } = useVIntl();

View File

@@ -145,7 +145,7 @@ import {
MailIcon,
SSOGitLabIcon,
} from "@modrinth/assets";
import { Checkbox } from "@modrinth/ui";
import { Checkbox, commonMessages } from "@modrinth/ui";
import HCaptcha from "@/components/ui/HCaptcha.vue";
const { formatMessage } = useVIntl();

View File

@@ -36,7 +36,7 @@
</div>
</template>
<script setup>
import { Checkbox } from "@modrinth/ui";
import { Checkbox, commonMessages } from "@modrinth/ui";
import { RightArrowIcon } from "@modrinth/assets";
const { formatMessage } = useVIntl();

View File

@@ -380,7 +380,14 @@ import {
LibraryIcon,
BoxIcon,
} from "@modrinth/assets";
import { PopoutMenu, FileInput, DropdownSelect, Avatar, Button } from "@modrinth/ui";
import {
PopoutMenu,
FileInput,
DropdownSelect,
Avatar,
Button,
commonMessages,
} from "@modrinth/ui";
import WorldIcon from "assets/images/utils/world.svg";
import UpToDate from "assets/images/illustrations/up_to_date.svg";

View File

@@ -42,17 +42,20 @@
</div>
</template>
<script setup>
import { LibraryIcon, ChartIcon } from "@modrinth/assets";
import {
DashboardIcon,
CurrencyIcon,
ListIcon,
ReportIcon,
BellIcon as NotificationsIcon,
OrganizationIcon,
LibraryIcon,
ChartIcon,
} from "@modrinth/assets";
import { commonMessages } from "@modrinth/ui";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import DashboardIcon from "~/assets/images/utils/dashboard.svg?component";
import CurrencyIcon from "~/assets/images/utils/currency.svg?component";
import ListIcon from "~/assets/images/utils/list.svg?component";
import ReportIcon from "~/assets/images/utils/report.svg?component";
import NotificationsIcon from "~/assets/images/utils/bell.svg?component";
import OrganizationIcon from "~/assets/images/utils/organization.svg?component";
const { formatMessage } = useVIntl();
definePageMeta({

View File

@@ -95,7 +95,7 @@
</template>
<script setup>
import { BoxIcon, SearchIcon, XIcon, PlusIcon, LinkIcon, LockIcon } from "@modrinth/assets";
import { Avatar, Button } from "@modrinth/ui";
import { Avatar, Button, commonMessages } from "@modrinth/ui";
import WorldIcon from "~/assets/images/utils/world.svg?component";
import CollectionCreateModal from "~/components/ui/CollectionCreateModal.vue";

View File

@@ -301,6 +301,18 @@
<script>
import { Multiselect } from "vue-multiselect";
import {
SettingsIcon,
TrashIcon,
PlusIcon,
XIcon as CrossIcon,
IssuesIcon,
EditIcon,
SaveIcon,
SortAscendingIcon as AscendingIcon,
SortDescendingIcon as DescendingIcon,
} from "@modrinth/assets";
import { commonMessages } from "@modrinth/ui";
import Badge from "~/components/ui/Badge.vue";
import Checkbox from "~/components/ui/Checkbox.vue";
@@ -309,16 +321,6 @@ import Avatar from "~/components/ui/Avatar.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import CopyCode from "~/components/ui/CopyCode.vue";
import SettingsIcon from "~/assets/images/utils/settings.svg?component";
import TrashIcon from "~/assets/images/utils/trash.svg?component";
import IssuesIcon from "~/assets/images/utils/issues.svg?component";
import PlusIcon from "~/assets/images/utils/plus.svg?component";
import CrossIcon from "~/assets/images/utils/x.svg?component";
import EditIcon from "~/assets/images/utils/edit.svg?component";
import SaveIcon from "~/assets/images/utils/save.svg?component";
import AscendingIcon from "~/assets/images/utils/sort-asc.svg?component";
import DescendingIcon from "~/assets/images/utils/sort-desc.svg?component";
export default defineNuxtComponent({
components: {
Avatar,
@@ -337,6 +339,7 @@ export default defineNuxtComponent({
CopyCode,
AscendingIcon,
DescendingIcon,
commonMessages,
},
async setup() {
const { formatMessage } = useVIntl();

View File

@@ -84,14 +84,14 @@
</template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
class="flex items-center gap-2 border-0 border-r border-solid border-divider 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"
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<BoxIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(projects?.length || 0) }}
@@ -252,7 +252,14 @@ import {
XIcon,
ClipboardCopyIcon,
} from "@modrinth/assets";
import { Avatar, ButtonStyled, Breadcrumbs, ContentPageHeader, OverflowMenu } from "@modrinth/ui";
import {
Avatar,
ButtonStyled,
Breadcrumbs,
ContentPageHeader,
OverflowMenu,
commonMessages,
} from "@modrinth/ui";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import ModalCreation from "~/components/ui/ModalCreation.vue";

View File

@@ -311,7 +311,7 @@ import {
SortAscendingIcon,
SortDescendingIcon,
} from "@modrinth/assets";
import { Button, Modal, Avatar, CopyCode, Badge, Checkbox } from "@modrinth/ui";
import { Button, Modal, Avatar, CopyCode, Badge, Checkbox, commonMessages } from "@modrinth/ui";
import ModalCreation from "~/components/ui/ModalCreation.vue";
import OrganizationProjectTransferModal from "~/components/ui/OrganizationProjectTransferModal.vue";

View File

@@ -98,6 +98,13 @@ import { useImageUpload } from "~/composables/image-upload.ts";
const tags = useTags();
const route = useNativeRoute();
const router = useRouter();
const auth = await useAuth();
if (!auth.value.user) {
router.push("/auth/sign-in?redirect=" + encodeURIComponent(route.fullPath));
}
const accessQuery = (id: string): string => {
return route.query?.[id]?.toString() || "";

File diff suppressed because it is too large Load Diff

View File

@@ -183,7 +183,7 @@
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
<template #rename> <EditIcon /> Rename </template>
<template #restore> <ClipboardCopyIcon /> Restore </template>
<template v-if="backup.locked" #lock> <OpenLockIcon /> Unlock </template>
<template v-if="backup.locked" #lock> <LockOpenIcon /> Unlock </template>
<template v-else #lock> <LockIcon /> Lock </template>
<template #download> <DownloadIcon /> Download </template>
<template #delete> <TrashIcon /> Delete </template>
@@ -231,7 +231,7 @@ import {
SettingsIcon,
BoxIcon,
LockIcon,
OpenLockIcon,
LockOpenIcon,
} from "@modrinth/assets";
import { ref, computed } from "vue";
import type { Server } from "~/composables/pyroServers";

View File

@@ -89,12 +89,11 @@ import {
LanguagesIcon,
CardIcon,
} from "@modrinth/assets";
import { commonMessages, commonSettingsMessages } from "@modrinth/ui";
import NavStack from "~/components/ui/NavStack.vue";
import NavStackItem from "~/components/ui/NavStackItem.vue";
import MonitorSmartphoneIcon from "~/assets/images/utils/monitor-smartphone.svg?component";
import { commonMessages, commonSettingsMessages } from "~/utils/common-messages.ts";
const { formatMessage } = useVIntl();
const route = useNativeRoute();

View File

@@ -216,7 +216,15 @@
</template>
<script setup>
import { UploadIcon, PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { CopyCode, ConfirmModal, Button, Checkbox, Avatar, FileInput } from "@modrinth/ui";
import {
CopyCode,
ConfirmModal,
Button,
Checkbox,
Avatar,
FileInput,
commonSettingsMessages,
} from "@modrinth/ui";
import Modal from "~/components/ui/Modal.vue";
import {
@@ -226,7 +234,6 @@ import {
useScopes,
getScopeValue,
} from "~/composables/auth/scopes.ts";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
const { formatMessage } = useVIntl();

View File

@@ -88,9 +88,8 @@
</div>
</template>
<script setup>
import { Button, ConfirmModal, Avatar } from "@modrinth/ui";
import { Button, ConfirmModal, Avatar, commonSettingsMessages } from "@modrinth/ui";
import { TrashIcon, CheckIcon } from "@modrinth/assets";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
import { useScopes } from "~/composables/auth/scopes.ts";
const { formatMessage } = useVIntl();

View File

@@ -486,6 +486,7 @@ import {
PurchaseModal,
ButtonStyled,
CopyCode,
commonMessages,
} from "@modrinth/ui";
import {
PlusIcon,

View File

@@ -16,38 +16,12 @@
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(colorTheme.title) }}</h2>
<p>{{ formatMessage(colorTheme.description) }}</p>
<div class="theme-options mt-4">
<button
v-for="option in themeOptions"
:key="option"
class="preview-radio button-base"
:class="{ selected: theme.preferred === option }"
@click="() => updateColorTheme(option)"
>
<div class="preview" :class="`${option === 'system' ? systemTheme : option}-mode`">
<div class="example-card card card">
<div class="example-icon"></div>
<div class="example-text-1"></div>
<div class="example-text-2"></div>
</div>
</div>
<div class="label">
<RadioButtonChecked v-if="theme.preferred === option" class="radio" />
<RadioButtonIcon v-else class="radio" />
{{ colorTheme[option] ? formatMessage(colorTheme[option]) : option }}
<SunIcon
v-if="theme.preferences.light === option"
v-tooltip="formatMessage(colorTheme.preferredLight)"
class="theme-icon"
/>
<MoonIcon
v-else-if="theme.preferences.dark === option"
v-tooltip="formatMessage(colorTheme.preferredDark)"
class="theme-icon"
/>
</div>
</button>
</div>
<ThemeSelector
:update-color-theme="updateColorTheme"
:current-theme="theme.preferred"
:theme-options="themeOptions"
:system-theme-color="systemTheme"
/>
</section>
<section class="universal-card">
<h2 class="text-2xl">{{ formatMessage(projectListLayouts.title) }}</h2>
@@ -224,8 +198,8 @@
</template>
<script setup lang="ts">
import { CodeIcon, MoonIcon, RadioButtonChecked, RadioButtonIcon, SunIcon } from "@modrinth/assets";
import { Button } from "@modrinth/ui";
import { CodeIcon, RadioButtonChecked, RadioButtonIcon } from "@modrinth/assets";
import { Button, ThemeSelector } from "@modrinth/ui";
import MessageBanner from "~/components/ui/MessageBanner.vue";
import type { DisplayLocation } from "~/plugins/cosmetics";
import { formatProjectType } from "~/plugins/shorthands.js";
@@ -258,34 +232,6 @@ const colorTheme = defineMessages({
id: "settings.display.theme.description",
defaultMessage: "Select your preferred color theme for Modrinth on this device.",
},
system: {
id: "settings.display.theme.system",
defaultMessage: "Sync with system",
},
light: {
id: "settings.display.theme.light",
defaultMessage: "Light",
},
dark: {
id: "settings.display.theme.dark",
defaultMessage: "Dark",
},
oled: {
id: "settings.display.theme.oled",
defaultMessage: "OLED",
},
retro: {
id: "settings.display.theme.retro",
defaultMessage: "Retro",
},
preferredLight: {
id: "settings.display.theme.preferred-light-theme",
defaultMessage: "Preferred light theme",
},
preferredDark: {
id: "settings.display.theme.preferred-dark-theme",
defaultMessage: "Preferred dark theme",
},
});
const projectListLayouts = defineMessages({
@@ -457,107 +403,6 @@ const listTypes = computed(() => {
});
</script>
<style scoped lang="scss">
.preview-radio {
width: 100%;
border-radius: var(--radius-md);
padding: 0;
overflow: hidden;
border: 1px solid var(--color-divider);
background-color: var(--color-button-bg);
color: var(--color-base);
display: flex;
flex-direction: column;
outline: 2px solid transparent;
&.selected {
color: var(--color-contrast);
.label {
.radio {
color: var(--color-brand);
}
.theme-icon {
color: var(--color-text);
}
}
}
.preview {
background-color: var(--color-bg);
padding: 1.5rem;
outline: 2px solid transparent;
width: 100%;
.example-card {
margin: 0;
padding: 1rem;
outline: 2px solid transparent;
min-height: 0;
}
}
.label {
display: flex;
align-items: center;
text-align: left;
flex-grow: 1;
padding: var(--gap-md) var(--gap-lg);
.radio {
margin-right: 0.5rem;
}
.theme-icon {
color: var(--color-secondary);
margin-left: 0.25rem;
}
}
}
.theme-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12.5rem, 1fr));
gap: var(--gap-lg);
.preview .example-card {
margin: 0;
padding: 1rem;
display: grid;
grid-template: "icon text1" "icon text2";
grid-template-columns: auto 1fr;
gap: 0.5rem;
outline: 2px solid transparent;
.example-icon {
grid-area: icon;
width: 2rem;
height: 2rem;
background-color: var(--color-button-bg);
border-radius: var(--radius-sm);
outline: 2px solid transparent;
}
.example-text-1,
.example-text-2 {
height: 0.5rem;
border-radius: var(--radius-sm);
outline: 2px solid transparent;
}
.example-text-1 {
grid-area: text1;
width: 100%;
background-color: var(--color-base);
}
.example-text-2 {
grid-area: text2;
width: 60%;
background-color: var(--color-secondary);
}
}
}
.project-lists {
display: flex;
flex-direction: column;

View File

@@ -1,10 +1,10 @@
<script setup lang="ts">
import Fuse from "fuse.js/dist/fuse.basic";
import { commonSettingsMessages } from "@modrinth/ui";
import RadioButtonIcon from "~/assets/images/utils/radio-button.svg?component";
import RadioButtonCheckedIcon from "~/assets/images/utils/radio-button-checked.svg?component";
import WarningIcon from "~/assets/images/utils/issues.svg?component";
import { isModifierKeyDown } from "~/helpers/events.ts";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
const vintl = useVIntl();
const { formatMessage } = vintl;

View File

@@ -203,9 +203,8 @@
</template>
<script setup>
import { PlusIcon, XIcon, TrashIcon, EditIcon, SaveIcon } from "@modrinth/assets";
import { Checkbox, ConfirmModal } from "@modrinth/ui";
import { Checkbox, ConfirmModal, commonSettingsMessages, commonMessages } from "@modrinth/ui";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
import {
hasScope,
scopeList,

View File

@@ -87,8 +87,7 @@
<script setup>
import { UserIcon, SaveIcon, UploadIcon, UndoIcon, XIcon } from "@modrinth/assets";
import { Avatar, FileInput, Button } from "@modrinth/ui";
import { commonMessages } from "~/utils/common-messages.ts";
import { Avatar, FileInput, Button, commonMessages } from "@modrinth/ui";
useHead({
title: "Profile settings - Modrinth",

View File

@@ -57,7 +57,7 @@
</template>
<script setup>
import { XIcon } from "@modrinth/assets";
import { commonSettingsMessages } from "~/utils/common-messages.ts";
import { commonMessages, commonSettingsMessages } from "@modrinth/ui";
definePageMeta({
middleware: "auth",

View File

@@ -22,14 +22,14 @@
</template>
<template #stats>
<div
class="flex items-center gap-2 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
class="flex items-center gap-2 border-0 border-r border-solid border-divider 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 border-0 border-r border-solid border-button-bg pr-4 font-semibold"
class="flex items-center gap-2 border-0 border-r border-solid border-divider pr-4 font-semibold"
>
<DownloadIcon class="h-6 w-6 text-secondary" />
{{ formatCompactNumber(sumDownloads) }}
@@ -265,7 +265,7 @@ import {
ClipboardCopyIcon,
MoreVerticalIcon,
} from "@modrinth/assets";
import { OverflowMenu, ButtonStyled, ContentPageHeader } from "@modrinth/ui";
import { OverflowMenu, ButtonStyled, ContentPageHeader, commonMessages } from "@modrinth/ui";
import NavTabs from "~/components/ui/NavTabs.vue";
import ProjectCard from "~/components/ui/ProjectCard.vue";
import { reportUser } from "~/utils/report-helpers.ts";

View File

@@ -0,0 +1,15 @@
import FloatingVue from "floating-vue";
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(FloatingVue, {
themes: {
"ribbit-popout": {
$extend: "dropdown",
placement: "bottom-end",
instantMove: true,
distance: 8,
triggers: ["click"],
},
},
});
});

View File

@@ -1,193 +0,0 @@
export const commonMessages = defineMessages({
allProjectType: {
id: "project-type.all",
defaultMessage: "All",
},
cancelButton: {
id: "button.cancel",
defaultMessage: "Cancel",
},
collectionsLabel: {
id: "label.collections",
defaultMessage: "Collections",
},
continueButton: {
id: "button.continue",
defaultMessage: "Continue",
},
copyIdButton: {
id: "button.copy-id",
defaultMessage: "Copy ID",
},
changesSavedLabel: {
id: "label.changes-saved",
defaultMessage: "Changes saved",
},
createAProjectButton: {
id: "button.create-a-project",
defaultMessage: "Create a project",
},
createdAgoLabel: {
id: "label.created-ago",
defaultMessage: "Created {ago}",
},
dashboardLabel: {
id: "label.dashboard",
defaultMessage: "Dashboard",
},
dateAtTimeTooltip: {
id: "tooltip.date-at-time",
defaultMessage: "{date, date, long} at {time, time, short}",
},
deleteLabel: {
id: "label.delete",
defaultMessage: "Delete",
},
descriptionLabel: {
id: "label.description",
defaultMessage: "Description",
},
editButton: {
id: "button.edit",
defaultMessage: "Edit",
},
errorLabel: {
id: "label.error",
defaultMessage: "Error",
},
errorNotificationTitle: {
id: "notification.error.title",
defaultMessage: "An error occurred",
},
followedProjectsLabel: {
id: "label.followed-projects",
defaultMessage: "Followed projects",
},
galleryInputView: {
id: "input.view.gallery",
defaultMessage: "Gallery view",
},
gridInputView: {
id: "input.view.grid",
defaultMessage: "Grid view",
},
listInputView: {
id: "input.view.list",
defaultMessage: "Rows view",
},
moderationLabel: {
id: "label.moderation",
defaultMessage: "Moderation",
},
notificationsLabel: {
id: "label.notifications",
defaultMessage: "Notifications",
},
privateLabel: {
id: "collection.label.private",
defaultMessage: "Private",
},
publicLabel: {
id: "label.public",
defaultMessage: "Public",
},
rejectedLabel: {
id: "label.rejected",
defaultMessage: "Rejected",
},
reportButton: {
id: "button.report",
defaultMessage: "Report",
},
passwordLabel: {
id: "label.password",
defaultMessage: "Password",
},
saveButton: {
id: "button.save",
defaultMessage: "Save",
},
saveChangesButton: {
id: "button.save-changes",
defaultMessage: "Save changes",
},
scopesLabel: {
id: "label.scopes",
defaultMessage: "Scopes",
},
serversLabel: {
id: "label.servers",
defaultMessage: "Servers",
},
settingsLabel: {
id: "label.settings",
defaultMessage: "Settings",
},
signInButton: {
id: "button.sign-in",
defaultMessage: "Sign in",
},
signOutButton: {
id: "button.sign-out",
defaultMessage: "Sign out",
},
titleLabel: {
id: "label.title",
defaultMessage: "Title",
},
unlistedLabel: {
id: "label.unlisted",
defaultMessage: "Unlisted",
},
uploadImageButton: {
id: "button.upload-image",
defaultMessage: "Upload image",
},
visibilityLabel: {
id: "label.visibility",
defaultMessage: "Visibility",
},
visitYourProfile: {
id: "label.visit-your-profile",
defaultMessage: "Visit your profile",
},
});
export const commonSettingsMessages = defineMessages({
appearance: {
id: "settings.appearance.title",
defaultMessage: "Appearance",
},
language: {
id: "settings.language.title",
defaultMessage: "Language",
},
profile: {
id: "settings.profile.title",
defaultMessage: "Public profile",
},
account: {
id: "settings.account.title",
defaultMessage: "Account and security",
},
authorizedApps: {
id: "settings.authorized-apps.title",
defaultMessage: "Authorized apps",
},
sessions: {
id: "settings.sessions.title",
defaultMessage: "Sessions",
},
pats: {
id: "settings.pats.title",
defaultMessage: "Personal access tokens",
},
applications: {
id: "settings.applications.title",
defaultMessage: "Your applications",
},
billing: {
id: "settings.billing.title",
defaultMessage: "Billing and subscriptions",
},
});