You've already forked AstralRinth
forked from didirus/AstralRinth
Merge commit '7fa442fb28a2b9156690ff147206275163e7aec8' into beta
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
**/.nuxt
|
||||
**/dist
|
||||
**/.output
|
||||
**/.data
|
||||
src/generated/**
|
||||
src/locales/**
|
||||
src/public/news/feed
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
project_id: 518556
|
||||
preserve_hierarchy: true
|
||||
commit_message: '[ci skip]'
|
||||
|
||||
files:
|
||||
- source: /locales/en-US/*
|
||||
dest: /%original_file_name%
|
||||
translation: /locales/%locale%/%original_file_name%
|
||||
@@ -1,6 +1,7 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
import { match as matchLocale } from '@formatjs/intl-localematcher'
|
||||
import serverSidedVue from '@vitejs/plugin-vue'
|
||||
import { consola } from 'consola'
|
||||
import { promises as fs } from 'fs'
|
||||
import { globIterate } from 'glob'
|
||||
@@ -111,6 +112,23 @@ export default defineNuxtConfig({
|
||||
],
|
||||
},
|
||||
hooks: {
|
||||
async 'nitro:config'(nitroConfig) {
|
||||
const emailTemplates = Object.keys(
|
||||
await import('./src/templates/emails/index.ts').then((m) => m.default),
|
||||
)
|
||||
const docTemplates = Object.keys(
|
||||
await import('./src/templates/docs/index.ts').then((m) => m.default),
|
||||
)
|
||||
|
||||
nitroConfig.prerender = nitroConfig.prerender || {}
|
||||
nitroConfig.prerender.routes = nitroConfig.prerender.routes || []
|
||||
for (const template of emailTemplates) {
|
||||
nitroConfig.prerender.routes.push(`/_internal/templates/email/${template}`)
|
||||
}
|
||||
for (const template of docTemplates) {
|
||||
nitroConfig.prerender.routes.push(`/_internal/templates/doc/${template}`)
|
||||
}
|
||||
},
|
||||
async 'build:before'() {
|
||||
// 30 minutes
|
||||
const TTL = 30 * 60 * 1000
|
||||
@@ -435,6 +453,10 @@ export default defineNuxtConfig({
|
||||
},
|
||||
nitro: {
|
||||
moduleSideEffects: ['@vintl/compact-number/locale-data'],
|
||||
rollupConfig: {
|
||||
// @ts-expect-error it's not infinite.
|
||||
plugins: [serverSidedVue()],
|
||||
},
|
||||
},
|
||||
devtools: {
|
||||
enabled: true,
|
||||
@@ -453,6 +475,23 @@ export default defineNuxtConfig({
|
||||
'Critical-CH': 'Sec-CH-Prefers-Color-Scheme',
|
||||
},
|
||||
},
|
||||
'/email/**': {
|
||||
redirect: '/_internal/templates/email/**',
|
||||
},
|
||||
'/_internal/templates/email/**': {
|
||||
prerender: true,
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
},
|
||||
'/_internal/templates/doc/**': {
|
||||
prerender: true,
|
||||
headers: {
|
||||
'Content-Type': 'text/html',
|
||||
'Cache-Control': 'public, max-age=3600',
|
||||
},
|
||||
},
|
||||
},
|
||||
compatibilityDate: '2024-07-03',
|
||||
telemetry: false,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"postinstall": "nuxi prepare",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore '**/*.d.ts' --ignore 'node_modules' --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"intl:extract": "formatjs extract \"{,src/components,src/composables,src/layouts,src/middleware,src/modules,src/pages,src/plugins,src/utils}/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" \"src/error.vue\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"test": "nuxi build"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -45,6 +45,9 @@
|
||||
"@pinia/nuxt": "^0.5.1",
|
||||
"@types/three": "^0.172.0",
|
||||
"@vintl/vintl": "^4.4.1",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue-email/components": "^0.0.21",
|
||||
"@vue-email/render": "^0.0.9",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"ace-builds": "^1.36.2",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
|
||||
@@ -7,10 +7,30 @@
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { NotificationPanel, provideNotificationManager } from '@modrinth/ui'
|
||||
import { provideApi } from '@modrinth/ui/src/providers/api.ts'
|
||||
import { RestModrinthApi } from '@modrinth/utils'
|
||||
|
||||
import ModrinthLoadingIndicator from '~/components/ui/modrinth-loading-indicator.ts'
|
||||
|
||||
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
|
||||
|
||||
provideNotificationManager(new FrontendNotificationManager())
|
||||
|
||||
provideApi(
|
||||
new RestModrinthApi((url: string, options?: object) => {
|
||||
const match = url.match(/^\/v(\d+)\/(.+)$/)
|
||||
|
||||
if (match) {
|
||||
const apiVersion = Number(match[1])
|
||||
const path = match[2]
|
||||
|
||||
return useBaseFetch(path, {
|
||||
...options,
|
||||
apiVersion,
|
||||
}) as Promise<Response>
|
||||
} else {
|
||||
throw new Error('Invalid format')
|
||||
}
|
||||
}),
|
||||
)
|
||||
</script>
|
||||
|
||||
11
apps/frontend/src/assets/images/illustrations/medal_icon.svg
Normal file
11
apps/frontend/src/assets/images/illustrations/medal_icon.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg viewBox="7 18 57 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M22.562 46.8959L31.189 42.2804C31.8922 41.9026 31.8873 40.9333 31.1763 40.558L22.426 35.9147C21.8463 35.6082 21.1377 36.0016 21.1373 36.6321L21.1319 46.0958C21.1315 46.7967 21.9244 47.2404 22.562 46.8985L22.562 46.8959Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M48.7804 47.0911L40.1588 42.3472C39.4561 41.9589 39.4621 40.9896 40.1735 40.625L48.9288 36.112C49.5092 35.8141 50.2172 36.218 50.2168 36.8485L50.2114 46.3122C50.211 47.0131 49.4178 47.445 48.7804 47.0937L48.7804 47.0911Z"
|
||||
fill="currentColor" />
|
||||
<path
|
||||
d="M62.1735 23.588L54.919 19.3778C54.3937 19.0742 53.7374 19.0615 53.2066 19.3468L36.5152 28.3219C35.9844 28.6072 35.3333 28.6024 34.8028 28.3092L18.1193 19.0882C17.5888 18.7951 16.9323 18.7954 16.4069 19.0937L9.14477 23.1933C8.62214 23.4891 8.30179 24.0235 8.30145 24.6046L8.29042 43.8064C8.2901 44.3589 8.58797 44.877 9.07744 45.1777L15.2929 48.9763C15.4214 49.0554 15.6784 49.2085 15.971 49.3853C16.5917 49.7573 17.3935 49.3359 17.3884 48.6401C17.3886 48.3431 17.3887 48.0721 17.3888 47.9053L17.4315 30.8117C17.432 29.9049 18.4636 29.3471 19.2894 29.8041L34.7506 38.3879C35.3084 38.697 35.9922 38.7021 36.5477 38.4013L52.0186 30.0477C52.845 29.603 53.8731 30.1762 53.8753 31.083L53.8983 48.177C53.8982 48.3282 53.9008 48.6044 53.9033 48.9171C53.9084 49.6077 54.707 50.0358 55.3225 49.6729C55.599 49.5108 55.8509 49.3642 55.9931 49.2792L62.2128 45.5732C62.7025 45.2798 63.0011 44.7687 63.0014 44.2137L63.0125 25.0118C63.0128 24.4307 62.693 23.8889 62.1707 23.588L62.1735 23.588Z"
|
||||
fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,5 +1,4 @@
|
||||
html {
|
||||
@extend .dark-mode;
|
||||
--dark-color-text: #b0bac5;
|
||||
--dark-color-text-dark: #ecf9fb;
|
||||
--color-text-secondary: var(--color-icon);
|
||||
@@ -16,7 +15,6 @@ html {
|
||||
--gap-24: calc(3 * var(--gap-8));
|
||||
|
||||
--radius-card: 1rem;
|
||||
--radius-max: 999999999px;
|
||||
|
||||
--radius-btn: 0.75rem;
|
||||
--radius-btn-large: 1rem;
|
||||
@@ -42,28 +40,19 @@ html {
|
||||
}
|
||||
|
||||
.light-mode {
|
||||
--color-secondary: #6b7280;
|
||||
--color-icon: var(--color-secondary);
|
||||
--color-text: hsl(221, 39%, 11%);
|
||||
--color-text-inactive: hsl(215, 14%, 34%);
|
||||
--color-text-dark: #1a202c;
|
||||
--color-heading: #2c313d;
|
||||
--color-bg: #e5e7eb;
|
||||
--color-raised-bg: #ffffff;
|
||||
--color-divider: hsl(220, 13%, 91%);
|
||||
--color-divider-dark: #c8cdd3;
|
||||
|
||||
--color-text-inverted: var(--color-bg);
|
||||
--color-bg-inverted: var(--color-text);
|
||||
|
||||
--color-brand: var(--color-green);
|
||||
--color-brand-highlight: rgba(0, 175, 92, 0.25);
|
||||
--color-brand-shadow: rgba(0, 175, 92, 0.7);
|
||||
--color-brand-inverted: #ffffff;
|
||||
|
||||
--tab-underline-hovered: #e2e8f0;
|
||||
|
||||
--color-button-bg: hsl(220, 13%, 91%);
|
||||
--color-button-text: var(--color-text-dark);
|
||||
--color-button-bg-hover: #d9dce0;
|
||||
--color-button-text-hover: #1b1e24;
|
||||
@@ -80,17 +69,9 @@ html {
|
||||
|
||||
--color-kbd-shadow: rgba(0, 0, 0, 0.25);
|
||||
|
||||
--color-ad: #d6e6f9;
|
||||
--color-ad-raised: #b1c8e4;
|
||||
--color-ad-contrast: var(--color-text);
|
||||
--color-ad-highlight: #088cdb;
|
||||
|
||||
--color-grey-link: var(--color-text);
|
||||
--color-grey-link-hover: var(--color-heading);
|
||||
--color-grey-link-active: var(--color-text-dark);
|
||||
--color-link: #0d60bb;
|
||||
--color-link-hover: #1a76e7;
|
||||
--color-link-active: #146fd7;
|
||||
|
||||
--color-warning-bg: hsl(355, 70%, 88%);
|
||||
--color-warning-text: hsl(342, 70%, 35%);
|
||||
@@ -110,21 +91,6 @@ html {
|
||||
--color-table-border: #dfe2e5;
|
||||
--color-table-alternate-row: #f2f4f7;
|
||||
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 2px hsla(221, 39%, 11%, 0.15);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised:
|
||||
0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
|
||||
1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12),
|
||||
4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09);
|
||||
--shadow-floating:
|
||||
hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
|
||||
|
||||
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing-light.webp');
|
||||
--landing-maze-gradient-bg: url('https://cdn.modrinth.com/landing-new/landing-lower-light.webp');
|
||||
--landing-maze-outer-bg: linear-gradient(180deg, #f0f0f0 0%, #ffffff 100%);
|
||||
@@ -165,6 +131,15 @@ html {
|
||||
|
||||
--landing-raw-bg: #fff;
|
||||
|
||||
--medal-promotion-bg: #fff;
|
||||
--medal-promotion-bg-orange: #48aaff;
|
||||
--medal-promotion-text-orange: #156db8;
|
||||
--medal-promotion-bg-gradient: linear-gradient(
|
||||
90deg,
|
||||
rgba(152, 207, 255, 0.2) 20%,
|
||||
rgba(152, 207, 255, 0.1) 100%
|
||||
);
|
||||
|
||||
--banner-error-bg: #fee2e2;
|
||||
--banner-error-text: #991b1b;
|
||||
--banner-error-border: #ef4444;
|
||||
@@ -182,28 +157,19 @@ html {
|
||||
.dark-mode,
|
||||
.oled-mode,
|
||||
.retro-mode {
|
||||
--color-secondary: #96a2b0;
|
||||
--color-icon: var(--color-secondary);
|
||||
--color-text: var(--dark-color-text);
|
||||
--color-text-inactive: #929aa3;
|
||||
--color-text-dark: var(--dark-color-text-dark);
|
||||
--color-heading: #c4cfdd;
|
||||
--color-bg: #16181c;
|
||||
--color-raised-bg: #26292f;
|
||||
--color-divider: #474b54;
|
||||
--color-divider-dark: #646c75;
|
||||
|
||||
--color-text-inverted: var(--color-bg);
|
||||
--color-bg-inverted: var(--color-text);
|
||||
|
||||
--color-brand: var(--color-green);
|
||||
--color-brand-highlight: rgba(27, 217, 106, 0.25);
|
||||
--color-brand-shadow: rgba(27, 217, 106, 0.7);
|
||||
--color-brand-inverted: #000;
|
||||
|
||||
--tab-underline-hovered: #414146;
|
||||
|
||||
--color-button-bg: hsl(222, 13%, 30%);
|
||||
--color-button-text: var(--color-text);
|
||||
--color-button-bg-hover: #494f58;
|
||||
--color-button-text-hover: #ffffff;
|
||||
@@ -220,15 +186,6 @@ html {
|
||||
|
||||
--color-kbd-shadow: rgba(0, 0, 0, 0.35);
|
||||
|
||||
--color-ad: #1f324a;
|
||||
--color-ad-raised: #2e4057;
|
||||
--color-ad-contrast: var(--color-text);
|
||||
--color-ad-highlight: #088cdb;
|
||||
|
||||
--color-link: #74b6f3;
|
||||
--color-link-hover: #92c0f5;
|
||||
--color-link-active: #b5d5fd;
|
||||
|
||||
--color-warning-bg: hsl(355, 70%, 20%);
|
||||
--color-warning-text: hsl(342, 70%, 75%);
|
||||
|
||||
@@ -247,18 +204,6 @@ html {
|
||||
--color-table-border: #4f5864;
|
||||
--color-table-alternate-row: #202228;
|
||||
|
||||
--shadow-inset-lg: inset 0px -2px 2px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-inset: inset 0px -2px 2px hsla(221, 39%, 11%, 0.05);
|
||||
--shadow-inset-sm: inset 0px -1px 1px hsla(221, 39%, 11%, 0.25);
|
||||
|
||||
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
|
||||
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
|
||||
--shadow-floating:
|
||||
hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
|
||||
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
|
||||
|
||||
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
|
||||
|
||||
--landing-maze-bg: url('https://cdn.modrinth.com/landing-new/landing.webp');
|
||||
--landing-maze-gradient-bg:
|
||||
linear-gradient(0deg, #31375f 0%, rgba(8, 14, 55, 0) 100%),
|
||||
@@ -301,34 +246,38 @@ html {
|
||||
|
||||
--landing-raw-bg: #000;
|
||||
|
||||
--medal-promotion-bg: #000;
|
||||
--medal-promotion-bg-orange: rgba(208, 246, 255, 0.25);
|
||||
--medal-promotion-text-orange: #42abff;
|
||||
--medal-promotion-bg-gradient: linear-gradient(
|
||||
90deg,
|
||||
rgba(66, 170, 255, 0.15),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
|
||||
--hover-filter: brightness(120%);
|
||||
--active-filter: brightness(140%);
|
||||
|
||||
--banner-error-bg: #4c1515;
|
||||
--banner-error-bg: #45222c;
|
||||
--banner-error-text: #fee2e2;
|
||||
--banner-error-border: #7f1d1d;
|
||||
--banner-error-border: var(--color-red);
|
||||
|
||||
--banner-warning-bg: #4a2a0a;
|
||||
--banner-warning-text: #ffe6c0;
|
||||
--banner-warning-border: #b54708;
|
||||
--banner-warning-bg: #453425;
|
||||
--banner-warning-text: #e4d9ca;
|
||||
--banner-warning-border: var(--color-orange);
|
||||
|
||||
--banner-info-bg: #1e2a44;
|
||||
--banner-info-bg: #28374b;
|
||||
--banner-info-text: #dbeafe;
|
||||
--banner-info-border: #2563eb;
|
||||
--banner-info-border: var(--color-blue);
|
||||
}
|
||||
|
||||
.oled-mode {
|
||||
--color-bg: #000000;
|
||||
--color-raised-bg: #101013;
|
||||
|
||||
--color-button-bg: #222329;
|
||||
--color-button-bg-hover: #2d2d32;
|
||||
--color-button-bg-active: #3c3c40;
|
||||
--color-table-alternate-row: #19191c;
|
||||
--color-divider-dark: #2c3134;
|
||||
|
||||
--color-warning-banner-bg: hsl(0, 45%, 11%);
|
||||
--color-ad: #0d1828;
|
||||
}
|
||||
|
||||
.retro-mode {
|
||||
@@ -389,7 +338,7 @@ body {
|
||||
|
||||
--size-navbar-height: 3.5rem;
|
||||
--size-mobile-navbar-height: 3.5rem;
|
||||
--size-mobile-navbar-height-expanded: 13.75rem;
|
||||
--size-mobile-navbar-height-expanded: 26.5rem;
|
||||
|
||||
--spacing-card-lg: 1.5rem;
|
||||
--spacing-card-bg: 1rem;
|
||||
@@ -418,16 +367,8 @@ body {
|
||||
--font-weight-heading: var(--font-weight-extrabold);
|
||||
--font-weight-title: var(--font-weight-extrabold);
|
||||
|
||||
@media screen and (min-width: 320px) {
|
||||
--size-mobile-navbar-height-expanded: 11.5rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 432px) {
|
||||
--size-mobile-navbar-height-expanded: 9.25rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 765px) {
|
||||
--size-mobile-navbar-height-expanded: 7rem;
|
||||
@media screen and (min-width: 354px) {
|
||||
--size-mobile-navbar-height-expanded: 15.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +472,8 @@ kbd {
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
[tabindex='0']:focus-visible {
|
||||
[tabindex='0']:focus-visible,
|
||||
[type='button']:focus-visible {
|
||||
outline: 0.25rem solid #ea80ff;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -42,13 +42,14 @@
|
||||
padding: 0 1.5rem;
|
||||
|
||||
grid-template:
|
||||
'header'
|
||||
'sidebar'
|
||||
'content'
|
||||
'info'
|
||||
/ 100%;
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
margin-top: var(--spacing-card-md);
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.normal-page__sidebar {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
<template>
|
||||
<div class="ad-parent relative mb-3 flex w-full justify-center rounded-2xl bg-bg-raised">
|
||||
<nuxt-link
|
||||
to="/servers"
|
||||
:to="flags.enableMedalPromotion ? '/servers?plan&ref=medal' : '/servers'"
|
||||
class="flex max-h-[250px] min-h-[250px] min-w-[300px] max-w-[300px] flex-col gap-4 rounded-[inherit]"
|
||||
>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-light.webp"
|
||||
:src="`https://cdn-raw.modrinth.com/${flags.enableMedalPromotion ? 'medal-modrinth-servers' : 'modrinth-servers-placeholder'}-light.webp`"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="light-image hidden rounded-[inherit]"
|
||||
/>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/modrinth-servers-placeholder-dark.webp"
|
||||
:src="`https://cdn-raw.modrinth.com/${flags.enableMedalPromotion ? 'medal-modrinth-servers' : 'modrinth-servers-placeholder'}-dark.webp`"
|
||||
alt="Host your next server with Modrinth Servers"
|
||||
class="dark-image rounded-[inherit]"
|
||||
/>
|
||||
@@ -23,6 +23,8 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
useHead({
|
||||
script: [
|
||||
// {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Creating a collection">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="name">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Name
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="name"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
:placeholder="`Enter collection name...`"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="additional-information" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast"> Summary </span>
|
||||
<span>A sentence or two that describes your collection.</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea id="additional-information" v-model="description" maxlength="256" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 max-w-[30rem]">
|
||||
Your new collection will be created as a public collection with
|
||||
{{ projectIds.length > 0 ? projectIds.length : 'no' }}
|
||||
{{ projectIds.length !== 1 ? 'projects' : 'project' }}.
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="create">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
Create collection
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button @click="modal.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
Cancel
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const router = useNativeRouter()
|
||||
|
||||
const name = ref('')
|
||||
const description = ref('')
|
||||
|
||||
const modal = ref()
|
||||
|
||||
const props = defineProps({
|
||||
projectIds: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
async function create() {
|
||||
startLoading()
|
||||
try {
|
||||
const result = await useBaseFetch('collection', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim() || undefined,
|
||||
projects: props.projectIds,
|
||||
},
|
||||
apiVersion: 3,
|
||||
})
|
||||
|
||||
await initUserCollections()
|
||||
|
||||
modal.value.hide()
|
||||
await router.push(`/collection/${result.id}`)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: err?.data?.description || err?.message || err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
function show(event) {
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
modal.value.show(event)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-creation {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: var(--gap-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,94 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="drop_area"
|
||||
class="drop-area"
|
||||
@drop.stop.prevent="
|
||||
(event) => {
|
||||
$refs.drop_area.style.visibility = 'hidden'
|
||||
|
||||
if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
|
||||
$emit('change', event.dataTransfer.files)
|
||||
}
|
||||
}
|
||||
"
|
||||
@dragenter.prevent="allowDrag"
|
||||
@dragover.prevent="allowDrag"
|
||||
@dragleave.prevent="$refs.drop_area.style.visibility = 'hidden'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
fileAllowed: false,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('dragenter', this.allowDrag)
|
||||
},
|
||||
methods: {
|
||||
allowDrag(event) {
|
||||
const file = event.dataTransfer?.items[0]
|
||||
|
||||
if (
|
||||
file &&
|
||||
this.accept
|
||||
.split(',')
|
||||
.reduce((acc, t) => acc || file.type.startsWith(t) || file.type === t || t === '*', false)
|
||||
) {
|
||||
this.fileAllowed = true
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
event.preventDefault()
|
||||
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = 'visible'
|
||||
}
|
||||
} else {
|
||||
this.fileAllowed = false
|
||||
|
||||
if (this.$refs.drop_area) {
|
||||
this.$refs.drop_area.style.visibility = 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.drop-area {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
visibility: hidden;
|
||||
background-color: hsla(0, 0%, 0%, 0.5);
|
||||
transition:
|
||||
visibility 0.2s ease-in-out,
|
||||
background-color 0.1s ease-in-out;
|
||||
display: flex;
|
||||
|
||||
&::before {
|
||||
--indent: 4rem;
|
||||
|
||||
content: ' ';
|
||||
position: relative;
|
||||
top: var(--indent);
|
||||
left: var(--indent);
|
||||
width: calc(100% - (2 * var(--indent)));
|
||||
height: calc(100% - (2 * var(--indent)));
|
||||
border-radius: 1rem;
|
||||
border: 0.25rem dashed var(--color-button-bg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,18 @@
|
||||
<template>
|
||||
<NuxtLink v-if="link !== null" class="nav-link button-base" :to="link">
|
||||
<div class="nav-content">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="beta" class="beta-badge">BETA</span>
|
||||
<span v-if="chevron" class="chevron"><ChevronRightIcon /></span>
|
||||
</div>
|
||||
<NuxtLink v-if="link !== null" :to="link" class="nav-item">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
|
||||
badge
|
||||
}}</span>
|
||||
<span v-if="chevron" class="ml-auto"><ChevronRightIcon /></span>
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-else-if="action"
|
||||
class="nav-link button-base"
|
||||
:class="{ 'danger-button': danger }"
|
||||
@click="action"
|
||||
>
|
||||
<span class="nav-content">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="beta" class="beta-badge">BETA</span>
|
||||
</span>
|
||||
<button v-else-if="action" class="nav-item" :class="{ 'danger-button': danger }" @click="action">
|
||||
<slot />
|
||||
<span>{{ label }}</span>
|
||||
<span v-if="badge" class="rounded-full bg-brand-highlight px-2 text-sm font-bold text-brand">{{
|
||||
badge
|
||||
}}</span>
|
||||
</button>
|
||||
<span v-else>i forgor 💀</span>
|
||||
</template>
|
||||
@@ -42,9 +37,9 @@ export default {
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
beta: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
badge: {
|
||||
default: null,
|
||||
type: String,
|
||||
},
|
||||
chevron: {
|
||||
default: false,
|
||||
@@ -59,58 +54,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.nav-link {
|
||||
font-weight: var(--font-weight-bold);
|
||||
background-color: transparent;
|
||||
color: var(--text-color);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.25rem;
|
||||
box-shadow: none;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
outline: none;
|
||||
.nav-item {
|
||||
@apply flex w-full cursor-pointer items-center gap-2 text-nowrap rounded-xl border-none bg-transparent px-4 py-2 text-left font-semibold text-button-text transition-all hover:bg-button-bg hover:text-contrast active:scale-[0.97];
|
||||
}
|
||||
|
||||
:where(.nav-link) {
|
||||
--text-color: var(--color-text);
|
||||
--background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--size-rounded-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
flex-grow: 1;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
.nav-content {
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.router-link-exact-active {
|
||||
outline: 2px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
|
||||
.nav-content {
|
||||
color: var(--color-button-text-active);
|
||||
background-color: var(--color-button-bg);
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.beta-badge {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chevron {
|
||||
margin-left: auto;
|
||||
}
|
||||
.router-link-exact-active.nav-item {
|
||||
@apply bg-button-bgSelected text-button-textSelected;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
<span class="version-info">
|
||||
for
|
||||
<Categories
|
||||
:categories="notif.extra_data.version.loaders"
|
||||
:categories="getLoaderCategories(notif.extra_data.version)"
|
||||
:type="notif.extra_data.project.project_type"
|
||||
class="categories"
|
||||
/>
|
||||
@@ -331,7 +331,22 @@ import {
|
||||
VersionIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { injectNotificationManager, useRelativeTime } from '@modrinth/ui'
|
||||
import {
|
||||
Avatar,
|
||||
Categories,
|
||||
CopyCode,
|
||||
DoubleIcon,
|
||||
injectNotificationManager,
|
||||
ProjectStatusBadge,
|
||||
useRelativeTime,
|
||||
} from '@modrinth/ui'
|
||||
import { getUserLink, renderString } from '@modrinth/utils'
|
||||
|
||||
import { markAsRead } from '~/helpers/platform-notifications'
|
||||
import { getProjectLink, getVersionLink } from '~/helpers/projects'
|
||||
import { acceptTeamInvite, removeSelfFromTeam } from '~/helpers/teams'
|
||||
|
||||
import ThreadSummary from './thread/ThreadSummary.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const emit = defineEmits(['update:notifications'])
|
||||
@@ -441,6 +456,12 @@ function getMessages() {
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
function getLoaderCategories(ver) {
|
||||
return tags.value.loaders.filter((loader) => {
|
||||
return ver?.loaders?.includes(loader.name)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -113,6 +113,7 @@ import { BoxIcon, SettingsIcon, TransferIcon, XIcon } from '@modrinth/assets'
|
||||
import { Avatar, Button, Checkbox, CopyCode, Modal } from '@modrinth/ui'
|
||||
import { formatProjectType } from '@modrinth/utils'
|
||||
|
||||
const EDIT_DETAILS = 1 << 2
|
||||
const modalOpen = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
@@ -478,6 +478,7 @@ export default {
|
||||
margin-block: 0;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.tags {
|
||||
|
||||
@@ -20,113 +20,33 @@
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
<ModerationProjectNags
|
||||
v-if="
|
||||
currentMember &&
|
||||
visibleNags.length > 0 &&
|
||||
(project.status === 'draft' || tags.rejectedStatuses.includes(project.status))
|
||||
(currentMember && project.status === 'draft') ||
|
||||
tags.rejectedStatuses.includes(project.status)
|
||||
"
|
||||
class="universal-card my-4"
|
||||
>
|
||||
<div class="flex max-w-full flex-wrap items-center gap-x-6 gap-y-4">
|
||||
<div class="flex flex-auto flex-wrap items-center gap-x-6 gap-y-4">
|
||||
<h2 class="my-0 mr-auto">
|
||||
{{ getFormattedMessage(messages.publishingChecklist) }}
|
||||
</h2>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<AsteriskIcon class="size-4 text-red" />
|
||||
<span class="text-secondary">{{ getFormattedMessage(messages.required) }}</span>
|
||||
</div>
|
||||
|
|
||||
<div class="flex items-center gap-1">
|
||||
<TriangleAlertIcon class="size-4 text-orange" />
|
||||
<span class="text-secondary">{{ getFormattedMessage(messages.warning) }}</span>
|
||||
</div>
|
||||
|
|
||||
<div class="flex items-center gap-1">
|
||||
<LightBulbIcon class="size-4 text-purple" />
|
||||
<span class="text-secondary">{{ getFormattedMessage(messages.suggestion) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<ButtonStyled circular>
|
||||
<button :class="!collapsed && '[&>svg]:rotate-180'" @click="handleToggleCollapsed()">
|
||||
<DropdownIcon class="duration-250 transition-transform ease-in-out" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="grid-display width-16 mt-4">
|
||||
<div v-for="nag in visibleNags" :key="nag.id" class="grid-display__item">
|
||||
<span class="flex items-center gap-2 font-semibold">
|
||||
<component
|
||||
:is="nag.icon || getDefaultIcon(nag.status)"
|
||||
v-tooltip="getStatusTooltip(nag.status)"
|
||||
:class="[
|
||||
'size-4',
|
||||
nag.status === 'required' && 'text-red',
|
||||
nag.status === 'warning' && 'text-orange',
|
||||
nag.status === 'suggestion' && 'text-purple',
|
||||
]"
|
||||
:aria-label="getStatusTooltip(nag.status)"
|
||||
/>
|
||||
{{ getFormattedMessage(nag.title) }}
|
||||
</span>
|
||||
{{ getNagDescription(nag) }}
|
||||
<NuxtLink
|
||||
v-if="nag.link && shouldShowLink(nag)"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
||||
nag.link.path
|
||||
}`"
|
||||
class="goto-link"
|
||||
>
|
||||
{{ getFormattedMessage(nag.link.title) }}
|
||||
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
|
||||
</NuxtLink>
|
||||
<ButtonStyled
|
||||
v-if="nag.status === 'special-submit-action' && nag.id === 'submit-for-review'"
|
||||
color="orange"
|
||||
@click="submitForReview"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
!canSubmitForReview ? getFormattedMessage(messages.submitChecklistTooltip) : undefined
|
||||
"
|
||||
:disabled="!canSubmitForReview"
|
||||
>
|
||||
<SendIcon />
|
||||
{{ getFormattedMessage(messages.submitForReview) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:project="project"
|
||||
:versions="versions"
|
||||
:current-member="currentMember"
|
||||
:collapsed="collapsed"
|
||||
:route-name="routeName"
|
||||
:tags="tags"
|
||||
@toggle-collapsed="handleToggleCollapsed"
|
||||
@set-processing="handleSetProcessing"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AsteriskIcon,
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DropdownIcon,
|
||||
LightBulbIcon,
|
||||
ScaleIcon,
|
||||
SendIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { Nag, NagContext, NagStatus } from '@modrinth/moderation'
|
||||
import { nags } from '@modrinth/moderation'
|
||||
import { CheckIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager } from '@modrinth/ui'
|
||||
import type { Project, User, Version } from '@modrinth/utils'
|
||||
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { acceptTeamInvite, removeTeamMember } from '~/helpers/teams.js'
|
||||
|
||||
import ModerationProjectNags from './moderation/ModerationProjectNags.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
interface Tags {
|
||||
@@ -167,7 +87,8 @@ const messages = defineMessages({
|
||||
},
|
||||
invitationWithRole: {
|
||||
id: 'project-member-header.invitation-with-role',
|
||||
defaultMessage: "You've been invited be a member of this project with the role of '{role}'.",
|
||||
defaultMessage:
|
||||
"You've been invited to be a member of this project with the role of ''{role}''.",
|
||||
},
|
||||
invitationNoRole: {
|
||||
id: 'project-member-header.invitation-no-role',
|
||||
@@ -182,48 +103,6 @@ const messages = defineMessages({
|
||||
id: 'project-member-header.decline',
|
||||
defaultMessage: 'Decline',
|
||||
},
|
||||
publishingChecklist: {
|
||||
id: 'project-member-header.publishing-checklist',
|
||||
defaultMessage: 'Publishing checklist',
|
||||
},
|
||||
submitForReview: {
|
||||
id: 'project-member-header.submit-for-review',
|
||||
defaultMessage: 'Submit for review',
|
||||
},
|
||||
submitForReviewDesc: {
|
||||
id: 'project-member-header.submit-for-review-desc',
|
||||
defaultMessage:
|
||||
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||
},
|
||||
resubmitForReview: {
|
||||
id: 'project-member-header.resubmit-for-review',
|
||||
defaultMessage: 'Resubmit for review',
|
||||
},
|
||||
resubmitForReviewDesc: {
|
||||
id: 'project-member-header.resubmit-for-review-desc',
|
||||
defaultMessage:
|
||||
"Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
|
||||
},
|
||||
showKey: {
|
||||
id: 'project-member-header.show-key',
|
||||
defaultMessage: 'Toggle key',
|
||||
},
|
||||
keyTitle: {
|
||||
id: 'project-member-header.key-title',
|
||||
defaultMessage: 'Status Key',
|
||||
},
|
||||
action: {
|
||||
id: 'project-member-header.action',
|
||||
defaultMessage: 'Action',
|
||||
},
|
||||
visitModerationPage: {
|
||||
id: 'project-member-header.visit-moderation-page',
|
||||
defaultMessage: 'Visit moderation page',
|
||||
},
|
||||
submitChecklistTooltip: {
|
||||
id: 'project-member-header.submit-checklist-tooltip',
|
||||
defaultMessage: 'You must complete the required steps in the publishing checklist!',
|
||||
},
|
||||
successJoin: {
|
||||
id: 'project-member-header.success-join',
|
||||
defaultMessage: 'You have joined the project team',
|
||||
@@ -248,29 +127,10 @@ const messages = defineMessages({
|
||||
id: 'project-member-header.error',
|
||||
defaultMessage: 'Error',
|
||||
},
|
||||
required: {
|
||||
id: 'project-member-header.required',
|
||||
defaultMessage: 'Required',
|
||||
},
|
||||
warning: {
|
||||
id: 'project-member-header.warning',
|
||||
defaultMessage: 'Warning',
|
||||
},
|
||||
suggestion: {
|
||||
id: 'project-member-header.suggestion',
|
||||
defaultMessage: 'Suggestion',
|
||||
},
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
function getNagDescription(nag: Nag): string {
|
||||
if (typeof nag.description === 'function') {
|
||||
return nag.description(nagContext.value)
|
||||
}
|
||||
return formatMessage(nag.description)
|
||||
}
|
||||
|
||||
function getFormattedMessage(message: string | MessageDescriptor): string {
|
||||
if (typeof message === 'string') {
|
||||
return message
|
||||
@@ -296,108 +156,6 @@ const emit = defineEmits<{
|
||||
setProcessing: [processing: boolean]
|
||||
}>()
|
||||
|
||||
const nagContext = computed<NagContext>(() => ({
|
||||
project: props.project,
|
||||
versions: props.versions,
|
||||
currentMember: props.currentMember as User,
|
||||
currentRoute: props.routeName,
|
||||
tags: props.tags,
|
||||
submitProject: submitForReview,
|
||||
}))
|
||||
|
||||
const canSubmitForReview = computed(() => {
|
||||
return (
|
||||
applicableNags.value.filter((nag) => nag.status === 'required' && !isNagComplete(nag))
|
||||
.length === 0
|
||||
)
|
||||
})
|
||||
|
||||
async function submitForReview() {
|
||||
if (canSubmitForReview.value) {
|
||||
await handleSetProcessing(true)
|
||||
}
|
||||
}
|
||||
|
||||
const applicableNags = computed<Nag[]>(() => {
|
||||
return nags.filter((nag) => {
|
||||
return nag.shouldShow(nagContext.value)
|
||||
})
|
||||
})
|
||||
|
||||
function isNagComplete(nag: Nag): boolean {
|
||||
const context = nagContext.value
|
||||
return !nag.shouldShow(context)
|
||||
}
|
||||
|
||||
const visibleNags = computed<Nag[]>(() => {
|
||||
const finalNags = applicableNags.value.filter((nag) => !isNagComplete(nag))
|
||||
|
||||
if (props.project.status === 'draft') {
|
||||
finalNags.push({
|
||||
id: 'submit-for-review',
|
||||
title: messages.submitForReview,
|
||||
description: () => formatMessage(messages.submitForReviewDesc),
|
||||
status: 'special-submit-action',
|
||||
shouldShow: (ctx) => ctx.project.status === 'draft',
|
||||
})
|
||||
}
|
||||
|
||||
if (props.tags.rejectedStatuses.includes(props.project.status)) {
|
||||
finalNags.push({
|
||||
id: 'resubmit-for-review',
|
||||
title: messages.resubmitForReview,
|
||||
description: (ctx) =>
|
||||
formatMessage(messages.resubmitForReviewDesc, { status: ctx.project.status }),
|
||||
status: 'special-submit-action',
|
||||
shouldShow: (ctx) => ctx.tags.rejectedStatuses.includes(ctx.project.status),
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: messages.visitModerationPage,
|
||||
shouldShow: () => props.routeName !== 'type-id-moderation',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
finalNags.sort((a, b) => {
|
||||
const statusOrder = { required: 0, warning: 1, suggestion: 2, 'special-submit-action': 3 }
|
||||
return statusOrder[a.status] - statusOrder[b.status]
|
||||
})
|
||||
|
||||
return finalNags
|
||||
})
|
||||
|
||||
function shouldShowLink(nag: Nag): boolean {
|
||||
return nag.link?.shouldShow ? nag.link.shouldShow(nagContext.value) : false
|
||||
}
|
||||
|
||||
function getDefaultIcon(status: NagStatus): Component {
|
||||
switch (status) {
|
||||
case 'required':
|
||||
return AsteriskIcon
|
||||
case 'warning':
|
||||
return TriangleAlertIcon
|
||||
case 'suggestion':
|
||||
return LightBulbIcon
|
||||
case 'special-submit-action':
|
||||
return ScaleIcon
|
||||
default:
|
||||
return AsteriskIcon
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusTooltip(status: NagStatus): string {
|
||||
switch (status) {
|
||||
case 'required':
|
||||
return formatMessage(messages.required)
|
||||
case 'warning':
|
||||
return formatMessage(messages.warning)
|
||||
case 'suggestion':
|
||||
return formatMessage(messages.suggestion)
|
||||
default:
|
||||
return formatMessage(messages.required)
|
||||
}
|
||||
}
|
||||
|
||||
const showInvitation = computed<boolean>(() => {
|
||||
if (props.allMembers && props.auth) {
|
||||
const member = props.allMembers.find((x) => x?.user?.id === props.auth.user.id)
|
||||
@@ -472,9 +230,3 @@ async function declineInvite(): Promise<void> {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.duration-250 {
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -312,7 +312,14 @@ import { computed } from 'vue'
|
||||
|
||||
import { UiChartsChart as Chart, UiChartsCompactChart as CompactChart } from '#components'
|
||||
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||
import { analyticsSetToCSVString, intToRgba } from '~/utils/analytics.js'
|
||||
import {
|
||||
analyticsSetToCSVString,
|
||||
countryCodeToFlag,
|
||||
countryCodeToName,
|
||||
formatPercent,
|
||||
getDefaultColor,
|
||||
intToRgba,
|
||||
} from '~/utils/analytics.js'
|
||||
|
||||
const router = useNativeRouter()
|
||||
const theme = useTheme()
|
||||
|
||||
185
apps/frontend/src/components/ui/create/CollectionCreateModal.vue
Normal file
185
apps/frontend/src/components/ui/create/CollectionCreateModal.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<NewModal ref="modal" :header="formatMessage(messages.title)">
|
||||
<div class="min-w-md flex max-w-md flex-col gap-3">
|
||||
<CreateLimitAlert v-model="hasHitLimit" type="collection" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="name">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.nameLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
v-model="name"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
:placeholder="formatMessage(messages.namePlaceholder)"
|
||||
autocomplete="off"
|
||||
:disabled="hasHitLimit"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="additional-information" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">{{
|
||||
formatMessage(messages.summaryLabel)
|
||||
}}</span>
|
||||
<span>{{ formatMessage(messages.summaryDescription) }}</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea
|
||||
id="additional-information"
|
||||
v-model="description"
|
||||
maxlength="256"
|
||||
:placeholder="formatMessage(messages.summaryPlaceholder)"
|
||||
:disabled="hasHitLimit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.collectionInfo, { count: projectIds.length }) }}
|
||||
</p>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled class="w-24">
|
||||
<button @click="modal.hide()">
|
||||
<XIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" class="w-36">
|
||||
<button :disabled="hasHitLimit" @click="create">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createCollection) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { defineMessages } from '@vintl/vintl'
|
||||
|
||||
import CreateLimitAlert from './CreateLimitAlert.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useNativeRouter()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'create.collection.title',
|
||||
defaultMessage: 'Creating a collection',
|
||||
},
|
||||
nameLabel: {
|
||||
id: 'create.collection.name-label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
namePlaceholder: {
|
||||
id: 'create.collection.name-placeholder',
|
||||
defaultMessage: 'Enter collection name...',
|
||||
},
|
||||
summaryLabel: {
|
||||
id: 'create.collection.summary-label',
|
||||
defaultMessage: 'Summary',
|
||||
},
|
||||
summaryDescription: {
|
||||
id: 'create.collection.summary-description',
|
||||
defaultMessage: 'A sentence or two that describes your collection.',
|
||||
},
|
||||
summaryPlaceholder: {
|
||||
id: 'create.collection.summary-placeholder',
|
||||
defaultMessage: 'This is a collection of...',
|
||||
},
|
||||
collectionInfo: {
|
||||
id: 'create.collection.collection-info',
|
||||
defaultMessage:
|
||||
'Your new collection will be created as a public collection with {count, plural, =0 {no projects} one {# project} other {# projects}}.',
|
||||
},
|
||||
cancel: {
|
||||
id: 'create.collection.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
createCollection: {
|
||||
id: 'create.collection.create-collection',
|
||||
defaultMessage: 'Create collection',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'create.collection.error-title',
|
||||
defaultMessage: 'An error occurred',
|
||||
},
|
||||
})
|
||||
|
||||
const name = ref('')
|
||||
const description = ref('')
|
||||
const hasHitLimit = ref(false)
|
||||
|
||||
const modal = ref()
|
||||
|
||||
const props = defineProps({
|
||||
projectIds: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
async function create() {
|
||||
startLoading()
|
||||
try {
|
||||
const result = await useBaseFetch('collection', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
name: name.value.trim(),
|
||||
description: description.value.trim() || undefined,
|
||||
projects: props.projectIds,
|
||||
},
|
||||
apiVersion: 3,
|
||||
})
|
||||
|
||||
await initUserCollections()
|
||||
|
||||
modal.value.hide()
|
||||
await router.push(`/collection/${result.id}`)
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: err?.data?.description || err?.message || err,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
stopLoading()
|
||||
}
|
||||
function show(event) {
|
||||
name.value = ''
|
||||
description.value = ''
|
||||
modal.value.show(event)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.modal-creation {
|
||||
input {
|
||||
width: 20rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.text-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
margin-top: var(--gap-md);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
168
apps/frontend/src/components/ui/create/CreateLimitAlert.vue
Normal file
168
apps/frontend/src/components/ui/create/CreateLimitAlert.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<Admonition
|
||||
v-if="shouldShowAlert"
|
||||
:type="hasHitLimit ? 'critical' : 'warning'"
|
||||
:header="
|
||||
hasHitLimit
|
||||
? capitalizeString(formatMessage(messages.limitReached, { type: typeName.singular }))
|
||||
: formatMessage(messages.approachingLimit, { type: typeName.singular, current, max })
|
||||
"
|
||||
class="mb-4"
|
||||
>
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<template v-if="hasHitLimit">
|
||||
{{ formatMessage(messages.limitReachedDescription, { type: typeName.singular, max }) }}
|
||||
<div class="w-min">
|
||||
<ButtonStyled color="red">
|
||||
<NuxtLink to="https://support.modrinth.com" target="_blank">
|
||||
<MessageIcon />{{ formatMessage(messages.contactSupport) }}</NuxtLink
|
||||
>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{
|
||||
formatMessage(messages.approachingLimitDescription, {
|
||||
type: typeName.singular,
|
||||
max,
|
||||
typePlural: typeName.plural,
|
||||
})
|
||||
}}
|
||||
<div class="w-min">
|
||||
<ButtonStyled color="orange">
|
||||
<NuxtLink to="https://support.modrinth.com" target="_blank">
|
||||
<MessageIcon />{{ formatMessage(messages.contactSupport) }}</NuxtLink
|
||||
>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Admonition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { MessageIcon } from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled } from '@modrinth/ui'
|
||||
import { capitalizeString } from '@modrinth/utils'
|
||||
import { defineMessages } from '@vintl/vintl'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
limitReached: {
|
||||
id: 'create.limit-alert.limit-reached',
|
||||
defaultMessage: '{type} limit reached',
|
||||
},
|
||||
approachingLimit: {
|
||||
id: 'create.limit-alert.approaching-limit',
|
||||
defaultMessage: 'Approaching {type} limit ({current}/{max})',
|
||||
},
|
||||
limitReachedDescription: {
|
||||
id: 'create.limit-alert.limit-reached-description',
|
||||
defaultMessage:
|
||||
"You've reached your {type} limit of {max}. Please contact support to increase your limit.",
|
||||
},
|
||||
approachingLimitDescription: {
|
||||
id: 'create.limit-alert.approaching-limit-description',
|
||||
defaultMessage:
|
||||
"You're about to hit the {type} limit, please contact support if you need more than {max} {typePlural}.",
|
||||
},
|
||||
contactSupport: {
|
||||
id: 'create.limit-alert.contact-support',
|
||||
defaultMessage: 'Contact support',
|
||||
},
|
||||
typeProject: {
|
||||
id: 'create.limit-alert.type-project',
|
||||
defaultMessage: 'project',
|
||||
},
|
||||
typeOrganization: {
|
||||
id: 'create.limit-alert.type-organization',
|
||||
defaultMessage: 'organization',
|
||||
},
|
||||
typeCollection: {
|
||||
id: 'create.limit-alert.type-collection',
|
||||
defaultMessage: 'collection',
|
||||
},
|
||||
typePluralProject: {
|
||||
id: 'create.limit-alert.type-plural-project',
|
||||
defaultMessage: 'projects',
|
||||
},
|
||||
typePluralOrganization: {
|
||||
id: 'create.limit-alert.type-plural-organization',
|
||||
defaultMessage: 'organizations',
|
||||
},
|
||||
typePluralCollection: {
|
||||
id: 'create.limit-alert.type-plural-collection',
|
||||
defaultMessage: 'collections',
|
||||
},
|
||||
})
|
||||
|
||||
interface UserLimits {
|
||||
current: number
|
||||
max: number
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
type: 'project' | 'org' | 'collection'
|
||||
}>()
|
||||
|
||||
const model = defineModel<boolean>()
|
||||
|
||||
const apiEndpoint = computed(() => {
|
||||
switch (props.type) {
|
||||
case 'project':
|
||||
return 'limits/projects'
|
||||
case 'org':
|
||||
return 'limits/organizations'
|
||||
case 'collection':
|
||||
return 'limits/collections'
|
||||
default:
|
||||
return 'limits/projects'
|
||||
}
|
||||
})
|
||||
|
||||
const { data: limits } = await useAsyncData<UserLimits | undefined>(
|
||||
`limits-${props.type}`,
|
||||
() => useBaseFetch(apiEndpoint.value, { apiVersion: 3 }) as Promise<UserLimits>,
|
||||
)
|
||||
|
||||
const typeName = computed<{ singular: string; plural: string }>(() => {
|
||||
switch (props.type) {
|
||||
case 'project':
|
||||
return {
|
||||
singular: formatMessage(messages.typeProject),
|
||||
plural: formatMessage(messages.typePluralProject),
|
||||
}
|
||||
case 'org':
|
||||
return {
|
||||
singular: formatMessage(messages.typeOrganization),
|
||||
plural: formatMessage(messages.typePluralOrganization),
|
||||
}
|
||||
case 'collection':
|
||||
return {
|
||||
singular: formatMessage(messages.typeCollection),
|
||||
plural: formatMessage(messages.typePluralCollection),
|
||||
}
|
||||
default:
|
||||
return {
|
||||
singular: formatMessage(messages.typeProject),
|
||||
plural: formatMessage(messages.typePluralProject),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const current = computed(() => limits.value?.current ?? 0)
|
||||
const max = computed(() => limits.value?.max ?? null)
|
||||
const percentage = computed(() => (max.value ? Math.round((current.value / max.value) * 100) : 0))
|
||||
const hasHitLimit = computed(() => max.value !== null && current.value >= max.value)
|
||||
const shouldShowAlert = computed(() => max.value !== null && percentage.value >= 75)
|
||||
|
||||
watch(
|
||||
hasHitLimit,
|
||||
(newValue) => {
|
||||
model.value = newValue
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Creating an organization">
|
||||
<div class="flex flex-col gap-3">
|
||||
<NewModal ref="modal" :header="formatMessage(messages.title)">
|
||||
<div class="min-w-md flex max-w-md flex-col gap-3">
|
||||
<CreateLimitAlert v-model="hasHitLimit" type="org" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="name">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Name
|
||||
{{ formatMessage(messages.nameLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -13,15 +14,16 @@
|
||||
v-model="name"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
:placeholder="`Enter organization name...`"
|
||||
:placeholder="formatMessage(messages.namePlaceholder)"
|
||||
autocomplete="off"
|
||||
:disabled="hasHitLimit"
|
||||
@input="updateSlug"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="slug">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
URL
|
||||
{{ formatMessage(messages.urlLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -33,6 +35,7 @@
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
:disabled="hasHitLimit"
|
||||
@input="setManualSlug"
|
||||
/>
|
||||
</div>
|
||||
@@ -40,30 +43,35 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="additional-information" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Summary
|
||||
{{ formatMessage(messages.summaryLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
<span>A sentence or two that describes your organization.</span>
|
||||
<span>{{ formatMessage(messages.summaryDescription) }}</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea id="additional-information" v-model="description" maxlength="256" />
|
||||
<textarea
|
||||
id="additional-information"
|
||||
v-model="description"
|
||||
maxlength="256"
|
||||
:placeholder="formatMessage(messages.summaryPlaceholder)"
|
||||
:disabled="hasHitLimit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="m-0 max-w-[30rem]">
|
||||
You will be the owner of this organization, but you can invite other members and transfer
|
||||
ownership at any time.
|
||||
<p class="m-0">
|
||||
{{ formatMessage(messages.ownershipInfo) }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="createOrganization">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
Create organization
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled class="w-24">
|
||||
<button @click="hide">
|
||||
<XIcon aria-hidden="true" />
|
||||
Cancel
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" class="w-40">
|
||||
<button :disabled="hasHitLimit" @click="createOrganization">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createOrganization) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -74,15 +82,68 @@
|
||||
<script setup lang="ts">
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { defineMessages } from '@vintl/vintl'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import CreateLimitAlert from './CreateLimitAlert.vue'
|
||||
|
||||
const router = useNativeRouter()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'create.organization.title',
|
||||
defaultMessage: 'Creating an organization',
|
||||
},
|
||||
nameLabel: {
|
||||
id: 'create.organization.name-label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
namePlaceholder: {
|
||||
id: 'create.organization.name-placeholder',
|
||||
defaultMessage: 'Enter organization name...',
|
||||
},
|
||||
urlLabel: {
|
||||
id: 'create.organization.url-label',
|
||||
defaultMessage: 'URL',
|
||||
},
|
||||
summaryLabel: {
|
||||
id: 'create.organization.summary-label',
|
||||
defaultMessage: 'Summary',
|
||||
},
|
||||
summaryDescription: {
|
||||
id: 'create.organization.summary-description',
|
||||
defaultMessage: 'A sentence or two that describes your organization.',
|
||||
},
|
||||
summaryPlaceholder: {
|
||||
id: 'create.organization.summary-placeholder',
|
||||
defaultMessage: 'An organization for...',
|
||||
},
|
||||
ownershipInfo: {
|
||||
id: 'create.organization.ownership-info',
|
||||
defaultMessage:
|
||||
'You will be the owner of this organization, but you can invite other members and transfer ownership at any time.',
|
||||
},
|
||||
cancel: {
|
||||
id: 'create.organization.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
createOrganization: {
|
||||
id: 'create.organization.create-organization',
|
||||
defaultMessage: 'Create organization',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'create.organization.error-title',
|
||||
defaultMessage: 'An error occurred',
|
||||
},
|
||||
})
|
||||
|
||||
const name = ref<string>('')
|
||||
const slug = ref<string>('')
|
||||
const description = ref<string>('')
|
||||
const manualSlug = ref<boolean>(false)
|
||||
const hasHitLimit = ref<boolean>(false)
|
||||
const modal = ref<InstanceType<typeof NewModal>>()
|
||||
|
||||
async function createOrganization(): Promise<void> {
|
||||
@@ -106,7 +167,7 @@ async function createOrganization(): Promise<void> {
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<NewModal ref="modal" header="Creating a project">
|
||||
<div class="flex flex-col gap-3">
|
||||
<NewModal ref="modal" :header="formatMessage(messages.title)">
|
||||
<div class="min-w-md flex max-w-md flex-col gap-3">
|
||||
<CreateLimitAlert v-model="hasHitLimit" type="project" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="name">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Name
|
||||
{{ formatMessage(messages.nameLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -13,15 +14,16 @@
|
||||
v-model="name"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
placeholder="Enter project name..."
|
||||
:placeholder="formatMessage(messages.namePlaceholder)"
|
||||
autocomplete="off"
|
||||
:disabled="hasHitLimit"
|
||||
@input="updatedName()"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="slug">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
URL
|
||||
{{ formatMessage(messages.urlLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
@@ -33,6 +35,7 @@
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
:disabled="hasHitLimit"
|
||||
@input="manualSlug = true"
|
||||
/>
|
||||
</div>
|
||||
@@ -40,42 +43,49 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="visibility" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Visibility
|
||||
{{ formatMessage(messages.visibilityLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
<span> The visibility of your project after it has been approved. </span>
|
||||
<span>{{ formatMessage(messages.visibilityDescription) }}</span>
|
||||
</label>
|
||||
<DropdownSelect
|
||||
<Chips
|
||||
id="visibility"
|
||||
v-model="visibility"
|
||||
:options="visibilities"
|
||||
:display-name="(x) => x.display"
|
||||
name="Visibility"
|
||||
:items="visibilities"
|
||||
:format-label="(x) => x.display"
|
||||
:capitalize="false"
|
||||
:disabled="hasHitLimit"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="additional-information" class="flex flex-col gap-1">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
Summary
|
||||
{{ formatMessage(messages.summaryLabel) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
<span> A sentence or two that describes your project. </span>
|
||||
<span>{{ formatMessage(messages.summaryDescription) }}</span>
|
||||
</label>
|
||||
<div class="textarea-wrapper">
|
||||
<textarea id="additional-information" v-model="description" maxlength="256" />
|
||||
<textarea
|
||||
id="additional-information"
|
||||
v-model="description"
|
||||
maxlength="256"
|
||||
:placeholder="formatMessage(messages.summaryPlaceholder)"
|
||||
:disabled="hasHitLimit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<button @click="createProject">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
Create project
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<div class="flex justify-end gap-2">
|
||||
<ButtonStyled class="w-24">
|
||||
<button @click="cancel">
|
||||
<XIcon aria-hidden="true" />
|
||||
Cancel
|
||||
{{ formatMessage(messages.cancel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="brand" class="w-32">
|
||||
<button :disabled="hasHitLimit" @click="createProject">
|
||||
<PlusIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.createProject) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -85,12 +95,78 @@
|
||||
|
||||
<script setup>
|
||||
import { PlusIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, DropdownSelect, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { ButtonStyled, Chips, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { defineMessages } from '@vintl/vintl'
|
||||
|
||||
import CreateLimitAlert from './CreateLimitAlert.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const router = useRouter()
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'create.project.title',
|
||||
defaultMessage: 'Creating a project',
|
||||
},
|
||||
nameLabel: {
|
||||
id: 'create.project.name-label',
|
||||
defaultMessage: 'Name',
|
||||
},
|
||||
namePlaceholder: {
|
||||
id: 'create.project.name-placeholder',
|
||||
defaultMessage: 'Enter project name...',
|
||||
},
|
||||
urlLabel: {
|
||||
id: 'create.project.url-label',
|
||||
defaultMessage: 'URL',
|
||||
},
|
||||
visibilityLabel: {
|
||||
id: 'create.project.visibility-label',
|
||||
defaultMessage: 'Visibility',
|
||||
},
|
||||
visibilityDescription: {
|
||||
id: 'create.project.visibility-description',
|
||||
defaultMessage: 'The visibility of your project after it has been approved.',
|
||||
},
|
||||
summaryLabel: {
|
||||
id: 'create.project.summary-label',
|
||||
defaultMessage: 'Summary',
|
||||
},
|
||||
summaryDescription: {
|
||||
id: 'create.project.summary-description',
|
||||
defaultMessage: 'A sentence or two that describes your project.',
|
||||
},
|
||||
summaryPlaceholder: {
|
||||
id: 'create.project.summary-placeholder',
|
||||
defaultMessage: 'This project adds...',
|
||||
},
|
||||
cancel: {
|
||||
id: 'create.project.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
createProject: {
|
||||
id: 'create.project.create-project',
|
||||
defaultMessage: 'Create project',
|
||||
},
|
||||
errorTitle: {
|
||||
id: 'create.project.error-title',
|
||||
defaultMessage: 'An error occurred',
|
||||
},
|
||||
visibilityPublic: {
|
||||
id: 'create.project.visibility-public',
|
||||
defaultMessage: 'Public',
|
||||
},
|
||||
visibilityUnlisted: {
|
||||
id: 'create.project.visibility-unlisted',
|
||||
defaultMessage: 'Unlisted',
|
||||
},
|
||||
visibilityPrivate: {
|
||||
id: 'create.project.visibility-private',
|
||||
defaultMessage: 'Private',
|
||||
},
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
organizationId: {
|
||||
type: String,
|
||||
@@ -100,6 +176,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const modal = ref()
|
||||
const hasHitLimit = ref(false)
|
||||
|
||||
const name = ref('')
|
||||
const slug = ref('')
|
||||
@@ -108,21 +185,18 @@ const manualSlug = ref(false)
|
||||
const visibilities = ref([
|
||||
{
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
display: formatMessage(messages.visibilityPublic),
|
||||
},
|
||||
{
|
||||
actual: 'unlisted',
|
||||
display: 'Unlisted',
|
||||
display: formatMessage(messages.visibilityUnlisted),
|
||||
},
|
||||
{
|
||||
actual: 'private',
|
||||
display: 'Private',
|
||||
display: formatMessage(messages.visibilityPrivate),
|
||||
},
|
||||
])
|
||||
const visibility = ref({
|
||||
actual: 'approved',
|
||||
display: 'Public',
|
||||
})
|
||||
const visibility = ref(visibilities.value[0])
|
||||
|
||||
const cancel = () => {
|
||||
modal.value.hide()
|
||||
@@ -182,7 +256,7 @@ async function createProject() {
|
||||
})
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
title: formatMessage(messages.errorTitle),
|
||||
text: err.data ? err.data.description : err,
|
||||
type: 'error',
|
||||
})
|
||||
@@ -0,0 +1,427 @@
|
||||
<template>
|
||||
<NewModal
|
||||
ref="taxFormModal"
|
||||
:header="formatMessage(messages.taxFormHeader)"
|
||||
:hide-header="currentStage === 'download-confirmation'"
|
||||
:close-on-click-outside="currentStage !== 'download-confirmation'"
|
||||
:close-on-esc="currentStage !== 'download-confirmation'"
|
||||
>
|
||||
<div
|
||||
class="w-full"
|
||||
:class="[currentStage === 'form-selection' ? 'sm:w-[540px]' : 'sm:w-[400px]']"
|
||||
>
|
||||
<div v-if="currentStage === 'form-selection'">
|
||||
<Admonition type="info" :header="formatMessage(messages.securityHeader)">
|
||||
<IntlFormatted :message-id="messages.securityDescription">
|
||||
<template #security-link="{ children }">
|
||||
<a
|
||||
href="https://www.track1099.com/info/security"
|
||||
class="flex w-fit flex-row gap-1 align-middle text-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
<ExternalIcon class="my-auto" />
|
||||
</a>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</Admonition>
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<label>
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.usCitizenQuestion) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<Chips
|
||||
v-model="isUSCitizen"
|
||||
:items="['yes', 'no']"
|
||||
:format-label="
|
||||
(item) => (item === 'yes' ? formatMessage(messages.yes) : formatMessage(messages.no))
|
||||
"
|
||||
:never-empty="false"
|
||||
:capitalize="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-in-out"
|
||||
enter-from-class="h-0 overflow-hidden opacity-0"
|
||||
enter-to-class="h-auto overflow-visible opacity-100"
|
||||
leave-active-class="transition-all duration-300 ease-in-out"
|
||||
leave-from-class="h-auto overflow-visible opacity-100"
|
||||
leave-to-class="h-0 overflow-hidden opacity-0"
|
||||
>
|
||||
<div v-if="isUSCitizen === 'no'" class="flex flex-col gap-1">
|
||||
<label class="mt-4">
|
||||
<span class="text-lg font-semibold text-contrast">
|
||||
{{ formatMessage(messages.entityQuestion) }}
|
||||
<span class="text-brand-red">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<Chips
|
||||
v-model="entityType"
|
||||
:items="['private-individual', 'foreign-entity']"
|
||||
:format-label="
|
||||
(item) =>
|
||||
item === 'private-individual'
|
||||
? formatMessage(messages.privateIndividual)
|
||||
: formatMessage(messages.foreignEntity)
|
||||
"
|
||||
:never-empty="false"
|
||||
:capitalize="false"
|
||||
class="mt-2"
|
||||
/>
|
||||
<span class="text-md mt-2 leading-tight">
|
||||
{{ formatMessage(messages.entityDescription) }}
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
<div class="mt-4 flex justify-end gap-3">
|
||||
<ButtonStyled @click="handleCancel">
|
||||
<button><XIcon /> {{ formatMessage(messages.cancel) }}</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<button :disabled="!canContinue || loading" @click="continueForm">
|
||||
{{ formatMessage(messages.continue) }}
|
||||
<RightArrowIcon v-if="!loading" />
|
||||
<SpinnerIcon v-else class="animate-spin" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="currentStage === 'download-confirmation'" class="flex flex-col gap-6">
|
||||
<div class="relative block h-[180px] w-[400px] overflow-hidden rounded-xl rounded-b-none">
|
||||
<div
|
||||
class="absolute inset-0 rounded-xl rounded-b-none bg-gradient-to-r from-brand-green to-brand-blue"
|
||||
></div>
|
||||
<div
|
||||
class="absolute inset-0 rounded-xl rounded-b-none"
|
||||
style="
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(39, 41, 46, 0.15) 0%,
|
||||
var(--color-raised-bg) 100%
|
||||
);
|
||||
"
|
||||
></div>
|
||||
<BrowserWindowSuccessIllustration
|
||||
class="absolute left-[90px] top-[48px] h-[140px] w-[220px]"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<span class="text-2xl font-semibold text-contrast">{{
|
||||
formatMessage(messages.confirmationTitle)
|
||||
}}</span>
|
||||
<span>{{
|
||||
formatMessage(messages.confirmationSuccess, { formType: determinedFormType })
|
||||
}}</span>
|
||||
<span>
|
||||
<IntlFormatted :message-id="messages.confirmationSupportText">
|
||||
<template #support-link="{ children }">
|
||||
<nuxt-link
|
||||
to="https://support.modrinth.com"
|
||||
class="text-link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<component :is="() => normalizeChildren(children)" />
|
||||
</nuxt-link>
|
||||
</template>
|
||||
</IntlFormatted>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex w-full flex-row justify-stretch gap-2">
|
||||
<ButtonStyled>
|
||||
<button class="w-full text-contrast" @click="handleClose">{{ closeButtonText }}</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled color="green">
|
||||
<button class="w-full text-contrast" @click="downloadTaxForm">
|
||||
<DownloadIcon />{{
|
||||
formatMessage(messages.downloadButton, { formType: determinedFormType })
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NewModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BrowserWindowSuccessIllustration,
|
||||
DownloadIcon,
|
||||
ExternalIcon,
|
||||
RightArrowIcon,
|
||||
SpinnerIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Admonition, ButtonStyled, Chips, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { IntlFormatted } from '@vintl/vintl/components'
|
||||
|
||||
import { type FormRequestResponse, useAvalara1099 } from '@/composables/avalara1099'
|
||||
import { normalizeChildren } from '@/utils/vue-children.ts'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
closeButtonText?: string
|
||||
emitSuccessOnClose?: boolean
|
||||
}>(),
|
||||
{
|
||||
closeButtonText: 'Close',
|
||||
emitSuccessOnClose: true,
|
||||
},
|
||||
)
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const taxFormModal = ref<InstanceType<typeof NewModal> | null>(null)
|
||||
|
||||
type ModalStage = 'form-selection' | 'download-confirmation'
|
||||
const currentStage = ref<ModalStage>('form-selection')
|
||||
|
||||
async function startTaxForm(e: MouseEvent) {
|
||||
currentStage.value = 'form-selection'
|
||||
taxFormModal.value?.show(e)
|
||||
}
|
||||
|
||||
async function showDownloadConfirmation(e: MouseEvent) {
|
||||
currentStage.value = 'download-confirmation'
|
||||
taxFormModal.value?.show(e)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
startTaxForm,
|
||||
showDownloadConfirmation,
|
||||
})
|
||||
|
||||
const auth = await useAuth()
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const messages = defineMessages({
|
||||
taxFormHeader: {
|
||||
id: 'dashboard.creator-tax-form-modal.header',
|
||||
defaultMessage: 'Tax form',
|
||||
},
|
||||
securityHeader: {
|
||||
id: 'dashboard.creator-tax-form-modal.security.header',
|
||||
defaultMessage: 'Security practices',
|
||||
},
|
||||
securityDescription: {
|
||||
id: 'dashboard.creator-tax-form-modal.security.description',
|
||||
defaultMessage:
|
||||
'Modrinth uses third-party provider Track1099 to securely collect and store your tax forms. <security-link>Learn more here.</security-link>',
|
||||
},
|
||||
usCitizenQuestion: {
|
||||
id: 'dashboard.creator-tax-form-modal.us-citizen.question',
|
||||
defaultMessage: 'Are you a US citizen?',
|
||||
},
|
||||
yes: { id: 'common.yes', defaultMessage: 'Yes' },
|
||||
no: { id: 'common.no', defaultMessage: 'No' },
|
||||
entityQuestion: {
|
||||
id: 'dashboard.creator-tax-form-modal.entity.question',
|
||||
defaultMessage: 'Are you a private individual or part of a foreign entity?',
|
||||
},
|
||||
entityDescription: {
|
||||
id: 'dashboard.creator-tax-form-modal.entity.description',
|
||||
defaultMessage:
|
||||
'A foreign entity means a business entity organized outside the United States (such as a non-US corporation, partnership, or LLC).',
|
||||
},
|
||||
privateIndividual: {
|
||||
id: 'dashboard.creator-tax-form-modal.entity.private-individual',
|
||||
defaultMessage: 'Private individual',
|
||||
},
|
||||
foreignEntity: {
|
||||
id: 'dashboard.creator-tax-form-modal.entity.foreign-entity',
|
||||
defaultMessage: 'Foreign entity',
|
||||
},
|
||||
cancel: { id: 'action.cancel', defaultMessage: 'Cancel' },
|
||||
continue: { id: 'action.continue', defaultMessage: 'Continue' },
|
||||
confirmationTitle: {
|
||||
id: 'dashboard.creator-tax-form-modal.confirmation.title',
|
||||
defaultMessage: "You're all set! 🎉",
|
||||
},
|
||||
confirmationSuccess: {
|
||||
id: 'dashboard.creator-tax-form-modal.confirmation.success',
|
||||
defaultMessage: 'Your {formType} tax form has been submitted successfully!',
|
||||
},
|
||||
confirmationSupportText: {
|
||||
id: 'dashboard.creator-tax-form-modal.confirmation.support-text',
|
||||
defaultMessage:
|
||||
'You can freely withdraw now. If you have questions or need to update your details <support-link>contact support</support-link>.',
|
||||
},
|
||||
downloadButton: {
|
||||
id: 'dashboard.creator-tax-form-modal.confirmation.download-button',
|
||||
defaultMessage: 'Download {formType}',
|
||||
},
|
||||
})
|
||||
|
||||
const isUSCitizen = ref<'yes' | 'no' | null>(null)
|
||||
const entityType = ref<'private-individual' | 'foreign-entity' | null>(null)
|
||||
|
||||
function hideModal() {
|
||||
manualLoading.value = false
|
||||
taxFormModal.value?.hide()
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
emit('cancelled')
|
||||
hideModal()
|
||||
setTimeout(() => {
|
||||
currentStage.value = 'form-selection'
|
||||
}, 300)
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
if (currentStage.value === 'download-confirmation' && props.emitSuccessOnClose) {
|
||||
emit('success')
|
||||
}
|
||||
hideModal()
|
||||
setTimeout(() => {
|
||||
currentStage.value = 'form-selection'
|
||||
}, 300)
|
||||
}
|
||||
|
||||
const determinedFormType = computed(() => {
|
||||
if (isUSCitizen.value === 'yes') {
|
||||
return 'W-9'
|
||||
} else if (isUSCitizen.value === 'no' && entityType.value === 'private-individual') {
|
||||
return 'W-8BEN'
|
||||
} else if (isUSCitizen.value === 'no' && entityType.value === 'foreign-entity') {
|
||||
return 'W-8BEN-E'
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const canContinue = computed(() => {
|
||||
if (isUSCitizen.value === 'yes') {
|
||||
return true
|
||||
} else if (isUSCitizen.value === 'no' && entityType.value) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'success' | 'cancelled'): void
|
||||
}>()
|
||||
|
||||
const avalaraState = ref<ReturnType<typeof useAvalara1099> | null>(null)
|
||||
const formResponse = ref<any>(null)
|
||||
const manualLoading = ref(false)
|
||||
const loading = computed(
|
||||
() =>
|
||||
manualLoading.value ||
|
||||
(avalaraState.value ? ((avalaraState.value as any).loading?.value ?? false) : false),
|
||||
)
|
||||
|
||||
async function continueForm() {
|
||||
if (!import.meta.client) return
|
||||
if (!determinedFormType.value) return
|
||||
|
||||
manualLoading.value = true
|
||||
|
||||
// Skip Avalara if testTaxForm flag is enabled
|
||||
if (flags.value.testTaxForm) {
|
||||
currentStage.value = 'download-confirmation'
|
||||
manualLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const response = (await useBaseFetch('payout/compliance', {
|
||||
apiVersion: 3,
|
||||
method: 'POST',
|
||||
body: {
|
||||
form_type: determinedFormType.value,
|
||||
},
|
||||
})) as FormRequestResponse
|
||||
|
||||
if (!avalaraState.value) {
|
||||
avalaraState.value = useAvalara1099(response, {
|
||||
prefill: {
|
||||
email: (auth.value.user as any)?.email ?? '',
|
||||
account_number: (auth.value.user as any)?.id ?? '',
|
||||
reference_number: (auth.value.user as any)?.id ?? '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
if (avalaraState.value) {
|
||||
const response = await avalaraState.value.start()
|
||||
formResponse.value = response
|
||||
if (avalaraState.value.status === 'signed') {
|
||||
currentStage.value = 'download-confirmation'
|
||||
manualLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
addNotification({
|
||||
title: 'Tax form incomplete',
|
||||
text: 'You have not completed the tax form. Please try again.',
|
||||
type: 'warning',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error occurred while continuing tax form:', error)
|
||||
handleCancel()
|
||||
} finally {
|
||||
manualLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function downloadTaxForm() {
|
||||
if (!formResponse.value) return
|
||||
|
||||
const signedPdfUrl = formResponse.value.links?.signed_pdf
|
||||
if (signedPdfUrl) {
|
||||
window.open(signedPdfUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
watch(isUSCitizen, (newValue) => {
|
||||
if (newValue === 'yes') {
|
||||
entityType.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
dialog[open]:has(> iframe[src*='form_embed']) {
|
||||
width: min(960px, calc(100vw - 2rem)) !important;
|
||||
max-width: 100% !important;
|
||||
height: min(95vh, max(640px, 75vh)) !important;
|
||||
background: var(--color-raised-bg) !important;
|
||||
border: 1px solid var(--color-button-border) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
box-shadow: var(--shadow-floating) !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
dialog[open] > iframe[src*='form_embed'] {
|
||||
position: absolute !important;
|
||||
inset: 0 !important;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: block !important;
|
||||
border: none !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
dialog[open]:has(> iframe[src*='form_embed']) {
|
||||
width: calc(100vw - 1rem) !important;
|
||||
height: 95vh !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
dialog[open] > iframe[src*='form_embed'] {
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -175,7 +175,7 @@ const quickActions: OverflowMenuOption[] = [
|
||||
]
|
||||
|
||||
const versionUrl = computed(() => {
|
||||
return `/${props.report.project.project_type}/${props.report.project.slug}/versions/${props.report.version.id}`
|
||||
return `/${props.report.project.project_type}/${props.report.project.slug}/version/${props.report.version.id}`
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
<template>
|
||||
<div v-if="visibleNags.length > 0" class="universal-card my-4">
|
||||
<div class="flex max-w-full flex-wrap items-center gap-x-6 gap-y-4">
|
||||
<div class="flex flex-auto flex-wrap items-center gap-x-6 gap-y-4">
|
||||
<h2 class="my-0 mr-auto">
|
||||
{{ getFormattedMessage(messages.publishingChecklist) }}
|
||||
</h2>
|
||||
<div class="flex flex-row gap-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<AsteriskIcon class="size-4 shrink-0 text-red" />
|
||||
<span class="text-secondary">{{ getFormattedMessage(messages.required) }}</span>
|
||||
</div>
|
||||
|
|
||||
<div class="flex items-center gap-1">
|
||||
<TriangleAlertIcon class="size-4 shrink-0 text-orange" />
|
||||
<span class="text-secondary">{{ getFormattedMessage(messages.warning) }}</span>
|
||||
</div>
|
||||
|
|
||||
<div class="flex items-center gap-1">
|
||||
<LightBulbIcon class="size-4 shrink-0 text-purple" />
|
||||
<span class="text-secondary">{{ getFormattedMessage(messages.suggestion) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<ButtonStyled circular>
|
||||
<button :class="!collapsed && '[&>svg]:rotate-180'" @click="$emit('toggleCollapsed')">
|
||||
<DropdownIcon class="duration-250 transition-transform ease-in-out" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!collapsed" class="grid-display width-16 mt-4">
|
||||
<div v-for="nag in visibleNags" :key="nag.id" class="grid-display__item">
|
||||
<span class="flex items-center gap-2 font-semibold">
|
||||
<component
|
||||
:is="nag.icon || getDefaultIcon(nag.status)"
|
||||
v-tooltip="getStatusTooltip(nag.status)"
|
||||
:class="[
|
||||
'size-4',
|
||||
nag.status === 'required' && 'text-red',
|
||||
nag.status === 'warning' && 'text-orange',
|
||||
nag.status === 'suggestion' && 'text-purple',
|
||||
]"
|
||||
:aria-label="getStatusTooltip(nag.status)"
|
||||
/>
|
||||
{{ getFormattedMessage(nag.title) }}
|
||||
</span>
|
||||
{{ getNagDescription(nag) }}
|
||||
<NuxtLink
|
||||
v-if="nag.link && shouldShowLink(nag)"
|
||||
:to="`/${project.project_type}/${project.slug ? project.slug : project.id}/${
|
||||
nag.link.path
|
||||
}`"
|
||||
class="goto-link"
|
||||
>
|
||||
{{ getFormattedMessage(nag.link.title) }}
|
||||
<ChevronRightIcon aria-hidden="true" class="featured-header-chevron" />
|
||||
</NuxtLink>
|
||||
<ButtonStyled
|
||||
v-if="nag.status === 'special-submit-action' && nag.id === 'submit-for-review'"
|
||||
color="orange"
|
||||
@click="submitForReview"
|
||||
>
|
||||
<button
|
||||
v-tooltip="
|
||||
!canSubmitForReview ? getFormattedMessage(messages.submitChecklistTooltip) : undefined
|
||||
"
|
||||
:disabled="!canSubmitForReview"
|
||||
>
|
||||
<SendIcon />
|
||||
{{ getFormattedMessage(messages.submitForReviewButton) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AsteriskIcon,
|
||||
ChevronRightIcon,
|
||||
DropdownIcon,
|
||||
LightBulbIcon,
|
||||
ScaleIcon,
|
||||
SendIcon,
|
||||
TriangleAlertIcon,
|
||||
} from '@modrinth/assets'
|
||||
import type { Nag, NagContext, NagStatus } from '@modrinth/moderation'
|
||||
import { nags } from '@modrinth/moderation'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import type { Project, User, Version } from '@modrinth/utils'
|
||||
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
|
||||
import type { Component } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
interface Tags {
|
||||
rejectedStatuses: string[]
|
||||
}
|
||||
|
||||
interface Member {
|
||||
accepted?: boolean
|
||||
project_role?: string
|
||||
user?: Partial<User>
|
||||
}
|
||||
|
||||
interface Props {
|
||||
project: Project
|
||||
versions?: Version[]
|
||||
currentMember?: Member | null
|
||||
collapsed?: boolean
|
||||
routeName?: string
|
||||
tags: Tags
|
||||
}
|
||||
|
||||
const messages = defineMessages({
|
||||
publishingChecklist: {
|
||||
id: 'project-moderation-nags.publishing-checklist',
|
||||
defaultMessage: 'Publishing checklist',
|
||||
},
|
||||
submitForReview: {
|
||||
id: 'project-moderation-nags.submit-for-review',
|
||||
defaultMessage: 'Submit for review',
|
||||
},
|
||||
submitForReviewDesc: {
|
||||
id: 'project-moderation-nags.submit-for-review-desc',
|
||||
defaultMessage:
|
||||
'Your project is only viewable by members of the project. It must be reviewed by moderators in order to be published.',
|
||||
},
|
||||
submitForReviewButton: {
|
||||
id: 'project-moderation-nags.submit-for-review-button',
|
||||
defaultMessage: 'Submit for review',
|
||||
},
|
||||
resubmitForReview: {
|
||||
id: 'project-moderation-nags.resubmit-for-review',
|
||||
defaultMessage: 'Resubmit for review',
|
||||
},
|
||||
resubmitForReviewDesc: {
|
||||
id: 'project-moderation-nags.resubmit-for-review-desc',
|
||||
defaultMessage:
|
||||
"Your project has been {status} by Modrinth's staff. In most cases, you can resubmit for review after addressing the staff's message.",
|
||||
},
|
||||
visitModerationPage: {
|
||||
id: 'project-moderation-nags.visit-moderation-page',
|
||||
defaultMessage: 'Visit moderation page',
|
||||
},
|
||||
submitChecklistTooltip: {
|
||||
id: 'project-moderation-nags.submit-checklist-tooltip',
|
||||
defaultMessage: 'You must complete the required steps in the publishing checklist!',
|
||||
},
|
||||
required: {
|
||||
id: 'project-moderation-nags.required',
|
||||
defaultMessage: 'Required',
|
||||
},
|
||||
warning: {
|
||||
id: 'project-moderation-nags.warning',
|
||||
defaultMessage: 'Warning',
|
||||
},
|
||||
suggestion: {
|
||||
id: 'project-moderation-nags.suggestion',
|
||||
defaultMessage: 'Suggestion',
|
||||
},
|
||||
})
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
versions: () => [],
|
||||
currentMember: null,
|
||||
collapsed: false,
|
||||
routeName: '',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggleCollapsed: []
|
||||
setProcessing: [processing: boolean]
|
||||
}>()
|
||||
|
||||
const nagContext = computed<NagContext>(() => ({
|
||||
project: props.project,
|
||||
versions: props.versions,
|
||||
currentMember: props.currentMember as User,
|
||||
currentRoute: props.routeName,
|
||||
tags: props.tags,
|
||||
submitProject: submitForReview,
|
||||
}))
|
||||
|
||||
const canSubmitForReview = computed(() => {
|
||||
return (
|
||||
applicableNags.value.filter((nag) => nag.status === 'required' && !isNagComplete(nag))
|
||||
.length === 0
|
||||
)
|
||||
})
|
||||
|
||||
async function submitForReview() {
|
||||
if (canSubmitForReview.value) {
|
||||
emit('setProcessing', true)
|
||||
}
|
||||
}
|
||||
|
||||
const applicableNags = computed<Nag[]>(() => {
|
||||
return nags.filter((nag) => {
|
||||
return nag.shouldShow(nagContext.value)
|
||||
})
|
||||
})
|
||||
|
||||
function isNagComplete(nag: Nag): boolean {
|
||||
const context = nagContext.value
|
||||
return !nag.shouldShow(context)
|
||||
}
|
||||
|
||||
const visibleNags = computed<Nag[]>(() => {
|
||||
const finalNags = applicableNags.value.filter((nag) => !isNagComplete(nag))
|
||||
|
||||
if (props.project.status === 'draft') {
|
||||
finalNags.push({
|
||||
id: 'submit-for-review',
|
||||
title: messages.submitForReview,
|
||||
description: () => formatMessage(messages.submitForReviewDesc),
|
||||
status: 'special-submit-action',
|
||||
shouldShow: (ctx) => ctx.project.status === 'draft',
|
||||
})
|
||||
}
|
||||
|
||||
if (props.tags.rejectedStatuses.includes(props.project.status)) {
|
||||
finalNags.push({
|
||||
id: 'resubmit-for-review',
|
||||
title: messages.resubmitForReview,
|
||||
description: (ctx) =>
|
||||
formatMessage(messages.resubmitForReviewDesc, { status: ctx.project.status }),
|
||||
status: 'special-submit-action',
|
||||
shouldShow: (ctx) => ctx.tags.rejectedStatuses.includes(ctx.project.status),
|
||||
link: {
|
||||
path: 'moderation',
|
||||
title: messages.visitModerationPage,
|
||||
shouldShow: () => props.routeName !== 'type-id-moderation',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
finalNags.sort((a, b) => {
|
||||
const statusOrder = { required: 0, warning: 1, suggestion: 2, 'special-submit-action': 3 }
|
||||
return statusOrder[a.status] - statusOrder[b.status]
|
||||
})
|
||||
|
||||
return finalNags
|
||||
})
|
||||
|
||||
function shouldShowLink(nag: Nag): boolean {
|
||||
return nag.link?.shouldShow ? nag.link.shouldShow(nagContext.value) : false
|
||||
}
|
||||
|
||||
function getDefaultIcon(status: NagStatus): Component {
|
||||
switch (status) {
|
||||
case 'required':
|
||||
return AsteriskIcon
|
||||
case 'warning':
|
||||
return TriangleAlertIcon
|
||||
case 'suggestion':
|
||||
return LightBulbIcon
|
||||
case 'special-submit-action':
|
||||
return ScaleIcon
|
||||
default:
|
||||
return AsteriskIcon
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusTooltip(status: NagStatus): string {
|
||||
switch (status) {
|
||||
case 'required':
|
||||
return formatMessage(messages.required)
|
||||
case 'warning':
|
||||
return formatMessage(messages.warning)
|
||||
case 'suggestion':
|
||||
return formatMessage(messages.suggestion)
|
||||
default:
|
||||
return formatMessage(messages.required)
|
||||
}
|
||||
}
|
||||
|
||||
function getNagDescription(nag: Nag): string {
|
||||
if (typeof nag.description === 'function') {
|
||||
return nag.description(nagContext.value)
|
||||
}
|
||||
return formatMessage(nag.description)
|
||||
}
|
||||
|
||||
function getFormattedMessage(message: string | MessageDescriptor): string {
|
||||
if (typeof message === 'string') {
|
||||
return message
|
||||
}
|
||||
return formatMessage(message)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.duration-250 {
|
||||
transition-duration: 250ms;
|
||||
}
|
||||
</style>
|
||||
@@ -78,7 +78,7 @@
|
||||
props.queueEntry.project.project_types.map(formatProjectType).join(', ')
|
||||
}}</span>
|
||||
<span class="sm:hidden">{{
|
||||
formatProjectType(props.queueEntry.project.project_type ?? 'project').substring(0, 3)
|
||||
props.queueEntry.project.project_types.map(formatProjectType).join(', ')
|
||||
}}</span>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -259,7 +259,7 @@ const reportItemUrl = computed(() => {
|
||||
case 'project':
|
||||
return `/${props.report.project?.project_type}/${props.report.project?.slug}`
|
||||
case 'version':
|
||||
return `/${props.report.project?.project_type}/${props.report.project?.slug}/versions/${props.report.version?.id}`
|
||||
return `/${props.report.project?.project_type}/${props.report.project?.slug}/version/${props.report.version?.id}`
|
||||
default:
|
||||
return `/${props.report.item_type}/${props.report.id}`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div class="mx-2 p-4 !py-8 sm:mx-8 sm:p-32">
|
||||
<div class="my-8 flex items-center justify-between">
|
||||
<h2 class="m-0 mx-auto text-3xl font-extrabold sm:text-4xl">Latest news from Modrinth</h2>
|
||||
<h2 class="m-0 mx-auto text-3xl font-extrabold sm:text-4xl">
|
||||
{{ formatMessage(messages.latestNews) }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="latestArticles" class="grid grid-cols-[repeat(auto-fit,minmax(250px,1fr))] gap-4">
|
||||
@@ -17,7 +19,7 @@
|
||||
<ButtonStyled color="brand" size="large">
|
||||
<nuxt-link to="/news">
|
||||
<NewspaperIcon />
|
||||
View all news
|
||||
{{ formatMessage(messages.viewAll) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
@@ -28,8 +30,11 @@
|
||||
import { NewspaperIcon } from '@modrinth/assets'
|
||||
import { articles as rawArticles } from '@modrinth/blog'
|
||||
import { ButtonStyled, NewsArticleCard } from '@modrinth/ui'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const articles = ref(
|
||||
rawArticles
|
||||
.map((article) => ({
|
||||
@@ -41,9 +46,22 @@ const articles = ref(
|
||||
title: article.title,
|
||||
summary: article.summary,
|
||||
date: article.date,
|
||||
unlisted: article.unlisted,
|
||||
}))
|
||||
.filter((a) => !a.unlisted)
|
||||
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()),
|
||||
)
|
||||
|
||||
const messages = defineMessages({
|
||||
latestNews: {
|
||||
id: 'ui.latest-news-row.latest-news',
|
||||
defaultMessage: 'Latest news from Modrinth',
|
||||
},
|
||||
viewAll: {
|
||||
id: 'ui.latest-news-row.view-all',
|
||||
defaultMessage: 'View all news',
|
||||
},
|
||||
})
|
||||
|
||||
const latestArticles = computed(() => articles.value.slice(0, 3))
|
||||
</script>
|
||||
|
||||
@@ -17,13 +17,13 @@ import { ButtonStyled, commonMessages, OverflowMenu, ProgressBar } from '@modrin
|
||||
import type { Backup } from '@modrinth/utils'
|
||||
import { defineMessages, useVIntl } from '@vintl/vintl'
|
||||
import dayjs from 'dayjs'
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const flags = useFeatureFlags()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'prepare' | 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
|
||||
(e: 'download' | 'rename' | 'restore' | 'lock' | 'retry'): void
|
||||
(e: 'delete', skipConfirmation?: boolean): void
|
||||
}>()
|
||||
|
||||
@@ -49,14 +49,8 @@ const backupQueued = computed(
|
||||
const automated = computed(() => props.backup.automated)
|
||||
const failedToCreate = computed(() => props.backup.interrupted)
|
||||
|
||||
const preparedDownloadStates = ['ready', 'done']
|
||||
const inactiveStates = ['failed', 'cancelled']
|
||||
|
||||
const hasPreparedDownload = computed(() => {
|
||||
const fileState = props.backup.task?.file?.state ?? ''
|
||||
return preparedDownloadStates.includes(fileState)
|
||||
})
|
||||
|
||||
const creating = computed(() => {
|
||||
const task = props.backup.task?.create
|
||||
if (task && task.progress < 1 && !inactiveStates.includes(task.state)) {
|
||||
@@ -79,22 +73,7 @@ const restoring = computed(() => {
|
||||
return undefined
|
||||
})
|
||||
|
||||
const initiatedPrepare = ref(false)
|
||||
|
||||
const preparingFile = computed(() => {
|
||||
if (hasPreparedDownload.value) {
|
||||
return false
|
||||
}
|
||||
|
||||
const task = props.backup.task?.file
|
||||
return (
|
||||
(!task && initiatedPrepare.value) ||
|
||||
(task && task.progress < 1 && !inactiveStates.includes(task.state))
|
||||
)
|
||||
})
|
||||
|
||||
const failedToRestore = computed(() => props.backup.task?.restore?.state === 'failed')
|
||||
const failedToPrepareFile = computed(() => props.backup.task?.file?.state === 'failed')
|
||||
|
||||
const messages = defineMessages({
|
||||
locked: {
|
||||
@@ -121,22 +100,6 @@ const messages = defineMessages({
|
||||
id: 'servers.backups.item.queued-for-backup',
|
||||
defaultMessage: 'Queued for backup',
|
||||
},
|
||||
preparingDownload: {
|
||||
id: 'servers.backups.item.preparing-download',
|
||||
defaultMessage: 'Preparing download...',
|
||||
},
|
||||
prepareDownload: {
|
||||
id: 'servers.backups.item.prepare-download',
|
||||
defaultMessage: 'Prepare download',
|
||||
},
|
||||
prepareDownloadAgain: {
|
||||
id: 'servers.backups.item.prepare-download-again',
|
||||
defaultMessage: 'Try preparing again',
|
||||
},
|
||||
alreadyPreparing: {
|
||||
id: 'servers.backups.item.already-preparing',
|
||||
defaultMessage: 'Already preparing backup for download',
|
||||
},
|
||||
creatingBackup: {
|
||||
id: 'servers.backups.item.creating-backup',
|
||||
defaultMessage: 'Creating backup...',
|
||||
@@ -153,10 +116,6 @@ const messages = defineMessages({
|
||||
id: 'servers.backups.item.failed-to-restore-backup',
|
||||
defaultMessage: 'Failed to restore from backup',
|
||||
},
|
||||
failedToPrepareFile: {
|
||||
id: 'servers.backups.item.failed-to-prepare-backup',
|
||||
defaultMessage: 'Failed to prepare download',
|
||||
},
|
||||
automated: {
|
||||
id: 'servers.backups.item.automated',
|
||||
defaultMessage: 'Automated',
|
||||
@@ -200,17 +159,13 @@ const messages = defineMessages({
|
||||
</span>
|
||||
<span v-if="(failedToCreate || failedToRestore) && (automated || backup.locked)">•</span>
|
||||
<span
|
||||
v-if="failedToCreate || failedToRestore || failedToPrepareFile"
|
||||
v-if="failedToCreate || failedToRestore"
|
||||
class="flex items-center gap-1 text-sm text-red"
|
||||
>
|
||||
<XIcon />
|
||||
{{
|
||||
formatMessage(
|
||||
failedToCreate
|
||||
? messages.failedToCreateBackup
|
||||
: failedToRestore
|
||||
? messages.failedToRestoreBackup
|
||||
: messages.failedToPrepareFile,
|
||||
failedToCreate ? messages.failedToCreateBackup : messages.failedToRestoreBackup,
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
@@ -270,7 +225,6 @@ const messages = defineMessages({
|
||||
<template v-else>
|
||||
<ButtonStyled>
|
||||
<a
|
||||
v-if="hasPreparedDownload"
|
||||
:class="{
|
||||
disabled: !kyrosUrl || !jwt,
|
||||
}"
|
||||
@@ -280,28 +234,6 @@ const messages = defineMessages({
|
||||
<DownloadIcon />
|
||||
{{ formatMessage(commonMessages.downloadButton) }}
|
||||
</a>
|
||||
<button
|
||||
v-else
|
||||
:disabled="!!preparingFile"
|
||||
@click="
|
||||
() => {
|
||||
initiatedPrepare = true
|
||||
emit('prepare')
|
||||
}
|
||||
"
|
||||
>
|
||||
<SpinnerIcon v-if="preparingFile" class="animate-spin" />
|
||||
<DownloadIcon v-else />
|
||||
{{
|
||||
formatMessage(
|
||||
preparingFile
|
||||
? messages.preparingDownload
|
||||
: failedToPrepareFile
|
||||
? messages.prepareDownloadAgain
|
||||
: messages.prepareDownload,
|
||||
)
|
||||
}}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
@@ -310,7 +242,7 @@ const messages = defineMessages({
|
||||
{
|
||||
id: 'restore',
|
||||
action: () => emit('restore'),
|
||||
disabled: !!restoring || !!preparingFile,
|
||||
disabled: !!restoring,
|
||||
},
|
||||
{ id: 'lock', action: () => emit('lock') },
|
||||
{ divider: true },
|
||||
@@ -318,7 +250,7 @@ const messages = defineMessages({
|
||||
id: 'delete',
|
||||
color: 'red',
|
||||
action: () => emit('delete'),
|
||||
disabled: !!restoring || !!preparingFile,
|
||||
disabled: !!restoring,
|
||||
},
|
||||
]"
|
||||
>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<UiServersTeleportDropdownMenu
|
||||
<TeleportDropdownMenu
|
||||
:id="'interval-field'"
|
||||
v-model="backupIntervalsLabel"
|
||||
:disabled="!autoBackupEnabled || isSaving"
|
||||
@@ -57,7 +57,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SaveIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
TeleportDropdownMenu,
|
||||
} from '@modrinth/ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<UiServersTeleportDropdownMenu
|
||||
<TeleportDropdownMenu
|
||||
v-model="selectedVersion"
|
||||
name="Project"
|
||||
:options="filteredVersions"
|
||||
@@ -237,7 +237,14 @@ import {
|
||||
LockOpenIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Admonition, Avatar, ButtonStyled, CopyCode, NewModal } from '@modrinth/ui'
|
||||
import {
|
||||
Admonition,
|
||||
Avatar,
|
||||
ButtonStyled,
|
||||
CopyCode,
|
||||
NewModal,
|
||||
TeleportDropdownMenu,
|
||||
} from '@modrinth/ui'
|
||||
import TagItem from '@modrinth/ui/src/components/base/TagItem.vue'
|
||||
import { formatCategory, formatVersionsForDisplay, type Mod, type Version } from '@modrinth/utils'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
@@ -51,14 +51,14 @@
|
||||
{{ formattedModifiedDate }}
|
||||
</span>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu :options="menuOptions" direction="left" position="bottom">
|
||||
<TeleportOverflowMenu :options="menuOptions" direction="left" position="bottom">
|
||||
<MoreHorizontalIcon class="h-5 w-5 bg-transparent" />
|
||||
<template #extract><PackageOpenIcon /> Extract</template>
|
||||
<template #rename><EditIcon /> Rename</template>
|
||||
<template #move><RightArrowIcon /> Move</template>
|
||||
<template #download><DownloadIcon /> Download</template>
|
||||
<template #delete><TrashIcon /> Delete</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</li>
|
||||
@@ -90,6 +90,8 @@ import {
|
||||
} from '#components'
|
||||
import PaletteIcon from '~/assets/icons/palette.svg?component'
|
||||
|
||||
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
||||
|
||||
interface FileItemProps {
|
||||
name: string
|
||||
type: 'directory' | 'file'
|
||||
@@ -141,7 +143,7 @@ const codeExtensions = Object.freeze([
|
||||
'go',
|
||||
])
|
||||
|
||||
const textExtensions = Object.freeze(['txt', 'md', 'log', 'cfg', 'conf', 'properties', 'ini'])
|
||||
const textExtensions = Object.freeze(['txt', 'md', 'log', 'cfg', 'conf', 'properties', 'ini', 'sk'])
|
||||
const imageExtensions = Object.freeze(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'])
|
||||
const supportedArchiveExtensions = Object.freeze(['zip'])
|
||||
const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
<div class="flex h-full w-full items-center justify-center gap-6 p-20">
|
||||
<FileIcon class="size-28" />
|
||||
<div class="flex flex-col gap-2">
|
||||
<h3 class="text-red-500 m-0 text-2xl font-bold">{{ title }}</h3>
|
||||
<h3 class="m-0 text-2xl font-bold text-red-500">{{ title }}</h3>
|
||||
<p class="m-0 text-sm text-secondary">
|
||||
{{ message }}
|
||||
</p>
|
||||
<div class="flex gap-2">
|
||||
<ButtonStyled>
|
||||
<button size="sm" @click="$emit('refetch')">
|
||||
<UiServersIconsLoadingIcon class="h-5 w-5" />
|
||||
<LoadingIcon class="h-5 w-5" />
|
||||
Try again
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
@@ -28,6 +28,8 @@
|
||||
import { FileIcon, HomeIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
import LoadingIcon from './icons/LoadingIcon.vue'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
message: string
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
}"
|
||||
data-pyro-files-virtual-list
|
||||
>
|
||||
<UiServersFileItem
|
||||
<FileItem
|
||||
v-for="item in visibleItems"
|
||||
:key="item.path"
|
||||
:count="item.count"
|
||||
@@ -45,6 +45,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
import FileItem from './FileItem.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
items: any[]
|
||||
}>()
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<div class="flex flex-shrink-0 items-center gap-1">
|
||||
<div class="flex w-full flex-row-reverse sm:flex-row">
|
||||
<ButtonStyled type="transparent">
|
||||
<UiServersTeleportOverflowMenu
|
||||
<TeleportOverflowMenu
|
||||
position="bottom"
|
||||
direction="left"
|
||||
aria-label="Filter view"
|
||||
@@ -93,7 +93,7 @@
|
||||
<template #all>Show all</template>
|
||||
<template #filesOnly>Files only</template>
|
||||
<template #foldersOnly>Folders only</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
<div class="mx-1 w-full text-sm sm:w-48">
|
||||
<label for="search-folder" class="sr-only">Search folder</label>
|
||||
@@ -171,6 +171,8 @@ import { ButtonStyled, OverflowMenu } from '@modrinth/ui'
|
||||
import { useIntersectionObserver } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
breadcrumbSegments: string[]
|
||||
searchQuery: string
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<ShareIcon />
|
||||
</Button>
|
||||
<ButtonStyled type="transparent">
|
||||
<UiServersTeleportOverflowMenu
|
||||
<TeleportOverflowMenu
|
||||
position="bottom"
|
||||
direction="left"
|
||||
aria-label="Save file"
|
||||
@@ -100,7 +100,7 @@
|
||||
</svg>
|
||||
Save & restart
|
||||
</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</header>
|
||||
@@ -112,6 +112,8 @@ import { Button, ButtonStyled } from '@modrinth/ui'
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
breadcrumbSegments: string[]
|
||||
fileName?: string
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
v-if="state.hasError"
|
||||
class="flex h-full w-full flex-col items-center justify-center gap-8"
|
||||
>
|
||||
<UiServersIconsPanelErrorIcon />
|
||||
<PanelErrorIcon />
|
||||
<p class="m-0">{{ state.errorMessage || 'Invalid or empty image file.' }}</p>
|
||||
</div>
|
||||
<img
|
||||
@@ -57,6 +57,8 @@ import { ZoomInIcon, ZoomOutIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import PanelErrorIcon from './icons/PanelErrorIcon.vue'
|
||||
|
||||
const ZOOM_MIN = 0.1
|
||||
const ZOOM_MAX = 5
|
||||
const ZOOM_IN_FACTOR = 1.2
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
>
|
||||
<div class="flex flex-1 items-center gap-2 truncate">
|
||||
<transition-group name="status-icon" mode="out-in">
|
||||
<UiServersPanelSpinner
|
||||
<PanelSpinner
|
||||
v-show="item.status === 'uploading'"
|
||||
key="spinner"
|
||||
class="absolute !size-4"
|
||||
@@ -107,6 +107,8 @@ import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import type { FSModule } from '~/composables/servers/modules/fs.ts'
|
||||
|
||||
import PanelSpinner from './PanelSpinner.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
interface UploadItem {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:key="loader.name"
|
||||
class="group relative flex items-center justify-between rounded-2xl p-2 pr-2.5 hover:bg-bg"
|
||||
>
|
||||
<UiServersLoaderSelectorCard
|
||||
<LoaderSelectorCard
|
||||
:loader="loader"
|
||||
:is-current="isCurrentLoader(loader.name)"
|
||||
:loader-version="data.loader_version"
|
||||
@@ -24,7 +24,7 @@
|
||||
:key="loader.name"
|
||||
class="group relative flex items-center justify-between rounded-2xl p-2 pr-2.5 hover:bg-bg"
|
||||
>
|
||||
<UiServersLoaderSelectorCard
|
||||
<LoaderSelectorCard
|
||||
:loader="loader"
|
||||
:is-current="isCurrentLoader(loader.name)"
|
||||
:loader-version="data.loader_version"
|
||||
@@ -44,7 +44,7 @@
|
||||
:key="loader.name"
|
||||
class="group relative flex items-center justify-between rounded-2xl p-2 pr-2.5 hover:bg-bg"
|
||||
>
|
||||
<UiServersLoaderSelectorCard
|
||||
<LoaderSelectorCard
|
||||
:loader="loader"
|
||||
:is-current="isCurrentLoader(loader.name)"
|
||||
:loader-version="data.loader_version"
|
||||
@@ -58,6 +58,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LoaderSelectorCard from './LoaderSelectorCard.vue'
|
||||
const props = defineProps<{
|
||||
data: {
|
||||
loader: string | null
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
class="grid size-10 place-content-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
:class="isCurrentLoader ? '[&&]:bg-bg-green' : ''"
|
||||
>
|
||||
<UiServersIconsLoaderIcon
|
||||
<LoaderIcon
|
||||
:loader="loader.name"
|
||||
class="[&&]:size-6"
|
||||
:class="isCurrentLoader ? 'text-brand' : ''"
|
||||
@@ -43,6 +43,8 @@
|
||||
import { CheckIcon, DownloadIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
import LoaderIcon from './icons/LoaderIcon.vue'
|
||||
|
||||
interface LoaderInfo {
|
||||
name: 'Vanilla' | 'Fabric' | 'Forge' | 'Quilt' | 'Paper' | 'NeoForge' | 'Purpur'
|
||||
displayName: string
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Are you sure you want to
|
||||
<span class="lowercase">{{ confirmActionText }}</span> the server?
|
||||
</p>
|
||||
<UiCheckbox
|
||||
<Checkbox
|
||||
v-model="dontAskAgain"
|
||||
label="Don't ask me again"
|
||||
class="text-sm"
|
||||
@@ -34,7 +34,7 @@
|
||||
:header="`All of ${serverName || 'Server'} info`"
|
||||
@close="closeDetailsModal"
|
||||
>
|
||||
<UiServersServerInfoLabels
|
||||
<ServerInfoLabels
|
||||
:server-data="serverData"
|
||||
:show-game-label="true"
|
||||
:show-loader-label="true"
|
||||
@@ -53,7 +53,7 @@
|
||||
<div class="flex flex-row items-center gap-2 rounded-lg">
|
||||
<ButtonStyled v-if="isInstalling" type="standard" color="brand">
|
||||
<button disabled class="flex-shrink-0">
|
||||
<UiServersPanelSpinner class="size-5" /> Installing...
|
||||
<PanelSpinner class="size-5" /> Installing...
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
<ButtonStyled type="standard" color="brand">
|
||||
<button :disabled="!canTakeAction" @click="handlePrimaryAction">
|
||||
<div v-if="isTransitionState" class="grid place-content-center">
|
||||
<UiServersIconsLoadingIcon />
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
<component :is="isRunning ? UpdatedIcon : PlayIcon" v-else />
|
||||
<span>{{ primaryActionText }}</span>
|
||||
@@ -78,7 +78,7 @@
|
||||
</ButtonStyled>
|
||||
|
||||
<ButtonStyled circular type="transparent">
|
||||
<UiServersTeleportOverflowMenu :options="[...menuOptions]">
|
||||
<TeleportOverflowMenu :options="[...menuOptions]">
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #kill>
|
||||
<SlashIcon class="h-5 w-5" />
|
||||
@@ -96,7 +96,7 @@
|
||||
<ClipboardCopyIcon class="h-5 w-5" aria-hidden="true" />
|
||||
<span>Copy ID</span>
|
||||
</template>
|
||||
</UiServersTeleportOverflowMenu>
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</div>
|
||||
@@ -116,12 +116,17 @@ import {
|
||||
UpdatedIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { ButtonStyled, NewModal } from '@modrinth/ui'
|
||||
import { ButtonStyled, Checkbox, NewModal } from '@modrinth/ui'
|
||||
import type { PowerAction as ServerPowerAction, ServerState } from '@modrinth/utils'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import LoadingIcon from './icons/LoadingIcon.vue'
|
||||
import PanelSpinner from './PanelSpinner.vue'
|
||||
import ServerInfoLabels from './ServerInfoLabels.vue'
|
||||
import TeleportOverflowMenu from './TeleportOverflowMenu.vue'
|
||||
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
interface PowerAction {
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
class="group"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<UiServersLogLine :log="item" @show-full-log="showFullLogMessage" />
|
||||
<LogLine :log="item" @show-full-log="showFullLogMessage" />
|
||||
<div @mousedown.stop @click.stop>
|
||||
<button
|
||||
v-if="searchInput"
|
||||
@@ -223,8 +223,8 @@
|
||||
:class="{ hidden: searchInput || hasSelection || isSingleLineSelected }"
|
||||
@click="toggleFullscreen"
|
||||
>
|
||||
<LazyUiServersIconsMinimizeIconVue v-if="isFullScreen" />
|
||||
<LazyUiServersIconsFullscreenIcon v-else />
|
||||
<MinimizeIconVue v-if="isFullScreen" />
|
||||
<FullscreenIcon v-else />
|
||||
</button>
|
||||
|
||||
<Transition name="fade">
|
||||
@@ -306,6 +306,10 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useModrinthServersConsole } from '~/store/console.ts'
|
||||
|
||||
import FullscreenIcon from './icons/FullscreenIcon.vue'
|
||||
import MinimizeIconVue from './icons/MinimizeIcon.vue.vue'
|
||||
import LogLine from './LogLine.vue'
|
||||
|
||||
const { $cosmetics } = useNuxtApp()
|
||||
const cosmetics = $cosmetics
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col gap-4">
|
||||
<UiServersTeleportDropdownMenu
|
||||
<TeleportDropdownMenu
|
||||
v-if="props.versions?.length"
|
||||
v-model="selectedVersion"
|
||||
:options="versionOptions"
|
||||
@@ -68,7 +68,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DownloadIcon, XIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import {
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
TeleportDropdownMenu,
|
||||
} from '@modrinth/ui'
|
||||
import { ModrinthServersFetchError } from '@modrinth/utils'
|
||||
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
@@ -1,36 +1,7 @@
|
||||
<template>
|
||||
<NewModal ref="mrpackModal" header="Uploading mrpack" :closable="!isLoading" @show="onShow">
|
||||
<div class="flex flex-col gap-4 md:w-[600px]">
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 max-h-0"
|
||||
enter-to-class="opacity-100 max-h-20"
|
||||
leave-active-class="transition-all duration-200 ease-in"
|
||||
leave-from-class="opacity-100 max-h-20"
|
||||
leave-to-class="opacity-0 max-h-0"
|
||||
>
|
||||
<div v-if="isLoading" class="w-full">
|
||||
<div class="mb-2 flex justify-between text-sm">
|
||||
<Transition name="phrase-fade" mode="out-in">
|
||||
<span :key="currentPhrase" class="text-lg font-medium text-contrast">{{
|
||||
currentPhrase
|
||||
}}</span>
|
||||
</Transition>
|
||||
<div class="flex flex-col items-end">
|
||||
<span class="text-secondary">{{ Math.round(uploadProgress) }}%</span>
|
||||
<span class="text-xs text-secondary"
|
||||
>{{ formatBytes(uploadedBytes) }} / {{ formatBytes(totalBytes) }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-2 w-full rounded-full bg-divider">
|
||||
<div
|
||||
class="h-2 animate-pulse rounded-full bg-brand transition-all duration-300 ease-out"
|
||||
:style="{ width: `${uploadProgress}%` }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<AppearingProgressBar :max-value="totalBytes" :current-value="uploadedBytes" />
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
@@ -151,8 +122,14 @@ import {
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { BackupWarning, ButtonStyled, injectNotificationManager, NewModal } from '@modrinth/ui'
|
||||
import { formatBytes, ModrinthServersFetchError } from '@modrinth/utils'
|
||||
import {
|
||||
AppearingProgressBar,
|
||||
BackupWarning,
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
} from '@modrinth/ui'
|
||||
import { ModrinthServersFetchError } from '@modrinth/utils'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers'
|
||||
@@ -190,50 +167,9 @@ const hardReset = ref(false)
|
||||
const isLoading = ref(false)
|
||||
const loadingServerCheck = ref(false)
|
||||
const mrpackFile = ref<File | null>(null)
|
||||
const uploadProgress = ref(0)
|
||||
const uploadedBytes = ref(0)
|
||||
const totalBytes = ref(0)
|
||||
|
||||
const uploadPhrases = [
|
||||
'Removing Herobrine...',
|
||||
'Feeding parrots...',
|
||||
'Teaching villagers new trades...',
|
||||
'Convincing creepers to be friendly...',
|
||||
'Polishing diamonds...',
|
||||
'Training wolves to fetch...',
|
||||
'Building pixel art...',
|
||||
'Explaining redstone to beginners...',
|
||||
'Collecting all the cats...',
|
||||
'Negotiating with endermen...',
|
||||
'Planting suspicious stew ingredients...',
|
||||
'Calibrating TNT blast radius...',
|
||||
'Teaching chickens to fly...',
|
||||
'Sorting inventory alphabetically...',
|
||||
'Convincing iron golems to smile...',
|
||||
]
|
||||
|
||||
const currentPhrase = ref('Uploading...')
|
||||
let phraseInterval: NodeJS.Timeout | null = null
|
||||
const usedPhrases = ref(new Set<number>())
|
||||
|
||||
const getNextPhrase = () => {
|
||||
if (usedPhrases.value.size >= uploadPhrases.length) {
|
||||
const currentPhraseIndex = uploadPhrases.indexOf(currentPhrase.value)
|
||||
usedPhrases.value.clear()
|
||||
if (currentPhraseIndex !== -1) {
|
||||
usedPhrases.value.add(currentPhraseIndex)
|
||||
}
|
||||
}
|
||||
const availableIndices = uploadPhrases
|
||||
.map((_, index) => index)
|
||||
.filter((index) => !usedPhrases.value.has(index))
|
||||
|
||||
const randomIndex = availableIndices[Math.floor(Math.random() * availableIndices.length)]
|
||||
usedPhrases.value.add(randomIndex)
|
||||
|
||||
return uploadPhrases[randomIndex]
|
||||
}
|
||||
|
||||
const isDangerous = computed(() => hardReset.value)
|
||||
const canInstall = computed(() => !mrpackFile.value || isLoading.value || loadingServerCheck.value)
|
||||
|
||||
@@ -261,31 +197,17 @@ const handleReinstall = async () => {
|
||||
}
|
||||
|
||||
isLoading.value = true
|
||||
uploadProgress.value = 0
|
||||
uploadProgress.value = 0
|
||||
uploadedBytes.value = 0
|
||||
totalBytes.value = mrpackFile.value.size
|
||||
|
||||
currentPhrase.value = getNextPhrase()
|
||||
phraseInterval = setInterval(() => {
|
||||
currentPhrase.value = getNextPhrase()
|
||||
}, 4500)
|
||||
|
||||
const { onProgress, promise } = props.server.general.reinstallFromMrpack(
|
||||
mrpackFile.value,
|
||||
hardReset.value,
|
||||
)
|
||||
|
||||
onProgress(({ loaded, total, progress }) => {
|
||||
uploadProgress.value = progress
|
||||
onProgress(({ loaded, total }) => {
|
||||
uploadedBytes.value = loaded
|
||||
totalBytes.value = total
|
||||
|
||||
if (phraseInterval && progress >= 100) {
|
||||
clearInterval(phraseInterval)
|
||||
phraseInterval = null
|
||||
currentPhrase.value = 'Installing modpack...'
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
@@ -316,10 +238,6 @@ const handleReinstall = async () => {
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
if (phraseInterval) {
|
||||
clearInterval(phraseInterval)
|
||||
phraseInterval = null
|
||||
}
|
||||
}
|
||||
}
|
||||
const onShow = () => {
|
||||
@@ -328,15 +246,8 @@ const onShow = () => {
|
||||
loadingServerCheck.value = false
|
||||
isLoading.value = false
|
||||
mrpackFile.value = null
|
||||
uploadProgress.value = 0
|
||||
uploadedBytes.value = 0
|
||||
totalBytes.value = 0
|
||||
currentPhrase.value = 'Uploading...'
|
||||
usedPhrases.value.clear()
|
||||
if (phraseInterval) {
|
||||
clearInterval(phraseInterval)
|
||||
phraseInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
const show = () => mrpackModal.value?.show()
|
||||
@@ -349,14 +260,4 @@ defineExpose({ show, hide })
|
||||
.stylized-toggle:checked::after {
|
||||
background: var(--color-accent-contrast) !important;
|
||||
}
|
||||
|
||||
.phrase-fade-enter-active,
|
||||
.phrase-fade-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.phrase-fade-enter-from,
|
||||
.phrase-fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div
|
||||
class="grid size-16 place-content-center rounded-2xl border-[2px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
>
|
||||
<UiServersIconsLoaderIcon class="size-10" :loader="selectedLoader" />
|
||||
<LoaderIcon class="size-10" :loader="selectedLoader" />
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -54,7 +54,7 @@
|
||||
|
||||
<div class="flex w-full flex-col gap-2 rounded-2xl bg-table-alternateRow p-4">
|
||||
<div class="text-lg font-bold text-contrast">Minecraft version</div>
|
||||
<UiServersTeleportDropdownMenu
|
||||
<TeleportDropdownMenu
|
||||
v-model="selectedMCVersion"
|
||||
name="mcVersion"
|
||||
:options="mcVersions"
|
||||
@@ -102,13 +102,13 @@
|
||||
<div
|
||||
class="relative flex h-9 w-full items-center rounded-xl bg-button-bg px-4 opacity-50"
|
||||
>
|
||||
<UiServersIconsLoadingIcon class="mr-2 animate-spin" />
|
||||
<LoadingIcon class="mr-2 animate-spin" />
|
||||
Loading versions...
|
||||
<DropdownIcon class="absolute right-4" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="selectedLoaderVersions.length > 0">
|
||||
<UiServersTeleportDropdownMenu
|
||||
<TeleportDropdownMenu
|
||||
v-model="selectedLoaderVersion"
|
||||
name="loaderVersion"
|
||||
:options="selectedLoaderVersions"
|
||||
@@ -203,6 +203,7 @@ import {
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
NewModal,
|
||||
TeleportDropdownMenu,
|
||||
Toggle,
|
||||
} from '@modrinth/ui'
|
||||
import { type Loaders, ModrinthServersFetchError } from '@modrinth/utils'
|
||||
@@ -211,6 +212,9 @@ import { $fetch } from 'ofetch'
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||
|
||||
import LoaderIcon from './icons/LoaderIcon.vue'
|
||||
import LoadingIcon from './icons/LoadingIcon.vue'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="experimental-styles-within flex size-24 shrink-0 overflow-hidden rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
class="experimental-styles-within flex size-16 shrink-0 overflow-hidden rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
>
|
||||
<client-only>
|
||||
<img
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
<template>
|
||||
<div>
|
||||
<UiServersServerGameLabel
|
||||
<ServerGameLabel
|
||||
v-if="showGameLabel"
|
||||
:game="serverData.game"
|
||||
:mc-version="serverData.mc_version ?? ''"
|
||||
:is-link="linked"
|
||||
/>
|
||||
<UiServersServerLoaderLabel
|
||||
<ServerLoaderLabel
|
||||
:loader="serverData.loader"
|
||||
:loader-version="serverData.loader_version ?? ''"
|
||||
:no-separator="column"
|
||||
:is-link="linked"
|
||||
/>
|
||||
<UiServersServerSubdomainLabel
|
||||
<ServerSubdomainLabel
|
||||
v-if="serverData.net?.domain"
|
||||
:subdomain="serverData.net.domain"
|
||||
:no-separator="column"
|
||||
:is-link="linked"
|
||||
/>
|
||||
<UiServersServerUptimeLabel
|
||||
<ServerUptimeLabel
|
||||
v-if="uptimeSeconds"
|
||||
:uptime-seconds="uptimeSeconds"
|
||||
:no-separator="column"
|
||||
@@ -27,6 +27,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ServerGameLabel from './ServerGameLabel.vue'
|
||||
import ServerLoaderLabel from './ServerLoaderLabel.vue'
|
||||
import ServerSubdomainLabel from './ServerSubdomainLabel.vue'
|
||||
import ServerUptimeLabel from './ServerUptimeLabel.vue'
|
||||
|
||||
interface ServerInfoLabelsProps {
|
||||
serverData: Record<string, any>
|
||||
showGameLabel: boolean
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<LazyUiServersPlatformVersionSelectModal
|
||||
<PlatformVersionSelectModal
|
||||
ref="versionSelectModal"
|
||||
:server="props.server"
|
||||
:current-loader="ignoreCurrentInstallation ? undefined : (data?.loader as Loaders)"
|
||||
@@ -8,13 +8,13 @@
|
||||
@reinstall="emit('reinstall', $event)"
|
||||
/>
|
||||
|
||||
<LazyUiServersPlatformMrpackModal
|
||||
<PlatformMrpackModal
|
||||
ref="mrpackModal"
|
||||
:server="props.server"
|
||||
@reinstall="emit('reinstall', $event)"
|
||||
/>
|
||||
|
||||
<LazyUiServersPlatformChangeModpackVersionModal
|
||||
<PlatformChangeModpackVersionModal
|
||||
ref="modpackVersionModal"
|
||||
:server="props.server"
|
||||
:project="data?.project"
|
||||
@@ -137,7 +137,7 @@
|
||||
}"
|
||||
:tabindex="props.server.general?.status === 'installing' ? -1 : 0"
|
||||
>
|
||||
<UiServersLoaderSelector
|
||||
<LoaderSelector
|
||||
:data="
|
||||
ignoreCurrentInstallation
|
||||
? {
|
||||
@@ -165,6 +165,11 @@ import type { Loaders } from '@modrinth/utils'
|
||||
import type { ModrinthServer } from '~/composables/servers/modrinth-servers.ts'
|
||||
import type { BackupInProgressReason } from '~/pages/servers/manage/[id].vue'
|
||||
|
||||
import LoaderSelector from './LoaderSelector.vue'
|
||||
import PlatformChangeModpackVersionModal from './PlatformChangeModpackVersionModal.vue'
|
||||
import PlatformMrpackModal from './PlatformMrpackModal.vue'
|
||||
import PlatformVersionSelectModal from './PlatformVersionSelectModal.vue'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -1,113 +1,142 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
class="contents"
|
||||
:to="status === 'suspended' ? '' : `/servers/manage/${props.server_id}`"
|
||||
>
|
||||
<div
|
||||
v-tooltip="
|
||||
status === 'suspended'
|
||||
? suspension_reason === 'upgrading'
|
||||
? 'This server is being transferred to a new node. It will be unavailable until this process finishes.'
|
||||
: 'This server has been suspended. Please visit your billing settings or contact Modrinth Support for more information.'
|
||||
: ''
|
||||
"
|
||||
class="flex cursor-pointer flex-row items-center overflow-x-hidden rounded-3xl bg-bg-raised p-4 transition-transform duration-100"
|
||||
:class="status === 'suspended' ? '!rounded-b-none opacity-75' : 'active:scale-95'"
|
||||
data-pyro-server-listing
|
||||
:data-pyro-server-listing-id="server_id"
|
||||
>
|
||||
<UiServersServerIcon v-if="status !== 'suspended'" :image="image" />
|
||||
<div>
|
||||
<NuxtLink :to="status === 'suspended' ? '' : `/servers/manage/${props.server_id}`">
|
||||
<div
|
||||
v-else
|
||||
class="bg-bg-secondary flex size-24 items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
class="flex flex-row items-center overflow-x-hidden rounded-2xl border-[1px] border-solid border-button-bg bg-bg-raised p-4 transition-transform duration-100"
|
||||
:class="{
|
||||
'!rounded-b-none border-b-0': status === 'suspended' || !!pendingChange,
|
||||
'opacity-75': status === 'suspended',
|
||||
'active:scale-95': status !== 'suspended' && !pendingChange,
|
||||
}"
|
||||
data-pyro-server-listing
|
||||
:data-pyro-server-listing-id="server_id"
|
||||
>
|
||||
<LockIcon class="size-20 text-secondary" />
|
||||
</div>
|
||||
<div class="ml-8 flex flex-col gap-2.5">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 text-xl font-bold text-contrast">{{ name }}</h2>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
|
||||
<ServerIcon v-if="status !== 'suspended'" :image="image" />
|
||||
<div
|
||||
v-if="projectData?.title"
|
||||
class="m-0 flex flex-row items-center gap-2 text-sm font-medium text-secondary"
|
||||
>
|
||||
<Avatar
|
||||
:src="iconUrl"
|
||||
no-shadow
|
||||
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
|
||||
alt="Server Icon"
|
||||
/>
|
||||
Using {{ projectData?.title || 'Unknown' }}
|
||||
</div>
|
||||
<div v-else class="min-h-[20px]"></div>
|
||||
|
||||
<div
|
||||
v-if="isConfiguring"
|
||||
class="flex min-w-0 items-center gap-2 truncate text-sm font-semibold text-brand"
|
||||
>
|
||||
<SparklesIcon class="size-5 shrink-0" /> New server
|
||||
</div>
|
||||
<UiServersServerInfoLabels
|
||||
v-else
|
||||
:server-data="{ game, mc_version, loader, loader_version, net }"
|
||||
:show-game-label="showGameLabel"
|
||||
:show-loader-label="showLoaderLabel"
|
||||
:linked="false"
|
||||
class="pointer-events-none flex w-full flex-row flex-wrap items-center gap-4 text-secondary *:hidden sm:flex-row sm:*:flex"
|
||||
/>
|
||||
class="bg-bg-secondary flex size-16 items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
>
|
||||
<LockIcon class="size-12 text-secondary" />
|
||||
</div>
|
||||
<div class="ml-4 flex flex-col gap-2.5">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 text-xl font-bold text-contrast">{{ name }}</h2>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="projectData?.title"
|
||||
class="m-0 flex flex-row items-center gap-2 text-sm font-medium text-secondary"
|
||||
>
|
||||
<Avatar
|
||||
:src="iconUrl"
|
||||
no-shadow
|
||||
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
|
||||
alt="Server Icon"
|
||||
/>
|
||||
Using {{ projectData?.title || 'Unknown' }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isConfiguring"
|
||||
class="flex min-w-0 items-center gap-2 truncate text-sm font-semibold text-brand"
|
||||
>
|
||||
<SparklesIcon class="size-5 shrink-0" /> New server
|
||||
</div>
|
||||
<ServerInfoLabels
|
||||
v-else
|
||||
:server-data="{ game, mc_version, loader, loader_version, net }"
|
||||
:show-game-label="showGameLabel"
|
||||
:show-loader-label="showLoaderLabel"
|
||||
:linked="false"
|
||||
class="pointer-events-none flex w-full flex-row flex-wrap items-center gap-4 text-secondary *:hidden sm:flex-row sm:*:flex"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div
|
||||
v-if="status === 'suspended' && suspension_reason === 'upgrading'"
|
||||
class="relative -mt-4 flex w-full flex-row items-center gap-2 rounded-b-3xl bg-bg-blue p-4 text-sm font-bold text-contrast"
|
||||
class="relative flex w-full flex-row items-center gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-blue bg-bg-blue p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<UiServersPanelSpinner />
|
||||
<PanelSpinner />
|
||||
Your server's hardware is currently being upgraded and will be back online shortly.
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended' && suspension_reason === 'cancelled'"
|
||||
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been cancelled. Please
|
||||
update your billing information or contact Modrinth Support for more information.
|
||||
<PanelErrorIcon class="!size-5" /> Your server has been cancelled. Please update your
|
||||
billing information or contact Modrinth Support for more information.
|
||||
</div>
|
||||
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended' && suspension_reason"
|
||||
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been suspended:
|
||||
{{ suspension_reason }}. Please update your billing information or contact Modrinth Support
|
||||
for more information.
|
||||
<PanelErrorIcon class="!size-5" /> Your server has been suspended: {{ suspension_reason }}.
|
||||
Please update your billing information or contact Modrinth Support for more information.
|
||||
</div>
|
||||
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended'"
|
||||
class="relative -mt-4 flex w-full flex-col gap-2 rounded-b-3xl bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
<UiServersIconsPanelErrorIcon class="!size-5" /> Your server has been suspended. Please
|
||||
update your billing information or contact Modrinth Support for more information.
|
||||
<PanelErrorIcon class="!size-5" /> Your server has been suspended. Please update your
|
||||
billing information or contact Modrinth Support for more information.
|
||||
</div>
|
||||
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<div
|
||||
v-if="pendingChange && status !== 'suspended'"
|
||||
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-orange bg-bg-orange p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<div>
|
||||
Your server will {{ pendingChange.verb.toLowerCase() }} to the "{{
|
||||
pendingChange.planSize
|
||||
}}" plan on {{ formatDate(pendingChange.date) }}.
|
||||
</div>
|
||||
<ServersSpecs
|
||||
class="!font-normal !text-contrast"
|
||||
:ram="Math.round((pendingChange.ramGb ?? 0) * 1024)"
|
||||
:storage="Math.round((pendingChange.storageGb ?? 0) * 1024)"
|
||||
:cpus="pendingChange.cpuBurst"
|
||||
bursting-link="https://docs.modrinth.com/servers/bursting"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, LockIcon, SparklesIcon } from '@modrinth/assets'
|
||||
import { Avatar, CopyCode } from '@modrinth/ui'
|
||||
import { Avatar, CopyCode, ServersSpecs } from '@modrinth/ui'
|
||||
import type { Project, Server } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { useModrinthServers } from '~/composables/servers/modrinth-servers.ts'
|
||||
|
||||
const props = defineProps<Partial<Server>>()
|
||||
import PanelErrorIcon from './icons/PanelErrorIcon.vue'
|
||||
import PanelSpinner from './PanelSpinner.vue'
|
||||
import ServerIcon from './ServerIcon.vue'
|
||||
import ServerInfoLabels from './ServerInfoLabels.vue'
|
||||
|
||||
type PendingChange = {
|
||||
planSize: string
|
||||
cpu: number
|
||||
cpuBurst: number
|
||||
ramGb: number
|
||||
swapGb?: number
|
||||
storageGb?: number
|
||||
date: string | number | Date
|
||||
intervalChange?: string | null
|
||||
verb: string
|
||||
}
|
||||
|
||||
const props = defineProps<Partial<Server> & { pendingChange?: PendingChange }>()
|
||||
|
||||
if (props.server_id && props.status === 'available') {
|
||||
// Necessary only to get server icon
|
||||
@@ -134,4 +163,12 @@ if (props.upstream) {
|
||||
const image = useState<string | undefined>(`server-icon-${props.server_id}`, () => undefined)
|
||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined)
|
||||
const isConfiguring = computed(() => props.flows?.intro)
|
||||
|
||||
const formatDate = (d: unknown) => {
|
||||
try {
|
||||
return dayjs(d as any).format('MMMM D, YYYY')
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
src="~/assets/images/servers/minecraft_server_icon.png"
|
||||
/>
|
||||
<div class="absolute inset-0 grid place-content-center">
|
||||
<UiServersIconsLoadingIcon class="size-8 animate-spin text-contrast" />
|
||||
<LoadingIcon class="size-8 animate-spin text-contrast" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
@@ -18,3 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LoadingIcon from './icons/LoadingIcon.vue'
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div v-tooltip="'Change server loader'" class="flex min-w-0 flex-row items-center gap-4 truncate">
|
||||
<div v-if="!noSeparator" class="experimental-styles-within h-6 w-0.5 bg-button-border"></div>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<UiServersIconsLoaderIcon v-if="loader" :loader="loader" class="flex shrink-0 [&&]:size-5" />
|
||||
<LoaderIcon v-if="loader" :loader="loader" class="flex shrink-0 [&&]:size-5" />
|
||||
<div v-else class="size-5 shrink-0 animate-pulse rounded-full bg-button-border"></div>
|
||||
<NuxtLink
|
||||
v-if="isLink"
|
||||
@@ -34,6 +34,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import LoaderIcon from './icons/LoaderIcon.vue'
|
||||
defineProps<{
|
||||
noSeparator?: boolean
|
||||
loader?: 'Fabric' | 'Quilt' | 'Forge' | 'NeoForge' | 'Paper' | 'Spigot' | 'Bukkit' | 'Vanilla'
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div v-if="!noSeparator" class="experimental-styles-within h-6 w-0.5 bg-button-border"></div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<UiServersIconsTimer class="flex size-5 shrink-0" />
|
||||
<Timer class="flex size-5 shrink-0" />
|
||||
<time class="truncate text-sm font-semibold" :aria-label="verboseUptime">
|
||||
{{ formattedUptime }}
|
||||
</time>
|
||||
@@ -19,6 +19,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Timer from './icons/Timer.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
uptimeSeconds: number
|
||||
noSeparator?: boolean
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<ModrinthServersPurchaseModal
|
||||
v-if="customer"
|
||||
ref="purchaseModal"
|
||||
:publishable-key="config.public.stripePublishableKey"
|
||||
:initiate-payment="async (body) => await initiatePayment(body)"
|
||||
:available-products="pyroProducts"
|
||||
:on-error="handleError"
|
||||
:customer="customer"
|
||||
:payment-methods="paymentMethods"
|
||||
:currency="selectedCurrency"
|
||||
:return-url="`${config.public.siteUrl}/servers/manage`"
|
||||
:pings="regionPings"
|
||||
:regions="regions"
|
||||
:refresh-payment-methods="fetchPaymentData"
|
||||
:fetch-stock="fetchStock"
|
||||
:plan-stage="true"
|
||||
:existing-plan="currentPlanFromSubscription"
|
||||
:existing-subscription="subscription || undefined"
|
||||
:on-finalize-no-payment-change="finalizeDowngrade"
|
||||
@hide="
|
||||
() => {
|
||||
subscription = null
|
||||
}
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { injectNotificationManager, ModrinthServersPurchaseModal } from '@modrinth/ui'
|
||||
import type { ServerPlan } from '@modrinth/ui/src/utils/billing'
|
||||
import type { UserSubscription } from '@modrinth/utils'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
|
||||
import { useServersFetch } from '~/composables/servers/servers-fetch.ts'
|
||||
import { products } from '~/generated/state.json'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const purchaseModal = ref<InstanceType<typeof ModrinthServersPurchaseModal> | null>(null)
|
||||
const customer = ref<any>(null)
|
||||
const paymentMethods = ref<any[]>([])
|
||||
const selectedCurrency = ref<string>('USD')
|
||||
const regions = ref<any[]>([])
|
||||
const regionPings = ref<any[]>([])
|
||||
|
||||
const pyroProducts = (products as any[])
|
||||
.filter((p) => p?.metadata?.type === 'pyro')
|
||||
.sort((a, b) => (a?.metadata?.ram ?? 0) - (b?.metadata?.ram ?? 0))
|
||||
|
||||
function handleError(err: any) {
|
||||
console.error('Purchase modal error:', err)
|
||||
}
|
||||
|
||||
async function fetchPaymentData() {
|
||||
try {
|
||||
const [customerData, paymentMethodsData] = await Promise.all([
|
||||
useBaseFetch('billing/customer', { internal: true }),
|
||||
useBaseFetch('billing/payment_methods', { internal: true }),
|
||||
])
|
||||
customer.value = customerData as any
|
||||
paymentMethods.value = paymentMethodsData as any[]
|
||||
} catch (error) {
|
||||
console.error('Error fetching payment data:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function fetchStock(region: any, request: any) {
|
||||
return useServersFetch(`stock?region=${region.shortcode}`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
...request,
|
||||
},
|
||||
bypassAuth: true,
|
||||
}).then((res: any) => res.available as number)
|
||||
}
|
||||
|
||||
function pingRegions() {
|
||||
useServersFetch('regions', {
|
||||
method: 'GET',
|
||||
version: 1,
|
||||
bypassAuth: true,
|
||||
}).then((res: any) => {
|
||||
regions.value = res as any[]
|
||||
;(regions.value as any[]).forEach((region: any) => {
|
||||
runPingTest(region)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const PING_COUNT = 20
|
||||
const PING_INTERVAL = 200
|
||||
const MAX_PING_TIME = 1000
|
||||
|
||||
function runPingTest(region: any, index = 1) {
|
||||
if (index > 10) {
|
||||
regionPings.value.push({
|
||||
region: region.shortcode,
|
||||
ping: -1,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const wsUrl = `wss://${region.shortcode}${index}.${region.zone}/pingtest`
|
||||
try {
|
||||
const socket = new WebSocket(wsUrl)
|
||||
const pings: number[] = []
|
||||
|
||||
socket.onopen = () => {
|
||||
for (let i = 0; i < PING_COUNT; i++) {
|
||||
setTimeout(() => {
|
||||
socket.send(String(performance.now()))
|
||||
}, i * PING_INTERVAL)
|
||||
}
|
||||
setTimeout(
|
||||
() => {
|
||||
socket.close()
|
||||
const median = Math.round([...pings].sort((a, b) => a - b)[Math.floor(pings.length / 2)])
|
||||
if (median) {
|
||||
regionPings.value.push({
|
||||
region: region.shortcode,
|
||||
ping: median,
|
||||
})
|
||||
}
|
||||
},
|
||||
PING_COUNT * PING_INTERVAL + MAX_PING_TIME,
|
||||
)
|
||||
}
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const start = Number(event.data)
|
||||
pings.push(performance.now() - start)
|
||||
}
|
||||
|
||||
socket.onerror = () => {
|
||||
runPingTest(region, index + 1)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const subscription = ref<UserSubscription | null>(null)
|
||||
// Dry run state
|
||||
const dryRunResponse = ref<{
|
||||
requires_payment: boolean
|
||||
required_payment_is_proration: boolean
|
||||
} | null>(null)
|
||||
const pendingDowngradeBody = ref<any | null>(null)
|
||||
const currentPlanFromSubscription = computed<ServerPlan | undefined>(() => {
|
||||
return subscription.value
|
||||
? (pyroProducts.find(
|
||||
(p) =>
|
||||
p.prices.filter((price: { id: string }) => price.id === subscription.value?.price_id)
|
||||
.length > 0,
|
||||
) ?? undefined)
|
||||
: undefined
|
||||
})
|
||||
|
||||
async function initiatePayment(body: any): Promise<any> {
|
||||
if (subscription.value) {
|
||||
const transformedBody = {
|
||||
interval: body.charge?.interval,
|
||||
payment_method: body.id,
|
||||
product: body.charge?.product_id,
|
||||
region: body.metadata?.server_region,
|
||||
}
|
||||
|
||||
try {
|
||||
const dry = await useBaseFetch(`billing/subscription/${subscription.value.id}?dry=true`, {
|
||||
internal: true,
|
||||
method: 'PATCH',
|
||||
body: transformedBody,
|
||||
})
|
||||
|
||||
if (dry && typeof dry === 'object' && 'requires_payment' in dry) {
|
||||
dryRunResponse.value = dry as any
|
||||
pendingDowngradeBody.value = transformedBody
|
||||
if (dry.requires_payment) {
|
||||
return await finalizeImmediate(transformedBody)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
// Fallback if dry run not supported
|
||||
return await finalizeImmediate(transformedBody)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Dry run failed, attempting immediate patch', e)
|
||||
return await finalizeImmediate(transformedBody)
|
||||
}
|
||||
} else {
|
||||
addNotification({
|
||||
title: 'Unable to determine subscription ID.',
|
||||
text: 'Please contact support.',
|
||||
type: 'error',
|
||||
})
|
||||
return Promise.reject(new Error('Unable to determine subscription ID.'))
|
||||
}
|
||||
}
|
||||
|
||||
async function finalizeImmediate(body: any) {
|
||||
const result = await useBaseFetch(`billing/subscription/${subscription.value?.id}`, {
|
||||
internal: true,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
async function finalizeDowngrade() {
|
||||
if (!subscription.value || !pendingDowngradeBody.value) return
|
||||
try {
|
||||
await finalizeImmediate(pendingDowngradeBody.value)
|
||||
addNotification({
|
||||
title: 'Subscription updated',
|
||||
text: 'Your plan has been downgraded and will take effect next billing cycle.',
|
||||
type: 'success',
|
||||
})
|
||||
} catch (e) {
|
||||
addNotification({
|
||||
title: 'Failed to apply subscription changes',
|
||||
text: 'Please try again or contact support.',
|
||||
type: 'error',
|
||||
})
|
||||
throw e
|
||||
} finally {
|
||||
dryRunResponse.value = null
|
||||
pendingDowngradeBody.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function open(id?: string) {
|
||||
if (id) {
|
||||
const subscriptions = (await useBaseFetch(`billing/subscriptions`, {
|
||||
internal: true,
|
||||
})) as any[]
|
||||
for (const sub of subscriptions) {
|
||||
if (sub?.metadata?.id === id) {
|
||||
subscription.value = sub
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
subscription.value = null
|
||||
}
|
||||
|
||||
purchaseModal.value?.show('quarterly')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchPaymentData()
|
||||
pingRegions()
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="overlay"></div>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/medal-banner-background.webp"
|
||||
class="background-pattern dark-pattern shadow-xl"
|
||||
alt=""
|
||||
/>
|
||||
<img
|
||||
src="https://cdn-raw.modrinth.com/medal-banner-background-light.webp"
|
||||
class="background-pattern light-pattern shadow-xl"
|
||||
alt=""
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
<style scoped lang="scss">
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--medal-promotion-bg-gradient);
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.light-mode,
|
||||
.light {
|
||||
.background-pattern.dark-pattern {
|
||||
display: none;
|
||||
}
|
||||
.background-pattern.light-pattern {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.background-pattern.dark-pattern {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.background-pattern.light-pattern {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.background-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
object-fit: cover;
|
||||
object-position: bottom;
|
||||
background-color: var(--medal-promotion-bg);
|
||||
border-radius: inherit;
|
||||
color: var(--medal-promotion-bg-orange);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div
|
||||
id="medal"
|
||||
class="medal-promotion flex w-full flex-col items-start gap-4 rounded-xl px-4 py-4 shadow-xl md:flex-row md:items-center md:justify-between md:gap-0 md:px-8 md:py-6"
|
||||
>
|
||||
<MedalBackgroundImage />
|
||||
<div
|
||||
class="z-10 flex items-start gap-3 text-xl font-semibold text-contrast md:items-center md:gap-6 md:text-2xl"
|
||||
>
|
||||
<MedalIcon class="h-8 w-auto text-contrast md:h-10" />
|
||||
<div class="flex flex-col items-start gap-1">
|
||||
<span>
|
||||
Try a free
|
||||
<span class="text-medal-orange">3GB server</span> for 5 days powered by
|
||||
<span class="text-medal-orange">Medal</span>
|
||||
</span>
|
||||
<span class="text-xs font-medium text-secondary md:text-sm">
|
||||
Limited-time offer. No credit card required. Available for US servers.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonStyled color="medal-promo" type="outlined" size="large" class="z-10 my-auto mt-2">
|
||||
<nuxt-link
|
||||
to="https://medal.tv/modrinth"
|
||||
class="z-10 flex w-full items-center justify-center gap-1 md:mt-0 md:w-auto"
|
||||
>Learn more <ExternalIcon
|
||||
/></nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ExternalIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
|
||||
import MedalIcon from '~/assets/images/illustrations/medal_icon.svg?component'
|
||||
import MedalBackgroundImage from '~/components/ui/servers/marketing/MedalBackgroundImage.vue'
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.medal-promotion {
|
||||
position: relative;
|
||||
border: 1px solid var(--medal-promotion-bg-orange);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.clock-glow {
|
||||
filter: drop-shadow(0 0 72px var(--medal-promotion-bg-orange))
|
||||
drop-shadow(0 0 36px var(--medal-promotion-bg-orange))
|
||||
drop-shadow(0 0 18px var(--medal-promotion-bg-orange));
|
||||
}
|
||||
|
||||
.text-medal-orange {
|
||||
color: var(--medal-promotion-text-orange);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div
|
||||
class="medal-promotion relative flex w-full flex-row items-center justify-between rounded-2xl p-4 shadow-xl"
|
||||
>
|
||||
<MedalBackgroundImage />
|
||||
|
||||
<div class="z-10 mr-2 flex flex-col gap-1">
|
||||
<Transition
|
||||
enter-from-class="opacity-0 translate-y-1"
|
||||
enter-active-class="transition-all duration-300"
|
||||
enter-to-class="opacity-100 translate-y-0"
|
||||
leave-from-class="opacity-100 translate-y-0"
|
||||
leave-active-class="transition-all duration-150"
|
||||
leave-to-class="opacity-0 -translate-y-1"
|
||||
>
|
||||
<div
|
||||
v-if="expiryDate"
|
||||
class="flex items-center gap-2 whitespace-nowrap font-semibold text-contrast"
|
||||
>
|
||||
<ClockIcon class="clock-glow text-medal-orange size-5 shrink-0" />
|
||||
<span class="w-full text-wrap text-lg">
|
||||
Your <span class="text-medal-orange">Medal</span>-powered Modrinth Server will expire in
|
||||
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.days }}</span> days
|
||||
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.hours }}</span> hours
|
||||
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.minutes }}</span> minutes
|
||||
<span class="text-medal-orange font-bold">{{ timeLeftCountdown.seconds }}</span>
|
||||
seconds.
|
||||
</span>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<ButtonStyled color="medal-promo" type="outlined" size="large">
|
||||
<button class="z-10 my-auto" @click="openUpgradeModal"><RocketIcon /> Upgrade</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
<ServersUpgradeModalWrapper ref="upgradeModal" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ClockIcon, RocketIcon } from '@modrinth/assets'
|
||||
import { ButtonStyled } from '@modrinth/ui'
|
||||
import type { UserSubscription } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjsDuration from 'dayjs/plugin/duration'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
|
||||
import MedalBackgroundImage from '~/components/ui/servers/marketing/MedalBackgroundImage.vue'
|
||||
|
||||
import ServersUpgradeModalWrapper from '../ServersUpgradeModalWrapper.vue'
|
||||
|
||||
dayjs.extend(dayjsDuration)
|
||||
|
||||
type UpgradeWrapperRef = ComponentPublicInstance<{ open: (id?: string) => void | Promise<void> }>
|
||||
const upgradeModal = ref<UpgradeWrapperRef | null>(null)
|
||||
|
||||
const props = defineProps<{
|
||||
serverId?: string
|
||||
}>()
|
||||
|
||||
const { data: subscriptions } = await useLazyAsyncData(
|
||||
'countdown-subscriptions',
|
||||
() =>
|
||||
useBaseFetch(`billing/subscriptions`, {
|
||||
internal: true,
|
||||
}) as Promise<UserSubscription[]>,
|
||||
)
|
||||
|
||||
const expiryDate = computed(() => {
|
||||
for (const subscription of subscriptions.value || []) {
|
||||
if (subscription.metadata?.id === props.serverId) {
|
||||
return dayjs(subscription.created).add(5, 'days')
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
|
||||
function openUpgradeModal() {
|
||||
upgradeModal.value?.open(props.serverId)
|
||||
}
|
||||
|
||||
const timeLeftCountdown = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
|
||||
|
||||
function updateCountdown() {
|
||||
if (!expiryDate.value) {
|
||||
timeLeftCountdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
|
||||
return
|
||||
}
|
||||
|
||||
const now = dayjs()
|
||||
const diff = expiryDate.value.diff(now)
|
||||
|
||||
if (diff <= 0) {
|
||||
timeLeftCountdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
|
||||
return
|
||||
}
|
||||
|
||||
const duration = dayjs.duration(diff)
|
||||
timeLeftCountdown.value = {
|
||||
days: duration.days(),
|
||||
hours: duration.hours(),
|
||||
minutes: duration.minutes(),
|
||||
seconds: duration.seconds(),
|
||||
}
|
||||
}
|
||||
|
||||
updateCountdown()
|
||||
|
||||
const intervalId = ref<NodeJS.Timeout | null>(null)
|
||||
onMounted(() => {
|
||||
intervalId.value = setInterval(updateCountdown, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId.value) clearInterval(intervalId.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.medal-promotion {
|
||||
position: relative;
|
||||
border: 1px solid var(--medal-promotion-bg-orange);
|
||||
background: inherit; // allows overlay + pattern to take over
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--medal-promotion-bg-gradient);
|
||||
z-index: 1;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.background-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
background-color: var(--medal-promotion-bg);
|
||||
border-radius: inherit;
|
||||
color: var(--medal-promotion-text-orange);
|
||||
}
|
||||
|
||||
.clock-glow {
|
||||
filter: drop-shadow(0 0 72px var(--color-orange)) drop-shadow(0 0 36px var(--color-orange))
|
||||
drop-shadow(0 0 18px var(--color-orange));
|
||||
}
|
||||
|
||||
.text-medal-orange {
|
||||
color: var(--medal-promotion-text-orange);
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,226 @@
|
||||
<template>
|
||||
<div class="rounded-2xl shadow-xl">
|
||||
<div
|
||||
class="medal-promotion flex flex-row items-center overflow-x-hidden rounded-t-2xl p-4 transition-transform duration-100"
|
||||
:class="status === 'suspended' ? 'rounded-b-none border-b-0 opacity-75' : 'rounded-b-2xl'"
|
||||
data-pyro-server-listing
|
||||
:data-pyro-server-listing-id="server_id"
|
||||
>
|
||||
<MedalBackgroundImage />
|
||||
<AutoLink
|
||||
:to="status === 'suspended' ? '' : `/servers/manage/${props.server_id}`"
|
||||
class="z-10 flex flex-grow flex-row items-center overflow-x-hidden"
|
||||
:class="status !== 'suspended' && 'active:scale-95'"
|
||||
>
|
||||
<Avatar
|
||||
v-if="status !== 'suspended'"
|
||||
src="https://cdn-raw.modrinth.com/medal_icon.webp"
|
||||
size="64px"
|
||||
class="z-10"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="bg-bg-secondary z-10 flex size-16 shrink-0 items-center justify-center rounded-xl border-[1px] border-solid border-button-border bg-button-bg shadow-sm"
|
||||
>
|
||||
<LockIcon class="size-12 text-secondary" />
|
||||
</div>
|
||||
|
||||
<div class="z-10 ml-4 flex min-w-0 flex-col gap-2.5">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<h2 class="m-0 truncate text-xl font-bold text-contrast">{{ name }}</h2>
|
||||
<ChevronRightIcon />
|
||||
|
||||
<span class="truncate">
|
||||
<span class="text-medal-orange">
|
||||
{{ timeLeftCountdown.days }}
|
||||
</span>
|
||||
days
|
||||
<span class="text-medal-orange">
|
||||
{{ timeLeftCountdown.hours }}
|
||||
</span>
|
||||
hours
|
||||
<span class="text-medal-orange">
|
||||
{{ timeLeftCountdown.minutes }}
|
||||
</span>
|
||||
minutes
|
||||
<span class="text-medal-orange">
|
||||
{{ timeLeftCountdown.seconds }}
|
||||
</span>
|
||||
seconds remaining...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="projectData?.title"
|
||||
class="m-0 flex flex-row items-center gap-2 text-sm font-medium text-secondary"
|
||||
>
|
||||
<Avatar
|
||||
:src="iconUrl"
|
||||
no-shadow
|
||||
style="min-height: 20px; min-width: 20px; height: 20px; width: 20px"
|
||||
alt="Server Icon"
|
||||
/>
|
||||
Using {{ projectData?.title || 'Unknown' }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isConfiguring"
|
||||
class="text-medal-orange flex min-w-0 items-center gap-2 truncate text-sm font-semibold"
|
||||
>
|
||||
<SparklesIcon class="size-5 shrink-0" /> New server
|
||||
</div>
|
||||
<ServerInfoLabels
|
||||
v-else
|
||||
:server-data="{ game, mc_version, loader, loader_version, net }"
|
||||
:show-game-label="showGameLabel"
|
||||
:show-loader-label="showLoaderLabel"
|
||||
:linked="false"
|
||||
class="pointer-events-none flex w-full flex-row flex-wrap items-center gap-4 text-secondary *:hidden sm:flex-row sm:*:flex"
|
||||
/>
|
||||
</div>
|
||||
</AutoLink>
|
||||
|
||||
<div class="z-10 ml-auto">
|
||||
<ButtonStyled color="medal-promo" type="outlined" size="large">
|
||||
<button class="my-auto" @click="handleUpgrade"><RocketIcon /> Upgrade</button>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status === 'suspended' && suspension_reason === 'upgrading'"
|
||||
class="relative flex w-full flex-row items-center gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-blue bg-bg-blue p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<PanelSpinner />
|
||||
Your server's hardware is currently being upgraded and will be back online shortly.
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended' && suspension_reason === 'cancelled'"
|
||||
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
<PanelErrorIcon class="!size-5" /> Your Medal server trial has ended and your server has
|
||||
been suspended. Please upgrade to continue to use your server.
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended' && suspension_reason"
|
||||
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
<PanelErrorIcon class="!size-5" /> Your server has been suspended: {{ suspension_reason }}.
|
||||
Please update your billing information or contact Modrinth Support for more information.
|
||||
</div>
|
||||
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="status === 'suspended'"
|
||||
class="relative flex w-full flex-col gap-2 rounded-b-2xl border-[1px] border-t-0 border-solid border-bg-red bg-bg-red p-4 text-sm font-bold text-contrast"
|
||||
>
|
||||
<div class="flex flex-row gap-2">
|
||||
<PanelErrorIcon class="!size-5" /> Your server has been suspended. Please update your
|
||||
billing information or contact Modrinth Support for more information.
|
||||
</div>
|
||||
<CopyCode :text="`${props.server_id}`" class="ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, LockIcon, RocketIcon, SparklesIcon } from '@modrinth/assets'
|
||||
import { AutoLink, Avatar, ButtonStyled, CopyCode } from '@modrinth/ui'
|
||||
import type { Project, Server } from '@modrinth/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjsDuration from 'dayjs/plugin/duration'
|
||||
|
||||
import MedalBackgroundImage from '~/components/ui/servers/marketing/MedalBackgroundImage.vue'
|
||||
|
||||
import PanelErrorIcon from '../icons/PanelErrorIcon.vue'
|
||||
import PanelSpinner from '../PanelSpinner.vue'
|
||||
import ServerInfoLabels from '../ServerInfoLabels.vue'
|
||||
|
||||
dayjs.extend(dayjsDuration)
|
||||
|
||||
const props = defineProps<Partial<Server>>()
|
||||
const emit = defineEmits<{ (e: 'upgrade'): void }>()
|
||||
|
||||
const showGameLabel = computed(() => !!props.game)
|
||||
const showLoaderLabel = computed(() => !!props.loader)
|
||||
|
||||
let projectData: Ref<Project | null>
|
||||
if (props.upstream) {
|
||||
const { data } = await useAsyncData<Project>(
|
||||
`server-project-${props.server_id}`,
|
||||
async (): Promise<Project> => {
|
||||
const result = await useBaseFetch(`project/${props.upstream?.project_id}`)
|
||||
return result as Project
|
||||
},
|
||||
)
|
||||
projectData = data
|
||||
} else {
|
||||
projectData = ref(null)
|
||||
}
|
||||
|
||||
const iconUrl = computed(() => projectData.value?.icon_url || undefined)
|
||||
const isConfiguring = computed(() => props.flows?.intro)
|
||||
|
||||
const timeLeftCountdown = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
|
||||
const expiryDate = computed(() => (props.medal_expires ? dayjs(props.medal_expires) : null))
|
||||
|
||||
function handleUpgrade(event: Event) {
|
||||
event.stopPropagation()
|
||||
emit('upgrade')
|
||||
}
|
||||
|
||||
function updateCountdown() {
|
||||
if (!expiryDate.value) {
|
||||
timeLeftCountdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
|
||||
return
|
||||
}
|
||||
|
||||
const now = dayjs()
|
||||
const diff = expiryDate.value.diff(now)
|
||||
|
||||
if (diff <= 0) {
|
||||
timeLeftCountdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
|
||||
return
|
||||
}
|
||||
|
||||
const duration = dayjs.duration(diff)
|
||||
timeLeftCountdown.value = {
|
||||
days: duration.days(),
|
||||
hours: duration.hours(),
|
||||
minutes: duration.minutes(),
|
||||
seconds: duration.seconds(),
|
||||
}
|
||||
}
|
||||
|
||||
watch(expiryDate, () => updateCountdown(), { immediate: true })
|
||||
|
||||
const intervalId = ref<NodeJS.Timeout | null>(null)
|
||||
onMounted(() => {
|
||||
intervalId.value = setInterval(updateCountdown, 1000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId.value) clearInterval(intervalId.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.medal-promotion {
|
||||
position: relative;
|
||||
border: 1px solid var(--medal-promotion-bg-orange);
|
||||
background: inherit; // allows overlay + pattern to take over
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-medal-orange {
|
||||
color: var(--medal-promotion-text-orange);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.border-medal-orange {
|
||||
border-color: var(--medal-promotion-bg-orange);
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { EditIcon, SettingsIcon, TrashIcon } from '@modrinth/assets'
|
||||
import { SettingsIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
CopyCode,
|
||||
getDismissableMetadata,
|
||||
NOTICE_LEVELS,
|
||||
@@ -73,7 +71,7 @@ defineProps<{
|
||||
</TagItem>
|
||||
</div>
|
||||
<div class="col-span-2 flex gap-2 md:col-span-1">
|
||||
<ButtonStyled>
|
||||
<!-- <ButtonStyled>
|
||||
<button @click="() => startEditing(notice)">
|
||||
<EditIcon /> {{ formatMessage(commonMessages.editButton) }}
|
||||
</button>
|
||||
@@ -82,7 +80,7 @@ defineProps<{
|
||||
<button @click="() => deleteNotice(notice)">
|
||||
<TrashIcon /> {{ formatMessage(commonMessages.deleteLabel) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</ButtonStyled> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full grid">
|
||||
|
||||
@@ -109,7 +109,7 @@ export const getAuthUrl = (provider, redirect = '/dashboard') => {
|
||||
const route = useNativeRoute()
|
||||
|
||||
const fullURL = route.query.launcher
|
||||
? 'https://launcher-files.modrinth.com'
|
||||
? getLauncherRedirectUrl(route)
|
||||
: `${config.public.siteUrl}/auth/sign-in?redirect=${redirect}`
|
||||
|
||||
return `${config.public.apiBaseUrl}auth/init?provider=${provider}&url=${encodeURIComponent(fullURL)}`
|
||||
@@ -131,3 +131,12 @@ export const removeAuthProvider = async (provider) => {
|
||||
|
||||
stopLoading()
|
||||
}
|
||||
|
||||
export const getLauncherRedirectUrl = (route) => {
|
||||
const usesLocalhostRedirectionScheme =
|
||||
['4', '6'].includes(route.query.ipver) && Number(route.query.port) < 65536
|
||||
|
||||
return usesLocalhostRedirectionScheme
|
||||
? `http://${route.query.ipver === '4' ? '127.0.0.1' : '[::1]'}:${route.query.port}`
|
||||
: `https://launcher-files.modrinth.com`
|
||||
}
|
||||
|
||||
187
apps/frontend/src/composables/avalara1099.ts
Normal file
187
apps/frontend/src/composables/avalara1099.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export interface FormRequestAttributes {
|
||||
form_type: 'W-9' | 'W-8BEN' | 'W-8BEN-E' | string
|
||||
company_id: number
|
||||
company_name: string
|
||||
company_email: string
|
||||
reference_id: string | null
|
||||
form_id: string | null
|
||||
signed_at: string | null
|
||||
tin_match_status: string | null
|
||||
expires_at: string | null
|
||||
}
|
||||
|
||||
export interface FormRequestLinks {
|
||||
action_validate?: string
|
||||
action_complete?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface FormRequestData {
|
||||
id: string
|
||||
type: 'form_request' | string
|
||||
attributes: FormRequestAttributes
|
||||
links?: FormRequestLinks
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface FormRequestResponse {
|
||||
data: FormRequestData
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface UseAvalara1099Options {
|
||||
prefill?: Record<string, unknown>
|
||||
// Optional override for the origin (defaults to vendor CDN domain)
|
||||
origin?: string
|
||||
// Optional hook to further style the injected dialog/iframe
|
||||
styleDialog?: (dialog: HTMLDialogElement, iframe: HTMLIFrameElement | null) => void
|
||||
// Poll interval while waiting for global to appear
|
||||
pollIntervalMs?: number
|
||||
// Max time to wait for script before rejecting (ms); 0/undefined => no timeout
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
interface AvalaraGlobal {
|
||||
requestW9: (
|
||||
formRequest: FormRequestResponse | FormRequestData,
|
||||
opts?: { prefill?: Record<string, unknown> },
|
||||
) => Promise<FormRequestResponse> | any
|
||||
requestW8BEN: (
|
||||
formRequest: FormRequestResponse | FormRequestData,
|
||||
opts?: { prefill?: Record<string, unknown> },
|
||||
) => Promise<FormRequestResponse> | any
|
||||
requestW8BENE: (
|
||||
formRequest: FormRequestResponse | FormRequestData,
|
||||
opts?: { prefill?: Record<string, unknown> },
|
||||
) => Promise<FormRequestResponse> | any
|
||||
origin?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Avalara1099?: AvalaraGlobal
|
||||
}
|
||||
}
|
||||
|
||||
const injectedKey = '__avalara1099_script_injected__'
|
||||
|
||||
function ensureScriptInjected(origin: string) {
|
||||
if (import.meta.server) return
|
||||
const w = window as any
|
||||
if (w[injectedKey]) return
|
||||
w[injectedKey] = true
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
src: `${origin.replace(/\/$/, '')}/api/request_form.js`,
|
||||
crossorigin: 'anonymous',
|
||||
type: 'module',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForAvalara(opts: {
|
||||
pollIntervalMs: number
|
||||
timeoutMs?: number
|
||||
origin: string
|
||||
}): Promise<AvalaraGlobal> {
|
||||
if (import.meta.server) throw new Error('Avalara 1099 is client-side only')
|
||||
ensureScriptInjected(opts.origin)
|
||||
const start = Date.now()
|
||||
return await new Promise((resolve, reject) => {
|
||||
const poll = () => {
|
||||
const g = window.Avalara1099
|
||||
if (g) return resolve(g)
|
||||
if (opts.timeoutMs && opts.timeoutMs > 0 && Date.now() - start > opts.timeoutMs) {
|
||||
return reject(new Error('Timed out waiting for Avalara1099 script to load'))
|
||||
}
|
||||
setTimeout(poll, opts.pollIntervalMs)
|
||||
}
|
||||
poll()
|
||||
})
|
||||
}
|
||||
|
||||
export function useAvalara1099(
|
||||
initial: FormRequestResponse | FormRequestData,
|
||||
options: UseAvalara1099Options = {},
|
||||
) {
|
||||
const origin = options.origin || 'https://www.track1099.com'
|
||||
const pollIntervalMs = options.pollIntervalMs ?? 250
|
||||
const timeoutMs = options.timeoutMs
|
||||
|
||||
const request: Ref<FormRequestResponse | FormRequestData> = ref(initial)
|
||||
const loading = ref(false)
|
||||
const error: Ref<unknown> = ref(null)
|
||||
|
||||
const signedAt = computed(() => {
|
||||
const data = (request.value as FormRequestResponse).data || request.value
|
||||
return data.attributes?.signed_at ? new Date(data.attributes.signed_at) : null
|
||||
})
|
||||
|
||||
const status = computed(() => (signedAt.value ? 'signed' : 'incomplete'))
|
||||
|
||||
async function start(): Promise<FormRequestResponse | FormRequestData> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const g = await waitForAvalara({ pollIntervalMs, timeoutMs, origin })
|
||||
const data = (request.value as FormRequestResponse).data || request.value
|
||||
const formType = data.attributes?.form_type
|
||||
|
||||
// Defensive deep clone to strip proxies / non-cloneable refs before postMessage
|
||||
// (DataCloneError guard)
|
||||
let safeRequest: any
|
||||
try {
|
||||
safeRequest = JSON.parse(JSON.stringify(request.value))
|
||||
} catch {
|
||||
// Fallback shallow copy
|
||||
safeRequest = Array.isArray(request.value)
|
||||
? [...(request.value as any)]
|
||||
: { ...(request.value as any) }
|
||||
}
|
||||
let safePrefill: any = undefined
|
||||
if (options.prefill) {
|
||||
try {
|
||||
safePrefill = JSON.parse(JSON.stringify(options.prefill))
|
||||
} catch {
|
||||
safePrefill = { ...options.prefill }
|
||||
}
|
||||
}
|
||||
|
||||
let promise: any
|
||||
if (formType === 'W-8BEN') {
|
||||
promise = g.requestW8BEN(safeRequest, { prefill: safePrefill })
|
||||
} else if (formType === 'W-9') {
|
||||
promise = g.requestW9(safeRequest, { prefill: safePrefill })
|
||||
} else if (formType === 'W-8BEN-E' || formType === 'W-8BEN E') {
|
||||
promise = g.requestW8BENE(safeRequest, { prefill: safePrefill })
|
||||
} else {
|
||||
throw new Error(`Unsupported form_type: ${formType}`)
|
||||
}
|
||||
|
||||
// The vendor promise resolves with an updated form request (signed state)
|
||||
const newReq = await promise
|
||||
request.value = newReq
|
||||
return newReq
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
request,
|
||||
signedAt,
|
||||
status, // 'signed' | 'incomplete'
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,11 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
showVersionFilesInTable: false,
|
||||
// showAdsWithPlus: false,
|
||||
alwaysShowChecklistAsPopup: true,
|
||||
testTaxForm: false,
|
||||
|
||||
// Feature toggles
|
||||
projectTypesPrimaryNav: false,
|
||||
enableMedalPromotion: true,
|
||||
hidePlusPromoInUserMenu: false,
|
||||
oldProjectCards: true,
|
||||
newProjectCards: false,
|
||||
@@ -34,6 +36,9 @@ export const DEFAULT_FEATURE_FLAGS = validateValues({
|
||||
showProjectPageDownloadModalServersPromo: false,
|
||||
showProjectPageCreateServersTooltip: true,
|
||||
showProjectPageQuickServerButton: false,
|
||||
newProjectGeneralSettings: false,
|
||||
newProjectEnvironmentSettings: true,
|
||||
hideRussiaCensorshipBanner: false,
|
||||
// advancedRendering: true,
|
||||
// externalLinksNewTab: true,
|
||||
// notUsingBlockers: false,
|
||||
|
||||
@@ -41,12 +41,6 @@ export class BackupsModule extends ServerModule {
|
||||
await this.fetch() // Refresh this module
|
||||
}
|
||||
|
||||
async prepare(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/prepare-download`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
async lock(backupId: string): Promise<void> {
|
||||
await useServersFetch(`servers/${this.serverId}/backups/${backupId}/lock`, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -34,6 +34,8 @@ export class GeneralModule extends ServerModule implements ServerGeneral {
|
||||
node!: { token: string; instance: string }
|
||||
flows?: { intro?: boolean }
|
||||
|
||||
is_medal?: boolean
|
||||
|
||||
async fetch(): Promise<void> {
|
||||
const data = await useServersFetch<ServerGeneral>(`servers/${this.serverId}`, {}, 'general')
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ export const initUserProjects = async () => {
|
||||
if (auth.user && auth.user.id) {
|
||||
try {
|
||||
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`)
|
||||
user.projectsV3 = await useBaseFetch(`user/${auth.user.id}/projects`, { apiVersion: 3 })
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<ModrinthLoadingIndicator />
|
||||
<NotificationPanel />
|
||||
<div class="main experimental-styles-within">
|
||||
<div v-if="is404" class="error-graphic">
|
||||
<Logo404 />
|
||||
@@ -50,10 +52,16 @@
|
||||
|
||||
<script setup>
|
||||
import { SadRinthbot } from '@modrinth/assets'
|
||||
import { NotificationPanel, provideNotificationManager } from '@modrinth/ui'
|
||||
import { defineMessage, useVIntl } from '@vintl/vintl'
|
||||
import { IntlFormatted } from '@vintl/vintl/components'
|
||||
|
||||
import Logo404 from '~/assets/images/404.svg'
|
||||
|
||||
import ModrinthLoadingIndicator from './components/ui/modrinth-loading-indicator.ts'
|
||||
import { FrontendNotificationManager } from './providers/frontend-notifications.ts'
|
||||
|
||||
provideNotificationManager(new FrontendNotificationManager())
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const props = defineProps({
|
||||
@@ -93,6 +101,17 @@ const messages = {
|
||||
defaultMessage: "The page you were looking for doesn't seem to exist.",
|
||||
}),
|
||||
},
|
||||
451: {
|
||||
title: defineMessage({
|
||||
id: 'error.generic.451.title',
|
||||
defaultMessage: 'Content unavailable for legal reasons',
|
||||
}),
|
||||
subtitle: defineMessage({
|
||||
id: 'error.generic.451.subtitle',
|
||||
defaultMessage:
|
||||
'This page has been blocked for legal reasons, such as government censorship or ongoing legal proceedings.',
|
||||
}),
|
||||
},
|
||||
default: {
|
||||
title: defineMessage({
|
||||
id: 'error.generic.default.title',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { injectNotificationManager } from '@modrinth/ui'
|
||||
import type { Organization, Project, Report, User, Version } from '@modrinth/utils'
|
||||
|
||||
type Thread = { id: string }
|
||||
@@ -156,24 +155,14 @@ function isSimilar(a: PlatformNotification, b: PlatformNotification | undefined)
|
||||
export async function markAsRead(
|
||||
ids: string[],
|
||||
): Promise<(notifications: PlatformNotification[]) => PlatformNotification[]> {
|
||||
try {
|
||||
await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, {
|
||||
method: 'PATCH',
|
||||
await useBaseFetch(`notifications?ids=${JSON.stringify([...new Set(ids)])}`, {
|
||||
method: 'PATCH',
|
||||
})
|
||||
return (notifications: PlatformNotification[]) => {
|
||||
const newNotifs = notifications ?? []
|
||||
newNotifs.forEach((n) => {
|
||||
if (ids.includes(n.id)) n.read = true
|
||||
})
|
||||
return (notifications: PlatformNotification[]) => {
|
||||
const newNotifs = notifications ?? []
|
||||
newNotifs.forEach((n) => {
|
||||
if (ids.includes(n.id)) n.read = true
|
||||
})
|
||||
return newNotifs
|
||||
}
|
||||
} catch (err: any) {
|
||||
const { addNotification } = injectNotificationManager()
|
||||
addNotification({
|
||||
title: 'Error marking notification as read',
|
||||
text: err?.data?.description ?? err,
|
||||
type: 'error',
|
||||
})
|
||||
return () => []
|
||||
return newNotifs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,76 @@
|
||||
</div>
|
||||
</div>
|
||||
<div ref="main_page" class="layout" :class="{ 'expanded-mobile-nav': isBrowseMenuOpen }">
|
||||
<PagewideBanner v-if="isRussia && !flags.hideRussiaCensorshipBanner" variant="error">
|
||||
<template #title>
|
||||
<div class="flex flex-col gap-1 text-contrast">
|
||||
<span lang="ru">К сожалению, Modrinth скоро станет недоступен в России</span>
|
||||
<span class="text-sm font-medium opacity-50" lang="en">
|
||||
Modrinth will soon be unavailable in Russia
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #description>
|
||||
<p class="m-0" lang="ru">
|
||||
Российское правительство потребовало от нас заблокировать некоторые проекты на Modrinth,
|
||||
но мы решили отказать им в цензуре.
|
||||
</p>
|
||||
<p class="-mt-2 mb-0 text-sm opacity-50" lang="en">
|
||||
The Russian government has asked us to censor certain topics on Modrinth and we have
|
||||
decided to refuse to comply with their requests.
|
||||
</p>
|
||||
|
||||
<p class="m-0 font-semibold" lang="ru">
|
||||
Пожалуйста, найдите какой-нибудь надёжный VPN или прокси, чтобы не потерять доступ к
|
||||
Modrinth.
|
||||
</p>
|
||||
<p class="-mt-2 mb-0 text-sm opacity-50" lang="en">
|
||||
Please seek a reputable VPN or proxy of some kind to continue to access Modrinth in
|
||||
Russia.
|
||||
</p>
|
||||
</template>
|
||||
<template #actions>
|
||||
<div class="mt-2 flex w-fit gap-2">
|
||||
<ButtonStyled color="brand">
|
||||
<nuxt-link to="/news/article/standing-by-our-values-russian">
|
||||
<BookTextIcon /> Прочесть наше полное заявление
|
||||
<span class="text-xs font-medium">(Перевод на русский)</span>
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled>
|
||||
<nuxt-link to="/news/article/standing-by-our-values">
|
||||
<BookTextIcon /> Read our full statement
|
||||
<span class="text-xs font-medium">(English)</span>
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions_right>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="formatMessage(commonMessages.closeButton)"
|
||||
@click="hideRussiaCensorshipBanner"
|
||||
>
|
||||
<XIcon :aria-label="formatMessage(commonMessages.closeButton)" />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PagewideBanner>
|
||||
<PagewideBanner v-if="showTaxComplianceBanner" variant="warning">
|
||||
<template #title>
|
||||
<span>{{ formatMessage(taxBannerMessages.title) }}</span>
|
||||
</template>
|
||||
<template #description>
|
||||
<span>{{ formatMessage(taxBannerMessages.description) }}</span>
|
||||
</template>
|
||||
<template #actions>
|
||||
<ButtonStyled color="orange">
|
||||
<button @click="openTaxForm">
|
||||
<FileTextIcon /> {{ formatMessage(taxBannerMessages.action) }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</PagewideBanner>
|
||||
<PagewideBanner
|
||||
v-if="auth.user && !auth.user.email_verified && route.path !== '/auth/verify-email'"
|
||||
variant="warning"
|
||||
@@ -93,7 +163,12 @@
|
||||
{{ formatMessage(stagingBannerMessages.description) }}
|
||||
</template>
|
||||
<template #actions_right>
|
||||
<Button transparent icon-only aria-label="Close" @click="hideStagingBanner">
|
||||
<Button
|
||||
transparent
|
||||
icon-only
|
||||
:aria-label="formatMessage(commonMessages.closeButton)"
|
||||
@click="hideStagingBanner"
|
||||
>
|
||||
<XIcon aria-hidden="true" />
|
||||
</Button>
|
||||
</template>
|
||||
@@ -111,12 +186,18 @@
|
||||
}}
|
||||
</template>
|
||||
</PagewideBanner>
|
||||
|
||||
<CreatorTaxFormModal
|
||||
ref="taxFormModalRef"
|
||||
close-button-text="Close"
|
||||
:emit-success-on-close="false"
|
||||
/>
|
||||
<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-6 py-4 lg:grid-cols-[auto_1fr_auto]"
|
||||
>
|
||||
<div>
|
||||
<NuxtLink to="/" aria-label="Modrinth home page">
|
||||
<BrandTextLogo aria-hidden="true" class="h-7 w-auto text-contrast" />
|
||||
<NuxtLink to="/" :aria-label="formatMessage(messages.modrinthHomePage)">
|
||||
<TextLogo aria-hidden="true" class="h-7 w-auto text-contrast" />
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<div
|
||||
@@ -130,7 +211,10 @@
|
||||
route.name === 'search-mods' ? 'main-nav-primary' : 'main-nav-secondary'
|
||||
"
|
||||
>
|
||||
<nuxt-link to="/mods"> <BoxIcon aria-hidden="true" /> Mods </nuxt-link>
|
||||
<nuxt-link to="/mods">
|
||||
<BoxIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.mod) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
@@ -142,7 +226,8 @@
|
||||
"
|
||||
>
|
||||
<nuxt-link to="/resourcepacks">
|
||||
<PaintbrushIcon aria-hidden="true" /> Resource Packs
|
||||
<PaintbrushIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.resourcepack) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
@@ -152,7 +237,10 @@
|
||||
route.name === 'search-datapacks' ? 'main-nav-primary' : 'main-nav-secondary'
|
||||
"
|
||||
>
|
||||
<nuxt-link to="/datapacks"> <BracesIcon aria-hidden="true" /> Data Packs </nuxt-link>
|
||||
<nuxt-link to="/datapacks">
|
||||
<BracesIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.datapack) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
@@ -161,7 +249,10 @@
|
||||
route.name === 'search-modpacks' ? 'main-nav-primary' : 'main-nav-secondary'
|
||||
"
|
||||
>
|
||||
<nuxt-link to="/modpacks"> <PackageOpenIcon aria-hidden="true" /> Modpacks </nuxt-link>
|
||||
<nuxt-link to="/modpacks">
|
||||
<PackageOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.modpack) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
@@ -170,7 +261,10 @@
|
||||
route.name === 'search-shaders' ? 'main-nav-primary' : 'main-nav-secondary'
|
||||
"
|
||||
>
|
||||
<nuxt-link to="/shaders"> <GlassesIcon aria-hidden="true" /> Shaders </nuxt-link>
|
||||
<nuxt-link to="/shaders">
|
||||
<GlassesIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.shader) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
type="transparent"
|
||||
@@ -179,7 +273,10 @@
|
||||
route.name === 'search-plugins' ? 'main-nav-primary' : 'main-nav-secondary'
|
||||
"
|
||||
>
|
||||
<nuxt-link to="/plugins"> <PlugIcon aria-hidden="true" /> Plugins </nuxt-link>
|
||||
<nuxt-link to="/plugins">
|
||||
<PlugIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.plugin) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -244,18 +341,36 @@
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<CompassIcon v-else aria-hidden="true" />
|
||||
<span class="hidden md:contents">Discover content</span>
|
||||
<span class="contents md:hidden">Discover</span>
|
||||
<span class="hidden md:contents">{{
|
||||
formatMessage(navMenuMessages.discoverContent)
|
||||
}}</span>
|
||||
<span class="contents md:hidden">{{ formatMessage(navMenuMessages.discover) }}</span>
|
||||
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" />
|
||||
|
||||
<template #mods> <BoxIcon aria-hidden="true" /> Mods </template>
|
||||
<template #resourcepacks>
|
||||
<PaintbrushIcon aria-hidden="true" /> Resource Packs
|
||||
<template #mods>
|
||||
<BoxIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.mod) }}
|
||||
</template>
|
||||
<template #resourcepacks>
|
||||
<PaintbrushIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.resourcepack) }}
|
||||
</template>
|
||||
<template #datapacks>
|
||||
<BracesIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.datapack) }}
|
||||
</template>
|
||||
<template #plugins>
|
||||
<PlugIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.plugin) }}
|
||||
</template>
|
||||
<template #shaders>
|
||||
<GlassesIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.shader) }}
|
||||
</template>
|
||||
<template #modpacks>
|
||||
<PackageOpenIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonProjectTypeCategoryMessages.modpack) }}
|
||||
</template>
|
||||
<template #datapacks> <BracesIcon aria-hidden="true" /> Data Packs </template>
|
||||
<template #plugins> <PlugIcon aria-hidden="true" /> Plugins </template>
|
||||
<template #shaders> <GlassesIcon aria-hidden="true" /> Shaders </template>
|
||||
<template #modpacks> <PackageOpenIcon aria-hidden="true" /> Modpacks </template>
|
||||
</TeleportOverflowMenu>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled
|
||||
@@ -270,14 +385,18 @@
|
||||
>
|
||||
<nuxt-link to="/servers">
|
||||
<ServerIcon aria-hidden="true" />
|
||||
Host a server
|
||||
{{ formatMessage(navMenuMessages.hostAServer) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled type="transparent" :highlighted="route.name === 'app'">
|
||||
<nuxt-link to="/app">
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
<span class="hidden md:contents">Get Modrinth App</span>
|
||||
<span class="contents md:hidden">Modrinth App</span>
|
||||
<span class="hidden md:contents">{{
|
||||
formatMessage(navMenuMessages.getModrinthApp)
|
||||
}}</span>
|
||||
<span class="contents md:hidden">{{
|
||||
formatMessage(navMenuMessages.modrinthApp)
|
||||
}}</span>
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -290,7 +409,7 @@
|
||||
position="bottom"
|
||||
direction="left"
|
||||
:dropdown-id="`${basePopoutId}-staff`"
|
||||
aria-label="Create new..."
|
||||
:aria-label="formatMessage(messages.createNew)"
|
||||
:options="[
|
||||
{
|
||||
id: 'review-projects',
|
||||
@@ -302,6 +421,13 @@
|
||||
color: 'orange',
|
||||
link: '/moderation/reports',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'file-lookup',
|
||||
link: '/admin/file_lookup',
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
shown: isAdmin(auth.user),
|
||||
@@ -322,11 +448,20 @@
|
||||
>
|
||||
<ModrinthIcon aria-hidden="true" />
|
||||
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" />
|
||||
<template #review-projects> <ScaleIcon aria-hidden="true" /> Review projects </template>
|
||||
<template #review-reports> <ReportIcon aria-hidden="true" /> Reports </template>
|
||||
<template #user-lookup> <UserIcon aria-hidden="true" /> Lookup by email </template>
|
||||
<template #review-projects>
|
||||
<ScaleIcon aria-hidden="true" /> {{ formatMessage(messages.reviewProjects) }}
|
||||
</template>
|
||||
<template #review-reports>
|
||||
<ReportIcon aria-hidden="true" /> {{ formatMessage(messages.reports) }}
|
||||
</template>
|
||||
<template #user-lookup>
|
||||
<UserIcon aria-hidden="true" /> {{ formatMessage(messages.lookupByEmail) }}
|
||||
</template>
|
||||
<template #file-lookup>
|
||||
<FileIcon aria-hidden="true" /> {{ formatMessage(messages.fileLookup) }}
|
||||
</template>
|
||||
<template #servers-notices>
|
||||
<IssuesIcon aria-hidden="true" /> Manage server notices
|
||||
<IssuesIcon aria-hidden="true" /> {{ formatMessage(messages.manageServerNotices) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
@@ -337,7 +472,7 @@
|
||||
position="bottom"
|
||||
direction="left"
|
||||
:dropdown-id="`${basePopoutId}-create`"
|
||||
aria-label="Create new..."
|
||||
:aria-label="formatMessage(messages.createNew)"
|
||||
:options="[
|
||||
{
|
||||
id: 'new-project',
|
||||
@@ -356,13 +491,15 @@
|
||||
>
|
||||
<PlusIcon aria-hidden="true" />
|
||||
<DropdownIcon aria-hidden="true" class="h-5 w-5 text-secondary" />
|
||||
<template #new-project> <BoxIcon aria-hidden="true" /> New project </template>
|
||||
<template #new-project>
|
||||
<BoxIcon aria-hidden="true" /> {{ formatMessage(messages.newProject) }}
|
||||
</template>
|
||||
<!-- <template #import-project> <BoxImportIcon /> Import project </template>-->
|
||||
<template #new-collection>
|
||||
<CollectionIcon aria-hidden="true" /> New collection
|
||||
<CollectionIcon aria-hidden="true" /> {{ formatMessage(messages.newCollection) }}
|
||||
</template>
|
||||
<template #new-organization>
|
||||
<OrganizationIcon aria-hidden="true" /> New organization
|
||||
<OrganizationIcon aria-hidden="true" /> {{ formatMessage(messages.newOrganization) }}
|
||||
</template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
@@ -374,34 +511,57 @@
|
||||
>
|
||||
<Avatar :src="auth.user.avatar_url" aria-hidden="true" circle />
|
||||
<DropdownIcon class="h-5 w-5 text-secondary" />
|
||||
<template #profile> <UserIcon aria-hidden="true" /> Profile </template>
|
||||
<template #notifications> <BellIcon aria-hidden="true" /> Notifications </template>
|
||||
<template #saved> <BookmarkIcon aria-hidden="true" /> Saved projects </template>
|
||||
<template #servers> <ServerIcon aria-hidden="true" /> My servers </template>
|
||||
<template #profile>
|
||||
<UserIcon aria-hidden="true" /> {{ formatMessage(messages.profile) }}
|
||||
</template>
|
||||
<template #notifications>
|
||||
<BellIcon aria-hidden="true" /> {{ formatMessage(commonMessages.notificationsLabel) }}
|
||||
</template>
|
||||
<template #saved>
|
||||
<BookmarkIcon aria-hidden="true" /> {{ formatMessage(messages.savedProjects) }}
|
||||
</template>
|
||||
<template #servers>
|
||||
<ServerIcon aria-hidden="true" /> {{ formatMessage(commonMessages.serversLabel) }}
|
||||
</template>
|
||||
<template #plus>
|
||||
<ArrowBigUpDashIcon aria-hidden="true" /> Upgrade to Modrinth+
|
||||
<ArrowBigUpDashIcon aria-hidden="true" />
|
||||
{{ formatMessage(messages.upgradeToModrinthPlus) }}
|
||||
</template>
|
||||
<template #settings>
|
||||
<SettingsIcon aria-hidden="true" /> {{ formatMessage(commonMessages.settingsLabel) }}
|
||||
</template>
|
||||
<template #flags>
|
||||
<ReportIcon aria-hidden="true" /> {{ formatMessage(messages.featureFlags) }}
|
||||
</template>
|
||||
<template #projects>
|
||||
<BoxIcon aria-hidden="true" /> {{ formatMessage(messages.projects) }}
|
||||
</template>
|
||||
<template #settings> <SettingsIcon aria-hidden="true" /> Settings </template>
|
||||
<template #flags> <ReportIcon aria-hidden="true" /> Feature flags </template>
|
||||
<template #projects> <BoxIcon aria-hidden="true" /> Projects </template>
|
||||
<template #organizations>
|
||||
<OrganizationIcon aria-hidden="true" /> Organizations
|
||||
<OrganizationIcon aria-hidden="true" /> {{ formatMessage(messages.organizations) }}
|
||||
</template>
|
||||
<template #revenue>
|
||||
<CurrencyIcon aria-hidden="true" /> {{ formatMessage(messages.revenue) }}
|
||||
</template>
|
||||
<template #analytics>
|
||||
<ChartIcon aria-hidden="true" /> {{ formatMessage(messages.analytics) }}
|
||||
</template>
|
||||
<template #moderation>
|
||||
<ScaleIcon aria-hidden="true" /> {{ formatMessage(commonMessages.moderationLabel) }}
|
||||
</template>
|
||||
<template #sign-out>
|
||||
<LogOutIcon aria-hidden="true" /> {{ formatMessage(commonMessages.signOutButton) }}
|
||||
</template>
|
||||
<template #revenue> <CurrencyIcon aria-hidden="true" /> Revenue </template>
|
||||
<template #analytics> <ChartIcon aria-hidden="true" /> Analytics </template>
|
||||
<template #moderation> <ScaleIcon aria-hidden="true" /> Moderation </template>
|
||||
<template #sign-out> <LogOutIcon aria-hidden="true" /> Sign out </template>
|
||||
</OverflowMenu>
|
||||
<template v-else>
|
||||
<ButtonStyled color="brand">
|
||||
<nuxt-link to="/auth/sign-in">
|
||||
<LogInIcon aria-hidden="true" />
|
||||
Sign in
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular>
|
||||
<nuxt-link v-tooltip="'Settings'" to="/settings">
|
||||
<SettingsIcon aria-label="Settings" />
|
||||
<nuxt-link :v-tooltip="formatMessage(commonMessages.settingsLabel)" to="/settings">
|
||||
<SettingsIcon :aria-label="formatMessage(commonMessages.settingsLabel)" />
|
||||
</nuxt-link>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
@@ -450,8 +610,7 @@
|
||||
</div>
|
||||
</NuxtLink>
|
||||
<nuxt-link v-else class="iconified-button brand-button" to="/auth/sign-in">
|
||||
<LogInIcon aria-hidden="true" />
|
||||
{{ formatMessage(commonMessages.signInButton) }}
|
||||
<LogInIcon aria-hidden="true" /> {{ formatMessage(commonMessages.signInButton) }}
|
||||
</nuxt-link>
|
||||
</div>
|
||||
<div class="links">
|
||||
@@ -482,7 +641,7 @@
|
||||
</NuxtLink>
|
||||
<NuxtLink v-if="flags.developerMode" class="iconified-button" to="/flags">
|
||||
<ReportIcon aria-hidden="true" />
|
||||
Feature flags
|
||||
{{ formatMessage(messages.featureFlags) }}
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<NuxtLink class="iconified-button" to="/settings">
|
||||
@@ -503,7 +662,7 @@
|
||||
to="/"
|
||||
class="tab button-animation"
|
||||
:title="formatMessage(navMenuMessages.home)"
|
||||
aria-label="Home"
|
||||
:aria-label="formatMessage(navMenuMessages.home)"
|
||||
>
|
||||
<HomeIcon aria-hidden="true" />
|
||||
</NuxtLink>
|
||||
@@ -511,7 +670,7 @@
|
||||
class="tab button-animation"
|
||||
:class="{ 'router-link-exact-active': isBrowseMenuOpen }"
|
||||
:title="formatMessage(navMenuMessages.search)"
|
||||
aria-label="Search"
|
||||
:aria-label="formatMessage(navMenuMessages.search)"
|
||||
@click="toggleBrowseMenu()"
|
||||
>
|
||||
<template v-if="auth.user">
|
||||
@@ -526,7 +685,7 @@
|
||||
<NuxtLink
|
||||
to="/dashboard/notifications"
|
||||
class="tab button-animation"
|
||||
aria-label="Notifications"
|
||||
:aria-label="formatMessage(commonMessages.notificationsLabel)"
|
||||
:class="{
|
||||
'no-active': isMobileMenuOpen || isBrowseMenuOpen,
|
||||
}"
|
||||
@@ -543,7 +702,7 @@
|
||||
<NuxtLink
|
||||
to="/dashboard"
|
||||
class="tab button-animation"
|
||||
aria-label="Dashboard"
|
||||
:aria-label="formatMessage(commonMessages.dashboardLabel)"
|
||||
:title="formatMessage(commonMessages.dashboardLabel)"
|
||||
>
|
||||
<ChartIcon aria-hidden="true" />
|
||||
@@ -573,7 +732,7 @@
|
||||
</div>
|
||||
</header>
|
||||
<main class="min-h-[calc(100vh-4.5rem-310.59px)]">
|
||||
<ModalCreation v-if="auth.user" ref="modal_creation" />
|
||||
<ProjectCreateModal v-if="auth.user" ref="modal_creation" />
|
||||
<CollectionCreateModal ref="modal_collection_creation" />
|
||||
<OrganizationCreateModal ref="modal_organization_creation" />
|
||||
<slot id="main" />
|
||||
@@ -588,9 +747,9 @@
|
||||
<div
|
||||
class="flex flex-col items-center gap-3 md:items-start"
|
||||
role="region"
|
||||
aria-label="Modrinth information"
|
||||
:aria-label="formatMessage(messages.modrinthInformation)"
|
||||
>
|
||||
<BrandTextLogo
|
||||
<TextLogo
|
||||
aria-hidden="true"
|
||||
class="text-logo button-base h-6 w-auto text-contrast lg:h-8"
|
||||
@click="developerModeIncrement()"
|
||||
@@ -671,6 +830,7 @@ import {
|
||||
BellIcon,
|
||||
BlueskyIcon,
|
||||
BookmarkIcon,
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
BracesIcon,
|
||||
ChartIcon,
|
||||
@@ -680,6 +840,8 @@ import {
|
||||
DiscordIcon,
|
||||
DownloadIcon,
|
||||
DropdownIcon,
|
||||
FileIcon,
|
||||
FileTextIcon,
|
||||
GithubIcon,
|
||||
GlassesIcon,
|
||||
HamburgerIcon,
|
||||
@@ -711,25 +873,32 @@ import {
|
||||
Button,
|
||||
ButtonStyled,
|
||||
commonMessages,
|
||||
commonProjectTypeCategoryMessages,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
PagewideBanner,
|
||||
} from '@modrinth/ui'
|
||||
import { isAdmin, isStaff } from '@modrinth/utils'
|
||||
import { IntlFormatted } from '@vintl/vintl/components'
|
||||
|
||||
import CollectionCreateModal from '~/components/ui/CollectionCreateModal.vue'
|
||||
import ModalCreation from '~/components/ui/ModalCreation.vue'
|
||||
import OrganizationCreateModal from '~/components/ui/OrganizationCreateModal.vue'
|
||||
import TextLogo from '~/components/brand/TextLogo.vue'
|
||||
import CollectionCreateModal from '~/components/ui/create/CollectionCreateModal.vue'
|
||||
import OrganizationCreateModal from '~/components/ui/create/OrganizationCreateModal.vue'
|
||||
import ProjectCreateModal from '~/components/ui/create/ProjectCreateModal.vue'
|
||||
import CreatorTaxFormModal from '~/components/ui/dashboard/CreatorTaxFormModal.vue'
|
||||
import TeleportOverflowMenu from '~/components/ui/servers/TeleportOverflowMenu.vue'
|
||||
import { errors as generatedStateErrors } from '~/generated/state.json'
|
||||
import { getProjectTypeMessage } from '~/utils/i18n-project-type.ts'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const country = useUserCountry()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
const auth = await useAuth()
|
||||
const user = await useUser()
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
|
||||
const cosmetics = useCosmetics()
|
||||
const flags = useFeatureFlags()
|
||||
|
||||
@@ -738,7 +907,60 @@ const route = useNativeRoute()
|
||||
const router = useNativeRouter()
|
||||
const link = config.public.siteUrl + route.path.replace(/\/+$/, '')
|
||||
|
||||
const { data: payoutBalance } = await useAsyncData('payout/balance', () =>
|
||||
useBaseFetch('payout/balance', { apiVersion: 3 }),
|
||||
)
|
||||
|
||||
const showTaxComplianceBanner = computed(() => {
|
||||
if (flags.value.testTaxForm && auth.value.user) return true
|
||||
const bal = payoutBalance.value
|
||||
if (!bal) return false
|
||||
const thresholdMet = (bal.withdrawn_ytd ?? 0) >= 600
|
||||
const status = bal.form_completion_status ?? 'unknown'
|
||||
const isComplete = status === 'complete'
|
||||
return !!auth.value.user && thresholdMet && !isComplete
|
||||
})
|
||||
|
||||
const taxBannerMessages = defineMessages({
|
||||
title: {
|
||||
id: 'layout.banner.tax.title',
|
||||
defaultMessage: 'Tax form required',
|
||||
},
|
||||
description: {
|
||||
id: 'layout.banner.tax.description',
|
||||
defaultMessage:
|
||||
'You’ve already withdrawn over $600 from Modrinth this year. To comply with tax regulations, you need to complete a tax form. Your withdrawals are paused until this form is submitted.',
|
||||
},
|
||||
action: {
|
||||
id: 'layout.banner.tax.action',
|
||||
defaultMessage: 'Complete tax form',
|
||||
},
|
||||
})
|
||||
|
||||
const taxFormModalRef = ref(null)
|
||||
function openTaxForm(e) {
|
||||
if (taxFormModalRef.value && taxFormModalRef.value.startTaxForm) {
|
||||
taxFormModalRef.value.startTaxForm(e)
|
||||
}
|
||||
}
|
||||
|
||||
const basePopoutId = useId()
|
||||
async function handleResendEmailVerification() {
|
||||
try {
|
||||
await resendVerifyEmail()
|
||||
addNotification({
|
||||
title: 'Verification email sent',
|
||||
text: 'Please check your inbox for the verification email.',
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const verifyEmailBannerMessages = defineMessages({
|
||||
title: {
|
||||
@@ -821,6 +1043,26 @@ const navMenuMessages = defineMessages({
|
||||
id: 'layout.nav.search',
|
||||
defaultMessage: 'Search',
|
||||
},
|
||||
discoverContent: {
|
||||
id: 'layout.nav.discover-content',
|
||||
defaultMessage: 'Discover content',
|
||||
},
|
||||
discover: {
|
||||
id: 'layout.nav.discover',
|
||||
defaultMessage: 'Discover',
|
||||
},
|
||||
hostAServer: {
|
||||
id: 'layout.nav.host-a-server',
|
||||
defaultMessage: 'Host a server',
|
||||
},
|
||||
getModrinthApp: {
|
||||
id: 'layout.nav.get-modrinth-app',
|
||||
defaultMessage: 'Get Modrinth App',
|
||||
},
|
||||
modrinthApp: {
|
||||
id: 'layout.nav.modrinth-app',
|
||||
defaultMessage: 'Modrinth App',
|
||||
},
|
||||
})
|
||||
|
||||
const messages = defineMessages({
|
||||
@@ -832,14 +1074,86 @@ const messages = defineMessages({
|
||||
id: 'layout.avatar.alt',
|
||||
defaultMessage: 'Your avatar',
|
||||
},
|
||||
getModrinthApp: {
|
||||
id: 'layout.action.get-modrinth-app',
|
||||
defaultMessage: 'Get Modrinth App',
|
||||
},
|
||||
changeTheme: {
|
||||
id: 'layout.action.change-theme',
|
||||
defaultMessage: 'Change theme',
|
||||
},
|
||||
modrinthHomePage: {
|
||||
id: 'layout.nav.modrinth-home-page',
|
||||
defaultMessage: 'Modrinth home page',
|
||||
},
|
||||
modrinthInformation: {
|
||||
id: 'layout.footer.modrinth-information',
|
||||
defaultMessage: 'Modrinth information',
|
||||
},
|
||||
createNew: {
|
||||
id: 'layout.action.create-new',
|
||||
defaultMessage: 'Create new...',
|
||||
},
|
||||
reviewProjects: {
|
||||
id: 'layout.action.review-projects',
|
||||
defaultMessage: 'Review projects',
|
||||
},
|
||||
reports: {
|
||||
id: 'layout.action.reports',
|
||||
defaultMessage: 'Reports',
|
||||
},
|
||||
lookupByEmail: {
|
||||
id: 'layout.action.lookup-by-email',
|
||||
defaultMessage: 'Lookup by email',
|
||||
},
|
||||
fileLookup: {
|
||||
id: 'layout.action.file-lookup',
|
||||
defaultMessage: 'File lookup',
|
||||
},
|
||||
manageServerNotices: {
|
||||
id: 'layout.action.manage-server-notices',
|
||||
defaultMessage: 'Manage server notices',
|
||||
},
|
||||
newProject: {
|
||||
id: 'layout.action.new-project',
|
||||
defaultMessage: 'New project',
|
||||
},
|
||||
newCollection: {
|
||||
id: 'layout.action.new-collection',
|
||||
defaultMessage: 'New collection',
|
||||
},
|
||||
newOrganization: {
|
||||
id: 'layout.action.new-organization',
|
||||
defaultMessage: 'New organization',
|
||||
},
|
||||
profile: {
|
||||
id: 'layout.nav.profile',
|
||||
defaultMessage: 'Profile',
|
||||
},
|
||||
savedProjects: {
|
||||
id: 'layout.nav.saved-projects',
|
||||
defaultMessage: 'Saved projects',
|
||||
},
|
||||
upgradeToModrinthPlus: {
|
||||
id: 'layout.nav.upgrade-to-modrinth-plus',
|
||||
defaultMessage: 'Upgrade to Modrinth+',
|
||||
},
|
||||
featureFlags: {
|
||||
id: 'layout.nav.feature-flags',
|
||||
defaultMessage: 'Feature flags',
|
||||
},
|
||||
projects: {
|
||||
id: 'layout.nav.projects',
|
||||
defaultMessage: 'Projects',
|
||||
},
|
||||
organizations: {
|
||||
id: 'layout.nav.organizations',
|
||||
defaultMessage: 'Organizations',
|
||||
},
|
||||
revenue: {
|
||||
id: 'layout.nav.revenue',
|
||||
defaultMessage: 'Revenue',
|
||||
},
|
||||
analytics: {
|
||||
id: 'layout.nav.analytics',
|
||||
defaultMessage: 'Analytics',
|
||||
},
|
||||
})
|
||||
|
||||
const footerMessages = defineMessages({
|
||||
@@ -854,23 +1168,6 @@ const footerMessages = defineMessages({
|
||||
},
|
||||
})
|
||||
|
||||
async function handleResendEmailVerification() {
|
||||
try {
|
||||
await resendVerifyEmail()
|
||||
addNotification({
|
||||
title: 'Email sent',
|
||||
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
|
||||
type: 'success',
|
||||
})
|
||||
} catch (err) {
|
||||
addNotification({
|
||||
title: 'An error occurred',
|
||||
text: err.data.description,
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useHead({
|
||||
link: [
|
||||
{
|
||||
@@ -1023,6 +1320,8 @@ const isDiscoveringSubpage = computed(
|
||||
() => route.name && route.name.startsWith('type-id') && !route.query.sid,
|
||||
)
|
||||
|
||||
const isRussia = computed(() => country.value === 'ru')
|
||||
|
||||
const rCount = ref(0)
|
||||
|
||||
const randomProjects = ref([])
|
||||
@@ -1161,6 +1460,11 @@ function hideStagingBanner() {
|
||||
cosmetics.value.hideStagingBanner = true
|
||||
}
|
||||
|
||||
function hideRussiaCensorshipBanner() {
|
||||
flags.value.hideRussiaCensorshipBanner = true
|
||||
saveFeatureFlags()
|
||||
}
|
||||
|
||||
const socialLinks = [
|
||||
{
|
||||
label: formatMessage(
|
||||
@@ -1211,10 +1515,7 @@ const footerLinks = [
|
||||
{
|
||||
href: '/news/changelog',
|
||||
label: formatMessage(
|
||||
defineMessage({
|
||||
id: 'layout.footer.about.changelog',
|
||||
defaultMessage: 'Changelog',
|
||||
}),
|
||||
defineMessage({ id: 'layout.footer.about.changelog', defaultMessage: 'Changelog' }),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -1248,19 +1549,13 @@ const footerLinks = [
|
||||
{
|
||||
href: '/plus',
|
||||
label: formatMessage(
|
||||
defineMessage({
|
||||
id: 'layout.footer.products.plus',
|
||||
defaultMessage: 'Modrinth+',
|
||||
}),
|
||||
defineMessage({ id: 'layout.footer.products.plus', defaultMessage: 'Modrinth+' }),
|
||||
),
|
||||
},
|
||||
{
|
||||
href: '/app',
|
||||
label: formatMessage(
|
||||
defineMessage({
|
||||
id: 'layout.footer.products.app',
|
||||
defaultMessage: 'Modrinth App',
|
||||
}),
|
||||
defineMessage({ id: 'layout.footer.products.app', defaultMessage: 'Modrinth App' }),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -1289,12 +1584,9 @@ const footerLinks = [
|
||||
),
|
||||
},
|
||||
{
|
||||
href: 'https://crowdin.com/project/modrinth',
|
||||
href: 'https://translate.modrinth.com',
|
||||
label: formatMessage(
|
||||
defineMessage({
|
||||
id: 'layout.footer.resources.translate',
|
||||
defaultMessage: 'Translate',
|
||||
}),
|
||||
defineMessage({ id: 'layout.footer.resources.translate', defaultMessage: 'Translate' }),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -1323,19 +1615,13 @@ const footerLinks = [
|
||||
{
|
||||
href: '/legal/rules',
|
||||
label: formatMessage(
|
||||
defineMessage({
|
||||
id: 'layout.footer.legal.rules',
|
||||
defaultMessage: 'Content Rules',
|
||||
}),
|
||||
defineMessage({ id: 'layout.footer.legal.rules', defaultMessage: 'Content Rules' }),
|
||||
),
|
||||
},
|
||||
{
|
||||
href: '/legal/terms',
|
||||
label: formatMessage(
|
||||
defineMessage({
|
||||
id: 'layout.footer.legal.terms-of-use',
|
||||
defaultMessage: 'Terms of Use',
|
||||
}),
|
||||
defineMessage({ id: 'layout.footer.legal.terms-of-use', defaultMessage: 'Terms of Use' }),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
1
apps/frontend/src/locales/af-ZA/index.json
Normal file
1
apps/frontend/src/locales/af-ZA/index.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
apps/frontend/src/locales/af-ZA/languages.json
Normal file
1
apps/frontend/src/locales/af-ZA/languages.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
apps/frontend/src/locales/af-ZA/meta.json
Normal file
1
apps/frontend/src/locales/af-ZA/meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
422
apps/frontend/src/locales/ar-EG/index.json
Normal file
422
apps/frontend/src/locales/ar-EG/index.json
Normal file
@@ -0,0 +1,422 @@
|
||||
{
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "المستخدم غير موجود"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "تطبيقنا لسطح المكتب متاح على جميع المنصات، اختر الإصدار الذي تريده."
|
||||
},
|
||||
"app-marketing.download.download-appimage": {
|
||||
"message": "تنزيل AppImage"
|
||||
},
|
||||
"app-marketing.download.download-beta": {
|
||||
"message": "تنزيل النسخة التجريبية"
|
||||
},
|
||||
"app-marketing.download.download-deb": {
|
||||
"message": "تنزيل DEB"
|
||||
},
|
||||
"app-marketing.download.download-rpm": {
|
||||
"message": "تنزيل RPM"
|
||||
},
|
||||
"app-marketing.download.linux": {
|
||||
"message": "لينكس"
|
||||
},
|
||||
"app-marketing.download.linux-disclaimer": {
|
||||
"message": "الإصدارات الخاصة بـ Linux من Modrinth App <issues-link>معروفة بوجود مشاكل</issues-link> على بعض الأنظمة والتكوينات. إذا كان Modrinth App غير مستقر على نظامك، فننصحك بتجربة تطبيقات أخرى مثل <prism-link>Prism Launcher</prism-link> لتثبيت محتوى Modrinth بسهولة."
|
||||
},
|
||||
"app-marketing.download.mac": {
|
||||
"message": "ماك"
|
||||
},
|
||||
"app-marketing.download.options-title": {
|
||||
"message": "خيارات التنزيل"
|
||||
},
|
||||
"app-marketing.download.terms": {
|
||||
"message": "خلال تنزيل Modrinth App فإنك توافق على <terms-link>الشروط</terms-link> و <privacy-link>سياسة الخصوصية</privacy-link>."
|
||||
},
|
||||
"app-marketing.download.third-party-packages": {
|
||||
"message": "حزم الطرف الثالث"
|
||||
},
|
||||
"app-marketing.download.title": {
|
||||
"message": "تنزيل Modrinth App (النسخة التجريبية)"
|
||||
},
|
||||
"app-marketing.download.windows": {
|
||||
"message": "ويندوز"
|
||||
},
|
||||
"app-marketing.features.follow.description": {
|
||||
"message": "احفظ المحتوى الذي تحبه واحصل على التحديثات بنقرة واحدة."
|
||||
},
|
||||
"app-marketing.features.follow.title": {
|
||||
"message": "تابع المشروعات"
|
||||
},
|
||||
"app-marketing.features.importing.description": {
|
||||
"message": "استورد جميع ملفاتك الشخصية المفضلة من المشغل الذي كنت تستخدمه من قبل، وابدأ باستخدام Modrinth App في ثوانٍ!"
|
||||
},
|
||||
"app-marketing.features.importing.gdlauncher-alt": {
|
||||
"message": "GDLauncher"
|
||||
},
|
||||
"app-marketing.features.importing.multimc-alt": {
|
||||
"message": "MultiMC"
|
||||
},
|
||||
"app-marketing.features.importing.title": {
|
||||
"message": "استيراد الملفات الشخصية"
|
||||
},
|
||||
"app-marketing.features.mod-management.actions": {
|
||||
"message": "الإجراءات"
|
||||
},
|
||||
"app-marketing.features.mod-management.byAuthor": {
|
||||
"message": "بواسطة {author}"
|
||||
},
|
||||
"app-marketing.features.mod-management.description": {
|
||||
"message": "يجعل مودرنث من السهل إدارة جميع تعديلاتك في مكان واحد. يمكنك التثبيت، الإزالة، والتحديث بنقرة واحدة."
|
||||
},
|
||||
"app-marketing.features.mod-management.installed-mods": {
|
||||
"message": "المودات المثبتة"
|
||||
},
|
||||
"app-marketing.features.mod-management.name": {
|
||||
"message": "الاسم"
|
||||
},
|
||||
"app-marketing.features.mod-management.search-mods": {
|
||||
"message": "بحث عن المودات"
|
||||
},
|
||||
"app-marketing.features.mod-management.title": {
|
||||
"message": "إدارة المودات"
|
||||
},
|
||||
"app-marketing.features.mod-management.version": {
|
||||
"message": "الإصدار"
|
||||
},
|
||||
"app-marketing.features.offline.description": {
|
||||
"message": "العب موداتك سواء كنت متصلاً بالإنترنت أو غير متصل."
|
||||
},
|
||||
"app-marketing.features.offline.title": {
|
||||
"message": "الوضع غير المتصل"
|
||||
},
|
||||
"app-marketing.features.open-source.description": {
|
||||
"message": "المشغل الخاص بـ مودرنث مفتوح المصدر بالكامل. يمكنك عرض الشيفرة المصدرية على <github-link>GitHub</github-link>!"
|
||||
},
|
||||
"app-marketing.features.open-source.title": {
|
||||
"message": "مفتوح المصدر"
|
||||
},
|
||||
"app-marketing.features.performance.activity-monitor": {
|
||||
"message": "مراقب النشاط"
|
||||
},
|
||||
"app-marketing.features.performance.cpu-percent": {
|
||||
"message": "% CPU"
|
||||
},
|
||||
"app-marketing.features.performance.description": {
|
||||
"message": "يعمل Modrinth App بشكل أفضل من العديد من مديري المودات الرائدين، باستخدام 150 ميغابايت فقط من الذاكرة العشوائية!"
|
||||
},
|
||||
"app-marketing.features.performance.discord": {
|
||||
"message": "ديسكورد"
|
||||
},
|
||||
"app-marketing.features.performance.good-performance": {
|
||||
"message": "أداء جيد"
|
||||
},
|
||||
"app-marketing.features.performance.google-chrome": {
|
||||
"message": "جوجل كروم"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-mb": {
|
||||
"message": "∞ ميغابايت"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-times-infinite-mb": {
|
||||
"message": "∞ * ∞ ميغابايت"
|
||||
},
|
||||
"app-marketing.features.performance.less-than-150mb": {
|
||||
"message": "< 150 ميغابايت"
|
||||
},
|
||||
"app-marketing.features.performance.modrinth-app": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"app-marketing.features.performance.one-billion-percent": {
|
||||
"message": "% 1 مليار"
|
||||
},
|
||||
"app-marketing.features.performance.process-name": {
|
||||
"message": "اسم العملية"
|
||||
},
|
||||
"app-marketing.features.performance.ram": {
|
||||
"message": "الذاكرة العشوائية"
|
||||
},
|
||||
"app-marketing.features.performance.small": {
|
||||
"message": "صغير"
|
||||
},
|
||||
"app-marketing.features.performance.title": {
|
||||
"message": "أداء قوي"
|
||||
},
|
||||
"app-marketing.features.play.description": {
|
||||
"message": "استخدم Modrinth App لتنزيل ولعب موداتك وحزم المودات المفضلة لديك."
|
||||
},
|
||||
"app-marketing.features.play.title": {
|
||||
"message": "العب مع موداتك المفضلة"
|
||||
},
|
||||
"app-marketing.features.sharing.description": {
|
||||
"message": "أنشئ، شارك، والعب بحزم المودات مع أي من آلاف المودات والحزم المستضافة هنا على مودرنث."
|
||||
},
|
||||
"app-marketing.features.sharing.modpack": {
|
||||
"message": "حزم المودات"
|
||||
},
|
||||
"app-marketing.features.sharing.share-button": {
|
||||
"message": "مشاركة"
|
||||
},
|
||||
"app-marketing.features.sharing.title": {
|
||||
"message": "مشاركة حزم المودات"
|
||||
},
|
||||
"app-marketing.features.unlike-any-launcher": {
|
||||
"message": "مختلف عن أي مشغل آخر"
|
||||
},
|
||||
"app-marketing.features.website.description": {
|
||||
"message": "تم دمج Modrinth App بالكامل مع الموقع، بحيث يمكنك الوصول إلى جميع مشاريعك المفضلة بواسطة التطبيق!"
|
||||
},
|
||||
"app-marketing.features.website.title": {
|
||||
"message": "تكامل مع الموقع"
|
||||
},
|
||||
"app-marketing.features.youve-used-before": {
|
||||
"message": "الذي استخدمته من قبل"
|
||||
},
|
||||
"app-marketing.hero.app-screenshot-alt": {
|
||||
"message": "لقطة شاشة لـ Modrinth App مع نسخة Cobblemon مفتوحة في صفحة \"المحتوى\"."
|
||||
},
|
||||
"app-marketing.hero.description": {
|
||||
"message": "Modrinth App هو مشغل فريد ومفتوح المصدر يتيح لك لعب موداتك المفضلة والحفاظ عليها محدثة، وكل ذلك في حزمة واحدة بسيطة ومنظمة."
|
||||
},
|
||||
"app-marketing.hero.download-button": {
|
||||
"message": "تنزيل Modrinth App"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app": {
|
||||
"message": "تنزيل Modrinth App"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app-for-os": {
|
||||
"message": "تنزيل Modrinth App لـ {os}"
|
||||
},
|
||||
"app-marketing.hero.minecraft-screenshot-alt": {
|
||||
"message": "لقطة شاشة لقائمة Cobblemon الرئيسية."
|
||||
},
|
||||
"app-marketing.hero.more-download-options": {
|
||||
"message": "المزيد من خيارات التنزيل"
|
||||
},
|
||||
"auth.authorize.action.authorize": {
|
||||
"message": "سماح"
|
||||
},
|
||||
"auth.authorize.action.decline": {
|
||||
"message": "رفض"
|
||||
},
|
||||
"auth.authorize.app-info": {
|
||||
"message": "<strong>{appName}</strong> بواسطة <creator-link>{creator}</creator-link> سيكون قادرًا على:"
|
||||
},
|
||||
"auth.authorize.authorize-app-name": {
|
||||
"message": "السماح لـ {appName}"
|
||||
},
|
||||
"auth.authorize.error.no-redirect-url": {
|
||||
"message": "لم يتم العثور على وجهة إعادة التوجيه في الاستجابة"
|
||||
},
|
||||
"auth.authorize.redirect-url": {
|
||||
"message": "ستتم إعادة توجيهك إلى <redirect-url>{url}</redirect-url>"
|
||||
},
|
||||
"auth.reset-password.method-choice.action": {
|
||||
"message": "إرسال بريد استعادة الحساب"
|
||||
},
|
||||
"auth.reset-password.method-choice.description": {
|
||||
"message": "أدخل بريدك الإلكتروني في الحقل بالأسفل، وسنرسل لك رابطًا لاستعادة حسابك."
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.label": {
|
||||
"message": "البريد الإلكتروني أو اسم المستخدم"
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.placeholder": {
|
||||
"message": "البريد الإلكتروني"
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.text": {
|
||||
"message": "تم إرسال بريد يحتوي على التعليمات إليك، إذا كان بريدك الإلكتروني محفوظًا مسبقًا في حسابك."
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.title": {
|
||||
"message": "تم إرسال البريد الإلكتروني"
|
||||
},
|
||||
"auth.reset-password.notification.password-reset.text": {
|
||||
"message": "يمكنك الآن تسجيل الدخول إلى حسابك باستخدام كلمة المرور الجديدة."
|
||||
},
|
||||
"auth.reset-password.notification.password-reset.title": {
|
||||
"message": "تم تغيير كلمة المرور بنجاح"
|
||||
},
|
||||
"auth.reset-password.post-challenge.action": {
|
||||
"message": "تغيير كلمة المرور"
|
||||
},
|
||||
"auth.reset-password.post-challenge.confirm-password.label": {
|
||||
"message": "تأكيد كلمة المرور"
|
||||
},
|
||||
"auth.reset-password.post-challenge.description": {
|
||||
"message": "أدخل كلمة المرور الجديدة في الحقل بالأسفل للوصول إلى حسابك."
|
||||
},
|
||||
"auth.reset-password.title": {
|
||||
"message": "تغيير كلمة المرور"
|
||||
},
|
||||
"auth.reset-password.title.long": {
|
||||
"message": "تغيير كلمة المرور"
|
||||
},
|
||||
"auth.sign-in.2fa.description": {
|
||||
"message": "يرجى إدخال رمز التحقق الثنائي للمتابعة."
|
||||
},
|
||||
"auth.sign-in.2fa.label": {
|
||||
"message": "أدخل رمز التحقق الثنائي"
|
||||
},
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "أدخل الرمز..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>هل نسيت كلمة المرور؟</forgot-password-link> • <create-account-link>إنشاء حساب</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.email-username.label": {
|
||||
"message": "البريد الإلكتروني أو اسم المستخدم"
|
||||
},
|
||||
"auth.sign-in.password.label": {
|
||||
"message": "كلمة المرور"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "تسجيل الدخول باستخدام"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "تسجيل الدخول"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "أو استخدم كلمة المرور"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "إنشاء حساب"
|
||||
},
|
||||
"auth.sign-up.confirm-password.label": {
|
||||
"message": "تأكيد كلمة المرور"
|
||||
},
|
||||
"auth.sign-up.email.label": {
|
||||
"message": "البريد الإلكتروني"
|
||||
},
|
||||
"auth.sign-up.label.username": {
|
||||
"message": "اسم المستخدم"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link> الخاصة بها."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "كلمتا المرور غير متطابقتين!"
|
||||
},
|
||||
"auth.sign-up.password.label": {
|
||||
"message": "كلمة المرور"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "هل لديك حساب بالفعل؟"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "اشترك لتصلك التحديثات حول مودرنث"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "إنشاء حساب"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "أو أنشئ حسابًا بنفسك"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "سجّل باستخدام"
|
||||
},
|
||||
"auth.verify-email.action.account-settings": {
|
||||
"message": "إعدادات الحساب"
|
||||
},
|
||||
"auth.verify-email.action.sign-in": {
|
||||
"message": "تسجيل الدخول"
|
||||
},
|
||||
"auth.verify-email.already-verified.description": {
|
||||
"message": "تم توثيق بريدك الإلكتروني مسبقًا!"
|
||||
},
|
||||
"auth.verify-email.already-verified.title": {
|
||||
"message": "تم توثيق البريد الإلكتروني مسبقًا"
|
||||
},
|
||||
"auth.verify-email.failed-verification.action": {
|
||||
"message": "إعادة إرسال بريد التحقق"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "اشترك في التحديثات حول مودرنث"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "أنت الآن جزء من مجتمع رائع من المبدعين والمستكشفين الذين يقومون بالفعل بإنشاء المودات المذهلة، تثبيتها، والبقاء على اطلاع بآخر التحديثات."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "مرحبًا بك في مودرنث!"
|
||||
},
|
||||
"collection.delete-modal.description": {
|
||||
"message": "سيتم حذف هذه المجموعة نهائيًا. لا يمكن التراجع عن هذا الإجراء."
|
||||
},
|
||||
"collection.delete-modal.title": {
|
||||
"message": "هل أنت متأكد أنك تريد حذف هذه المجموعة؟"
|
||||
},
|
||||
"collection.description": {
|
||||
"message": "{description} - عرض المجموعة {name} بواسطة {username} على مودرنث"
|
||||
},
|
||||
"error.collection.404.list_item.3": {
|
||||
"message": "قد تكون هذه المجموعة قد أزالها فريق مراقبة مودرنث لانتهاك <tou-link>شروط الاستخدام</tou-link> الخاصة بنا."
|
||||
},
|
||||
"error.generic.default.list_item.1": {
|
||||
"message": "تحقق مما إذا كان مودرنث متوقفًا على <status-link>صفحة الحالة</status-link> الخاصة بنا."
|
||||
},
|
||||
"error.generic.default.list_item.2": {
|
||||
"message": "إذا استمر حدوث ذلك، قد ترغب في إعلام فريق مودرنث بالانضمام إلى <discord-link>سيرفر ديسكورد</discord-link> الخاص بنا."
|
||||
},
|
||||
"error.organization.404.list_item.3": {
|
||||
"message": "قد تكون هذه المنظمة قد أزالها فريق مراقبة مودرنث لانتهاك <tou-link>شروط الاستخدام</tou-link> الخاصة بنا."
|
||||
},
|
||||
"error.project.404.list_item.3": {
|
||||
"message": "قد يكون هذا المشروع قد أزاله فريق مراقبة مودرنث لانتهاك <tou-link>شروط الاستخدام</tou-link> الخاصة بنا."
|
||||
},
|
||||
"error.user.404.list_item.3": {
|
||||
"message": "تم تعطيل حساب هذا المستخدم لانتهاكه <tou-link>شروط الاستخدام</tou-link> الخاصة بمودرنث."
|
||||
},
|
||||
"landing.button.discover-mods": {
|
||||
"message": "اكتشف المودات"
|
||||
},
|
||||
"landing.creator.feature.constantly-evolving.description": {
|
||||
"message": "احصل على أفضل تجربة مودات ممكنة مع تحديثات مستمرة من فريق مودرنث"
|
||||
},
|
||||
"landing.feature.launcher.description": {
|
||||
"message": "واجهة برمجة التطبيقات مفتوحة المصدر لمودرنث تتيح للّانشرات إضافة تكامل عميق مع مودرنث. يمكنك استخدام مودرنث من خلال <link>تطبيقنا الخاص</link> وبعض أشهر اللانشرات مثل ATLauncher وMultiMC وPrism Launcher."
|
||||
},
|
||||
"landing.feature.search.description": {
|
||||
"message": "تتيح لك ميزة البحث السريع والفلاتر القوية في مودرنث العثور على ما تريد أثناء الكتابة."
|
||||
},
|
||||
"landing.heading.the-place-for-minecraft.modpacks": {
|
||||
"message": "حزم المودات"
|
||||
},
|
||||
"landing.heading.the-place-for-minecraft.mods": {
|
||||
"message": "مودات"
|
||||
},
|
||||
"landing.launcher.graphic-alt": {
|
||||
"message": "تمثيل مبسط لنافذة ماين كرافت، مع شعار Mojang Studios باللون الأخضر الخاص بمودرنث."
|
||||
},
|
||||
"landing.launcher.modrinth-app-label": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"layout.banner.add-email.description": {
|
||||
"message": "لأسباب أمنية، يحتاج مودرنث منك تسجيل عنوان بريد إلكتروني في حسابك."
|
||||
},
|
||||
"layout.banner.build-fail.description": {
|
||||
"message": "فشل هذا الإصدار من واجهة مودرنث في توليد الحالة من واجهة برمجة التطبيقات. قد يكون السبب انقطاع الخدمة أو خطأ في الإعدادات. أعد البناء عندما تكون واجهة البرمجة متاحة. رموز الخطأ: {errors}؛ عنوان واجهة البرمجة الحالي: {url}"
|
||||
},
|
||||
"layout.banner.staging.description": {
|
||||
"message": "بيئة الاختبار منفصلة تمامًا عن قاعدة بيانات مودرنث الإنتاجية. تُستخدم هذه البيئة للاختبار وتصحيح الأخطاء، وقد تعمل بإصدارات تطويرية من واجهة مودرنث الأمامية أو الخلفية أحدث من النسخة الإنتاجية."
|
||||
},
|
||||
"layout.banner.staging.title": {
|
||||
"message": "أنت الآن تشاهد بيئة الاختبار الخاصة بمودرنث"
|
||||
},
|
||||
"layout.banner.verify-email.description": {
|
||||
"message": "لأسباب أمنية، يحتاج مودرنث منك التحقق من عنوان البريد الإلكتروني المرتبط بحسابك."
|
||||
},
|
||||
"layout.footer.modrinth-information": {
|
||||
"message": "معلومات مودرنث"
|
||||
},
|
||||
"layout.footer.open-source": {
|
||||
"message": "مودرنث <github-link>مفتوح المصدر</github-link>."
|
||||
},
|
||||
"layout.footer.products.app": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"layout.footer.products.plus": {
|
||||
"message": "مودرنث+"
|
||||
},
|
||||
"layout.footer.products.servers": {
|
||||
"message": "خوادم مودرنث"
|
||||
}
|
||||
}
|
||||
58
apps/frontend/src/locales/ar-EG/languages.json
Normal file
58
apps/frontend/src/locales/ar-EG/languages.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"ar": "عربي",
|
||||
"be": "بيلاروسي",
|
||||
"bg": "بلغاري",
|
||||
"bn": "بنغالي",
|
||||
"ca": "كتالوني",
|
||||
"cs": "تشيكي",
|
||||
"da": "دنماركي",
|
||||
"de": "ألماني",
|
||||
"de-CH": "ألماني (سويسرا)",
|
||||
"el": "يوناني",
|
||||
"en-GB": "إنجليزي (المملكة المتحدة)",
|
||||
"en-US": "إنجليزي (الولايات المتحدة)",
|
||||
"en-x-lolcat": "لوكات",
|
||||
"en-x-pirate": "إنجليزي (قراصنة)",
|
||||
"en-x-updown": "إنجليزي (مقلوب)",
|
||||
"en-x-uwu": "إنجليزي (أوو)",
|
||||
"eo": "إسبرانتو",
|
||||
"es": "إسباني",
|
||||
"et": "إستوني",
|
||||
"fi": "فنلندي",
|
||||
"fr": "فرنساوي",
|
||||
"fr-BE": "فرنساوي (بلجيكا)",
|
||||
"fr-CA": "فرنساوي (كندا)",
|
||||
"he": "عبري",
|
||||
"hi": "هندي",
|
||||
"hr": "كرواتي",
|
||||
"hu": "مجري",
|
||||
"id": "إندونيسي",
|
||||
"it": "إيطالي",
|
||||
"ja": "ياباني",
|
||||
"kk": "كازاخستاني",
|
||||
"ko": "كوري",
|
||||
"ky": "قيرغيزي",
|
||||
"lt": "ليتواني",
|
||||
"lv": "لاتفي",
|
||||
"ms": "ماليزي",
|
||||
"nb": "نرويجي بوكمال",
|
||||
"nl": "هولندي",
|
||||
"nn": "نرويجي نينورسك",
|
||||
"pes": "فارسي",
|
||||
"pl": "بولندي",
|
||||
"pt": "برتغالي",
|
||||
"pt-BR": "برتغالي (البرازيل)",
|
||||
"ro": "روماني",
|
||||
"ru": "روسي",
|
||||
"ru-x-bandit": "روسي (قطاع طرق)",
|
||||
"sk": "سلوفاكي",
|
||||
"sv": "سويدي",
|
||||
"th": "تايلاندي",
|
||||
"tok": "توكي بونا",
|
||||
"tr": "تركي",
|
||||
"tt": "تتاري",
|
||||
"uk": "أوكراني",
|
||||
"vi": "فيتنامي",
|
||||
"zh-Hans": "صيني (مبسط)",
|
||||
"zh-Hant": "صيني (تقليدي)"
|
||||
}
|
||||
10
apps/frontend/src/locales/ar-EG/meta.json
Normal file
10
apps/frontend/src/locales/ar-EG/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "الإنجليزية (المملكة المتحدة)"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "الولايات المتحدة\nالإنجليزية الأمريكية"
|
||||
}
|
||||
}
|
||||
422
apps/frontend/src/locales/ar-SA/index.json
Normal file
422
apps/frontend/src/locales/ar-SA/index.json
Normal file
@@ -0,0 +1,422 @@
|
||||
{
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "المستخدم غير موجود"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "تطبيقنا لسطح المكتب متاح على جميع المنصات، اختر الإصدار الذي تريده."
|
||||
},
|
||||
"app-marketing.download.download-appimage": {
|
||||
"message": "تنزيل AppImage"
|
||||
},
|
||||
"app-marketing.download.download-beta": {
|
||||
"message": "تنزيل النسخة التجريبية"
|
||||
},
|
||||
"app-marketing.download.download-deb": {
|
||||
"message": "تنزيل DEB"
|
||||
},
|
||||
"app-marketing.download.download-rpm": {
|
||||
"message": "تنزيل RPM"
|
||||
},
|
||||
"app-marketing.download.linux": {
|
||||
"message": "لينكس"
|
||||
},
|
||||
"app-marketing.download.linux-disclaimer": {
|
||||
"message": "الإصدارات الخاصة بـ Linux من Modrinth App <issues-link>معروفة بوجود مشاكل</issues-link> على بعض الأنظمة والتكوينات. إذا كان Modrinth App غير مستقر على نظامك، فننصحك بتجربة تطبيقات أخرى مثل <prism-link>Prism Launcher</prism-link> لتثبيت محتوى Modrinth بسهولة."
|
||||
},
|
||||
"app-marketing.download.mac": {
|
||||
"message": "ماك"
|
||||
},
|
||||
"app-marketing.download.options-title": {
|
||||
"message": "خيارات التنزيل"
|
||||
},
|
||||
"app-marketing.download.terms": {
|
||||
"message": "خلال تنزيل Modrinth App فإنك توافق على <terms-link>الشروط</terms-link> و <privacy-link>سياسة الخصوصية</privacy-link>."
|
||||
},
|
||||
"app-marketing.download.third-party-packages": {
|
||||
"message": "حزم الطرف الثالث"
|
||||
},
|
||||
"app-marketing.download.title": {
|
||||
"message": "تنزيل Modrinth App (النسخة التجريبية)"
|
||||
},
|
||||
"app-marketing.download.windows": {
|
||||
"message": "ويندوز"
|
||||
},
|
||||
"app-marketing.features.follow.description": {
|
||||
"message": "احفظ المحتوى الذي تحبه واحصل على التحديثات بنقرة واحدة."
|
||||
},
|
||||
"app-marketing.features.follow.title": {
|
||||
"message": "تابع المشروعات"
|
||||
},
|
||||
"app-marketing.features.importing.description": {
|
||||
"message": "استورد جميع ملفاتك الشخصية المفضلة من المشغل الذي كنت تستخدمه من قبل، وابدأ باستخدام Modrinth App في ثوانٍ!"
|
||||
},
|
||||
"app-marketing.features.importing.gdlauncher-alt": {
|
||||
"message": "GDLauncher"
|
||||
},
|
||||
"app-marketing.features.importing.multimc-alt": {
|
||||
"message": "MultiMC"
|
||||
},
|
||||
"app-marketing.features.importing.title": {
|
||||
"message": "استيراد الملفات الشخصية"
|
||||
},
|
||||
"app-marketing.features.mod-management.actions": {
|
||||
"message": "الإجراءات"
|
||||
},
|
||||
"app-marketing.features.mod-management.byAuthor": {
|
||||
"message": "بواسطة {author}"
|
||||
},
|
||||
"app-marketing.features.mod-management.description": {
|
||||
"message": "يجعل مودرنث من السهل إدارة جميع تعديلاتك في مكان واحد. يمكنك التثبيت، الإزالة، والتحديث بنقرة واحدة."
|
||||
},
|
||||
"app-marketing.features.mod-management.installed-mods": {
|
||||
"message": "المودات المثبتة"
|
||||
},
|
||||
"app-marketing.features.mod-management.name": {
|
||||
"message": "الاسم"
|
||||
},
|
||||
"app-marketing.features.mod-management.search-mods": {
|
||||
"message": "بحث عن المودات"
|
||||
},
|
||||
"app-marketing.features.mod-management.title": {
|
||||
"message": "إدارة المودات"
|
||||
},
|
||||
"app-marketing.features.mod-management.version": {
|
||||
"message": "الإصدار"
|
||||
},
|
||||
"app-marketing.features.offline.description": {
|
||||
"message": "العب موداتك سواء كنت متصلاً بالإنترنت أو غير متصل."
|
||||
},
|
||||
"app-marketing.features.offline.title": {
|
||||
"message": "الوضع غير المتصل"
|
||||
},
|
||||
"app-marketing.features.open-source.description": {
|
||||
"message": "المشغل الخاص بـ مودرنث مفتوح المصدر بالكامل. يمكنك عرض الشيفرة المصدرية على <github-link>GitHub</github-link>!"
|
||||
},
|
||||
"app-marketing.features.open-source.title": {
|
||||
"message": "مفتوح المصدر"
|
||||
},
|
||||
"app-marketing.features.performance.activity-monitor": {
|
||||
"message": "مراقب النشاط"
|
||||
},
|
||||
"app-marketing.features.performance.cpu-percent": {
|
||||
"message": "% CPU"
|
||||
},
|
||||
"app-marketing.features.performance.description": {
|
||||
"message": "يعمل Modrinth App بشكل أفضل من العديد من مديري المودات الرائدين، باستخدام 150 ميغابايت فقط من الذاكرة العشوائية!"
|
||||
},
|
||||
"app-marketing.features.performance.discord": {
|
||||
"message": "ديسكورد"
|
||||
},
|
||||
"app-marketing.features.performance.good-performance": {
|
||||
"message": "أداء جيد"
|
||||
},
|
||||
"app-marketing.features.performance.google-chrome": {
|
||||
"message": "جوجل كروم"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-mb": {
|
||||
"message": "∞ ميغابايت"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-times-infinite-mb": {
|
||||
"message": "∞ * ∞ ميغابايت"
|
||||
},
|
||||
"app-marketing.features.performance.less-than-150mb": {
|
||||
"message": "< 150 ميغابايت"
|
||||
},
|
||||
"app-marketing.features.performance.modrinth-app": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"app-marketing.features.performance.one-billion-percent": {
|
||||
"message": "% 1 مليار"
|
||||
},
|
||||
"app-marketing.features.performance.process-name": {
|
||||
"message": "اسم العملية"
|
||||
},
|
||||
"app-marketing.features.performance.ram": {
|
||||
"message": "الذاكرة العشوائية"
|
||||
},
|
||||
"app-marketing.features.performance.small": {
|
||||
"message": "صغير"
|
||||
},
|
||||
"app-marketing.features.performance.title": {
|
||||
"message": "أداء قوي"
|
||||
},
|
||||
"app-marketing.features.play.description": {
|
||||
"message": "استخدم Modrinth App لتنزيل ولعب موداتك وحزم المودات المفضلة لديك."
|
||||
},
|
||||
"app-marketing.features.play.title": {
|
||||
"message": "العب مع موداتك المفضلة"
|
||||
},
|
||||
"app-marketing.features.sharing.description": {
|
||||
"message": "أنشئ، شارك، والعب بحزم المودات مع أي من آلاف المودات والحزم المستضافة هنا على مودرنث."
|
||||
},
|
||||
"app-marketing.features.sharing.modpack": {
|
||||
"message": "حزم المودات"
|
||||
},
|
||||
"app-marketing.features.sharing.share-button": {
|
||||
"message": "مشاركة"
|
||||
},
|
||||
"app-marketing.features.sharing.title": {
|
||||
"message": "مشاركة حزم المودات"
|
||||
},
|
||||
"app-marketing.features.unlike-any-launcher": {
|
||||
"message": "مختلف عن أي مشغل آخر"
|
||||
},
|
||||
"app-marketing.features.website.description": {
|
||||
"message": "تم دمج Modrinth App بالكامل مع الموقع، بحيث يمكنك الوصول إلى جميع مشاريعك المفضلة بواسطة التطبيق!"
|
||||
},
|
||||
"app-marketing.features.website.title": {
|
||||
"message": "تكامل مع الموقع"
|
||||
},
|
||||
"app-marketing.features.youve-used-before": {
|
||||
"message": "الذي استخدمته من قبل"
|
||||
},
|
||||
"app-marketing.hero.app-screenshot-alt": {
|
||||
"message": "لقطة شاشة لـ Modrinth App مع نسخة Cobblemon مفتوحة في صفحة \"المحتوى\"."
|
||||
},
|
||||
"app-marketing.hero.description": {
|
||||
"message": "Modrinth App هو مشغل فريد ومفتوح المصدر يتيح لك لعب موداتك المفضلة والحفاظ عليها محدثة، وكل ذلك في حزمة واحدة بسيطة ومنظمة."
|
||||
},
|
||||
"app-marketing.hero.download-button": {
|
||||
"message": "تنزيل Modrinth App"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app": {
|
||||
"message": "تنزيل Modrinth App"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app-for-os": {
|
||||
"message": "تنزيل Modrinth App لـ {os}"
|
||||
},
|
||||
"app-marketing.hero.minecraft-screenshot-alt": {
|
||||
"message": "لقطة شاشة لقائمة Cobblemon الرئيسية."
|
||||
},
|
||||
"app-marketing.hero.more-download-options": {
|
||||
"message": "المزيد من خيارات التنزيل"
|
||||
},
|
||||
"auth.authorize.action.authorize": {
|
||||
"message": "سماح"
|
||||
},
|
||||
"auth.authorize.action.decline": {
|
||||
"message": "رفض"
|
||||
},
|
||||
"auth.authorize.app-info": {
|
||||
"message": "<strong>{appName}</strong> بواسطة <creator-link>{creator}</creator-link> سيكون قادرًا على:"
|
||||
},
|
||||
"auth.authorize.authorize-app-name": {
|
||||
"message": "السماح لـ {appName}"
|
||||
},
|
||||
"auth.authorize.error.no-redirect-url": {
|
||||
"message": "لم يتم العثور على وجهة إعادة التوجيه في الاستجابة"
|
||||
},
|
||||
"auth.authorize.redirect-url": {
|
||||
"message": "ستتم إعادة توجيهك إلى <redirect-url>{url}</redirect-url>"
|
||||
},
|
||||
"auth.reset-password.method-choice.action": {
|
||||
"message": "إرسال بريد استعادة الحساب"
|
||||
},
|
||||
"auth.reset-password.method-choice.description": {
|
||||
"message": "أدخل بريدك الإلكتروني في الحقل بالأسفل، وسنرسل لك رابطًا لاستعادة حسابك."
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.label": {
|
||||
"message": "البريد الإلكتروني أو اسم المستخدم"
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.placeholder": {
|
||||
"message": "البريد الإلكتروني"
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.text": {
|
||||
"message": "تم إرسال بريد يحتوي على التعليمات إليك، إذا كان بريدك الإلكتروني محفوظًا مسبقًا في حسابك."
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.title": {
|
||||
"message": "تم إرسال البريد الإلكتروني"
|
||||
},
|
||||
"auth.reset-password.notification.password-reset.text": {
|
||||
"message": "يمكنك الآن تسجيل الدخول إلى حسابك باستخدام كلمة المرور الجديدة."
|
||||
},
|
||||
"auth.reset-password.notification.password-reset.title": {
|
||||
"message": "تم تغيير كلمة المرور بنجاح"
|
||||
},
|
||||
"auth.reset-password.post-challenge.action": {
|
||||
"message": "تغيير كلمة المرور"
|
||||
},
|
||||
"auth.reset-password.post-challenge.confirm-password.label": {
|
||||
"message": "تأكيد كلمة المرور"
|
||||
},
|
||||
"auth.reset-password.post-challenge.description": {
|
||||
"message": "أدخل كلمة المرور الجديدة في الحقل بالأسفل للوصول إلى حسابك."
|
||||
},
|
||||
"auth.reset-password.title": {
|
||||
"message": "تغيير كلمة المرور"
|
||||
},
|
||||
"auth.reset-password.title.long": {
|
||||
"message": "تغيير كلمة المرور"
|
||||
},
|
||||
"auth.sign-in.2fa.description": {
|
||||
"message": "يرجى إدخال رمز التحقق الثنائي للمتابعة."
|
||||
},
|
||||
"auth.sign-in.2fa.label": {
|
||||
"message": "أدخل رمز التحقق الثنائي"
|
||||
},
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "أدخل الرمز..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>هل نسيت كلمة المرور؟</forgot-password-link> • <create-account-link>إنشاء حساب</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.email-username.label": {
|
||||
"message": "البريد الإلكتروني أو اسم المستخدم"
|
||||
},
|
||||
"auth.sign-in.password.label": {
|
||||
"message": "كلمة المرور"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "تسجيل الدخول باستخدام"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "تسجيل الدخول"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "أو استخدم كلمة المرور"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "إنشاء حساب"
|
||||
},
|
||||
"auth.sign-up.confirm-password.label": {
|
||||
"message": "تأكيد كلمة المرور"
|
||||
},
|
||||
"auth.sign-up.email.label": {
|
||||
"message": "البريد الإلكتروني"
|
||||
},
|
||||
"auth.sign-up.label.username": {
|
||||
"message": "اسم المستخدم"
|
||||
},
|
||||
"auth.sign-up.legal-dislaimer": {
|
||||
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link> الخاصة بها."
|
||||
},
|
||||
"auth.sign-up.notification.password-mismatch.text": {
|
||||
"message": "كلمتا المرور غير متطابقتين!"
|
||||
},
|
||||
"auth.sign-up.password.label": {
|
||||
"message": "كلمة المرور"
|
||||
},
|
||||
"auth.sign-up.sign-in-option.title": {
|
||||
"message": "هل لديك حساب بالفعل؟"
|
||||
},
|
||||
"auth.sign-up.subscribe.label": {
|
||||
"message": "اشترك لتصلك التحديثات حول مودرنث"
|
||||
},
|
||||
"auth.sign-up.title": {
|
||||
"message": "إنشاء حساب"
|
||||
},
|
||||
"auth.sign-up.title.create-account": {
|
||||
"message": "أو أنشئ حسابًا بنفسك"
|
||||
},
|
||||
"auth.sign-up.title.sign-up-with": {
|
||||
"message": "سجّل باستخدام"
|
||||
},
|
||||
"auth.verify-email.action.account-settings": {
|
||||
"message": "إعدادات الحساب"
|
||||
},
|
||||
"auth.verify-email.action.sign-in": {
|
||||
"message": "تسجيل الدخول"
|
||||
},
|
||||
"auth.verify-email.already-verified.description": {
|
||||
"message": "تم توثيق بريدك الإلكتروني مسبقًا!"
|
||||
},
|
||||
"auth.verify-email.already-verified.title": {
|
||||
"message": "تم توثيق البريد الإلكتروني مسبقًا"
|
||||
},
|
||||
"auth.verify-email.failed-verification.action": {
|
||||
"message": "إعادة إرسال بريد التحقق"
|
||||
},
|
||||
"auth.welcome.checkbox.subscribe": {
|
||||
"message": "اشترك في التحديثات حول مودرنث"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "أنت الآن جزء من مجتمع رائع من المبدعين والمستكشفين الذين يقومون بالفعل بإنشاء المودات المذهلة، تثبيتها، والبقاء على اطلاع بآخر التحديثات."
|
||||
},
|
||||
"auth.welcome.label.tos": {
|
||||
"message": "بإنشائك حسابًا، فإنك توافق على <terms-link>شروط مودرنث</terms-link> و<privacy-policy-link>سياسة الخصوصية</privacy-policy-link>."
|
||||
},
|
||||
"auth.welcome.long-title": {
|
||||
"message": "مرحبًا بك في مودرنث!"
|
||||
},
|
||||
"collection.delete-modal.description": {
|
||||
"message": "سيتم حذف هذه المجموعة نهائيًا. لا يمكن التراجع عن هذا الإجراء."
|
||||
},
|
||||
"collection.delete-modal.title": {
|
||||
"message": "هل أنت متأكد أنك تريد حذف هذه المجموعة؟"
|
||||
},
|
||||
"collection.description": {
|
||||
"message": "{description} - عرض المجموعة {name} بواسطة {username} على مودرنث"
|
||||
},
|
||||
"error.collection.404.list_item.3": {
|
||||
"message": "قد تكون هذه المجموعة قد أزالها فريق مراقبة مودرنث لانتهاك <tou-link>شروط الاستخدام</tou-link> الخاصة بنا."
|
||||
},
|
||||
"error.generic.default.list_item.1": {
|
||||
"message": "تحقق مما إذا كان مودرنث متوقفًا على <status-link>صفحة الحالة</status-link> الخاصة بنا."
|
||||
},
|
||||
"error.generic.default.list_item.2": {
|
||||
"message": "إذا استمر حدوث ذلك، قد ترغب في إعلام فريق مودرنث بالانضمام إلى <discord-link>سيرفر ديسكورد</discord-link> الخاص بنا."
|
||||
},
|
||||
"error.organization.404.list_item.3": {
|
||||
"message": "قد تكون هذه المنظمة قد أزالها فريق مراقبة مودرنث لانتهاك <tou-link>شروط الاستخدام</tou-link> الخاصة بنا."
|
||||
},
|
||||
"error.project.404.list_item.3": {
|
||||
"message": "قد يكون هذا المشروع قد أزاله فريق مراقبة مودرنث لانتهاك <tou-link>شروط الاستخدام</tou-link> الخاصة بنا."
|
||||
},
|
||||
"error.user.404.list_item.3": {
|
||||
"message": "تم تعطيل حساب هذا المستخدم لانتهاكه <tou-link>شروط الاستخدام</tou-link> الخاصة بمودرنث."
|
||||
},
|
||||
"landing.button.discover-mods": {
|
||||
"message": "اكتشف المودات"
|
||||
},
|
||||
"landing.creator.feature.constantly-evolving.description": {
|
||||
"message": "احصل على أفضل تجربة مودات ممكنة مع تحديثات مستمرة من فريق مودرنث"
|
||||
},
|
||||
"landing.feature.launcher.description": {
|
||||
"message": "واجهة برمجة التطبيقات مفتوحة المصدر لمودرنث تتيح للّانشرات إضافة تكامل عميق مع مودرنث. يمكنك استخدام مودرنث من خلال <link>تطبيقنا الخاص</link> وبعض أشهر اللانشرات مثل ATLauncher وMultiMC وPrism Launcher."
|
||||
},
|
||||
"landing.feature.search.description": {
|
||||
"message": "تتيح لك ميزة البحث السريع والفلاتر القوية في مودرنث العثور على ما تريد أثناء الكتابة."
|
||||
},
|
||||
"landing.heading.the-place-for-minecraft.modpacks": {
|
||||
"message": "حزم المودات"
|
||||
},
|
||||
"landing.heading.the-place-for-minecraft.mods": {
|
||||
"message": "مودات"
|
||||
},
|
||||
"landing.launcher.graphic-alt": {
|
||||
"message": "تمثيل مبسط لنافذة ماين كرافت، مع شعار Mojang Studios باللون الأخضر الخاص بمودرنث."
|
||||
},
|
||||
"landing.launcher.modrinth-app-label": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"layout.banner.add-email.description": {
|
||||
"message": "لأسباب أمنية، يحتاج مودرنث منك تسجيل عنوان بريد إلكتروني في حسابك."
|
||||
},
|
||||
"layout.banner.build-fail.description": {
|
||||
"message": "فشل هذا الإصدار من واجهة مودرنث في توليد الحالة من واجهة برمجة التطبيقات. قد يكون السبب انقطاع الخدمة أو خطأ في الإعدادات. أعد البناء عندما تكون واجهة البرمجة متاحة. رموز الخطأ: {errors}؛ عنوان واجهة البرمجة الحالي: {url}"
|
||||
},
|
||||
"layout.banner.staging.description": {
|
||||
"message": "بيئة الاختبار منفصلة تمامًا عن قاعدة بيانات مودرنث الإنتاجية. تُستخدم هذه البيئة للاختبار وتصحيح الأخطاء، وقد تعمل بإصدارات تطويرية من واجهة مودرنث الأمامية أو الخلفية أحدث من النسخة الإنتاجية."
|
||||
},
|
||||
"layout.banner.staging.title": {
|
||||
"message": "أنت الآن تشاهد بيئة الاختبار الخاصة بمودرنث"
|
||||
},
|
||||
"layout.banner.verify-email.description": {
|
||||
"message": "لأسباب أمنية، يحتاج مودرنث منك التحقق من عنوان البريد الإلكتروني المرتبط بحسابك."
|
||||
},
|
||||
"layout.footer.modrinth-information": {
|
||||
"message": "معلومات مودرنث"
|
||||
},
|
||||
"layout.footer.open-source": {
|
||||
"message": "مودرنث <github-link>مفتوح المصدر</github-link>."
|
||||
},
|
||||
"layout.footer.products.app": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"layout.footer.products.plus": {
|
||||
"message": "مودرنث+"
|
||||
},
|
||||
"layout.footer.products.servers": {
|
||||
"message": "خوادم مودرنث"
|
||||
}
|
||||
}
|
||||
58
apps/frontend/src/locales/ar-SA/languages.json
Normal file
58
apps/frontend/src/locales/ar-SA/languages.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"ar": "العربيّة",
|
||||
"be": "البيلاروسيّة",
|
||||
"bg": "البلغاريّة",
|
||||
"bn": "البنغاليّة",
|
||||
"ca": "الكاتالونية",
|
||||
"cs": "التشيكية",
|
||||
"da": "الدنماركية",
|
||||
"de": "الألمانية",
|
||||
"de-CH": "الألمانية (سويسرا)",
|
||||
"el": "اليونانية",
|
||||
"en-GB": "الإنجليزية (المملكة المتحدة)",
|
||||
"en-US": "الإنجليزية (الولايات المتحدة)",
|
||||
"en-x-lolcat": "القطة المضحكة",
|
||||
"en-x-pirate": "الإنجليزية (القراصنة)",
|
||||
"en-x-updown": "الإنجليزية (رأسا على عقب)",
|
||||
"en-x-uwu": "الإنجليزية (UwU)",
|
||||
"eo": "الإسبرانتو",
|
||||
"es": "الإسبانية",
|
||||
"et": "الإستونية",
|
||||
"fi": "الفنلندية",
|
||||
"fr": "الفرنسية",
|
||||
"fr-BE": "الفرنسيّة (بلجيكيا)",
|
||||
"fr-CA": "الفرنسيّة (كندا)",
|
||||
"he": "العبريّة",
|
||||
"hi": "الهنديّة",
|
||||
"hr": "الكرواطيّة",
|
||||
"hu": "الهنغارية",
|
||||
"id": "الإندونيسيّة",
|
||||
"it": "الإيطاليّة",
|
||||
"ja": "اليابانيّة",
|
||||
"kk": "الكازاخية",
|
||||
"ko": "الكوريّة",
|
||||
"ky": "القيرغيزية",
|
||||
"lt": "الليتوانية",
|
||||
"lv": "اللاتفيّة",
|
||||
"ms": "الماليزية",
|
||||
"nb": "البوكماول النرويجية",
|
||||
"nl": "الهولنديّة",
|
||||
"nn": "لغة نينورسك النرويجية",
|
||||
"pes": "الفارسيّة",
|
||||
"pl": "البولنديّة",
|
||||
"pt": "البرتغاليّة",
|
||||
"pt-BR": "البرتغاليّة (البرازيليّة)",
|
||||
"ro": "الرومانيّة",
|
||||
"ru": "الروسيّة",
|
||||
"ru-x-bandit": "الروسيّة (بانديت)",
|
||||
"sk": "السلوفاكية",
|
||||
"sv": "السويديّة",
|
||||
"th": "التايلنديّة",
|
||||
"tok": "لغة التوكي بونا",
|
||||
"tr": "التركيّة",
|
||||
"tt": "لغة التتار",
|
||||
"uk": "الأوكرانية",
|
||||
"vi": "الفيتناميّة",
|
||||
"zh-Hans": "الصينيّة (المبسّطة)",
|
||||
"zh-Hant": "الصينيّة (القديمة)"
|
||||
}
|
||||
10
apps/frontend/src/locales/ar-SA/meta.json
Normal file
10
apps/frontend/src/locales/ar-SA/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "الإنجليزية (الولايات المتحدة)"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "الولايات المتحدة الأمريكية\nالإنجليزية الأمريكية"
|
||||
}
|
||||
}
|
||||
1
apps/frontend/src/locales/az-AZ/index.json
Normal file
1
apps/frontend/src/locales/az-AZ/index.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
apps/frontend/src/locales/az-AZ/languages.json
Normal file
1
apps/frontend/src/locales/az-AZ/languages.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
1
apps/frontend/src/locales/az-AZ/meta.json
Normal file
1
apps/frontend/src/locales/az-AZ/meta.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
179
apps/frontend/src/locales/be-BY/index.json
Normal file
179
apps/frontend/src/locales/be-BY/index.json
Normal file
@@ -0,0 +1,179 @@
|
||||
{
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "Карыстальнік не знойдзен"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "Наша настольнае прыкладанне даступна для ўсіх платформ, выберыце патрэбную вам версію."
|
||||
},
|
||||
"app-marketing.download.download-appimage": {
|
||||
"message": "Спампаваць AppImage"
|
||||
},
|
||||
"app-marketing.download.download-beta": {
|
||||
"message": "Спампаваць бэта-версію"
|
||||
},
|
||||
"app-marketing.download.download-deb": {
|
||||
"message": "Спампаваць DEB"
|
||||
},
|
||||
"app-marketing.download.download-rpm": {
|
||||
"message": "Спампаваць RPM"
|
||||
},
|
||||
"app-marketing.download.linux": {
|
||||
"message": "Linux"
|
||||
},
|
||||
"app-marketing.download.linux-disclaimer": {
|
||||
"message": "Вядома, што версіі Modrinth App для Linux <issues-link>маюць праблемы</issues-link> на пэўных сістэмах і ў пэўных канфігурацыях. Калі Modrinth App працуе нестабільна на вашай сістэме, мы рэкамендуем вам паспрабаваць іншыя праграмы, такія як <prism-link>Prism Launcher</prism-link>, каб лёгка ўсталяваць кантэнт Modrinth."
|
||||
},
|
||||
"app-marketing.download.mac": {
|
||||
"message": "Mac"
|
||||
},
|
||||
"app-marketing.download.options-title": {
|
||||
"message": "Опцыі спампоўкі"
|
||||
},
|
||||
"app-marketing.download.terms": {
|
||||
"message": "Спампоўваючы Modrinth App, вы згаджаецеся з нашымі <terms-link>Умовамі</terms-link> і <privacy-link>Палітыкай прыватнасці</privacy-link>."
|
||||
},
|
||||
"app-marketing.download.third-party-packages": {
|
||||
"message": "Пакеты іншых вытворцаў"
|
||||
},
|
||||
"app-marketing.download.title": {
|
||||
"message": "Спампаваць Modrinth App (бэта-версія)"
|
||||
},
|
||||
"app-marketing.download.windows": {
|
||||
"message": "Windows"
|
||||
},
|
||||
"app-marketing.features.follow.description": {
|
||||
"message": "Захоўвайце любімы кантэнт і атрымлівайце абнаўленні адным пстрычкай мышы."
|
||||
},
|
||||
"app-marketing.features.follow.title": {
|
||||
"message": "Сачыце за праектамі"
|
||||
},
|
||||
"app-marketing.features.importing.description": {
|
||||
"message": "Імпартуйце ўсе свае любімыя профілі з лаўнчара, якім вы карысталіся раней, і пачніце працаваць з Modrinth App за лічаныя секунды!"
|
||||
},
|
||||
"app-marketing.features.importing.gdlauncher-alt": {
|
||||
"message": "GDLauncher"
|
||||
},
|
||||
"app-marketing.features.importing.multimc-alt": {
|
||||
"message": "MultiMC"
|
||||
},
|
||||
"app-marketing.features.importing.title": {
|
||||
"message": "Імпартаванне профілю"
|
||||
},
|
||||
"app-marketing.features.mod-management.actions": {
|
||||
"message": "Дзеянні"
|
||||
},
|
||||
"app-marketing.features.mod-management.byAuthor": {
|
||||
"message": "ад {author}"
|
||||
},
|
||||
"app-marketing.features.mod-management.description": {
|
||||
"message": "Modrinth дазваляе лёгка кіраваць усімі вашымі модамі ў адным месцы. Вы можаце ўсталёўваць, выдаляць і абнаўляць моды адным пстрычкай мышы."
|
||||
},
|
||||
"app-marketing.features.mod-management.installed-mods": {
|
||||
"message": "Усталяваныя моды"
|
||||
},
|
||||
"app-marketing.features.mod-management.name": {
|
||||
"message": "Назва"
|
||||
},
|
||||
"app-marketing.features.mod-management.search-mods": {
|
||||
"message": "Шукаць моды"
|
||||
},
|
||||
"app-marketing.features.mod-management.title": {
|
||||
"message": "Кіраванне модамі"
|
||||
},
|
||||
"app-marketing.features.mod-management.version": {
|
||||
"message": "Версія"
|
||||
},
|
||||
"app-marketing.features.offline.description": {
|
||||
"message": "Гуляйце ў свае моды, незалежна ад таго, падключаны вы да Інтэрнэту ці не."
|
||||
},
|
||||
"app-marketing.features.offline.title": {
|
||||
"message": "Афлайн-рэжым"
|
||||
},
|
||||
"app-marketing.features.open-source.description": {
|
||||
"message": "Лаўнчар Modrinth мае цалкам адкрыты зыходны код. Вы можаце паглядзець зыходны код на нашым <github-link>GitHub</github-link>!"
|
||||
},
|
||||
"app-marketing.features.open-source.title": {
|
||||
"message": "Адкрыты зыходны код"
|
||||
},
|
||||
"app-marketing.features.performance.activity-monitor": {
|
||||
"message": "Маніторынг актыўнасці"
|
||||
},
|
||||
"app-marketing.features.performance.cpu-percent": {
|
||||
"message": "% CPU"
|
||||
},
|
||||
"app-marketing.features.performance.description": {
|
||||
"message": "Modrinth App працуе лепш, чым многія з вядучых мэнэджараў модаў, выкарыстоўваючы ўсяго 150 МБ аператыўнай памяці!"
|
||||
},
|
||||
"app-marketing.features.performance.discord": {
|
||||
"message": "Discord"
|
||||
},
|
||||
"app-marketing.features.performance.good-performance": {
|
||||
"message": "Добрая прадукцыйнасць"
|
||||
},
|
||||
"app-marketing.features.performance.google-chrome": {
|
||||
"message": "Google Chrome"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-mb": {
|
||||
"message": "∞ MB"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-times-infinite-mb": {
|
||||
"message": "∞ * ∞ MB"
|
||||
},
|
||||
"app-marketing.features.performance.less-than-150mb": {
|
||||
"message": "< 150 MB"
|
||||
},
|
||||
"app-marketing.features.performance.modrinth-app": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"app-marketing.features.performance.one-billion-percent": {
|
||||
"message": "1 billion %"
|
||||
},
|
||||
"app-marketing.features.performance.process-name": {
|
||||
"message": "Назва працэсу"
|
||||
},
|
||||
"app-marketing.features.performance.ram": {
|
||||
"message": "RAM"
|
||||
},
|
||||
"app-marketing.features.performance.small": {
|
||||
"message": "Мала"
|
||||
},
|
||||
"app-marketing.features.performance.title": {
|
||||
"message": "Прадукцыйны"
|
||||
},
|
||||
"app-marketing.features.play.description": {
|
||||
"message": "Выкарыстоўвайце Modrinth App, каб спампоўваць і гуляць у свае любімыя моды і модпакі."
|
||||
},
|
||||
"app-marketing.features.play.title": {
|
||||
"message": "Гуляйце з вашымі любімымі модамі"
|
||||
},
|
||||
"app-marketing.features.sharing.description": {
|
||||
"message": "Стварайце, дзяліцеся і гуляйце ў модпакі з любым з тысяч модаў і модпакаў, размешчаных тут, на Modrinth."
|
||||
},
|
||||
"app-marketing.features.sharing.modpack": {
|
||||
"message": "Модпак"
|
||||
},
|
||||
"app-marketing.features.sharing.share-button": {
|
||||
"message": "Падзяліцца"
|
||||
},
|
||||
"app-marketing.features.sharing.title": {
|
||||
"message": "Дзяліцеся модпакамі"
|
||||
},
|
||||
"app-marketing.features.unlike-any-launcher": {
|
||||
"message": "У адрозненне ад любога лаунчера"
|
||||
},
|
||||
"app-marketing.features.website.description": {
|
||||
"message": "Modrinth App цалкам інтэграваная з вэб-сайтам, таму вы можаце атрымаць доступ да ўсіх сваіх любімых праектаў праз праграму!"
|
||||
},
|
||||
"app-marketing.features.website.title": {
|
||||
"message": "Інтэграцыя з вэб-сайтам"
|
||||
},
|
||||
"app-marketing.features.youve-used-before": {
|
||||
"message": "які вы выкарыстоўвалі раней"
|
||||
},
|
||||
"app-marketing.hero.app-screenshot-alt": {
|
||||
"message": "Здымак экрана Modrinth App з адкрытым экзэмплярам Cobblemon на старонцы «Змест»."
|
||||
},
|
||||
"app-marketing.hero.description": {
|
||||
"message": "Modrinth App — гэта ўнікальны лаунчер з адкрытым зыходным кодам, які дазваляе вам гуляць у вашы любімыя моды і падтрымліваць іх у актуальным стане — усё ў адным акуратным невялікім пакеце."
|
||||
}
|
||||
}
|
||||
58
apps/frontend/src/locales/be-BY/languages.json
Normal file
58
apps/frontend/src/locales/be-BY/languages.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"ar": "Арабская",
|
||||
"be": "Беларуская",
|
||||
"bg": "Балгарская",
|
||||
"bn": "Бенгальская",
|
||||
"ca": "Каталонская",
|
||||
"cs": "Чэшская",
|
||||
"da": "Дацкая",
|
||||
"de": "Нямецкая",
|
||||
"de-CH": "Нямецкая (Швейцарыя)",
|
||||
"el": "Грэчаская",
|
||||
"en-GB": "Англійская (Вялікабрытанія)",
|
||||
"en-US": "Англійская (Злучаныя Штаты)",
|
||||
"en-x-lolcat": "LOLCAT",
|
||||
"en-x-pirate": "Англійская (пірацкая)",
|
||||
"en-x-updown": "Англійская (перакуленая)",
|
||||
"en-x-uwu": "Англійская (UwU)",
|
||||
"eo": "Эсперанта",
|
||||
"es": "Іспанская",
|
||||
"et": "Эстонская",
|
||||
"fi": "Фінская",
|
||||
"fr": "Французская",
|
||||
"fr-BE": "Французская (Бельгія)",
|
||||
"fr-CA": "Французская (Канада)",
|
||||
"he": "Іўрыт",
|
||||
"hi": "Хіндзі",
|
||||
"hr": "Харвацкая",
|
||||
"hu": "Вугорская",
|
||||
"id": "Інданезійская",
|
||||
"it": "Італьянская",
|
||||
"ja": "Японская",
|
||||
"kk": "Казахская",
|
||||
"ko": "Карэйская",
|
||||
"ky": "Кіргізская",
|
||||
"lt": "Літоўская",
|
||||
"lv": "Латышская",
|
||||
"ms": "Малайская",
|
||||
"nb": "Нарвежская (Букмол)",
|
||||
"nl": "Нідэрландская",
|
||||
"nn": "Нарвежская (Нюнорск)",
|
||||
"pes": "Персідская",
|
||||
"pl": "Польская",
|
||||
"pt": "Партугальская",
|
||||
"pt-BR": "Партугальская (Бразілія)",
|
||||
"ro": "Румынская",
|
||||
"ru": "Руская",
|
||||
"ru-x-bandit": "Руская (бандыцкая)",
|
||||
"sk": "Славацкая",
|
||||
"sv": "Шведская",
|
||||
"th": "Тайская",
|
||||
"tok": "Такіпона",
|
||||
"tr": "Турэцкая",
|
||||
"tt": "Татарская",
|
||||
"uk": "Украінская",
|
||||
"vi": "В'етнамская",
|
||||
"zh-Hans": "Кітайская (спрошчаная)",
|
||||
"zh-Hant": "Кітайская (традыцыйная)"
|
||||
}
|
||||
10
apps/frontend/src/locales/be-BY/meta.json
Normal file
10
apps/frontend/src/locales/be-BY/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "Беларуская"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "Беларуская"
|
||||
}
|
||||
}
|
||||
1
apps/frontend/src/locales/bg-BG/index.json
Normal file
1
apps/frontend/src/locales/bg-BG/index.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
57
apps/frontend/src/locales/bg-BG/languages.json
Normal file
57
apps/frontend/src/locales/bg-BG/languages.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"ar": "Арабски",
|
||||
"be": "Беларуски",
|
||||
"bg": "Български",
|
||||
"bn": "Бенгалски",
|
||||
"ca": "Каталонски",
|
||||
"cs": "Чешки",
|
||||
"da": "Датски",
|
||||
"de": "Немски",
|
||||
"de-CH": "Немски (Швейцария)",
|
||||
"el": "Гръцки",
|
||||
"en-GB": "Английски (Великобритания)",
|
||||
"en-US": "Английски (САЩ)",
|
||||
"en-x-pirate": "Английски (Пиратски)",
|
||||
"en-x-updown": "Английски (С главата надолу)",
|
||||
"en-x-uwu": "Английски (UwU)",
|
||||
"eo": "Есперанто",
|
||||
"es": "Испански",
|
||||
"et": "Естонски",
|
||||
"fi": "Финландски",
|
||||
"fr": "Френски",
|
||||
"fr-BE": "Френски (Белгия)",
|
||||
"fr-CA": "Френски (Канада)",
|
||||
"he": "Иврит",
|
||||
"hi": "Хинди",
|
||||
"hr": "Хърватски",
|
||||
"hu": "Унгарски",
|
||||
"id": "Индонезийски",
|
||||
"it": "Италиански",
|
||||
"ja": "Японски",
|
||||
"kk": "Казахски",
|
||||
"ko": "Корейски",
|
||||
"ky": "Киргизски",
|
||||
"lt": "Литовски",
|
||||
"lv": "Латвийски",
|
||||
"ms": "Малайски",
|
||||
"nb": "Норвежки (Букмол)",
|
||||
"nl": "Холандски",
|
||||
"nn": "Норвежки (Нюношк)",
|
||||
"pes": "Персийски",
|
||||
"pl": "Полски",
|
||||
"pt": "Португалски",
|
||||
"pt-BR": "Португалски (Бразилия)",
|
||||
"ro": "Румънски",
|
||||
"ru": "Руски",
|
||||
"ru-x-bandit": "Руски (Бандитски)",
|
||||
"sk": "Словашки",
|
||||
"sv": "Шведски",
|
||||
"th": "Тайландски",
|
||||
"tok": "Токи Пона",
|
||||
"tr": "Турски",
|
||||
"tt": "Татарски",
|
||||
"uk": "Украински",
|
||||
"vi": "Виетнамски",
|
||||
"zh-Hans": "Китайски (Опростен)",
|
||||
"zh-Hant": "Китайски (Традиционен)"
|
||||
}
|
||||
10
apps/frontend/src/locales/bg-BG/meta.json
Normal file
10
apps/frontend/src/locales/bg-BG/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "Български"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "България\nБългарски"
|
||||
}
|
||||
}
|
||||
1
apps/frontend/src/locales/bn-BD/index.json
Normal file
1
apps/frontend/src/locales/bn-BD/index.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
57
apps/frontend/src/locales/bn-BD/languages.json
Normal file
57
apps/frontend/src/locales/bn-BD/languages.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"ar": "আরবি",
|
||||
"be": "বেলারুশিয়ান",
|
||||
"bg": "বুলগেরিয়ান",
|
||||
"bn": "বাংলা",
|
||||
"ca": "কাতালান",
|
||||
"cs": "চেক",
|
||||
"da": "ডেনিশ",
|
||||
"de": "জার্মান",
|
||||
"de-CH": "জার্মান (সুইজারল্যান্ড)",
|
||||
"el": "গ্রীক",
|
||||
"en-GB": "ইংরেজি (যুক্তরাজ্য)",
|
||||
"en-US": "ইংরেজি (মার্কিন যুক্তরাষ্ট্র)",
|
||||
"en-x-pirate": "ইংরেজি (জলদস্যু)",
|
||||
"en-x-updown": "ইংরেজি (উল্টো)",
|
||||
"en-x-uwu": "ইংরেজি (ইউডাব্লুউ)",
|
||||
"eo": "এস্পেরান্টো",
|
||||
"es": "স্প্যানিশ",
|
||||
"et": "এস্তোনিয়ান",
|
||||
"fi": "ফিনিশ",
|
||||
"fr": "ফরাসি",
|
||||
"fr-BE": "ফরাসি (বেলজিয়াম)",
|
||||
"fr-CA": "ফরাসী (কানাডা)",
|
||||
"he": "হিব্রু",
|
||||
"hi": "হিন্দি",
|
||||
"hr": "ক্রোয়েশিয়ান",
|
||||
"hu": "হাঙ্গেরিয়ান",
|
||||
"id": "ইন্দোনেশিয়ান",
|
||||
"it": "ইতালিয়ান",
|
||||
"ja": "জাপানি",
|
||||
"kk": "কাজাখ",
|
||||
"ko": "কোরিয়ান",
|
||||
"ky": "কিরগিজ",
|
||||
"lt": "লিথুয়ানিয়ান",
|
||||
"lv": "লাত্ভিয়ান",
|
||||
"ms": "মালয়",
|
||||
"nb": "নরওয়েজিয়ান বোকমেল",
|
||||
"nl": "ডাচ",
|
||||
"nn": "নরওয়েজিয়ান নাইনোরস্ক",
|
||||
"pes": "পার্সিয়ান",
|
||||
"pl": "পোলিশ",
|
||||
"pt": "পর্তুগিজ",
|
||||
"pt-BR": "পর্তুগিজ (ব্রাজিল)",
|
||||
"ro": "রোমানিয়ান",
|
||||
"ru": "রাশিয়ান",
|
||||
"ru-x-bandit": "রাশিয়ান (ডাকাত)",
|
||||
"sk": "স্লোভাক",
|
||||
"sv": "সুইডিশ",
|
||||
"th": "থাই",
|
||||
"tok": "টোকি পোনা",
|
||||
"tr": "তুর্কি",
|
||||
"tt": "তাতার",
|
||||
"uk": "ইউক্রেনীয়",
|
||||
"vi": "ভিয়েতনামী",
|
||||
"zh-Hans": "চাইনিজ (সরলীকৃত)",
|
||||
"zh-Hant": "চাইনিজ (traditional তিহ্যবাহী)"
|
||||
}
|
||||
10
apps/frontend/src/locales/bn-BD/meta.json
Normal file
10
apps/frontend/src/locales/bn-BD/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "ইংরেজি (যুক্তরাজ্য)"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "ইউকে\nব্রিটিশ ইংরেজি"
|
||||
}
|
||||
}
|
||||
89
apps/frontend/src/locales/ca-ES/index.json
Normal file
89
apps/frontend/src/locales/ca-ES/index.json
Normal file
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "L'usuari no s'ha trobat"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "El programari és disponible en diverses plataformes, selecciona la teva versió."
|
||||
},
|
||||
"app-marketing.download.download-appimage": {
|
||||
"message": "Descarrega l'AppImage"
|
||||
},
|
||||
"app-marketing.download.download-beta": {
|
||||
"message": "Descarrega la beta"
|
||||
},
|
||||
"app-marketing.download.download-deb": {
|
||||
"message": "Descarrega l'arxiu DEB"
|
||||
},
|
||||
"app-marketing.download.download-rpm": {
|
||||
"message": "Descarrega l'arxiu RPM"
|
||||
},
|
||||
"app-marketing.download.linux": {
|
||||
"message": "Linux"
|
||||
},
|
||||
"app-marketing.download.mac": {
|
||||
"message": "Mac"
|
||||
},
|
||||
"app-marketing.download.options-title": {
|
||||
"message": "Opcions de descàrrega"
|
||||
},
|
||||
"auth.authorize.app-info": {
|
||||
"message": "<strong>{appName}</strong> per <creator-link>{creator}</creator-link> podrà:"
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.label": {
|
||||
"message": "Correu electrònic o nom d’usuari"
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.placeholder": {
|
||||
"message": "Correu electrònic"
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.title": {
|
||||
"message": "Correu electrònic enviat"
|
||||
},
|
||||
"auth.sign-in.password.label": {
|
||||
"message": "Contrasenya"
|
||||
},
|
||||
"auth.sign-up.email.label": {
|
||||
"message": "Correu electrònic"
|
||||
},
|
||||
"auth.sign-up.label.username": {
|
||||
"message": "Nom d'usuari"
|
||||
},
|
||||
"auth.sign-up.password.label": {
|
||||
"message": "Contrasenya"
|
||||
},
|
||||
"auth.welcome.title": {
|
||||
"message": "Us donem la benvinguda"
|
||||
},
|
||||
"error.collection.404.list_title": {
|
||||
"message": "Per què?"
|
||||
},
|
||||
"error.generic.404.title": {
|
||||
"message": "Pàgina no trobada"
|
||||
},
|
||||
"error.organization.404.list_title": {
|
||||
"message": "Per què?"
|
||||
},
|
||||
"error.project.404.list_title": {
|
||||
"message": "Per què?"
|
||||
},
|
||||
"error.project.404.title": {
|
||||
"message": "Projecte no trobat"
|
||||
},
|
||||
"error.user.404.list_title": {
|
||||
"message": "Per què?"
|
||||
},
|
||||
"error.user.404.title": {
|
||||
"message": "Usuari no trobat"
|
||||
},
|
||||
"frog": {
|
||||
"message": "Has estat granotejat! 🐸"
|
||||
},
|
||||
"frog.title": {
|
||||
"message": "Granota"
|
||||
},
|
||||
"layout.action.change-theme": {
|
||||
"message": "Canviar tema"
|
||||
},
|
||||
"settings.billing.payment_method.action.add": {
|
||||
"message": "Afegir mètode de pagament"
|
||||
}
|
||||
}
|
||||
51
apps/frontend/src/locales/ca-ES/languages.json
Normal file
51
apps/frontend/src/locales/ca-ES/languages.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"ar": "Àrab",
|
||||
"be": "Bielorús",
|
||||
"bg": "Búlgar",
|
||||
"ca": "Català",
|
||||
"cs": "Txec",
|
||||
"da": "Danès",
|
||||
"de": "Alemany",
|
||||
"de-CH": "Alemany (Suïssa)",
|
||||
"el": "Grec",
|
||||
"en-GB": "Anglès (Regne Unit)",
|
||||
"en-US": "Anglès (Estats Units)",
|
||||
"en-x-pirate": "Anglès (Pirata)",
|
||||
"en-x-updown": "Anglès (Capgirat)",
|
||||
"en-x-uwu": "Anglès (UwU)",
|
||||
"es": "Espanyol",
|
||||
"et": "Estonià",
|
||||
"fi": "Finès",
|
||||
"fr": "Francès",
|
||||
"fr-BE": "Francès (Bèlgica)",
|
||||
"fr-CA": "Francès (Canadà)",
|
||||
"he": "Hebreu",
|
||||
"hr": "Croat",
|
||||
"hu": "Hongarès",
|
||||
"id": "Indonèsi",
|
||||
"it": "Italià",
|
||||
"ja": "Japonès",
|
||||
"ko": "Coreà",
|
||||
"lt": "Lituà",
|
||||
"lv": "Letó",
|
||||
"ms": "Malai",
|
||||
"nb": "Bokmal noruec",
|
||||
"nl": "Neerlandès",
|
||||
"nn": "Noruec Nynorsk",
|
||||
"pes": "Persa",
|
||||
"pl": "Polonès",
|
||||
"pt": "Portuguès",
|
||||
"pt-BR": "Portuguès (Brasil)",
|
||||
"ro": "Romanès",
|
||||
"ru": "Rus",
|
||||
"ru-x-bandit": "Rus (Bandit)",
|
||||
"sk": "Eslovac",
|
||||
"sv": "Suec",
|
||||
"th": "Tailandès",
|
||||
"tr": "Turc",
|
||||
"tt": "Tàtar",
|
||||
"uk": "Ucraïnès",
|
||||
"vi": "Vietnamita",
|
||||
"zh-Hans": "Xinès (simplificat)",
|
||||
"zh-Hant": "Xinès (tradicional)"
|
||||
}
|
||||
10
apps/frontend/src/locales/ca-ES/meta.json
Normal file
10
apps/frontend/src/locales/ca-ES/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "Català (Catalan)"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "Català\nCatalunya\nValencià\nCAT"
|
||||
}
|
||||
}
|
||||
311
apps/frontend/src/locales/ceb-PH/index.json
Normal file
311
apps/frontend/src/locales/ceb-PH/index.json
Normal file
@@ -0,0 +1,311 @@
|
||||
{
|
||||
"app-marketing.download.description": {
|
||||
"message": "Magamit ang among desktop nga aplikasyon sa magkalahi-lahi nga plataporma, pamili sa imong giganahan nga bersiyon."
|
||||
},
|
||||
"app-marketing.download.download-appimage": {
|
||||
"message": "Karganugi ang AppImage"
|
||||
},
|
||||
"app-marketing.download.download-beta": {
|
||||
"message": "Karganugi ang beta"
|
||||
},
|
||||
"app-marketing.download.download-deb": {
|
||||
"message": "Karganugi ang DEB"
|
||||
},
|
||||
"app-marketing.download.download-rpm": {
|
||||
"message": "Karganugi ang RPM"
|
||||
},
|
||||
"app-marketing.download.linux": {
|
||||
"message": "Linux"
|
||||
},
|
||||
"app-marketing.download.linux-disclaimer": {
|
||||
"message": "<issues-link>Ila nga naay suliranin</issues-link> ang Linux nga bersiyon sa Modrinth App sa pipila nga mga sistema ug paghan-ay. Kung dili lig-on ang Modrinth App sa imong sistema, gidasig ka namo sa pagsulay sa uban nga mga aplikasyon sama sa <prism-link>Prism Launcher</prism-link> aron mapadali ang pagtaod sa sulod nga Modrinth."
|
||||
},
|
||||
"app-marketing.download.mac": {
|
||||
"message": "Mac"
|
||||
},
|
||||
"app-marketing.download.options-title": {
|
||||
"message": "Mga kapilian sa pagkarganug"
|
||||
},
|
||||
"app-marketing.download.terms": {
|
||||
"message": "Sa pagkarganug sa Modrinth App, gauyon ka sa among <terms-link>Mga Termino</terms-link> ug <privacy-link>Palisiya sa Pribasiya</privacy-link>."
|
||||
},
|
||||
"app-marketing.download.title": {
|
||||
"message": "Karganugi ang Modrinth App (Beta)"
|
||||
},
|
||||
"app-marketing.download.windows": {
|
||||
"message": "Windows"
|
||||
},
|
||||
"app-marketing.features.open-source.title": {
|
||||
"message": "Bukas nga tinubdan"
|
||||
},
|
||||
"app-marketing.features.performance.modrinth-app": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"app-marketing.features.play.description": {
|
||||
"message": "Gamita ang Modrinth App sa pagkarganug ug pagdula sa imong paborito nga mga kausaban ug putos sa kausaban."
|
||||
},
|
||||
"app-marketing.hero.download-button": {
|
||||
"message": "Karganugi ang Modrinth App"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app": {
|
||||
"message": "Karganugi ang Modrinth App"
|
||||
},
|
||||
"app-marketing.hero.download-modrinth-app-for-os": {
|
||||
"message": "Karganugi para sa {os} ang Modrinth App"
|
||||
},
|
||||
"app-marketing.hero.more-download-options": {
|
||||
"message": "Dugang nga Kapilian sa Pagkarganug"
|
||||
},
|
||||
"auth.verify-email.action.account-settings": {
|
||||
"message": "Mga gusto sa akawnt"
|
||||
},
|
||||
"auth.welcome.description": {
|
||||
"message": "Lakip na ka karon sa niining makapahingangha nga dakbalangay sama sa mga magbubuhat ug manunuhid nga gatukod, gakarganug, ug gasunod sa kapanahonan sa mga makapahibulong nga mga kausaban."
|
||||
},
|
||||
"collection.button.edit-icon": {
|
||||
"message": "Usba ang amoy"
|
||||
},
|
||||
"collection.button.remove-project": {
|
||||
"message": "Tangtangi ang proyekto"
|
||||
},
|
||||
"collection.delete-modal.description": {
|
||||
"message": "Hangtod sa kahangturan nga matangtang kining gitipon. Kini nga aksyon dili mabawi."
|
||||
},
|
||||
"dashboard.creator-tax-form-modal.entity.description": {
|
||||
"message": "Usa ka entidad sa negosyo ang langyaw nga entidad nga naghan-ay gawas sa Tinipong Bansa (sama sa di-US nga korporasyon, partnership, o LLC)."
|
||||
},
|
||||
"dashboard.creator-tax-form-modal.entity.foreign-entity": {
|
||||
"message": "Langyaw nga entidad"
|
||||
},
|
||||
"dashboard.creator-tax-form-modal.entity.question": {
|
||||
"message": "Usa ka ba nga pribado nga indibidwal o uban sa usa ka langyaw nga entidad?"
|
||||
},
|
||||
"landing.creator.feature.data-statistics.description": {
|
||||
"message": "Makadawat og detalyado nga pagtaho sa mga paglantaw og panid, ihap sa pagkarganug, ug kinitaan"
|
||||
},
|
||||
"landing.feature.launcher.description": {
|
||||
"message": "Ang bukas nga tinubdan nga API sa Modrinth nagtugot sa mga tiglansad nga makadugang sa lawom nga panagsama sa Modrinth. Mahimo nimong magamit ang Modrinth pinaagi sa <link>among aplikasyon</link> ug sa pipila nga labing inila nga mga tiglansad sama sa ATLauncher, MultiMC, ug Prism Launcher."
|
||||
},
|
||||
"landing.heading.the-place-for-minecraft.resource-packs": {
|
||||
"message": "mga putos sa kabtangan"
|
||||
},
|
||||
"landing.subheading": {
|
||||
"message": "Pagkaplag, pagdula, ug pagbahin og mga Minecraft nga kontento sa among bukas nga tinubdan nga plataporma nga gihimo alang sa dakbalangay."
|
||||
},
|
||||
"layout.action.create-new": {
|
||||
"message": "Pagbuhat og bag-o..."
|
||||
},
|
||||
"layout.action.reports": {
|
||||
"message": "Mga pagtaho"
|
||||
},
|
||||
"layout.banner.add-email.button": {
|
||||
"message": "Mga gusto sa akawnt"
|
||||
},
|
||||
"layout.banner.build-fail.description": {
|
||||
"message": "Napakyas sa pagmugna og kahimtang gikan sa API kining pagkatap sa frontend sa Modrinth. Basin tungod sa pagkabalda o sayop sa paghan-ay. Pagtukod pag-usab kon magamit na ang API. Mga kalagdaan sa sayop: {errors}; Ang kasamtangan nga URL sa API: {url}"
|
||||
},
|
||||
"layout.banner.build-fail.title": {
|
||||
"message": "Naay sayup sa pagmugna sa kahimtang gikan sa API samtang gapatukod."
|
||||
},
|
||||
"layout.footer.about": {
|
||||
"message": "Mahitungod"
|
||||
},
|
||||
"layout.footer.about.careers": {
|
||||
"message": "Mga Karera"
|
||||
},
|
||||
"layout.footer.about.changelog": {
|
||||
"message": "Talaan sa Kausaban"
|
||||
},
|
||||
"layout.footer.about.news": {
|
||||
"message": "Balita"
|
||||
},
|
||||
"layout.footer.about.rewards-program": {
|
||||
"message": "Programa sa Pagganti"
|
||||
},
|
||||
"layout.footer.about.status": {
|
||||
"message": "Kahimtang"
|
||||
},
|
||||
"layout.footer.legal": {
|
||||
"message": "Legal"
|
||||
},
|
||||
"layout.footer.legal-disclaimer": {
|
||||
"message": "DILI OPISYAL NGA SERBISYO SA MINECRAFT. WALA MAAPRUBAHAN UG DILI KAAKIBAT SA MOJANG O MICROSOFT."
|
||||
},
|
||||
"layout.footer.legal.copyright-policy": {
|
||||
"message": "Palisiya sa Katungod sa Pagkopya ug DMCA"
|
||||
},
|
||||
"layout.footer.legal.privacy-policy": {
|
||||
"message": "Palisiya sa Pribasiya"
|
||||
},
|
||||
"layout.footer.legal.rules": {
|
||||
"message": "Mga Lagda sa Content"
|
||||
},
|
||||
"layout.footer.legal.security-notice": {
|
||||
"message": "Pahibalo sa Kahilwasan"
|
||||
},
|
||||
"layout.footer.legal.terms-of-use": {
|
||||
"message": "Mga Kahimtang sa Paggamit"
|
||||
},
|
||||
"layout.footer.open-source": {
|
||||
"message": "<github-link>Bukas nga tinubdan</github-link> ang Modrinth."
|
||||
},
|
||||
"layout.footer.products": {
|
||||
"message": "Mga Produkto"
|
||||
},
|
||||
"layout.footer.products.app": {
|
||||
"message": "Modrinth App"
|
||||
},
|
||||
"layout.footer.products.plus": {
|
||||
"message": "Modrinth+"
|
||||
},
|
||||
"layout.footer.products.servers": {
|
||||
"message": "Modrinth Servers"
|
||||
},
|
||||
"layout.footer.resources": {
|
||||
"message": "Mga Kabtangan"
|
||||
},
|
||||
"layout.footer.resources.api-docs": {
|
||||
"message": "Dokumentasyon sa API"
|
||||
},
|
||||
"layout.footer.resources.help-center": {
|
||||
"message": "Sentro sa Pagtabang"
|
||||
},
|
||||
"layout.footer.resources.report-issues": {
|
||||
"message": "Pagtaho og mga isyu"
|
||||
},
|
||||
"layout.footer.resources.translate": {
|
||||
"message": "Paghubad"
|
||||
},
|
||||
"layout.footer.social.bluesky": {
|
||||
"message": "Bluesky"
|
||||
},
|
||||
"layout.footer.social.discord": {
|
||||
"message": "Discord"
|
||||
},
|
||||
"layout.footer.social.github": {
|
||||
"message": "GitHub"
|
||||
},
|
||||
"layout.footer.social.mastodon": {
|
||||
"message": "Mastodon"
|
||||
},
|
||||
"layout.footer.social.x": {
|
||||
"message": "X"
|
||||
},
|
||||
"layout.meta.description": {
|
||||
"message": "Pagkarganug ug mga pagbag-o, pagsukip, putos sa datos, tigpandong, putos sa kabtangan, ug putos sa pagbag-o sa Modrinth. Pagdiskobre ug pagmantala ug mga proyekto sa Modrinth nga adunay moderno, sayon gamiton nga dunggoanan ug API."
|
||||
},
|
||||
"moderation.page.projects": {
|
||||
"message": "Mga Proyekto"
|
||||
},
|
||||
"moderation.page.reports": {
|
||||
"message": "Mga Pagtaho"
|
||||
},
|
||||
"moderation.page.technicalReview": {
|
||||
"message": "Teknikal ng Pagsusi"
|
||||
},
|
||||
"profile.meta.description": {
|
||||
"message": "Pagkarganug og mga proyekto ni {username} sa Modrinth"
|
||||
},
|
||||
"profile.meta.description-with-bio": {
|
||||
"message": "{bio} - Pagkarganug og mga proyekto ni {username} sa Modrinth"
|
||||
},
|
||||
"profile.stats.downloads": {
|
||||
"message": "{count, plural, other {<stat>{count}</stat> ka karganug sa proyekto}}"
|
||||
},
|
||||
"project-type.datapack.plural": {
|
||||
"message": "Mga Putos sa Datos"
|
||||
},
|
||||
"project-type.datapack.singular": {
|
||||
"message": "Putos sa Datos"
|
||||
},
|
||||
"project-type.resourcepack.plural": {
|
||||
"message": "Mga Putos sa Kabtangan"
|
||||
},
|
||||
"project-type.resourcepack.singular": {
|
||||
"message": "Putos sa Kabtangan"
|
||||
},
|
||||
"project.download.game-version": {
|
||||
"message": "Bersiyon sa dula: {version}"
|
||||
},
|
||||
"project.download.no-app": {
|
||||
"message": "Wala kay Modrinth App?"
|
||||
},
|
||||
"project.download.platform": {
|
||||
"message": "Plataporma: {platform}"
|
||||
},
|
||||
"project.download.search-game-versions": {
|
||||
"message": "Mangita og mga bersiyon sa dula..."
|
||||
},
|
||||
"project.download.select-game-version": {
|
||||
"message": "Pilia tanang bersiyon sa dula"
|
||||
},
|
||||
"project.download.show-all-versions": {
|
||||
"message": "Ipakita ang tanang bersiyon"
|
||||
},
|
||||
"project.download.title": {
|
||||
"message": "Karganugi ang {title}"
|
||||
},
|
||||
"project.environment.migration-no-permission.message": {
|
||||
"message": "Bag-o ra namo gipalambo ang sistema sa Mga Kalikopan sa Modrinth ug ang mga bag-ong kapilian magamit na karon. Wala kay permiso sa pag-usab sa niining mga gusto, apan palihug pahibaloa ang ubang sakop sa proyekto nga kinahanglan nga mapamatud-an ang metadata sa kalikopan."
|
||||
},
|
||||
"project.environment.migration-no-permission.title": {
|
||||
"message": "Kinahanglang masusi ang metadata sa environment"
|
||||
},
|
||||
"project.environment.migration.message": {
|
||||
"message": "Bag-o ra namo gipalambo ang sistema sa Mga Kalikopan sa Modrinth ug ang mga bag-ong kapilian magamit na karon. Mahimo nga bisitahon ang mga gusto sa imong proyekto ug pamatud-i nga husto ang metadata."
|
||||
},
|
||||
"project.environment.migration.review-button": {
|
||||
"message": "Susihon ang mga gusto sa kalikopan"
|
||||
},
|
||||
"project.environment.migration.title": {
|
||||
"message": "Palihug sa pagsusi sa metadata sa kalikopan"
|
||||
},
|
||||
"project.settings.environment.notice.missing-env.description": {
|
||||
"message": "Wala pay metadata sa kalikopan ang imong proyekto, mahimo nga pilia ang husto sa ubos."
|
||||
},
|
||||
"project.settings.environment.notice.missing-env.title": {
|
||||
"message": "Palihug sa pagpili sa kalikopan sa imong proyekto"
|
||||
},
|
||||
"project.settings.environment.notice.multiple-environments.title": {
|
||||
"message": "Daghang kalikopan ang imong proyekto"
|
||||
},
|
||||
"project.settings.environment.notice.review-options.description": {
|
||||
"message": "Bag-o ra namo gipalambo ang sistema sa Mga Kalikopan sa Modrinth ug ang mga bag-ong kapilian magamit na karon. Mahimo nga susiha nga husto ang gipili sa ubos ug tuploka ang 'Pamatud-i' sa imong paghuman!"
|
||||
},
|
||||
"project.settings.title": {
|
||||
"message": "Mga Gusto"
|
||||
},
|
||||
"project.stats.downloads-label": {
|
||||
"message": "karganug{count, plural, one {} other {}}"
|
||||
},
|
||||
"servers.backups.item.failed-to-prepare-backup": {
|
||||
"message": "Napakyas ang pag-andam sa karganug"
|
||||
},
|
||||
"servers.backups.item.prepare-download": {
|
||||
"message": "Andami ang karganug"
|
||||
},
|
||||
"servers.backups.item.prepare-download-again": {
|
||||
"message": "Sulayi pag-usab ang pag-andam"
|
||||
},
|
||||
"servers.backups.item.preparing-download": {
|
||||
"message": "Gaandam sa karganug..."
|
||||
},
|
||||
"settings.display.flags.description": {
|
||||
"message": "Magpagana o di-magpagana og tino nga mga bahin sa imong himan."
|
||||
},
|
||||
"settings.display.project-list-layouts.datapack": {
|
||||
"message": "Panid sa mga Putos sa Datos"
|
||||
},
|
||||
"settings.display.project-list-layouts.description": {
|
||||
"message": "Pamili og imong gigusto nga paghan-ay alang sa matag usa nga panid nga mopakita sa mga listahan sa proyekto sa imong himan."
|
||||
},
|
||||
"settings.display.project-list-layouts.resourcepack": {
|
||||
"message": "Panid sa mga Putos sa Kabtangan"
|
||||
},
|
||||
"settings.display.theme.description": {
|
||||
"message": "Pamili og imong gigusto nga hiligotanong kulay alang sa Modrinth sa kini nga himan."
|
||||
},
|
||||
"settings.sessions.description": {
|
||||
"message": "Ania dini tanan ang mga himan nga karon nakalog-in sa imong akawnt sa Modrinth. Mahimo kang maglog-out sa matag usa.\n\nKung makakita ka og usa nga dili nimo mailhan, paglog-out sa maong himan ug usba dayon ang tinago nga pulong sa imong akawnt sa Modrinth."
|
||||
}
|
||||
}
|
||||
58
apps/frontend/src/locales/ceb-PH/languages.json
Normal file
58
apps/frontend/src/locales/ceb-PH/languages.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"ar": "Inarabigo",
|
||||
"be": "Binyeloruso",
|
||||
"bg": "Binulgaro",
|
||||
"bn": "Bangla",
|
||||
"ca": "Kinatalan",
|
||||
"cs": "Tsineko",
|
||||
"da": "Dinanes",
|
||||
"de": "Inaleman",
|
||||
"de-CH": "Inaleman (Suwisa)",
|
||||
"el": "Grinyego",
|
||||
"en-GB": "Ininggles (Hiniusang Gingharian)",
|
||||
"en-US": "Ininggles (Tinipong Bansa)",
|
||||
"en-x-lolcat": "LOLCAT",
|
||||
"en-x-pirate": "Inggles (Pirata)",
|
||||
"en-x-updown": "Ininggles (Baliskad)",
|
||||
"en-x-uwu": "Ininggles (UwU)",
|
||||
"eo": "Inesperanto",
|
||||
"es": "Kinatsila",
|
||||
"et": "Inestonyo",
|
||||
"fi": "Pinines",
|
||||
"fr": "Prinanses",
|
||||
"fr-BE": "Prinanses (Belhika)",
|
||||
"fr-CA": "Prinanses (Kanada)",
|
||||
"he": "Inebreyo",
|
||||
"hi": "Inindi",
|
||||
"hr": "Krinoato",
|
||||
"hu": "Inunggaro",
|
||||
"id": "Inindonesyo",
|
||||
"it": "Initalyano",
|
||||
"ja": "Hinapon",
|
||||
"kk": "Kinasaho",
|
||||
"ko": "Kinoreano",
|
||||
"ky": "Kinirgis",
|
||||
"lt": "Linitwano",
|
||||
"lv": "Lineton",
|
||||
"ms": "Minalay",
|
||||
"nb": "Ninorwego Bokmål",
|
||||
"nl": "Inolandes",
|
||||
"nn": "Ninorwego Nynorsk",
|
||||
"pes": "Pinersyano",
|
||||
"pl": "Pinolako",
|
||||
"pt": "Pinortuges",
|
||||
"pt-BR": "Pinortuges (Brasil)",
|
||||
"ro": "Rinumano",
|
||||
"ru": "Rinuso",
|
||||
"ru-x-bandit": "Rinuso (Bandido)",
|
||||
"sk": "Ineslobako",
|
||||
"sv": "Sinweko",
|
||||
"th": "Tinalaydiyanhon",
|
||||
"tok": "Toki Pona",
|
||||
"tr": "Tinurko",
|
||||
"tt": "Tinartaro",
|
||||
"uk": "Inukranyano",
|
||||
"vi": "Binyetnamita",
|
||||
"zh-Hans": "Ininsik (Yano)",
|
||||
"zh-Hant": "Ininsik (Naandan)"
|
||||
}
|
||||
10
apps/frontend/src/locales/ceb-PH/meta.json
Normal file
10
apps/frontend/src/locales/ceb-PH/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "Cebuano"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "Sinugboanong Binisaya"
|
||||
}
|
||||
}
|
||||
1151
apps/frontend/src/locales/cs-CZ/index.json
Normal file
1151
apps/frontend/src/locales/cs-CZ/index.json
Normal file
File diff suppressed because it is too large
Load Diff
58
apps/frontend/src/locales/cs-CZ/languages.json
Normal file
58
apps/frontend/src/locales/cs-CZ/languages.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"ar": "Arabština",
|
||||
"be": "Běloruština",
|
||||
"bg": "Bulharština",
|
||||
"bn": "Bengálština",
|
||||
"ca": "Katalánština",
|
||||
"cs": "Čeština",
|
||||
"da": "Dánština",
|
||||
"de": "Němčina",
|
||||
"de-CH": "Němčina (Švýcarsko)",
|
||||
"el": "Řečtina",
|
||||
"en-GB": "Angličtina (Velká Británie)",
|
||||
"en-US": "Angličtina (Spojené státy americké)",
|
||||
"en-x-lolcat": "LOLCAT",
|
||||
"en-x-pirate": "Angličtina (Pirátská)",
|
||||
"en-x-updown": "Angličtina (Vzhůru nohama)",
|
||||
"en-x-uwu": "Angličtina (UwU)",
|
||||
"eo": "Esperanto",
|
||||
"es": "Španělština",
|
||||
"et": "Estonština",
|
||||
"fi": "Finština",
|
||||
"fr": "Francouzština",
|
||||
"fr-BE": "Francouzština (Belgie)",
|
||||
"fr-CA": "Francouzština (Kanada)",
|
||||
"he": "Hebrejština",
|
||||
"hi": "Hindština",
|
||||
"hr": "Chorvatština",
|
||||
"hu": "Maďarština",
|
||||
"id": "Indonéština",
|
||||
"it": "Italština",
|
||||
"ja": "Japonština",
|
||||
"kk": "Kazaština",
|
||||
"ko": "Korejština",
|
||||
"ky": "Kyrgyzština",
|
||||
"lt": "Litevština",
|
||||
"lv": "Lotyština",
|
||||
"ms": "Malajština",
|
||||
"nb": "Norština",
|
||||
"nl": "Dánština",
|
||||
"nn": "Norština",
|
||||
"pes": "Perština",
|
||||
"pl": "Polština",
|
||||
"pt": "Portugalština",
|
||||
"pt-BR": "Portugalština (Brazílie)",
|
||||
"ro": "Rumunština",
|
||||
"ru": "Ruština",
|
||||
"ru-x-bandit": "Ruština (Bandit)",
|
||||
"sk": "Slovenština",
|
||||
"sv": "Švédština",
|
||||
"th": "Thajština",
|
||||
"tok": "Toki Ponština",
|
||||
"tr": "Turečtina",
|
||||
"tt": "Tatarština",
|
||||
"uk": "Ukrajinština",
|
||||
"vi": "Vietnamština",
|
||||
"zh-Hans": "Čínština (zjednodušená)",
|
||||
"zh-Hant": "Čínština (tradiční)"
|
||||
}
|
||||
10
apps/frontend/src/locales/cs-CZ/meta.json
Normal file
10
apps/frontend/src/locales/cs-CZ/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "Čeština (Česko)"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "ČR\nČesky"
|
||||
}
|
||||
}
|
||||
185
apps/frontend/src/locales/da-DK/index.json
Normal file
185
apps/frontend/src/locales/da-DK/index.json
Normal file
@@ -0,0 +1,185 @@
|
||||
{
|
||||
"admin.billing.error.not-found": {
|
||||
"message": "Bruger ikke fundet"
|
||||
},
|
||||
"app-marketing.download.description": {
|
||||
"message": "Vores skrivebords-app er tilgængelig til alle platforme, vælg din ønsket version."
|
||||
},
|
||||
"app-marketing.download.download-appimage": {
|
||||
"message": "Download AppImage'en"
|
||||
},
|
||||
"app-marketing.download.download-beta": {
|
||||
"message": "Download beta'en"
|
||||
},
|
||||
"app-marketing.download.download-deb": {
|
||||
"message": "Download DEB'en"
|
||||
},
|
||||
"app-marketing.download.download-rpm": {
|
||||
"message": "Download RPM'en"
|
||||
},
|
||||
"app-marketing.download.linux": {
|
||||
"message": "Linux"
|
||||
},
|
||||
"app-marketing.download.linux-disclaimer": {
|
||||
"message": "Linux versionerne af Modrinth er <issues-link>kendt for at have fejl</issues-link> på bestemte systemer og konfigurationer. Hvis Modrinth er ustabil på dit system, vi opfordre dig til at prøve andre apps så som <prism-link>Prism Launcher</prism-link> for at nemt installere Modrinth indhold."
|
||||
},
|
||||
"app-marketing.download.mac": {
|
||||
"message": "Mac"
|
||||
},
|
||||
"app-marketing.download.options-title": {
|
||||
"message": "Download muligheder"
|
||||
},
|
||||
"app-marketing.download.terms": {
|
||||
"message": "Ved at download Modrinth du acceptere vores <terms-link>servicevilkår</terms-link> og <privacy-link>privatlivspolitik</privacy-link>."
|
||||
},
|
||||
"app-marketing.download.third-party-packages": {
|
||||
"message": "Tredjepartspakker"
|
||||
},
|
||||
"app-marketing.download.title": {
|
||||
"message": "Download Modrinth (Beta)"
|
||||
},
|
||||
"app-marketing.download.windows": {
|
||||
"message": "Windows"
|
||||
},
|
||||
"app-marketing.features.follow.description": {
|
||||
"message": "Gem indhold du elsker, og få opdateringer med et klik."
|
||||
},
|
||||
"app-marketing.features.follow.title": {
|
||||
"message": "Følg projekter"
|
||||
},
|
||||
"app-marketing.features.importing.description": {
|
||||
"message": "Importer alle dine favorit profiler fra launcher du brugte før, og kom i gang med Modrinth inde for få sekunder!"
|
||||
},
|
||||
"app-marketing.features.importing.gdlauncher-alt": {
|
||||
"message": "GDLauncher"
|
||||
},
|
||||
"app-marketing.features.importing.multimc-alt": {
|
||||
"message": "MultiMC"
|
||||
},
|
||||
"app-marketing.features.importing.title": {
|
||||
"message": "Profil importering"
|
||||
},
|
||||
"app-marketing.features.mod-management.actions": {
|
||||
"message": "Handlinger"
|
||||
},
|
||||
"app-marketing.features.mod-management.byAuthor": {
|
||||
"message": "af {author}"
|
||||
},
|
||||
"app-marketing.features.performance.cpu-percent": {
|
||||
"message": "% CPU"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-mb": {
|
||||
"message": "∞ MB"
|
||||
},
|
||||
"app-marketing.features.performance.infinite-times-infinite-mb": {
|
||||
"message": "∞ * ∞ MB"
|
||||
},
|
||||
"app-marketing.features.performance.less-than-150mb": {
|
||||
"message": "< 150 MB"
|
||||
},
|
||||
"app-marketing.features.performance.modrinth-app": {
|
||||
"message": "Modrinth"
|
||||
},
|
||||
"app-marketing.features.performance.one-billion-percent": {
|
||||
"message": "1 billion %"
|
||||
},
|
||||
"auth.authorize.action.authorize": {
|
||||
"message": "Godkend"
|
||||
},
|
||||
"auth.authorize.action.decline": {
|
||||
"message": "Afvis"
|
||||
},
|
||||
"auth.authorize.app-info": {
|
||||
"message": "<strong>{appName}</strong> af <creator-link>{creator}</creator-link> vil kunne:"
|
||||
},
|
||||
"auth.authorize.authorize-app-name": {
|
||||
"message": "Godkend {appName}"
|
||||
},
|
||||
"auth.authorize.error.no-redirect-url": {
|
||||
"message": "Ingen omdirigeringslokation fundet i svaret"
|
||||
},
|
||||
"auth.authorize.redirect-url": {
|
||||
"message": "Du bliver omdirigeret til <redirect-url>{url}</redirect-url>"
|
||||
},
|
||||
"auth.reset-password.method-choice.action": {
|
||||
"message": "Send gendannelses e-mail"
|
||||
},
|
||||
"auth.reset-password.method-choice.description": {
|
||||
"message": "Indtast din e-mail nedenfor, så sender vi et gendannelseslink, så du kan gendanne din konto."
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.label": {
|
||||
"message": "E-mail eller brugernavn"
|
||||
},
|
||||
"auth.reset-password.method-choice.email-username.placeholder": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.text": {
|
||||
"message": "En e-mail med instruktioner er blevet sendt til dig, hvis e-mailen tidligere er blevet gemt på din konto."
|
||||
},
|
||||
"auth.reset-password.notification.email-sent.title": {
|
||||
"message": "E-mail sendt"
|
||||
},
|
||||
"auth.reset-password.notification.password-reset.text": {
|
||||
"message": "Du kan nu logge ind på din konto med din nye adgangskode."
|
||||
},
|
||||
"auth.reset-password.notification.password-reset.title": {
|
||||
"message": "Adgangskoden blev nulstillet"
|
||||
},
|
||||
"auth.reset-password.post-challenge.action": {
|
||||
"message": "Nulstil adgangskoden"
|
||||
},
|
||||
"auth.reset-password.post-challenge.confirm-password.label": {
|
||||
"message": "Bekræft adgangskoden"
|
||||
},
|
||||
"auth.reset-password.post-challenge.description": {
|
||||
"message": "Indtast din nye adgangskode nedenfor for at få adgang til din konto."
|
||||
},
|
||||
"auth.reset-password.title": {
|
||||
"message": "Nulstil adgangskoden"
|
||||
},
|
||||
"auth.reset-password.title.long": {
|
||||
"message": "Nulstil din adgangskode"
|
||||
},
|
||||
"auth.sign-in.2fa.description": {
|
||||
"message": "Indtast en to-faktor-kode for at fortsætte."
|
||||
},
|
||||
"auth.sign-in.2fa.label": {
|
||||
"message": "Indtast to-faktor-kode"
|
||||
},
|
||||
"auth.sign-in.2fa.placeholder": {
|
||||
"message": "Indtast kode..."
|
||||
},
|
||||
"auth.sign-in.additional-options": {
|
||||
"message": "<forgot-password-link>Glemt adgangskode?</forgot-password-link> • <create-account-link>Opret en konto</create-account-link>"
|
||||
},
|
||||
"auth.sign-in.email-username.label": {
|
||||
"message": "E-mail eller brugernavn"
|
||||
},
|
||||
"auth.sign-in.password.label": {
|
||||
"message": "Adgangskode"
|
||||
},
|
||||
"auth.sign-in.sign-in-with": {
|
||||
"message": "Log ind med"
|
||||
},
|
||||
"auth.sign-in.title": {
|
||||
"message": "Log ind"
|
||||
},
|
||||
"auth.sign-in.use-password": {
|
||||
"message": "Eller brug en adgangskode"
|
||||
},
|
||||
"auth.sign-up.action.create-account": {
|
||||
"message": "Opret konto"
|
||||
},
|
||||
"auth.sign-up.confirm-password.label": {
|
||||
"message": "Bekræft adgangskoden"
|
||||
},
|
||||
"auth.sign-up.email.label": {
|
||||
"message": "E-mail"
|
||||
},
|
||||
"auth.sign-up.label.username": {
|
||||
"message": "Brugernavn"
|
||||
},
|
||||
"settings.pats.modal.create.name.label": {
|
||||
"message": "Navn"
|
||||
}
|
||||
}
|
||||
58
apps/frontend/src/locales/da-DK/languages.json
Normal file
58
apps/frontend/src/locales/da-DK/languages.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"ar": "Arabisk",
|
||||
"be": "Hviderussisk",
|
||||
"bg": "Bulgarsk",
|
||||
"bn": "Bengali",
|
||||
"ca": "Catalansk",
|
||||
"cs": "Tjekkisk",
|
||||
"da": "Dansk",
|
||||
"de": "Tysk",
|
||||
"de-CH": "Tysk (Schweiz)",
|
||||
"el": "Græsk",
|
||||
"en-GB": "Engelsk (Storbritannien)",
|
||||
"en-US": "Engelsk (USA)",
|
||||
"en-x-lolcat": "LOLCAT",
|
||||
"en-x-pirate": "Engelsk (Pirat)",
|
||||
"en-x-updown": "Engelsk (På hovedet)",
|
||||
"en-x-uwu": "Engelsk (UwU)",
|
||||
"eo": "Esperanto",
|
||||
"es": "Spansk",
|
||||
"et": "Estisk",
|
||||
"fi": "Finsk",
|
||||
"fr": "Fransk",
|
||||
"fr-BE": "Fransk (Belgien)",
|
||||
"fr-CA": "Fransk (Canada)",
|
||||
"he": "Hebraisk",
|
||||
"hi": "Hindi",
|
||||
"hr": "Kroatisk",
|
||||
"hu": "Ungarsk",
|
||||
"id": "Indonesisk",
|
||||
"it": "Italiensk",
|
||||
"ja": "Japansk",
|
||||
"kk": "Kasakhisk",
|
||||
"ko": "Koreansk",
|
||||
"ky": "Kirgisisk",
|
||||
"lt": "Litauisk",
|
||||
"lv": "Lettisk",
|
||||
"ms": "Malajisk",
|
||||
"nb": "Norsk (Bokmål)",
|
||||
"nl": "Hollandsk",
|
||||
"nn": "Norsk (Nynorsk)",
|
||||
"pes": "Persisk",
|
||||
"pl": "Polsk",
|
||||
"pt": "Portugisisk",
|
||||
"pt-BR": "Portugisisk (Brasilien)",
|
||||
"ro": "Rumænsk",
|
||||
"ru": "Russisk",
|
||||
"ru-x-bandit": "Russisk (Bandit)",
|
||||
"sk": "Slovakisk",
|
||||
"sv": "Svensk",
|
||||
"th": "Thailandsk",
|
||||
"tok": "Toki pona",
|
||||
"tr": "Tyrkisk",
|
||||
"tt": "Tatarisk",
|
||||
"uk": "Ukrainsk",
|
||||
"vi": "Vietnamesisk",
|
||||
"zh-Hans": "Kinesisk (Forenklet)",
|
||||
"zh-Hant": "Kinesisk (Traditionel)"
|
||||
}
|
||||
10
apps/frontend/src/locales/da-DK/meta.json
Normal file
10
apps/frontend/src/locales/da-DK/meta.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"displayName": {
|
||||
"description": "Please enter the name of the language in its specific variant or regional form (e.g., English (US) for American English, not just English). If the language does not have any specific variant, simply enter the name of the language (e.g., Français, Deutsch).",
|
||||
"message": "Dansk"
|
||||
},
|
||||
"searchTerms": {
|
||||
"description": "Please provide additional search terms associated with the language, if needed, to enhance the search functionality (e.g., American English, Deutschland). Each search term should be entered on a separate line. Translate as a hyphen (-) if no additional terms are needed.",
|
||||
"message": "Dansk\nDanmark"
|
||||
}
|
||||
}
|
||||
2279
apps/frontend/src/locales/de-CH/index.json
Normal file
2279
apps/frontend/src/locales/de-CH/index.json
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user