mirror of
https://github.com/modrinth/code.git
synced 2026-08-28 18:45:15 +00:00
feat: pride 2026 frontend (#6205)
* feat: pride 2026 banner app sidebar * feat: use ProgressBar component * feat: pride skins * feat: pride skins * feat: blog post * fix: blogpost * fix: pride skin condition * fix: types * fix: show logic * fix: qa * fix: lint * fix: unused var
This commit is contained in:
@@ -84,6 +84,7 @@ import InstallToPlayModal from '@/components/ui/modal/InstallToPlayModal.vue'
|
|||||||
import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
|
import ModpackAlreadyInstalledModal from '@/components/ui/modal/ModpackAlreadyInstalledModal.vue'
|
||||||
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
import UpdateToPlayModal from '@/components/ui/modal/UpdateToPlayModal.vue'
|
||||||
import NavButton from '@/components/ui/NavButton.vue'
|
import NavButton from '@/components/ui/NavButton.vue'
|
||||||
|
import PrideFundraiserBanner from '@/components/ui/PrideFundraiserBanner.vue'
|
||||||
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
import PromotionWrapper from '@/components/ui/PromotionWrapper.vue'
|
||||||
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
|
||||||
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
import SplashScreen from '@/components/ui/SplashScreen.vue'
|
||||||
@@ -101,6 +102,7 @@ import { list } from '@/helpers/profile.js'
|
|||||||
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
|
import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts'
|
||||||
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
import { get as getSettings, set as setSettings } from '@/helpers/settings.ts'
|
||||||
import { get_opening_command, initialize_state } from '@/helpers/state'
|
import { get_opening_command, initialize_state } from '@/helpers/state'
|
||||||
|
import { hasActivePride26Midas, hasMidasBadge } from '@/helpers/user-campaigns.ts'
|
||||||
import {
|
import {
|
||||||
areUpdatesEnabled,
|
areUpdatesEnabled,
|
||||||
enqueueUpdateForInstallation,
|
enqueueUpdateForInstallation,
|
||||||
@@ -134,6 +136,7 @@ const route = useRoute()
|
|||||||
const APP_LEFT_NAV_WIDTH = '4rem'
|
const APP_LEFT_NAV_WIDTH = '4rem'
|
||||||
const APP_SIDEBAR_WIDTH = 300
|
const APP_SIDEBAR_WIDTH = 300
|
||||||
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
const INTERCOM_BUBBLE_DEFAULT_PADDING = 20
|
||||||
|
const PRIDE_FUNDRAISER_END_DATE = new Date('2026-07-01T00:00:00Z').getTime()
|
||||||
const credentials = ref()
|
const credentials = ref()
|
||||||
const sidebarToggled = ref(true)
|
const sidebarToggled = ref(true)
|
||||||
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
|
const unsubscribeSidebarToggle = themeStore.$subscribe(() => {
|
||||||
@@ -144,6 +147,9 @@ const forceSidebar = computed(
|
|||||||
)
|
)
|
||||||
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
const sidebarVisible = computed(() => sidebarToggled.value || forceSidebar.value)
|
||||||
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
|
const hostingRouteActive = computed(() => route.path.startsWith('/hosting'))
|
||||||
|
const prideFundraiserEnabled = computed(
|
||||||
|
() => themeStore.getFeatureFlag('pride_fundraiser') && Date.now() < PRIDE_FUNDRAISER_END_DATE,
|
||||||
|
)
|
||||||
const hostingIntercomIdentityKey = computed(() => {
|
const hostingIntercomIdentityKey = computed(() => {
|
||||||
const rawServerId = route.params.id
|
const rawServerId = route.params.id
|
||||||
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
|
const serverId = Array.isArray(rawServerId) ? rawServerId[0] : rawServerId
|
||||||
@@ -192,6 +198,12 @@ const tauriApiClient = new TauriModrinthClient({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
provideModrinthClient(tauriApiClient)
|
provideModrinthClient(tauriApiClient)
|
||||||
|
const { data: authenticatedModrinthUser } = useQuery({
|
||||||
|
queryKey: computed(() => ['authenticated-user', 'campaigns', credentials.value?.user?.id]),
|
||||||
|
queryFn: () => tauriApiClient.labrinth.users_v3.getAuthenticated(),
|
||||||
|
enabled: () => !!credentials.value?.session,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
providePageContext({
|
providePageContext({
|
||||||
hierarchicalSidebarAvailable: ref(true),
|
hierarchicalSidebarAvailable: ref(true),
|
||||||
showAds: ref(false),
|
showAds: ref(false),
|
||||||
@@ -676,12 +688,11 @@ async function logOut() {
|
|||||||
await fetchCredentials()
|
await fetchCredentials()
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIDAS_BITFLAG = 1 << 0
|
|
||||||
const hasPlus = computed(
|
const hasPlus = computed(
|
||||||
() =>
|
() =>
|
||||||
credentials.value &&
|
!!credentials.value?.user &&
|
||||||
credentials.value.user &&
|
(hasMidasBadge(credentials.value.user) ||
|
||||||
(credentials.value.user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG,
|
hasActivePride26Midas(authenticatedModrinthUser.value?.campaigns?.pride_26)),
|
||||||
)
|
)
|
||||||
|
|
||||||
const showAd = computed(
|
const showAd = computed(
|
||||||
@@ -1479,6 +1490,10 @@ provideAppUpdateDownloadProgress(appUpdateDownload)
|
|||||||
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
<FriendsList :credentials="credentials" :sign-in="() => signIn()" />
|
||||||
</suspense>
|
</suspense>
|
||||||
</div>
|
</div>
|
||||||
|
<PrideFundraiserBanner
|
||||||
|
v-if="prideFundraiserEnabled"
|
||||||
|
class="p-4 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid"
|
||||||
|
/>
|
||||||
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
|
<div v-if="news && news.length > 0" class="p-4 flex flex-col items-center">
|
||||||
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
|
<h3 class="text-base mb-4 text-primary font-medium m-0 text-left w-full">News</h3>
|
||||||
<div class="space-y-4 flex flex-col items-center w-full">
|
<div class="space-y-4 flex flex-col items-center w-full">
|
||||||
|
|||||||
@@ -0,0 +1,123 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CalendarIcon, UsersIcon, XIcon } from '@modrinth/assets'
|
||||||
|
import { injectModrinthClient, ProgressBar } from '@modrinth/ui'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
|
||||||
|
const DISMISSED_STORAGE_KEY = 'pride-fundraiser-2026-dismissed'
|
||||||
|
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
const dismissed = ref(localStorage.getItem(DISMISSED_STORAGE_KEY) === 'true')
|
||||||
|
|
||||||
|
const { data: campaignInfo } = useQuery({
|
||||||
|
queryKey: ['campaign', 'pride-26'],
|
||||||
|
queryFn: () => client.labrinth.campaign_internal.getPride26(),
|
||||||
|
enabled: () => !dismissed.value,
|
||||||
|
staleTime: 15 * 60 * 1000,
|
||||||
|
refetchInterval: 15 * 60 * 1000,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
const shouldShowBanner = computed(
|
||||||
|
() => !dismissed.value && Number(campaignInfo.value?.target_usd) > 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
async function openPrideFundraiser() {
|
||||||
|
await openUrl('https://modrinth.com/pride')
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissBanner() {
|
||||||
|
dismissed.value = true
|
||||||
|
localStorage.setItem(DISMISSED_STORAGE_KEY, 'true')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUsd(amount: string | number) {
|
||||||
|
return Number(amount).toLocaleString('en-US', {
|
||||||
|
style: 'currency',
|
||||||
|
currency: 'USD',
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysLeft() {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
Math.ceil((new Date('2026-07-01T00:00:00Z').getTime() - Date.now()) / (24 * 60 * 60 * 1000)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="shouldShowBanner && campaignInfo">
|
||||||
|
<section
|
||||||
|
role="link"
|
||||||
|
tabindex="0"
|
||||||
|
class="flex w-full cursor-pointer flex-col gap-3 rounded-xl border border-solid border-surface-5 bg-button-bg p-3 text-primary transition-[border-color,filter] hover:border-surface-6 hover:brightness-125 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
|
||||||
|
aria-label="Open Pride fundraiser"
|
||||||
|
@click="openPrideFundraiser"
|
||||||
|
@keydown.enter="openPrideFundraiser"
|
||||||
|
@keydown.space.prevent="openPrideFundraiser"
|
||||||
|
>
|
||||||
|
<div class="flex w-full items-center justify-between gap-2">
|
||||||
|
<h2 class="m-0 min-w-0 truncate text-base font-semibold text-contrast">
|
||||||
|
Pride Fundraiser 2026
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="m-0 flex size-5 shrink-0 cursor-pointer items-center justify-center border-0 bg-transparent p-0 text-primary transition-colors hover:text-contrast focus-visible:text-contrast"
|
||||||
|
aria-label="Dismiss Pride fundraiser"
|
||||||
|
@click.stop="dismissBanner"
|
||||||
|
@keydown.stop
|
||||||
|
>
|
||||||
|
<XIcon aria-hidden="true" class="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="h-px w-full bg-surface-5" />
|
||||||
|
<div class="flex w-full flex-col gap-2.5">
|
||||||
|
<div class="flex items-end gap-1 whitespace-nowrap">
|
||||||
|
<span class="text-base font-semibold leading-5 text-contrast">
|
||||||
|
{{ formatUsd(campaignInfo.total_donations_usd) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs font-medium leading-4">
|
||||||
|
of {{ formatUsd(campaignInfo.target_usd) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ProgressBar
|
||||||
|
class="pride-fundraiser-banner__progress"
|
||||||
|
:progress="Number(campaignInfo.total_donations_usd)"
|
||||||
|
:max="Number(campaignInfo.target_usd)"
|
||||||
|
color="purple"
|
||||||
|
full-width
|
||||||
|
:gradient-border="false"
|
||||||
|
:aria-label="`${formatUsd(campaignInfo.total_donations_usd)} of ${formatUsd(
|
||||||
|
campaignInfo.target_usd,
|
||||||
|
)} raised`"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-wrap items-center gap-2 text-xs font-medium leading-4">
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<UsersIcon aria-hidden="true" class="size-4 shrink-0" />
|
||||||
|
{{ campaignInfo.num_donators.toLocaleString('en-US') }}
|
||||||
|
{{ campaignInfo.num_donators === 1 ? 'supporter' : 'supporters' }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1">
|
||||||
|
<CalendarIcon aria-hidden="true" class="size-4 shrink-0" />
|
||||||
|
{{ daysLeft() }} {{ daysLeft() === 1 ? 'day left' : 'days left' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.pride-fundraiser-banner__progress :deep(.progress-bar) {
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--color-red) 0%,
|
||||||
|
var(--color-orange) 20%,
|
||||||
|
var(--color-green) 50%,
|
||||||
|
var(--color-blue) 75%,
|
||||||
|
var(--color-purple) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -393,7 +393,6 @@ const messages = defineMessages({
|
|||||||
<FriendsSection
|
<FriendsSection
|
||||||
v-if="pendingFriends.length > 0"
|
v-if="pendingFriends.length > 0"
|
||||||
:is-searching="!!search"
|
:is-searching="!!search"
|
||||||
open-by-default
|
|
||||||
:friends="pendingFriends"
|
:friends="pendingFriends"
|
||||||
:heading="formatMessage(messages.pending)"
|
:heading="formatMessage(messages.pending)"
|
||||||
:remove-friend="removeFriend"
|
:remove-friend="removeFriend"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon } from '@modrinth/assets'
|
import { DropdownIcon, EditIcon, PlusIcon, TrashIcon, UnknownIcon } from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { useElementSize, useWindowSize } from '@vueuse/core'
|
import { useElementSize, useWindowSize } from '@vueuse/core'
|
||||||
|
import { Tooltip } from 'floating-vue'
|
||||||
import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
import { computed, nextTick, onUnmounted, ref, useTemplateRef, watch } from 'vue'
|
||||||
|
|
||||||
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
|
||||||
@@ -24,6 +25,7 @@ type AddSkinButtonRef = SkinLikeTextButtonExpose | SkinLikeTextButtonExpose[]
|
|||||||
|
|
||||||
interface DefaultSkinSection {
|
interface DefaultSkinSection {
|
||||||
title: string
|
title: string
|
||||||
|
infoTooltip?: string
|
||||||
skins: Skin[]
|
skins: Skin[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,6 +33,7 @@ interface SkinSection {
|
|||||||
key: string
|
key: string
|
||||||
title: string
|
title: string
|
||||||
kind: SkinSectionKind
|
kind: SkinSectionKind
|
||||||
|
infoTooltip?: string
|
||||||
skins: Skin[]
|
skins: Skin[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +148,7 @@ const sections = computed<SkinSection[]>(() => [
|
|||||||
key: defaultSkinSectionKey(section.title),
|
key: defaultSkinSectionKey(section.title),
|
||||||
title: section.title,
|
title: section.title,
|
||||||
kind: 'default' as const,
|
kind: 'default' as const,
|
||||||
|
infoTooltip: section.infoTooltip,
|
||||||
skins: section.skins,
|
skins: section.skins,
|
||||||
})),
|
})),
|
||||||
])
|
])
|
||||||
@@ -330,6 +334,24 @@ defineExpose({ getAddSkinButtonElement })
|
|||||||
<span class="min-w-0 text-xl font-semibold leading-7 text-primary">
|
<span class="min-w-0 text-xl font-semibold leading-7 text-primary">
|
||||||
{{ section.title }}
|
{{ section.title }}
|
||||||
</span>
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
v-if="section.infoTooltip"
|
||||||
|
theme="dismissable-prompt"
|
||||||
|
placement="top"
|
||||||
|
:triggers="['hover', 'focus']"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="inline-flex size-6 shrink-0 items-center justify-center text-secondary transition-colors group-hover:text-primary"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
|
<UnknownIcon class="size-5" />
|
||||||
|
</span>
|
||||||
|
<template #popper>
|
||||||
|
<p class="m-0 max-w-96 text-wrap text-sm font-medium leading-tight">
|
||||||
|
{{ section.infoTooltip }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</Tooltip>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { Labrinth } from '@modrinth/api-client'
|
||||||
|
|
||||||
|
const MIDAS_BITFLAG = 1 << 0
|
||||||
|
const PRIDE_26_MIDAS_DURATION_MS = 30 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
type Pride26Campaign = Labrinth.Users.v3.Pride26CampaignDonation | null | undefined
|
||||||
|
|
||||||
|
export function hasMidasBadge(user?: { badges?: number } | null) {
|
||||||
|
return !!user?.badges && (user.badges & MIDAS_BITFLAG) === MIDAS_BITFLAG
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPride26Badge(campaign: Pride26Campaign) {
|
||||||
|
return campaign?.has_badge === true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasActivePride26Midas(campaign: Pride26Campaign, now = Date.now()) {
|
||||||
|
if (campaign?.has_midas !== true) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const donatedAt = new Date(campaign.last_donated_at).getTime()
|
||||||
|
return Number.isFinite(donatedAt) && donatedAt + PRIDE_26_MIDAS_DURATION_MS > now
|
||||||
|
}
|
||||||
@@ -431,6 +431,15 @@
|
|||||||
"app.skins.section.minecon-earth-2017": {
|
"app.skins.section.minecon-earth-2017": {
|
||||||
"message": "MINECON Earth 2017"
|
"message": "MINECON Earth 2017"
|
||||||
},
|
},
|
||||||
|
"app.skins.section.modrinth": {
|
||||||
|
"message": "Modrinth"
|
||||||
|
},
|
||||||
|
"app.skins.section.modrinth-pride": {
|
||||||
|
"message": "Modrinth Pride"
|
||||||
|
},
|
||||||
|
"app.skins.section.modrinth-pride.tooltip": {
|
||||||
|
"message": "You received these skins for donating to a Modrinth Pride fundraiser during Pride Month."
|
||||||
|
},
|
||||||
"app.skins.section.mounts-of-mayhem": {
|
"app.skins.section.mounts-of-mayhem": {
|
||||||
"message": "Mounts of Mayhem"
|
"message": "Mounts of Mayhem"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,11 +13,14 @@ import {
|
|||||||
commonMessages,
|
commonMessages,
|
||||||
ConfirmModal,
|
ConfirmModal,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
|
injectAuth,
|
||||||
|
injectModrinthClient,
|
||||||
injectNotificationManager,
|
injectNotificationManager,
|
||||||
SkinPreviewRenderer,
|
SkinPreviewRenderer,
|
||||||
useVIntl,
|
useVIntl,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
import { arrayBufferToBase64 } from '@modrinth/utils'
|
import { arrayBufferToBase64 } from '@modrinth/utils'
|
||||||
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { type DragDropEvent, getCurrentWebview } from '@tauri-apps/api/webview'
|
import { type DragDropEvent, getCurrentWebview } from '@tauri-apps/api/webview'
|
||||||
import { computedAsync } from '@vueuse/core'
|
import { computedAsync } from '@vueuse/core'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
@@ -44,6 +47,7 @@ import {
|
|||||||
normalize_skin_texture,
|
normalize_skin_texture,
|
||||||
remove_custom_skin,
|
remove_custom_skin,
|
||||||
} from '@/helpers/skins.ts'
|
} from '@/helpers/skins.ts'
|
||||||
|
import { hasPride26Badge } from '@/helpers/user-campaigns.ts'
|
||||||
import { handleSevereError } from '@/store/error'
|
import { handleSevereError } from '@/store/error'
|
||||||
import { useTheming } from '@/store/state'
|
import { useTheming } from '@/store/state'
|
||||||
|
|
||||||
@@ -53,11 +57,25 @@ type VirtualSkinSectionListExpose = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PENDING_SKIN_REFRESH_DELAY_MS = 11_000
|
const PENDING_SKIN_REFRESH_DELAY_MS = 11_000
|
||||||
|
const DEFAULT_SKIN_SECTION_SORT_ORDER = ['Default skins', 'Modrinth Pride']
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
skinSelectorTitle: {
|
skinSelectorTitle: {
|
||||||
id: 'app.skins.title',
|
id: 'app.skins.title',
|
||||||
defaultMessage: 'Skin selector',
|
defaultMessage: 'Skin selector',
|
||||||
},
|
},
|
||||||
|
modrinthPrideSection: {
|
||||||
|
id: 'app.skins.section.modrinth-pride',
|
||||||
|
defaultMessage: 'Modrinth Pride',
|
||||||
|
},
|
||||||
|
modrinthPrideTooltip: {
|
||||||
|
id: 'app.skins.section.modrinth-pride.tooltip',
|
||||||
|
defaultMessage:
|
||||||
|
'You received these skins for donating to a Modrinth Pride fundraiser during Pride Month.',
|
||||||
|
},
|
||||||
|
modrinthSection: {
|
||||||
|
id: 'app.skins.section.modrinth',
|
||||||
|
defaultMessage: 'Modrinth',
|
||||||
|
},
|
||||||
defaultSkinsSection: {
|
defaultSkinsSection: {
|
||||||
id: 'app.skins.section.default-skins',
|
id: 'app.skins.section.default-skins',
|
||||||
defaultMessage: 'Default skins',
|
defaultMessage: 'Default skins',
|
||||||
@@ -157,6 +175,8 @@ const skinSectionList = useTemplateRef<VirtualSkinSectionListExpose>('skinSectio
|
|||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
const notifications = injectNotificationManager()
|
const notifications = injectNotificationManager()
|
||||||
const { addNotification, handleError } = notifications
|
const { addNotification, handleError } = notifications
|
||||||
|
const auth = injectAuth()
|
||||||
|
const client = injectModrinthClient()
|
||||||
|
|
||||||
const themeStore = useTheming()
|
const themeStore = useTheming()
|
||||||
const skins = ref<Skin[]>([])
|
const skins = ref<Skin[]>([])
|
||||||
@@ -180,22 +200,42 @@ const savedSkins = computed(() => {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const defaultSkins = computed(() => filterDefaultSkins(skins.value))
|
const { data: modrinthUser } = useQuery({
|
||||||
|
queryKey: computed(() => ['authenticated-user', 'campaigns', auth.user.value?.id]),
|
||||||
|
queryFn: () => client.labrinth.users_v3.getAuthenticated(),
|
||||||
|
enabled: () => !!auth.session_token.value,
|
||||||
|
retry: false,
|
||||||
|
})
|
||||||
|
const hasModrinthPrideCampaign = computed(
|
||||||
|
() => !!auth.session_token.value && hasPride26Badge(modrinthUser.value?.campaigns?.pride_26),
|
||||||
|
)
|
||||||
|
const defaultSkins = computed(() =>
|
||||||
|
filterDefaultSkins(skins.value).filter(
|
||||||
|
(skin) => skin.section !== 'Modrinth Pride' || hasModrinthPrideCampaign.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
const defaultSkinSections = computed(() => {
|
const defaultSkinSections = computed(() => {
|
||||||
const sections = new Map<string, Skin[]>()
|
const sections = new Map<string, Skin[]>()
|
||||||
|
|
||||||
for (const skin of defaultSkins.value) {
|
for (const skin of defaultSkins.value) {
|
||||||
const sectionTitle = getDefaultSkinSectionTitle(skin.section)
|
const section = skin.section ?? 'Default skins'
|
||||||
const sectionSkins = sections.get(sectionTitle)
|
const sectionSkins = sections.get(section)
|
||||||
|
|
||||||
if (sectionSkins) {
|
if (sectionSkins) {
|
||||||
sectionSkins.push(skin)
|
sectionSkins.push(skin)
|
||||||
} else {
|
} else {
|
||||||
sections.set(sectionTitle, [skin])
|
sections.set(section, [skin])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(sections, ([title, skins]) => ({ title, skins }))
|
return Array.from(sections, ([section, skins]) => ({
|
||||||
|
section,
|
||||||
|
title: getDefaultSkinSectionTitle(section),
|
||||||
|
infoTooltip: getDefaultSkinSectionInfoTooltip(section),
|
||||||
|
skins,
|
||||||
|
})).sort(
|
||||||
|
(a, b) => getDefaultSkinSectionSortIndex(a.section) - getDefaultSkinSectionSortIndex(b.section),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentCape = computed(() => {
|
const currentCape = computed(() => {
|
||||||
@@ -303,6 +343,10 @@ function isMinecraftSkinRateLimitError(error: unknown) {
|
|||||||
|
|
||||||
function getDefaultSkinSectionTitle(section?: string) {
|
function getDefaultSkinSectionTitle(section?: string) {
|
||||||
switch (section) {
|
switch (section) {
|
||||||
|
case 'Modrinth Pride':
|
||||||
|
return formatMessage(messages.modrinthPrideSection)
|
||||||
|
case 'Modrinth':
|
||||||
|
return formatMessage(messages.modrinthSection)
|
||||||
case 'MINECON Earth 2017':
|
case 'MINECON Earth 2017':
|
||||||
return formatMessage(messages.mineconEarth2017Section)
|
return formatMessage(messages.mineconEarth2017Section)
|
||||||
case 'Builders & Biomes':
|
case 'Builders & Biomes':
|
||||||
@@ -326,6 +370,20 @@ function getDefaultSkinSectionTitle(section?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultSkinSectionInfoTooltip(section: string) {
|
||||||
|
switch (section) {
|
||||||
|
case 'Modrinth Pride':
|
||||||
|
return formatMessage(messages.modrinthPrideTooltip)
|
||||||
|
default:
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultSkinSectionSortIndex(section: string) {
|
||||||
|
const index = DEFAULT_SKIN_SECTION_SORT_ORDER.indexOf(section)
|
||||||
|
return index === -1 ? DEFAULT_SKIN_SECTION_SORT_ORDER.length : index
|
||||||
|
}
|
||||||
|
|
||||||
function changeSkin(newSkin: Skin) {
|
function changeSkin(newSkin: Skin) {
|
||||||
selectedSkin.value = newSkin
|
selectedSkin.value = newSkin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export const DEFAULT_FEATURE_FLAGS = {
|
|||||||
server_ram_as_bytes_always_on: false,
|
server_ram_as_bytes_always_on: false,
|
||||||
always_show_app_controls: false,
|
always_show_app_controls: false,
|
||||||
skip_unknown_pack_warning: false,
|
skip_unknown_pack_warning: false,
|
||||||
|
pride_fundraiser: true,
|
||||||
i18n_debug: false,
|
i18n_debug: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
ssr: {
|
ssr: {
|
||||||
// https://github.com/Akryum/floating-vue/issues/809#issuecomment-1002996240
|
// https://github.com/Akryum/floating-vue/issues/809#issuecomment-1002996240
|
||||||
noExternal: ['v-tooltip'],
|
noExternal: ['floating-vue', '@floating-ui/core', '@floating-ui/dom'],
|
||||||
optimizeDeps: {
|
},
|
||||||
include: ['vue-router'],
|
optimizeDeps: {
|
||||||
},
|
include: ['vue-router', 'floating-vue', '@floating-ui/dom'],
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
global: {},
|
global: {},
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 446 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 891 KiB |
@@ -1,5 +1,12 @@
|
|||||||
{
|
{
|
||||||
"articles": [
|
"articles": [
|
||||||
|
{
|
||||||
|
"title": "Pride 2026 Fundraiser: Matching up to $5,000",
|
||||||
|
"summary": "Celebrating our community and working together to make a difference.",
|
||||||
|
"thumbnail": "https://modrinth.com/news/article/pride-campaign-2026/thumbnail.webp",
|
||||||
|
"date": "2026-06-01T16:00:00.000Z",
|
||||||
|
"link": "https://modrinth.com/news/article/pride-campaign-2026"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Project Analytics are good now",
|
"title": "Project Analytics are good now",
|
||||||
"summary": "Get deeper insights into how people are using your projects with breakdowns, filtering, and more!",
|
"summary": "Get deeper insights into how people are using your projects with breakdowns, filtering, and more!",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -18,6 +18,7 @@ import { LabrinthAnalyticsV3Module } from './labrinth/analytics/v3'
|
|||||||
import { LabrinthAuthInternalModule } from './labrinth/auth/internal'
|
import { LabrinthAuthInternalModule } from './labrinth/auth/internal'
|
||||||
import { LabrinthAuthV2Module } from './labrinth/auth/v2'
|
import { LabrinthAuthV2Module } from './labrinth/auth/v2'
|
||||||
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
|
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
|
||||||
|
import { LabrinthCampaignInternalModule } from './labrinth/campaign/internal'
|
||||||
import { LabrinthCollectionsModule } from './labrinth/collections'
|
import { LabrinthCollectionsModule } from './labrinth/collections'
|
||||||
import { LabrinthExternalProjectsInternalModule } from './labrinth/external-projects/internal'
|
import { LabrinthExternalProjectsInternalModule } from './labrinth/external-projects/internal'
|
||||||
import { LabrinthGlobalsInternalModule } from './labrinth/globals/internal'
|
import { LabrinthGlobalsInternalModule } from './labrinth/globals/internal'
|
||||||
@@ -80,6 +81,7 @@ export const MODULE_REGISTRY = {
|
|||||||
labrinth_auth_internal: LabrinthAuthInternalModule,
|
labrinth_auth_internal: LabrinthAuthInternalModule,
|
||||||
labrinth_auth_v2: LabrinthAuthV2Module,
|
labrinth_auth_v2: LabrinthAuthV2Module,
|
||||||
labrinth_billing_internal: LabrinthBillingInternalModule,
|
labrinth_billing_internal: LabrinthBillingInternalModule,
|
||||||
|
labrinth_campaign_internal: LabrinthCampaignInternalModule,
|
||||||
labrinth_collections: LabrinthCollectionsModule,
|
labrinth_collections: LabrinthCollectionsModule,
|
||||||
labrinth_external_projects_internal: LabrinthExternalProjectsInternalModule,
|
labrinth_external_projects_internal: LabrinthExternalProjectsInternalModule,
|
||||||
labrinth_globals_internal: LabrinthGlobalsInternalModule,
|
labrinth_globals_internal: LabrinthGlobalsInternalModule,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { AbstractModule } from '../../../core/abstract-module'
|
||||||
|
import type { Labrinth } from '../types'
|
||||||
|
|
||||||
|
export class LabrinthCampaignInternalModule extends AbstractModule {
|
||||||
|
public getModuleID(): string {
|
||||||
|
return 'labrinth_campaign_internal'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Pride 2026 campaign fundraising progress.
|
||||||
|
* GET /_internal/campaign/pride-26
|
||||||
|
*/
|
||||||
|
public async getPride26(): Promise<Labrinth.Campaign.Internal.CampaignInfo> {
|
||||||
|
return this.client.request<Labrinth.Campaign.Internal.CampaignInfo>('/campaign/pride-26', {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 'internal',
|
||||||
|
method: 'GET',
|
||||||
|
skipAuth: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,17 @@
|
|||||||
|
import type { RawDecimal } from '../../utils/types'
|
||||||
import type { ISO3166 } from '../iso3166/types'
|
import type { ISO3166 } from '../iso3166/types'
|
||||||
|
|
||||||
export namespace Labrinth {
|
export namespace Labrinth {
|
||||||
|
export namespace Campaign {
|
||||||
|
export namespace Internal {
|
||||||
|
export type CampaignInfo = {
|
||||||
|
total_donations_usd: RawDecimal
|
||||||
|
target_usd: RawDecimal
|
||||||
|
num_donators: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export namespace Billing {
|
export namespace Billing {
|
||||||
export namespace Internal {
|
export namespace Internal {
|
||||||
export type PriceDuration = 'five-days' | 'monthly' | 'quarterly' | 'yearly'
|
export type PriceDuration = 'five-days' | 'monthly' | 'quarterly' | 'yearly'
|
||||||
@@ -1025,6 +1036,7 @@ export namespace Labrinth {
|
|||||||
icon_url: string | null
|
icon_url: string | null
|
||||||
color: number | null
|
color: number | null
|
||||||
members: Projects.v3.TeamMember[]
|
members: Projects.v3.TeamMember[]
|
||||||
|
moderation_notes?: Users.Common.ModerationNote | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateOrganizationRequest = {
|
export type CreateOrganizationRequest = {
|
||||||
@@ -1249,7 +1261,7 @@ export namespace Labrinth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export namespace Users {
|
export namespace Users {
|
||||||
namespace Common {
|
export namespace Common {
|
||||||
export type Role = 'developer' | 'moderator' | 'admin'
|
export type Role = 'developer' | 'moderator' | 'admin'
|
||||||
|
|
||||||
export type AuthProvider =
|
export type AuthProvider =
|
||||||
@@ -1267,6 +1279,15 @@ export namespace Labrinth {
|
|||||||
venmo_handle?: string
|
venmo_handle?: string
|
||||||
balance: number
|
balance: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ModerationNote = {
|
||||||
|
notes: string
|
||||||
|
last_modified: string
|
||||||
|
created_at: string
|
||||||
|
last_author: string
|
||||||
|
user_rating: number
|
||||||
|
version: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace v2 {
|
export namespace v2 {
|
||||||
@@ -1316,6 +1337,7 @@ export namespace Labrinth {
|
|||||||
created: string
|
created: string
|
||||||
role: Role
|
role: Role
|
||||||
badges: number
|
badges: number
|
||||||
|
campaigns: UserCampaigns
|
||||||
auth_providers?: AuthProvider[]
|
auth_providers?: AuthProvider[]
|
||||||
email?: string
|
email?: string
|
||||||
email_verified?: boolean
|
email_verified?: boolean
|
||||||
@@ -1324,7 +1346,7 @@ export namespace Labrinth {
|
|||||||
payout_data?: UserPayoutData
|
payout_data?: UserPayoutData
|
||||||
stripe_customer_id?: string
|
stripe_customer_id?: string
|
||||||
allow_friend_requests?: boolean
|
allow_friend_requests?: boolean
|
||||||
campaigns: UserCampaigns
|
moderation_notes?: Common.ModerationNote | null
|
||||||
github_id?: number
|
github_id?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,18 @@ export class LabrinthUsersV3Module extends AbstractModule {
|
|||||||
return 'labrinth_users_v3'
|
return 'labrinth_users_v3'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the authenticated user.
|
||||||
|
* GET /v3/user
|
||||||
|
*/
|
||||||
|
public async getAuthenticated(): Promise<Labrinth.Users.v3.User> {
|
||||||
|
return this.client.request<Labrinth.Users.v3.User>('/user', {
|
||||||
|
api: 'labrinth',
|
||||||
|
version: 3,
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a user by ID or username
|
* Get a user by ID or username
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::{minecraft_skins::SkinSource, state::MinecraftSkinVariant};
|
|||||||
|
|
||||||
use super::super::super::Skin;
|
use super::super::super::Skin;
|
||||||
|
|
||||||
|
const MODRINTH_PRIDE_SKIN_PACK_SECTION: &str = "Modrinth Pride";
|
||||||
const DEFAULT_SKINS_SECTION: &str = "Default skins";
|
const DEFAULT_SKINS_SECTION: &str = "Default skins";
|
||||||
const MINECON_EARTH_2017_SKIN_PACK_SECTION: &str = "MINECON Earth 2017";
|
const MINECON_EARTH_2017_SKIN_PACK_SECTION: &str = "MINECON Earth 2017";
|
||||||
const BUILDERS_AND_BIOMES_SKIN_PACK_SECTION: &str = "Builders & Biomes";
|
const BUILDERS_AND_BIOMES_SKIN_PACK_SECTION: &str = "Builders & Biomes";
|
||||||
@@ -16,14 +17,122 @@ const THE_COPPER_AGE_SKIN_PACK_SECTION: &str = "The Copper Age";
|
|||||||
const MOUNTS_OF_MAYHEM_SKIN_PACK_SECTION: &str = "Mounts of Mayhem";
|
const MOUNTS_OF_MAYHEM_SKIN_PACK_SECTION: &str = "Mounts of Mayhem";
|
||||||
const TINY_TAKEOVER_SKIN_PACK_SECTION: &str = "Tiny Takeover";
|
const TINY_TAKEOVER_SKIN_PACK_SECTION: &str = "Tiny Takeover";
|
||||||
|
|
||||||
/// A list of default Minecraft skins to make available to the user, created by Mojang.
|
/// A list of bundled Minecraft skins to make available to the user.
|
||||||
pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
pub static DEFAULT_SKINS: LazyLock<Vec<Skin>> = LazyLock::new(|| {
|
||||||
//
|
//
|
||||||
// The skins below are available in the vanilla Minecraft launcher, and were found
|
// The Mojang skins below are available in the vanilla Minecraft launcher, and were found
|
||||||
// by reverse engineering the behavior of the Minecraft launcher. The textures are
|
// by reverse engineering the behavior of the Minecraft launcher. The textures are
|
||||||
// publicly available at `https://textures.minecraft.net/texture/<texture_key>`.
|
// publicly available at `https://textures.minecraft.net/texture/<texture_key>`.
|
||||||
//
|
//
|
||||||
vec and [Shyyell Diamond Sanchez-McCray](https://www.advocate.com/news/crime/black-drag-artist-virginia-murdered), trans rights activists [Zehrish Khanzadi and Bindiya Rana](https://www.theguardian.com/global-development/2026/jan/27/pakistan-transgender-attacks-killings) surviving gunfire in Pakistan, and [many more](https://en.wikipedia.org/wiki/List_of_acts_of_violence_against_LGBTQ_people) cases like these are reminders that safety is not guaranteed for some members of our community.
|
||||||
|
|
||||||
|
That's why this Pride Month, Modrinth will be running a fundraiser supporting [**Rainbow Railroad**](https://www.rainbowrailroad.org/).
|
||||||
|
|
||||||
|
Rainbow Railroad works with LGBTQIA+ people who are facing danger, helping to connect them with support, resources, and pathways to safe countries. Through their **Solidarity in Pride 2026** campaign, they're raising funds to help at-risk LGBTQIA+ people access safety when staying in place is no longer an option.
|
||||||
|
|
||||||
|
<a href="https://tiltify.com/+modrinth/modrinth-pride-2026" class="!text-2xl font-semibold">You can support the fundraiser here.</a>
|
||||||
|
|
||||||
|
**When donating, please enter your _Modrinth username_ as the _Tiltify display name_ so we can apply any eligible rewards to your account!**
|
||||||
|
|
||||||
|
## Donation Matching Commitment
|
||||||
|
|
||||||
|
We want to maximize the impact our community can make.
|
||||||
|
|
||||||
|
To help achieve this, we've committed to matching all donations 1:1 until our fundraising goal of $5,000 has been met.
|
||||||
|
|
||||||
|
## Donor Rewards
|
||||||
|
|
||||||
|
This year, we will also be providing rewards as a small thank you to everyone who supports the fundraiser. Including a few limited rewards specific to this year's fundraiser!
|
||||||
|
|
||||||
|
### Pride Badge 2026 & Pride Skins
|
||||||
|
|
||||||
|
Everyone who donates will receive a special Pride 2026 profile badge on Modrinth.
|
||||||
|
|
||||||
|
<img width="204px" height="204px" class="mx-auto mb-4" src="/news/article/pride-campaign-2026/pride-badge.webp" />
|
||||||
|
|
||||||
|
Supporters with this badge will also unlock a special Pride section in the Skin selector in the Modrinth App featuring the incredibly cute Mr. Pack in Pride-themed, exclusive Modrinth skins:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Modrinth+
|
||||||
|
|
||||||
|
Donations of $5 (USD) or more will grant you with free [Modrinth+](https://modrinth.com/plus) benefits for 1 month as an additional thank-you - available to users who do not already have an active Modrinth+ subscription.
|
||||||
|
|
||||||
|
## Pride Collection 2026
|
||||||
|
|
||||||
|
Alongside the fundraiser, we're also launching the [Modrinth Pride Collection!](https://modrinth.com/collection/M4c3ITvd)
|
||||||
|
|
||||||
|
This collection highlights projects made by and for the queer community, including mods, resource packs, modpacks, plugins, and more across Modrinth.
|
||||||
|
|
||||||
|
[**You can submit projects all month long**](https://tally.so/r/GxQBbO)
|
||||||
|
|
||||||
|
Throughout June, we'll be featuring selected projects from the collection across our social media alongside our usual creator spotlights. Pride is about visibility, and we want to use this month to celebrate the LGBTQIA+ creators and communities that help make Modrinth what it is.
|
||||||
@@ -27,6 +27,7 @@ import { article as new_environments } from "./new_environments";
|
|||||||
import { article as new_site_beta } from "./new_site_beta";
|
import { article as new_site_beta } from "./new_site_beta";
|
||||||
import { article as plugins_resource_packs } from "./plugins_resource_packs";
|
import { article as plugins_resource_packs } from "./plugins_resource_packs";
|
||||||
import { article as pride_campaign_2025 } from "./pride_campaign_2025";
|
import { article as pride_campaign_2025 } from "./pride_campaign_2025";
|
||||||
|
import { article as pride_campaign_2026 } from "./pride_campaign_2026";
|
||||||
import { article as redesign } from "./redesign";
|
import { article as redesign } from "./redesign";
|
||||||
import { article as russian_censorship } from "./russian_censorship";
|
import { article as russian_censorship } from "./russian_censorship";
|
||||||
import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
|
import { article as skins_now_in_modrinth_app } from "./skins_now_in_modrinth_app";
|
||||||
@@ -67,6 +68,7 @@ export const articles = [
|
|||||||
new_site_beta,
|
new_site_beta,
|
||||||
plugins_resource_packs,
|
plugins_resource_packs,
|
||||||
pride_campaign_2025,
|
pride_campaign_2025,
|
||||||
|
pride_campaign_2026,
|
||||||
redesign,
|
redesign,
|
||||||
russian_censorship,
|
russian_censorship,
|
||||||
skins_now_in_modrinth_app,
|
skins_now_in_modrinth_app,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||||
|
export const html = `<h2>Happy Pride Month!</h2><p>Pride is a time for celebration, self-expression, and community. Queer individuals are part of Modrinth at every level: across our team, our creator community, and the wider Minecraft community as a whole. We're very proud to be vocal in our support for LGBTQIA+ rights all year round.</p><p>But for many people, Pride is a celebration that comes with great risk. Recent reporting shows there's no shortage of tragedies facing LGBTQIA+ people. Threats, attacks, and even worse happen around the world - <a href="https://www.yahoo.com/news/articles/most-amazing-human-being-ever-024305055.html" rel="noopener nofollow ugc">Juniper Blessing</a> and <a href="https://www.advocate.com/news/crime/black-drag-artist-virginia-murdered" rel="noopener nofollow ugc">Shyyell Diamond Sanchez-McCray</a>, trans rights activists <a href="https://www.theguardian.com/global-development/2026/jan/27/pakistan-transgender-attacks-killings" rel="noopener nofollow ugc">Zehrish Khanzadi and Bindiya Rana</a> surviving gunfire in Pakistan, and <a href="https://en.wikipedia.org/wiki/List_of_acts_of_violence_against_LGBTQ_people" rel="noopener nofollow ugc">many more</a> cases like these are reminders that safety is not guaranteed for some members of our community.</p><p>That's why this Pride Month, Modrinth will be running a fundraiser supporting <a href="https://www.rainbowrailroad.org/" rel="noopener nofollow ugc"><strong>Rainbow Railroad</strong></a>.</p><p>Rainbow Railroad works with LGBTQIA+ people who are facing danger, helping to connect them with support, resources, and pathways to safe countries. Through their <strong>Solidarity in Pride 2026</strong> campaign, they're raising funds to help at-risk LGBTQIA+ people access safety when staying in place is no longer an option.</p><p><a href="https://tiltify.com/+modrinth/modrinth-pride-2026" class="!text-2xl font-semibold">You can support the fundraiser here.</a></p><p><strong>When donating, please enter your <em>Modrinth username</em> as the <em>Tiltify display name</em> so we can apply any eligible rewards to your account!</strong></p><h2>Donation Matching Commitment</h2><p>We want to maximize the impact our community can make.</p><p>To help achieve this, we've committed to matching all donations 1:1 until our fundraising goal of $5,000 has been met.</p><h2>Donor Rewards</h2><p>This year, we will also be providing rewards as a small thank you to everyone who supports the fundraiser. Including a few limited rewards specific to this year's fundraiser!</p><h3>Pride Badge 2026 & Pride Skins</h3><p>Everyone who donates will receive a special Pride 2026 profile badge on Modrinth.</p><img width="204px" height="204px" class="mx-auto mb-4" src="/news/article/pride-campaign-2026/pride-badge.webp"><p>Supporters with this badge will also unlock a special Pride section in the Skin selector in the Modrinth App featuring the incredibly cute Mr. Pack in Pride-themed, exclusive Modrinth skins:</p><p><img src="/news/article/pride-campaign-2026/mrpack-pride-banner.webp" alt="Mr. Pack Pride skins"></p><h3>Modrinth+</h3><p>Donations of $5 (USD) or more will grant you with free <a href="/plus" rel="noopener nofollow ugc">Modrinth+</a> benefits for 1 month as an additional thank-you - available to users who do not already have an active Modrinth+ subscription.</p><h2>Pride Collection 2026</h2><p>Alongside the fundraiser, we're also launching the <a href="/collection/M4c3ITvd" rel="noopener nofollow ugc">Modrinth Pride Collection!</a></p><p>This collection highlights projects made by and for the queer community, including mods, resource packs, modpacks, plugins, and more across Modrinth.</p><p><a href="https://tally.so/r/GxQBbO" rel="noopener nofollow ugc"><strong>You can submit projects all month long</strong></a></p><p>Throughout June, we'll be featuring selected projects from the collection across our social media alongside our usual creator spotlights. Pride is about visibility, and we want to use this month to celebrate the LGBTQIA+ creators and communities that help make Modrinth what it is.</p>`;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// AUTO-GENERATED FILE - DO NOT EDIT
|
||||||
|
export const article = {
|
||||||
|
html: () => import(`./pride_campaign_2026.content`).then(m => m.html),
|
||||||
|
title: "Pride 2026 Fundraiser: Matching up to $5,000",
|
||||||
|
summary: "Celebrating our community and working together to make a difference.",
|
||||||
|
date: "2026-06-01T16:00:00.000Z",
|
||||||
|
slug: "pride-campaign-2026",
|
||||||
|
authors: [],
|
||||||
|
unlisted: false,
|
||||||
|
thumbnail: true,
|
||||||
|
short_title: "Pride 2026 Fundraiser",
|
||||||
|
short_summary: "Celebrating our community and working together to make a difference.",
|
||||||
|
};
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 446 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 528 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 891 KiB |
@@ -49,14 +49,14 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-tooltip="tooltip ?? undefined"
|
v-tooltip="tooltip ?? undefined"
|
||||||
class="skin-button group relative flex items-end justify-center overflow-hidden border border-solid transition-[border-color,box-shadow] duration-200 focus-within:outline focus-within:outline-2 focus-within:outline-offset-2 focus-within:outline-brand"
|
class="skin-button group relative flex items-end justify-center overflow-hidden border border-solid transition-[border-color,box-shadow] duration-200"
|
||||||
:class="[
|
:class="[
|
||||||
selected ? 'skin-button--selected' : '',
|
selected ? 'skin-button--selected' : '',
|
||||||
{ 'skin-button--with-actions': $slots['overlay-buttons'] },
|
{ 'skin-button--with-actions': $slots['overlay-buttons'] },
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
class="absolute inset-0 z-10 cursor-pointer border-none bg-transparent p-0"
|
class="absolute inset-0 z-10 cursor-pointer border-none bg-transparent p-0 focus-visible:outline-none"
|
||||||
:aria-label="tooltip ? `Select ${tooltip}` : 'Select skin'"
|
:aria-label="tooltip ? `Select ${tooltip}` : 'Select skin'"
|
||||||
:aria-pressed="selected"
|
:aria-pressed="selected"
|
||||||
@click="emit('select')"
|
@click="emit('select')"
|
||||||
@@ -151,6 +151,11 @@ watch(
|
|||||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(37, 39, 45, 0.2) 100%);
|
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(37, 39, 45, 0.2) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skin-button:has(:focus-visible) {
|
||||||
|
outline: 2px solid var(--color-brand);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.skin-button:hover,
|
.skin-button:hover,
|
||||||
.skin-button:focus-within,
|
.skin-button:focus-within,
|
||||||
.skin-button--with-actions:hover,
|
.skin-button--with-actions:hover,
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export const INTERACT_ANIMATION_NAME = 'interact'
|
|||||||
const INTERACT_VISIBLE_DURATION_SECONDS = 0.5
|
const INTERACT_VISIBLE_DURATION_SECONDS = 0.5
|
||||||
const CLICK_IMPULSE_MAX_ENERGY = 5
|
const CLICK_IMPULSE_MAX_ENERGY = 5
|
||||||
const CLICK_IMPULSE_ENERGY_PER_CLICK = 1
|
const CLICK_IMPULSE_ENERGY_PER_CLICK = 1
|
||||||
const CLICK_IMPULSE_DECAY_PER_SECOND = 6
|
const DAMAGE_FLASH_MIN_CLICKS_PER_SECOND = 2
|
||||||
|
const CLICK_IMPULSE_DECAY_PER_SECOND =
|
||||||
|
DAMAGE_FLASH_MIN_CLICKS_PER_SECOND * CLICK_IMPULSE_ENERGY_PER_CLICK
|
||||||
const CLICK_IMPULSE_BASE_SPEED = 18
|
const CLICK_IMPULSE_BASE_SPEED = 18
|
||||||
const CLICK_IMPULSE_SPEED_BOOST = 7
|
const CLICK_IMPULSE_SPEED_BOOST = 7
|
||||||
const CLICK_IMPULSE_OFFSET_X = 0.035
|
const CLICK_IMPULSE_OFFSET_X = 0.035
|
||||||
|
|||||||
Reference in New Issue
Block a user