From 18a4647aceb3965f6acfd5e2282d46fd3dd721b0 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Fri, 7 Nov 2025 10:25:12 +0000 Subject: [PATCH 01/18] feat: abstract api-client DI into ui package --- apps/frontend/src/app.vue | 7 +++---- apps/frontend/src/error.vue | 3 ++- .../src/{providers/api-client.ts => helpers/api.ts} | 10 +++------- .../src/pages/[type]/[id]/settings/environment.vue | 3 +-- .../src/pages/[type]/[id]/settings/general.vue | 3 +-- packages/ui/package.json | 1 + packages/ui/src/providers/api-client.ts | 7 +++++++ packages/ui/src/providers/index.ts | 1 + pnpm-lock.yaml | 3 +++ 9 files changed, 22 insertions(+), 16 deletions(-) rename apps/frontend/src/{providers/api-client.ts => helpers/api.ts} (78%) create mode 100644 packages/ui/src/providers/api-client.ts diff --git a/apps/frontend/src/app.vue b/apps/frontend/src/app.vue index 5c58f2f238..d37d0e715f 100644 --- a/apps/frontend/src/app.vue +++ b/apps/frontend/src/app.vue @@ -6,12 +6,11 @@ diff --git a/apps/frontend/src/components/ui/servers/ServerManageEmptyState.vue b/apps/frontend/src/components/ui/servers/ServerManageEmptyState.vue deleted file mode 100644 index cc2ab2baee..0000000000 --- a/apps/frontend/src/components/ui/servers/ServerManageEmptyState.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue b/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue index a11d49e9d0..61b7fe3744 100644 --- a/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue +++ b/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue @@ -128,8 +128,9 @@ + + diff --git a/apps/frontend/src/pages/servers/manage/index_old.vue b/apps/frontend/src/pages/servers/manage/index_old.vue deleted file mode 100644 index b67fb9d6b6..0000000000 --- a/apps/frontend/src/pages/servers/manage/index_old.vue +++ /dev/null @@ -1,257 +0,0 @@ - - - diff --git a/packages/ui/src/pages/index.ts b/packages/ui/src/pages/index.ts index 0043beaa13..5504c308c2 100644 --- a/packages/ui/src/pages/index.ts +++ b/packages/ui/src/pages/index.ts @@ -3,7 +3,8 @@ import type { RouteRecordRaw } from 'vue-router' export const ServersOverviewPage = () => import('./servers/manage.vue') export { createComponentResolver, toNuxtPages } from './route-helpers' -export const sharedRoutes: RouteRecordRaw[] = [ +/** + * { path: '/servers/manage', name: 'Servers - Modrinth', @@ -12,4 +13,7 @@ export const sharedRoutes: RouteRecordRaw[] = [ breadcrumb: [{ name: 'Servers' }], }, }, -] + + */ + +export const sharedRoutes: RouteRecordRaw[] = [] From 48970cef43068ff8e2ab3f7317c27f0020d57b68 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sat, 8 Nov 2025 16:48:58 +0000 Subject: [PATCH 07/18] feat: migrate servers manage page to api-client before page system --- apps/frontend/nuxt.config.ts | 17 -- .../components/ui/servers/ServerListing.vue | 2 +- .../ui/servers/ServersUpgradeModalWrapper.vue | 170 ++++++++++-------- .../servers/marketing/MedalServerListing.vue | 2 +- apps/frontend/src/error.vue | 3 +- apps/frontend/src/helpers/api.ts | 6 +- .../src/pages/servers/manage/index.vue | 2 +- apps/frontend/src/plugins/tanstack.ts | 2 +- packages/api-client/src/index.ts | 3 +- .../api-client/src/modules/archon/index.ts | 3 +- .../src/modules/archon/servers/types/index.ts | 9 + .../archon/servers/{types.ts => types/v0.ts} | 11 ++ .../src/modules/archon/servers/types/v1.ts | 14 ++ .../src/modules/archon/servers/v0.ts | 21 ++- .../src/modules/archon/servers/v1.ts | 20 +++ packages/api-client/src/modules/index.ts | 2 + .../src/modules/labrinth/billing/internal.ts | 15 +- packages/api-client/src/modules/types.ts | 2 +- packages/api-client/src/types/request.ts | 2 +- packages/api-client/tsconfig.json | 5 +- packages/tooling-config/eslint/common.mjs | 2 + packages/ui/index.ts | 1 + .../billing/ModalBasedServerPlan.vue | 32 ++-- .../billing/ModrinthServersPurchaseModal.vue | 46 ++--- .../billing/ServersPurchase0Plan.vue | 29 +-- .../billing/ServersPurchase1Region.vue | 143 +++++++++------ .../billing/ServersPurchase3Review.vue | 61 ++++--- .../billing/ServersRegionButton.vue | 4 +- packages/ui/src/composables/stripe.ts | 138 ++++++-------- packages/ui/src/pages/index.ts | 20 +-- packages/ui/src/pages/route-helpers.ts | 49 ----- packages/ui/src/pages/servers/manage.vue | 1 - packages/ui/src/providers/api-client.ts | 1 + packages/ui/src/utils/product-utils.ts | 54 ++++++ 34 files changed, 500 insertions(+), 392 deletions(-) create mode 100644 packages/api-client/src/modules/archon/servers/types/index.ts rename packages/api-client/src/modules/archon/servers/{types.ts => types/v0.ts} (90%) create mode 100644 packages/api-client/src/modules/archon/servers/types/v1.ts create mode 100644 packages/api-client/src/modules/archon/servers/v1.ts delete mode 100644 packages/ui/src/pages/route-helpers.ts delete mode 100644 packages/ui/src/pages/servers/manage.vue create mode 100644 packages/ui/src/utils/product-utils.ts diff --git a/apps/frontend/nuxt.config.ts b/apps/frontend/nuxt.config.ts index bbcef65169..0e41c083ab 100644 --- a/apps/frontend/nuxt.config.ts +++ b/apps/frontend/nuxt.config.ts @@ -11,12 +11,6 @@ import Papa from 'papaparse' import { basename, relative, resolve } from 'pathe' import svgLoader from 'vite-svg-loader' -import { - ServersOverviewPage, - createComponentResolver, - sharedRoutes, - toNuxtPages, -} from '@modrinth/ui/pages' import type { GeneratedState } from './src/composables/generated' const STAGING_API_URL = 'https://staging-api.modrinth.com/v2/' @@ -309,17 +303,6 @@ export default defineNuxtConfig({ children: [], }), ) - - // Shared routes - const uiPackagePath = resolve(__dirname, '../../packages/ui') - const componentResolver = createComponentResolver(uiPackagePath) - - componentResolver.register(ServersOverviewPage, 'src/pages/servers/manage.vue') - - const nuxtPages = toNuxtPages(sharedRoutes, (component) => - componentResolver.resolve(component), - ) - routes.push(...nuxtPages) }, async 'vintl:extendOptions'(opts) { opts.locales ??= [] diff --git a/apps/frontend/src/components/ui/servers/ServerListing.vue b/apps/frontend/src/components/ui/servers/ServerListing.vue index 47d847b152..8fda7e3e0b 100644 --- a/apps/frontend/src/components/ui/servers/ServerListing.vue +++ b/apps/frontend/src/components/ui/servers/ServerListing.vue @@ -112,8 +112,8 @@ diff --git a/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue b/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue index 61b7fe3744..cc37b28ce4 100644 --- a/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue +++ b/apps/frontend/src/components/ui/servers/marketing/MedalServerListing.vue @@ -127,8 +127,8 @@ + diff --git a/apps/frontend/src/pages/settings/billing/index.vue b/apps/frontend/src/pages/settings/billing/index.vue index 30041f2462..b5dcb3d69a 100644 --- a/apps/frontend/src/pages/settings/billing/index.vue +++ b/apps/frontend/src/pages/settings/billing/index.vue @@ -630,8 +630,8 @@ import { calculateSavings, formatPrice, getCurrency } from '@modrinth/utils' import { computed, ref } from 'vue' import { useBaseFetch } from '@/composables/fetch.js' +import ServerListing from '@modrinth/ui/src/components/servers/ServerListing.vue' import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue' -import ServerListing from '~/components/ui/servers/ServerListing.vue' import ServersUpgradeModalWrapper from '~/components/ui/servers/ServersUpgradeModalWrapper.vue' import { useServersFetch } from '~/composables/servers/servers-fetch.ts' import { products } from '~/generated/state.json' diff --git a/packages/api-client/src/core/abstract-client.ts b/packages/api-client/src/core/abstract-client.ts index 2c6bd53aab..5e7557127a 100644 --- a/packages/api-client/src/core/abstract-client.ts +++ b/packages/api-client/src/core/abstract-client.ts @@ -25,6 +25,7 @@ export abstract class AbstractModrinthClient { public readonly labrinth!: InferredClientModules['labrinth'] public readonly archon!: InferredClientModules['archon'] + public readonly kyros!: InferredClientModules['kyros'] constructor(config: ClientConfig) { this.config = { @@ -171,7 +172,7 @@ export abstract class AbstractModrinthClient { /** * Build the full URL for a request */ - protected buildUrl(path: string, baseUrl: string, version: number | 'internal'): string { + protected buildUrl(path: string, baseUrl: string, version: number | 'internal' | string): string { // Remove trailing slash from base URL const base = baseUrl.replace(/\/$/, '') @@ -181,6 +182,9 @@ export abstract class AbstractModrinthClient { versionPath = '/_internal' } else if (typeof version === 'number') { versionPath = `/v${version}` + } else if (typeof version === 'string') { + // Custom version string (e.g., 'v0', 'modrinth/v0') + versionPath = `/${version}` } const cleanPath = path.startsWith('/') ? path : `/${path}` diff --git a/packages/api-client/src/features/auth.ts b/packages/api-client/src/features/auth.ts index 2b075cc6cc..b7e67a2eae 100644 --- a/packages/api-client/src/features/auth.ts +++ b/packages/api-client/src/features/auth.ts @@ -69,6 +69,12 @@ export class AuthFeature extends AbstractFeature { return false } + // Skip if Authorization header is already explicitly set + const headerName = this.config.headerName ?? 'Authorization' + if (context.options.headers?.[headerName]) { + return false + } + return super.shouldApply(context) } diff --git a/packages/api-client/src/modules/archon/servers/types/index.ts b/packages/api-client/src/modules/archon/servers/types/index.ts deleted file mode 100644 index 9e0ab8db39..0000000000 --- a/packages/api-client/src/modules/archon/servers/types/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Archon as A0 } from './v0' -import { Archon as A1 } from './v1' - -export namespace Archon { - export namespace Servers { - export import v0 = A0.Servers.v0 - export import v1 = A1.Servers.v1 - } -} diff --git a/packages/api-client/src/modules/archon/servers/types/v0.ts b/packages/api-client/src/modules/archon/servers/types/v0.ts index 0afe327c3f..1809986f37 100644 --- a/packages/api-client/src/modules/archon/servers/types/v0.ts +++ b/packages/api-client/src/modules/archon/servers/types/v0.ts @@ -1,112 +1,106 @@ -export namespace Archon { - export namespace Servers { - export namespace v0 { - export type ServerGetResponse = { - servers: Server[] - pagination: Pagination - } - - export type Pagination = { - current_page: number - page_size: number - total_pages: number - total_items: number - } - - export type Status = 'installing' | 'broken' | 'available' | 'suspended' - - export type SuspensionReason = - | 'moderated' - | 'paymentfailed' - | 'cancelled' - | 'upgrading' - | 'other' - - export type Loader = - | 'Forge' - | 'NeoForge' - | 'Fabric' - | 'Quilt' - | 'Purpur' - | 'Spigot' - | 'Vanilla' - | 'Paper' - - export type Game = 'Minecraft' - - export type UpstreamKind = 'modpack' | 'none' - - export type Server = { - server_id: string - name: string - owner_id: string - net: Net - game: Game - backup_quota: number - used_backup_quota: number - status: Status - suspension_reason: SuspensionReason | null - loader: Loader | null - loader_version: string | null - mc_version: string | null - upstream: Upstream | null - sftp_username: string - sftp_password: string - sftp_host: string - datacenter: string - notices: Notice[] - node: NodeInfo | null - flows: Flows - is_medal: boolean - - medal_expires?: string - } - - export type Net = { - ip: string - port: number - domain: string - } - - export type Upstream = { - kind: UpstreamKind - version_id: string - project_id: string - } - - export type Notice = { - id: number - dismissable: boolean - title: string - message: string - level: string - announced: string - } - - export type NodeInfo = { - token: string - instance: string - } - - export type Flows = { - intro: boolean - } - - export type GetServersOptions = { - limit?: number - offset?: number - } - - export type StockRequest = { - cpu?: number - memory_mb?: number - swap_mb?: number - storage_mb?: number - } - - export type StockResponse = { - available: number - } - } - } +export type ServerGetResponse = { + servers: Server[] + pagination: Pagination +} + +export type Pagination = { + current_page: number + page_size: number + total_pages: number + total_items: number +} + +export type Status = 'installing' | 'broken' | 'available' | 'suspended' + +export type SuspensionReason = 'moderated' | 'paymentfailed' | 'cancelled' | 'upgrading' | 'other' + +export type Loader = + | 'Forge' + | 'NeoForge' + | 'Fabric' + | 'Quilt' + | 'Purpur' + | 'Spigot' + | 'Vanilla' + | 'Paper' + +export type Game = 'Minecraft' + +export type UpstreamKind = 'modpack' | 'none' + +export type Server = { + server_id: string + name: string + owner_id: string + net: Net + game: Game + backup_quota: number + used_backup_quota: number + status: Status + suspension_reason: SuspensionReason | null + loader: Loader | null + loader_version: string | null + mc_version: string | null + upstream: Upstream | null + sftp_username: string + sftp_password: string + sftp_host: string + datacenter: string + notices: Notice[] + node: NodeInfo | null + flows: Flows + is_medal: boolean + + medal_expires?: string +} + +export type Net = { + ip: string + port: number + domain: string +} + +export type Upstream = { + kind: UpstreamKind + version_id: string + project_id: string +} + +export type Notice = { + id: number + dismissable: boolean + title: string + message: string + level: string + announced: string +} + +export type NodeInfo = { + token: string + instance: string +} + +export type Flows = { + intro: boolean +} + +export type GetServersOptions = { + limit?: number + offset?: number +} + +export type StockRequest = { + cpu?: number + memory_mb?: number + swap_mb?: number + storage_mb?: number +} + +export type StockResponse = { + available: number +} + +export type JWTAuth = { + url: string // e.g., "node-xyz.modrinth.com/modrinth/v0/fs" + token: string // JWT token for filesystem access } diff --git a/packages/api-client/src/modules/archon/servers/types/v1.ts b/packages/api-client/src/modules/archon/servers/types/v1.ts index 3872609613..b6abb736b9 100644 --- a/packages/api-client/src/modules/archon/servers/types/v1.ts +++ b/packages/api-client/src/modules/archon/servers/types/v1.ts @@ -1,14 +1,8 @@ -export namespace Archon { - export namespace Servers { - export namespace v1 { - export type Region = { - shortcode: string - country_code: string - display_name: string - lat: number - lon: number - zone: string - } - } - } +export type Region = { + shortcode: string + country_code: string + display_name: string + lat: number + lon: number + zone: string } diff --git a/packages/api-client/src/modules/archon/servers/v0.ts b/packages/api-client/src/modules/archon/servers/v0.ts index 396ef82c5a..a4a3de1c4a 100644 --- a/packages/api-client/src/modules/archon/servers/v0.ts +++ b/packages/api-client/src/modules/archon/servers/v0.ts @@ -41,4 +41,17 @@ export class ArchonServersV0Module extends AbstractModule { body: request, }) } + + /** + * Get filesystem authentication credentials for a server + * Returns URL and JWT token for accessing the server's filesystem via Kyros + * GET /modrinth/v0/servers/:id/fs + */ + public async getFilesystemAuth(serverId: string): Promise { + return this.client.request(`/servers/${serverId}/fs`, { + api: 'archon', + version: 'modrinth/v0', + method: 'GET', + }) + } } diff --git a/packages/api-client/src/modules/archon/types.ts b/packages/api-client/src/modules/archon/types.ts new file mode 100644 index 0000000000..fbb126dcaa --- /dev/null +++ b/packages/api-client/src/modules/archon/types.ts @@ -0,0 +1,9 @@ +import * as ServersV0 from './servers/types/v0' +import * as ServersV1 from './servers/types/v1' + +export namespace Archon { + export namespace Servers { + export import v0 = ServersV0 + export import v1 = ServersV1 + } +} diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index c50c644fc8..431f65f1d8 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -2,6 +2,7 @@ import type { AbstractModrinthClient } from '../core/abstract-client' import type { AbstractModule } from '../core/abstract-module' import { ArchonServersV0Module } from './archon/servers/v0' import { ArchonServersV1Module } from './archon/servers/v1' +import { KyrosFilesV0Module } from './kyros/files/v0' import { LabrinthBillingInternalModule } from './labrinth/billing/internal' import { LabrinthProjectsV2Module } from './labrinth/projects/v2' import { LabrinthProjectsV3Module } from './labrinth/projects/v3' @@ -20,6 +21,7 @@ type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule export const MODULE_REGISTRY = { archon_servers_v0: ArchonServersV0Module, archon_servers_v1: ArchonServersV1Module, + kyros_files_v0: KyrosFilesV0Module, labrinth_billing_internal: LabrinthBillingInternalModule, labrinth_projects_v2: LabrinthProjectsV2Module, labrinth_projects_v3: LabrinthProjectsV3Module, diff --git a/packages/api-client/src/modules/kyros/files/types/v0.ts b/packages/api-client/src/modules/kyros/files/types/v0.ts new file mode 100644 index 0000000000..fdfe53fbde --- /dev/null +++ b/packages/api-client/src/modules/kyros/files/types/v0.ts @@ -0,0 +1,7 @@ +export namespace Kyros { + export namespace Files { + export namespace v0 { + // No special types needed - methods return Blob or void + } + } +} diff --git a/packages/api-client/src/modules/kyros/files/v0.ts b/packages/api-client/src/modules/kyros/files/v0.ts new file mode 100644 index 0000000000..6d1f86b634 --- /dev/null +++ b/packages/api-client/src/modules/kyros/files/v0.ts @@ -0,0 +1,52 @@ +import { AbstractModule } from '../../../core/abstract-module' + +export class KyrosFilesV0Module extends AbstractModule { + public getModuleID(): string { + return 'kyros_files_v0' + } + + /** + * Download a file from a server's filesystem + * + * @param nodeInstance - Node instance URL (e.g., "node-xyz.modrinth.com/modrinth/v0/fs") + * @param nodeToken - JWT token from getFilesystemAuth + * @param path - File path (e.g., "/server-icon-original.png") + * @returns Promise resolving to file Blob + */ + public async downloadFile(nodeInstance: string, nodeToken: string, path: string): Promise { + return this.client.request(`/fs/download`, { + api: `https://${nodeInstance.replace('v0/fs', '')}`, + method: 'GET', + version: 'v0', + params: { path }, + headers: { Authorization: `Bearer ${nodeToken}` }, + }) + } + + /** + * Upload a file to a server's filesystem + * + * @param nodeInstance - Node instance URL + * @param nodeToken - JWT token from getFilesystemAuth + * @param path - Destination path (e.g., "/server-icon.png") + * @param file - File to upload + */ + public async uploadFile( + nodeInstance: string, + nodeToken: string, + path: string, + file: File, + ): Promise { + return this.client.request(`/fs/create`, { + api: `https://${nodeInstance.replace('v0/fs', '')}`, + method: 'POST', + version: 'v0', + params: { path, type: 'file' }, + headers: { + Authorization: `Bearer ${nodeToken}`, + 'Content-Type': 'application/octet-stream', + }, + body: file, + }) + } +} diff --git a/packages/api-client/src/modules/kyros/types.ts b/packages/api-client/src/modules/kyros/types.ts new file mode 100644 index 0000000000..c352931672 --- /dev/null +++ b/packages/api-client/src/modules/kyros/types.ts @@ -0,0 +1,7 @@ +import * as FilesV0 from './files/types/v0' + +export namespace Kyros { + export namespace Files { + export import v0 = FilesV0 + } +} diff --git a/packages/api-client/src/modules/labrinth/billing/internal.ts b/packages/api-client/src/modules/labrinth/billing/internal.ts index dc1476dc15..d728173c4d 100644 --- a/packages/api-client/src/modules/labrinth/billing/internal.ts +++ b/packages/api-client/src/modules/labrinth/billing/internal.ts @@ -1,5 +1,5 @@ import { AbstractModule } from '../../../core/abstract-module' -import type { Labrinth } from './types' +import type { Labrinth } from '../../types' export class LabrinthBillingInternalModule extends AbstractModule { public getModuleID(): string { diff --git a/packages/api-client/src/modules/labrinth/billing/types.ts b/packages/api-client/src/modules/labrinth/billing/types.ts deleted file mode 100644 index c15258927d..0000000000 --- a/packages/api-client/src/modules/labrinth/billing/types.ts +++ /dev/null @@ -1,147 +0,0 @@ -export namespace Labrinth { - export namespace Billing { - export namespace Internal { - export type PriceDuration = 'five-days' | 'monthly' | 'quarterly' | 'yearly' - - export type SubscriptionStatus = 'provisioned' | 'unprovisioned' - - export type UserSubscription = { - id: string - user_id: string - price_id: string - interval: PriceDuration - status: SubscriptionStatus - created: string // ISO datetime string - metadata?: SubscriptionMetadata - } - - export type SubscriptionMetadata = - | { type: 'pyro'; id: string; region?: string } - | { type: 'medal'; id: string } - - export type ChargeStatus = - | 'open' - | 'processing' - | 'succeeded' - | 'failed' - | 'cancelled' - | 'expiring' - - export type ChargeType = 'one-time' | 'subscription' | 'proration' | 'refund' - - export type PaymentPlatform = 'Stripe' | 'None' - - export type Charge = { - id: string - user_id: string - price_id: string - amount: number - currency_code: string - status: ChargeStatus - due: string // ISO datetime string - last_attempt: string | null // ISO datetime string - type: ChargeType - subscription_id: string | null - subscription_interval: PriceDuration | null - platform: PaymentPlatform - parent_charge_id: string | null - net: number | null - } - - export type ProductMetadata = - | { type: 'midas' } - | { - type: 'pyro' - cpu: number - ram: number - swap: number - storage: number - } - | { - type: 'medal' - cpu: number - ram: number - swap: number - storage: number - region: string - } - - export type Price = - | { type: 'one-time'; price: number } - | { type: 'recurring'; intervals: Record } - - export type ProductPrice = { - id: string - product_id: string - prices: Price - currency_code: string - } - - export type Product = { - id: string - metadata: ProductMetadata - prices: ProductPrice[] - unitary: boolean - } - - export type EditSubscriptionRequest = { - interval?: PriceDuration - payment_method?: string - cancelled?: boolean - region?: string - product?: string - } - - export type EditSubscriptionResponse = { - payment_intent_id: string - client_secret: string - tax: number - total: number - } - - export type AddPaymentMethodFlowResponse = { - client_secret: string - } - - export type EditPaymentMethodRequest = { - primary: boolean - } - - export type InitiatePaymentRequest = { - type: 'payment_method' | 'confirmation_token' - id?: string - token?: string - charge: - | { type: 'existing'; id: string } - | { type: 'new'; product_id: string; interval?: PriceDuration } - existing_payment_intent?: string - metadata?: { - type: 'pyro' - server_name?: string - server_region?: string - source: unknown - } - } - - export type InitiatePaymentResponse = { - payment_intent_id?: string - client_secret?: string - price_id: string - tax: number - total: number - payment_method?: string - } - - export type RefundChargeRequest = { - type: 'full' | 'partial' | 'none' - amount?: number - unprovision?: boolean - } - - export type CreditRequest = - | { subscription_ids: string[]; days: number; send_email: boolean; message: string } - | { nodes: string[]; days: number; send_email: boolean; message: string } - | { region: string; days: number; send_email: boolean; message: string } - } - } -} diff --git a/packages/api-client/src/modules/labrinth/billing/types/internal.ts b/packages/api-client/src/modules/labrinth/billing/types/internal.ts new file mode 100644 index 0000000000..1c377133e1 --- /dev/null +++ b/packages/api-client/src/modules/labrinth/billing/types/internal.ts @@ -0,0 +1,141 @@ +export type PriceDuration = 'five-days' | 'monthly' | 'quarterly' | 'yearly' + +export type SubscriptionStatus = 'provisioned' | 'unprovisioned' + +export type UserSubscription = { + id: string + user_id: string + price_id: string + interval: PriceDuration + status: SubscriptionStatus + created: string // ISO datetime string + metadata?: SubscriptionMetadata +} + +export type SubscriptionMetadata = + | { type: 'pyro'; id: string; region?: string } + | { type: 'medal'; id: string } + +export type ChargeStatus = + | 'open' + | 'processing' + | 'succeeded' + | 'failed' + | 'cancelled' + | 'expiring' + +export type ChargeType = 'one-time' | 'subscription' | 'proration' | 'refund' + +export type PaymentPlatform = 'Stripe' | 'None' + +export type Charge = { + id: string + user_id: string + price_id: string + amount: number + currency_code: string + status: ChargeStatus + due: string // ISO datetime string + last_attempt: string | null // ISO datetime string + type: ChargeType + subscription_id: string | null + subscription_interval: PriceDuration | null + platform: PaymentPlatform + parent_charge_id: string | null + net: number | null +} + +export type ProductMetadata = + | { type: 'midas' } + | { + type: 'pyro' + cpu: number + ram: number + swap: number + storage: number + } + | { + type: 'medal' + cpu: number + ram: number + swap: number + storage: number + region: string + } + +export type Price = + | { type: 'one-time'; price: number } + | { type: 'recurring'; intervals: Record } + +export type ProductPrice = { + id: string + product_id: string + prices: Price + currency_code: string +} + +export type Product = { + id: string + metadata: ProductMetadata + prices: ProductPrice[] + unitary: boolean +} + +export type EditSubscriptionRequest = { + interval?: PriceDuration + payment_method?: string + cancelled?: boolean + region?: string + product?: string +} + +export type EditSubscriptionResponse = { + payment_intent_id: string + client_secret: string + tax: number + total: number +} + +export type AddPaymentMethodFlowResponse = { + client_secret: string +} + +export type EditPaymentMethodRequest = { + primary: boolean +} + +export type InitiatePaymentRequest = { + type: 'payment_method' | 'confirmation_token' + id?: string + token?: string + charge: + | { type: 'existing'; id: string } + | { type: 'new'; product_id: string; interval?: PriceDuration } + existing_payment_intent?: string + metadata?: { + type: 'pyro' + server_name?: string + server_region?: string + source: unknown + } +} + +export type InitiatePaymentResponse = { + payment_intent_id?: string + client_secret?: string + price_id: string + tax: number + total: number + payment_method?: string +} + +export type RefundChargeRequest = { + type: 'full' | 'partial' | 'none' + amount?: number + unprovision?: boolean +} + +export type CreditRequest = + | { subscription_ids: string[]; days: number; send_email: boolean; message: string } + | { nodes: string[]; days: number; send_email: boolean; message: string } + | { region: string; days: number; send_email: boolean; message: string } diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts new file mode 100644 index 0000000000..38670b55d5 --- /dev/null +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -0,0 +1,7 @@ +import * as BillingInternal from './billing/types/internal' + +export namespace Labrinth { + export namespace Billing { + export import Internal = BillingInternal + } +} diff --git a/packages/api-client/src/modules/types.ts b/packages/api-client/src/modules/types.ts index 584d2f2741..6ecee07ba2 100644 --- a/packages/api-client/src/modules/types.ts +++ b/packages/api-client/src/modules/types.ts @@ -1,4 +1,5 @@ -export * from './archon/servers/types/' -export * from './labrinth/billing/types' +export * from './archon/types' +export * from './kyros/types' export * from './labrinth/projects/types/v2' export * from './labrinth/projects/types/v3' +export * from './labrinth/types' diff --git a/packages/ui/src/components/billing/PurchaseModal.vue b/packages/ui/src/components/billing/PurchaseModal.vue index 28c7bc50c1..c100168018 100644 --- a/packages/ui/src/components/billing/PurchaseModal.vue +++ b/packages/ui/src/components/billing/PurchaseModal.vue @@ -552,7 +552,7 @@ import Checkbox from '../base/Checkbox.vue' import Slider from '../base/Slider.vue' import AnimatedLogo from '../brand/AnimatedLogo.vue' import NewModal from '../modal/NewModal.vue' -import LoaderIcon from '../servers/LoaderIcon.vue' +import LoaderIcon from '../servers/icons/LoaderIcon.vue' const { locale, formatMessage } = useVIntl() diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index c26f8ece2f..a5c39655d3 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -23,18 +23,18 @@ export { default as DropdownSelect } from './base/DropdownSelect.vue' export { default as EnvironmentIndicator } from './base/EnvironmentIndicator.vue' export { default as ErrorInformationCard } from './base/ErrorInformationCard.vue' export { default as FileInput } from './base/FileInput.vue' -export type { FilterBarOption } from './base/FilterBar.vue' export { default as FilterBar } from './base/FilterBar.vue' +export type { FilterBarOption } from './base/FilterBar.vue' export { default as HeadingLink } from './base/HeadingLink.vue' export { default as IconSelect } from './base/IconSelect.vue' -export type { JoinedButtonAction } from './base/JoinedButtons.vue' export { default as JoinedButtons } from './base/JoinedButtons.vue' +export type { JoinedButtonAction } from './base/JoinedButtons.vue' export { default as LoadingIndicator } from './base/LoadingIndicator.vue' export { default as ManySelect } from './base/ManySelect.vue' export { default as MarkdownEditor } from './base/MarkdownEditor.vue' export { default as OptionGroup } from './base/OptionGroup.vue' -export type { Option as OverflowMenuOption } from './base/OverflowMenu.vue' export { default as OverflowMenu } from './base/OverflowMenu.vue' +export type { Option as OverflowMenuOption } from './base/OverflowMenu.vue' export { default as Page } from './base/Page.vue' export { default as Pagination } from './base/Pagination.vue' export { default as PopoutMenu } from './base/PopoutMenu.vue' @@ -69,16 +69,16 @@ export { default as CompactChart } from './chart/CompactChart.vue' // Content export { default as ContentListPanel } from './content/ContentListPanel.vue' -export type { Article as NewsArticle } from './content/NewsArticleCard.vue' export { default as NewsArticleCard } from './content/NewsArticleCard.vue' +export type { Article as NewsArticle } from './content/NewsArticleCard.vue' // Modals export { default as ConfirmModal } from './modal/ConfirmModal.vue' export { default as Modal } from './modal/Modal.vue' export { default as NewModal } from './modal/NewModal.vue' export { default as ShareModal } from './modal/ShareModal.vue' -export type { Tab as TabbedModalTab } from './modal/TabbedModal.vue' export { default as TabbedModal } from './modal/TabbedModal.vue' +export type { Tab as TabbedModalTab } from './modal/TabbedModal.vue' // Navigation export { default as Breadcrumbs } from './nav/Breadcrumbs.vue' @@ -127,4 +127,10 @@ export { default as ThemeSelector } from './settings/ThemeSelector.vue' // Servers export { default as ServersSpecs } from './billing/ServersSpecs.vue' export { default as BackupWarning } from './servers/backups/BackupWarning.vue' +export { default as LoaderIcon } from './servers/icons/LoaderIcon.vue' +export { default as ServerIcon } from './servers/icons/ServerIcon.vue' +export { default as ServerInfoLabels } from './servers/labels/ServerInfoLabels.vue' +export { default as MedalBackgroundImage } from './servers/marketing/MedalBackgroundImage.vue' +export { default as MedalServerListing } from './servers/marketing/MedalServerListing.vue' +export type { PendingChange } from './servers/ServerListing.vue' export { default as ServersPromo } from './servers/ServersPromo.vue' diff --git a/apps/frontend/src/components/ui/servers/ServerListing.vue b/packages/ui/src/components/servers/ServerListing.vue similarity index 58% rename from apps/frontend/src/components/ui/servers/ServerListing.vue rename to packages/ui/src/components/servers/ServerListing.vue index 8fda7e3e0b..aed4543e6e 100644 --- a/apps/frontend/src/components/ui/servers/ServerListing.vue +++ b/packages/ui/src/components/servers/ServerListing.vue @@ -58,7 +58,7 @@ 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" > - + Your server's hardware is currently being upgraded and will be back online shortly.
- Your server has been cancelled. Please update your + Your server has been cancelled. Please update your billing information or contact Modrinth Support for more information.
@@ -76,8 +76,9 @@ 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" >
- Your server has been suspended: {{ suspension_reason }}. - Please update your billing information or contact Modrinth Support for more information. + Your server has been suspended: + {{ suspension_reason }}. Please update your billing information or contact Modrinth Support + for more information.
@@ -86,7 +87,7 @@ 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" >
- Your server has been suspended. Please update your + Your server has been suspended. Please update your billing information or contact Modrinth Support for more information.
@@ -112,20 +113,26 @@ diff --git a/apps/frontend/src/components/ui/servers/ServerInfoLabels.vue b/packages/ui/src/components/servers/labels/ServerInfoLabels.vue similarity index 100% rename from apps/frontend/src/components/ui/servers/ServerInfoLabels.vue rename to packages/ui/src/components/servers/labels/ServerInfoLabels.vue diff --git a/apps/frontend/src/components/ui/servers/ServerLoaderLabel.vue b/packages/ui/src/components/servers/labels/ServerLoaderLabel.vue similarity index 89% rename from apps/frontend/src/components/ui/servers/ServerLoaderLabel.vue rename to packages/ui/src/components/servers/labels/ServerLoaderLabel.vue index c90267e4e4..79d28397b0 100644 --- a/apps/frontend/src/components/ui/servers/ServerLoaderLabel.vue +++ b/packages/ui/src/components/servers/labels/ServerLoaderLabel.vue @@ -4,7 +4,7 @@
- - +
{{ loader }} @@ -34,7 +34,10 @@ diff --git a/apps/frontend/src/components/ui/servers/ServerSubdomainLabel.vue b/packages/ui/src/components/servers/labels/ServerSubdomainLabel.vue similarity index 93% rename from apps/frontend/src/components/ui/servers/ServerSubdomainLabel.vue rename to packages/ui/src/components/servers/labels/ServerSubdomainLabel.vue index 2a0ca5d318..2b528ce8c6 100644 --- a/apps/frontend/src/components/ui/servers/ServerSubdomainLabel.vue +++ b/packages/ui/src/components/servers/labels/ServerSubdomainLabel.vue @@ -22,6 +22,8 @@ import { LinkIcon } from '@modrinth/assets' import { injectNotificationManager } from '@modrinth/ui' import { useStorage } from '@vueuse/core' +import { computed } from 'vue' +import { useRoute } from 'vue-router' const { addNotification } = injectNotificationManager() @@ -39,7 +41,7 @@ const copySubdomain = () => { }) } -const route = useNativeRoute() +const route = useRoute() const serverId = computed(() => route.params.id as string) const userPreferences = useStorage(`pyro-server-${serverId.value}-preferences`, { diff --git a/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue b/packages/ui/src/components/servers/labels/ServerUptimeLabel.vue similarity index 94% rename from apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue rename to packages/ui/src/components/servers/labels/ServerUptimeLabel.vue index ab735b996b..9b067513d2 100644 --- a/apps/frontend/src/components/ui/servers/ServerUptimeLabel.vue +++ b/packages/ui/src/components/servers/labels/ServerUptimeLabel.vue @@ -8,7 +8,7 @@
- + @@ -17,10 +17,9 @@ From 5f3ad680941df0378a8a944bc8ee8d7277134e13 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Sun, 9 Nov 2025 12:16:33 +0000 Subject: [PATCH 09/18] fix: type issues --- apps/frontend/src/helpers/api.ts | 8 +- .../[type]/[id]/settings/environment.vue | 6 +- packages/api-client/src/index.ts | 15 +- .../src/modules/archon/servers/types/v0.ts | 106 ------ .../src/modules/archon/servers/types/v1.ts | 8 - .../src/modules/archon/servers/v0.ts | 2 +- .../src/modules/archon/servers/v1.ts | 2 +- .../api-client/src/modules/archon/types.ts | 129 ++++++- .../src/modules/kyros/files/types/v0.ts | 7 - .../api-client/src/modules/kyros/types.ts | 6 +- .../src/modules/labrinth/billing/index.ts | 2 - .../src/modules/labrinth/billing/internal.ts | 2 +- .../labrinth/billing/types/internal.ts | 141 ------- .../api-client/src/modules/labrinth/index.ts | 2 +- .../src/modules/labrinth/projects/types/v2.ts | 115 ------ .../src/modules/labrinth/projects/types/v3.ts | 54 --- .../src/modules/labrinth/projects/v2.ts | 18 +- .../src/modules/labrinth/projects/v3.ts | 12 +- .../api-client/src/modules/labrinth/types.ts | 360 +++++++++++++++++- packages/api-client/src/modules/types.ts | 2 - .../ProjectSettingsEnvSelector.vue | 5 +- .../src/components/servers/ServerListing.vue | 2 +- .../servers/marketing/MedalServerListing.vue | 24 +- packages/ui/src/providers/project-page.ts | 9 +- 24 files changed, 542 insertions(+), 495 deletions(-) delete mode 100644 packages/api-client/src/modules/archon/servers/types/v0.ts delete mode 100644 packages/api-client/src/modules/archon/servers/types/v1.ts delete mode 100644 packages/api-client/src/modules/kyros/files/types/v0.ts delete mode 100644 packages/api-client/src/modules/labrinth/billing/index.ts delete mode 100644 packages/api-client/src/modules/labrinth/billing/types/internal.ts delete mode 100644 packages/api-client/src/modules/labrinth/projects/types/v2.ts delete mode 100644 packages/api-client/src/modules/labrinth/projects/types/v3.ts diff --git a/apps/frontend/src/helpers/api.ts b/apps/frontend/src/helpers/api.ts index 96d67fb4e6..affdf82e51 100644 --- a/apps/frontend/src/helpers/api.ts +++ b/apps/frontend/src/helpers/api.ts @@ -1,12 +1,12 @@ -import type { - AbstractFeature, - type AuthConfig, +import { AuthFeature, CircuitBreakerFeature, NuxtCircuitBreakerStorage, - type NuxtClientConfig, NuxtModrinthClient, VerboseLoggingFeature, + type AbstractFeature, + type AuthConfig, + type NuxtClientConfig, } from '@modrinth/api-client' export function createModrinthClient( diff --git a/apps/frontend/src/pages/[type]/[id]/settings/environment.vue b/apps/frontend/src/pages/[type]/[id]/settings/environment.vue index aee87d9207..8a43a07776 100644 --- a/apps/frontend/src/pages/[type]/[id]/settings/environment.vue +++ b/apps/frontend/src/pages/[type]/[id]/settings/environment.vue @@ -27,7 +27,7 @@ const supportsEnvironment = computed(() => const needsToVerify = computed( () => projectV3.value.side_types_migration_review_status === 'pending' && - projectV3.value.environment?.length > 0 && + (projectV3.value.environment?.length ?? 0) > 0 && projectV3.value.environment?.[0] !== 'unknown' && supportsEnvironment.value, ) @@ -156,12 +156,12 @@ const messages = defineMessages({ />
} - -export type ProductPrice = { - id: string - product_id: string - prices: Price - currency_code: string -} - -export type Product = { - id: string - metadata: ProductMetadata - prices: ProductPrice[] - unitary: boolean -} - -export type EditSubscriptionRequest = { - interval?: PriceDuration - payment_method?: string - cancelled?: boolean - region?: string - product?: string -} - -export type EditSubscriptionResponse = { - payment_intent_id: string - client_secret: string - tax: number - total: number -} - -export type AddPaymentMethodFlowResponse = { - client_secret: string -} - -export type EditPaymentMethodRequest = { - primary: boolean -} - -export type InitiatePaymentRequest = { - type: 'payment_method' | 'confirmation_token' - id?: string - token?: string - charge: - | { type: 'existing'; id: string } - | { type: 'new'; product_id: string; interval?: PriceDuration } - existing_payment_intent?: string - metadata?: { - type: 'pyro' - server_name?: string - server_region?: string - source: unknown - } -} - -export type InitiatePaymentResponse = { - payment_intent_id?: string - client_secret?: string - price_id: string - tax: number - total: number - payment_method?: string -} - -export type RefundChargeRequest = { - type: 'full' | 'partial' | 'none' - amount?: number - unprovision?: boolean -} - -export type CreditRequest = - | { subscription_ids: string[]; days: number; send_email: boolean; message: string } - | { nodes: string[]; days: number; send_email: boolean; message: string } - | { region: string; days: number; send_email: boolean; message: string } diff --git a/packages/api-client/src/modules/labrinth/index.ts b/packages/api-client/src/modules/labrinth/index.ts index aca48d2bee..986d949802 100644 --- a/packages/api-client/src/modules/labrinth/index.ts +++ b/packages/api-client/src/modules/labrinth/index.ts @@ -1,3 +1,3 @@ -export * from './billing' +export * from './billing/internal' export * from './projects/v2' export * from './projects/v3' diff --git a/packages/api-client/src/modules/labrinth/projects/types/v2.ts b/packages/api-client/src/modules/labrinth/projects/types/v2.ts deleted file mode 100644 index 92f0d7cd58..0000000000 --- a/packages/api-client/src/modules/labrinth/projects/types/v2.ts +++ /dev/null @@ -1,115 +0,0 @@ -export type Environment = 'required' | 'optional' | 'unsupported' | 'unknown' - -export type ProjectStatus = - | 'approved' - | 'archived' - | 'rejected' - | 'draft' - | 'unlisted' - | 'processing' - | 'withheld' - | 'scheduled' - | 'private' - | 'unknown' - -export type MonetizationStatus = 'monetized' | 'demonetized' | 'force-demonetized' - -export type ProjectType = 'mod' | 'modpack' | 'resourcepack' | 'shader' | 'plugin' | 'datapack' - -export type GalleryImageV2 = { - url: string - featured: boolean - title?: string - description?: string - created: string - ordering: number -} - -export type DonationLinkV2 = { - id: string - platform: string - url: string -} - -export type ProjectV2 = { - id: string - slug: string - project_type: ProjectType - team: string - title: string - description: string - body: string - published: string - updated: string - approved?: string - queued?: string - status: ProjectStatus - requested_status?: ProjectStatus - moderator_message?: { - message: string - body?: string - } - license: { - id: string - name: string - url?: string - } - client_side: Environment - server_side: Environment - downloads: number - followers: number - categories: string[] - additional_categories: string[] - game_versions: string[] - loaders: string[] - versions: string[] - icon_url?: string - issues_url?: string - source_url?: string - wiki_url?: string - discord_url?: string - donation_urls?: DonationLinkV2[] - gallery?: GalleryImageV2[] - color?: number - thread_id: string - monetization_status: MonetizationStatus -} - -export type SearchResultHit = { - project_id: string - project_type: ProjectType - slug: string - author: string - title: string - description: string - categories: string[] - display_categories: string[] - versions: string[] - downloads: number - follows: number - icon_url: string - date_created: string - date_modified: string - latest_version?: string - license: string - client_side: Environment - server_side: Environment - gallery: string[] - color?: number -} - -export type SearchResult = { - hits: SearchResultHit[] - offset: number - limit: number - total_hits: number -} - -export type ProjectSearchParams = { - query?: string - facets?: string[][] - filters?: string - index?: 'relevance' | 'downloads' | 'follows' | 'newest' | 'updated' - offset?: number - limit?: number -} diff --git a/packages/api-client/src/modules/labrinth/projects/types/v3.ts b/packages/api-client/src/modules/labrinth/projects/types/v3.ts deleted file mode 100644 index 3b23f86be7..0000000000 --- a/packages/api-client/src/modules/labrinth/projects/types/v3.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { MonetizationStatus, ProjectStatus } from './v2' - -export type GalleryItemV3 = { - url: string - raw_url: string - featured: boolean - name?: string - description?: string - created: string - ordering: number -} - -export type LinkV3 = { - platform: string - donation: boolean - url: string -} - -export type ProjectV3 = { - id: string - slug?: string - project_types: string[] - games: string[] - team_id: string - organization?: string - name: string - summary: string - description: string - published: string - updated: string - approved?: string - queued?: string - status: ProjectStatus - requested_status?: ProjectStatus - license: { - id: string - name: string - url?: string - } - downloads: number - followers: number - categories: string[] - additional_categories: string[] - loaders: string[] - versions: string[] - icon_url?: string - link_urls: Record - gallery: GalleryItemV3[] - color?: number - thread_id: string - monetization_status: MonetizationStatus - side_types_migration_review_status: 'reviewed' | 'pending' - [key: string]: unknown -} diff --git a/packages/api-client/src/modules/labrinth/projects/v2.ts b/packages/api-client/src/modules/labrinth/projects/v2.ts index b14c80fff5..c36b71c8d6 100644 --- a/packages/api-client/src/modules/labrinth/projects/v2.ts +++ b/packages/api-client/src/modules/labrinth/projects/v2.ts @@ -1,5 +1,5 @@ import { AbstractModule } from '../../../core/abstract-module' -import type { ProjectSearchParams, ProjectV2, SearchResult } from './types/v2' +import type { Labrinth } from '../types' export class LabrinthProjectsV2Module extends AbstractModule { public getModuleID(): string { @@ -18,8 +18,8 @@ export class LabrinthProjectsV2Module extends AbstractModule { * console.log(project.title) // "Sodium" * ``` */ - public async get(id: string): Promise { - return this.client.request(`/project/${id}`, { + public async get(id: string): Promise { + return this.client.request(`/project/${id}`, { api: 'labrinth', version: 2, method: 'GET', @@ -37,8 +37,8 @@ export class LabrinthProjectsV2Module extends AbstractModule { * const projects = await client.labrinth.projects_v2.getMultiple(['sodium', 'lithium', 'phosphor']) * ``` */ - public async getMultiple(ids: string[]): Promise { - return this.client.request(`/projects`, { + public async getMultiple(ids: string[]): Promise { + return this.client.request(`/projects`, { api: 'labrinth', version: 2, method: 'GET', @@ -61,8 +61,10 @@ export class LabrinthProjectsV2Module extends AbstractModule { * }) * ``` */ - public async search(params: ProjectSearchParams): Promise { - return this.client.request(`/search`, { + public async search( + params: Labrinth.Projects.v2.ProjectSearchParams, + ): Promise { + return this.client.request(`/search`, { api: 'labrinth', version: 2, method: 'GET', @@ -83,7 +85,7 @@ export class LabrinthProjectsV2Module extends AbstractModule { * }) * ``` */ - public async edit(id: string, data: Partial): Promise { + public async edit(id: string, data: Partial): Promise { return this.client.request(`/project/${id}`, { api: 'labrinth', version: 2, diff --git a/packages/api-client/src/modules/labrinth/projects/v3.ts b/packages/api-client/src/modules/labrinth/projects/v3.ts index 0dd397d074..f4e653049b 100644 --- a/packages/api-client/src/modules/labrinth/projects/v3.ts +++ b/packages/api-client/src/modules/labrinth/projects/v3.ts @@ -1,5 +1,5 @@ import { AbstractModule } from '../../../core/abstract-module' -import type { ProjectV3 } from './types/v3' +import type { Labrinth } from '../types' export class LabrinthProjectsV3Module extends AbstractModule { public getModuleID(): string { @@ -18,8 +18,8 @@ export class LabrinthProjectsV3Module extends AbstractModule { * console.log(project.project_types) // v3 field * ``` */ - public async get(id: string): Promise { - return this.client.request(`/project/${id}`, { + public async get(id: string): Promise { + return this.client.request(`/project/${id}`, { api: 'labrinth', version: 3, method: 'GET', @@ -37,8 +37,8 @@ export class LabrinthProjectsV3Module extends AbstractModule { * const projects = await client.labrinth.projects_v3.getMultiple(['sodium', 'lithium']) * ``` */ - public async getMultiple(ids: string[]): Promise { - return this.client.request(`/projects`, { + public async getMultiple(ids: string[]): Promise { + return this.client.request(`/projects`, { api: 'labrinth', version: 3, method: 'GET', @@ -59,7 +59,7 @@ export class LabrinthProjectsV3Module extends AbstractModule { * }) * ``` */ - public async edit(id: string, data: Partial): Promise { + public async edit(id: string, data: Labrinth.Projects.v3.EditProjectRequest): Promise { return this.client.request(`/project/${id}`, { api: 'labrinth', version: 3, diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 38670b55d5..d185b1cfa0 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -1,7 +1,361 @@ -import * as BillingInternal from './billing/types/internal' - export namespace Labrinth { export namespace Billing { - export import Internal = BillingInternal + export namespace Internal { + export type PriceDuration = 'five-days' | 'monthly' | 'quarterly' | 'yearly' + + export type SubscriptionStatus = 'provisioned' | 'unprovisioned' + + export type UserSubscription = { + id: string + user_id: string + price_id: string + interval: PriceDuration + status: SubscriptionStatus + created: string // ISO datetime string + metadata?: SubscriptionMetadata + } + + export type SubscriptionMetadata = + | { type: 'pyro'; id: string; region?: string } + | { type: 'medal'; id: string } + + export type ChargeStatus = + | 'open' + | 'processing' + | 'succeeded' + | 'failed' + | 'cancelled' + | 'expiring' + + export type ChargeType = 'one-time' | 'subscription' | 'proration' | 'refund' + + export type PaymentPlatform = 'Stripe' | 'None' + + export type Charge = { + id: string + user_id: string + price_id: string + amount: number + currency_code: string + status: ChargeStatus + due: string // ISO datetime string + last_attempt: string | null // ISO datetime string + type: ChargeType + subscription_id: string | null + subscription_interval: PriceDuration | null + platform: PaymentPlatform + parent_charge_id: string | null + net: number | null + } + + export type ProductMetadata = + | { type: 'midas' } + | { + type: 'pyro' + cpu: number + ram: number + swap: number + storage: number + } + | { + type: 'medal' + cpu: number + ram: number + swap: number + storage: number + region: string + } + + export type Price = + | { type: 'one-time'; price: number } + | { type: 'recurring'; intervals: Record } + + export type ProductPrice = { + id: string + product_id: string + prices: Price + currency_code: string + } + + export type Product = { + id: string + metadata: ProductMetadata + prices: ProductPrice[] + unitary: boolean + } + + export type EditSubscriptionRequest = { + interval?: PriceDuration + payment_method?: string + cancelled?: boolean + region?: string + product?: string + } + + export type EditSubscriptionResponse = { + payment_intent_id: string + client_secret: string + tax: number + total: number + } + + export type AddPaymentMethodFlowResponse = { + client_secret: string + } + + export type EditPaymentMethodRequest = { + primary: boolean + } + + export type InitiatePaymentRequest = { + type: 'payment_method' | 'confirmation_token' + id?: string + token?: string + charge: + | { type: 'existing'; id: string } + | { type: 'new'; product_id: string; interval?: PriceDuration } + existing_payment_intent?: string + metadata?: { + type: 'pyro' + server_name?: string + server_region?: string + source: unknown + } + } + + export type InitiatePaymentResponse = { + payment_intent_id?: string + client_secret?: string + price_id: string + tax: number + total: number + payment_method?: string + } + + export type RefundChargeRequest = { + type: 'full' | 'partial' | 'none' + amount?: number + unprovision?: boolean + } + + export type CreditRequest = + | { subscription_ids: string[]; days: number; send_email: boolean; message: string } + | { nodes: string[]; days: number; send_email: boolean; message: string } + | { region: string; days: number; send_email: boolean; message: string } + } + } + + export namespace Projects { + export namespace v2 { + export type Environment = 'required' | 'optional' | 'unsupported' | 'unknown' + + export type ProjectStatus = + | 'approved' + | 'archived' + | 'rejected' + | 'draft' + | 'unlisted' + | 'processing' + | 'withheld' + | 'scheduled' + | 'private' + | 'unknown' + + export type MonetizationStatus = 'monetized' | 'demonetized' | 'force-demonetized' + + export type ProjectType = + | 'mod' + | 'modpack' + | 'resourcepack' + | 'shader' + | 'plugin' + | 'datapack' + + export type GalleryImage = { + url: string + featured: boolean + title?: string + description?: string + created: string + ordering: number + } + + export type DonationLink = { + id: string + platform: string + url: string + } + + export type Project = { + id: string + slug: string + project_type: ProjectType + team: string + title: string + description: string + body: string + published: string + updated: string + approved?: string + queued?: string + status: ProjectStatus + requested_status?: ProjectStatus + moderator_message?: { + message: string + body?: string + } + license: { + id: string + name: string + url?: string + } + client_side: Environment + server_side: Environment + downloads: number + followers: number + categories: string[] + additional_categories: string[] + game_versions: string[] + loaders: string[] + versions: string[] + icon_url?: string + issues_url?: string + source_url?: string + wiki_url?: string + discord_url?: string + donation_urls?: DonationLink[] + gallery?: GalleryImage[] + color?: number + thread_id: string + monetization_status: MonetizationStatus + } + + export type SearchResultHit = { + project_id: string + project_type: ProjectType + slug: string + author: string + title: string + description: string + categories: string[] + display_categories: string[] + versions: string[] + downloads: number + follows: number + icon_url: string + date_created: string + date_modified: string + latest_version?: string + license: string + client_side: Environment + server_side: Environment + gallery: string[] + color?: number + } + + export type SearchResult = { + hits: SearchResultHit[] + offset: number + limit: number + total_hits: number + } + + export type ProjectSearchParams = { + query?: string + facets?: string[][] + filters?: string + index?: 'relevance' | 'downloads' | 'follows' | 'newest' | 'updated' + offset?: number + limit?: number + } + } + + export namespace v3 { + export type Environment = + | 'client_and_server' + | 'client_only' + | 'client_only_server_optional' + | 'singleplayer_only' + | 'server_only' + | 'server_only_client_optional' + | 'dedicated_server_only' + | 'client_or_server' + | 'client_or_server_prefers_both' + | 'unknown' + + export type GalleryItem = { + url: string + raw_url: string + featured: boolean + name?: string + description?: string + created: string + ordering: number + } + + export type Link = { + platform: string + donation: boolean + url: string + } + + export type Project = { + id: string + slug?: string + project_types: string[] + games: string[] + team_id: string + organization?: string + name: string + summary: string + description: string + published: string + updated: string + approved?: string + queued?: string + status: v2.ProjectStatus + requested_status?: v2.ProjectStatus + license: { + id: string + name: string + url?: string + } + downloads: number + followers: number + categories: string[] + additional_categories: string[] + loaders: string[] + versions: string[] + icon_url?: string + link_urls: Record + gallery: GalleryItem[] + color?: number + thread_id: string + monetization_status: v2.MonetizationStatus + side_types_migration_review_status: 'reviewed' | 'pending' + environment?: Environment[] + [key: string]: unknown + } + + export type EditProjectRequest = { + name?: string + summary?: string + description?: string + categories?: string[] + additional_categories?: string[] + license_url?: string | null + link_urls?: Record + license_id?: string + slug?: string + status?: v2.ProjectStatus + requested_status?: v2.ProjectStatus | null + moderation_message?: string | null + moderation_message_body?: string | null + monetization_status?: v2.MonetizationStatus + side_types_migration_review_status?: 'reviewed' | 'pending' + environment?: Environment + [key: string]: unknown + } + } } } diff --git a/packages/api-client/src/modules/types.ts b/packages/api-client/src/modules/types.ts index 6ecee07ba2..c54668b23b 100644 --- a/packages/api-client/src/modules/types.ts +++ b/packages/api-client/src/modules/types.ts @@ -1,5 +1,3 @@ export * from './archon/types' export * from './kyros/types' -export * from './labrinth/projects/types/v2' -export * from './labrinth/projects/types/v3' export * from './labrinth/types' diff --git a/packages/ui/src/components/project/settings/environment/ProjectSettingsEnvSelector.vue b/packages/ui/src/components/project/settings/environment/ProjectSettingsEnvSelector.vue index 070fcb3c6a..2cddf20438 100644 --- a/packages/ui/src/components/project/settings/environment/ProjectSettingsEnvSelector.vue +++ b/packages/ui/src/components/project/settings/environment/ProjectSettingsEnvSelector.vue @@ -1,5 +1,4 @@ diff --git a/apps/frontend/src/composables/generated.ts b/apps/frontend/src/composables/generated.ts index cd7415afe1..bd8a254bfb 100644 --- a/apps/frontend/src/composables/generated.ts +++ b/apps/frontend/src/composables/generated.ts @@ -1,3 +1,5 @@ +import type { ISO3166, Labrinth } from '@modrinth/api-client' + import generatedState from '~/generated/state.json' export interface ProjectType { @@ -15,38 +17,12 @@ export interface LoaderData { hiddenModLoaders: string[] } -export interface Country { - alpha2: string - alpha3: string - numeric: string - nameShort: string - nameLong: string -} - -export interface Subdivision { - code: string // Full ISO 3166-2 code (e.g., "US-NY") - name: string // Official name in local language - localVariant: string | null // English variant if different - category: string // STATE, PROVINCE, REGION, etc. - parent: string | null // Parent subdivision code - language: string // Language code -} - -export interface GeneratedState { - categories: any[] - loaders: any[] - gameVersions: any[] - donationPlatforms: any[] - reportTypes: any[] - muralBankDetails: Record< - string, - { - bankNames: string[] - } - > - countries: Country[] - subdivisions: Record +// Re-export types from api-client for convenience +export type Country = ISO3166.Country +export type Subdivision = ISO3166.Subdivision +export interface GeneratedState extends Labrinth.State.GeneratedState { + // Additional runtime-defined fields not from the API projectTypes: ProjectType[] loaderData: LoaderData projectViewModes: string[] @@ -54,11 +30,6 @@ export interface GeneratedState { rejectedStatuses: string[] staffRoles: string[] - homePageProjects?: any[] - homePageSearch?: any - homePageNotifs?: any - products?: any[] - // Metadata lastGenerated?: string apiUrl?: string @@ -71,14 +42,18 @@ export interface GeneratedState { */ export const useGeneratedState = () => useState('generatedState', () => ({ - categories: generatedState.categories ?? [], - loaders: generatedState.loaders ?? [], - gameVersions: generatedState.gameVersions ?? [], - donationPlatforms: generatedState.donationPlatforms ?? [], - reportTypes: generatedState.reportTypes ?? [], - muralBankDetails: generatedState.muralBankDetails ?? null, - countries: generatedState.countries ?? [], - subdivisions: generatedState.subdivisions ?? {}, + // Cast JSON data to typed API responses + categories: (generatedState.categories ?? []) as Labrinth.Tags.v2.Category[], + loaders: (generatedState.loaders ?? []) as Labrinth.Tags.v2.Loader[], + gameVersions: (generatedState.gameVersions ?? []) as Labrinth.Tags.v2.GameVersion[], + donationPlatforms: (generatedState.donationPlatforms ?? + []) as Labrinth.Tags.v2.DonationPlatform[], + reportTypes: (generatedState.reportTypes ?? []) as string[], + muralBankDetails: generatedState.muralBankDetails as + | Record + | undefined, + countries: (generatedState.countries ?? []) as ISO3166.Country[], + subdivisions: (generatedState.subdivisions ?? {}) as Record, projectTypes: [ { @@ -135,10 +110,12 @@ export const useGeneratedState = () => rejectedStatuses: ['rejected', 'withheld'], staffRoles: ['moderator', 'admin'], - homePageProjects: generatedState.homePageProjects, - homePageSearch: generatedState.homePageSearch, - homePageNotifs: generatedState.homePageNotifs, - products: generatedState.products, + homePageProjects: generatedState.homePageProjects as unknown as + | Labrinth.Projects.v2.Project[] + | undefined, + homePageSearch: generatedState.homePageSearch as Labrinth.Search.v2.SearchResults | undefined, + homePageNotifs: generatedState.homePageNotifs as Labrinth.Search.v2.SearchResults | undefined, + products: generatedState.products as Labrinth.Billing.Internal.Product[] | undefined, lastGenerated: generatedState.lastGenerated, apiUrl: generatedState.apiUrl, diff --git a/packages/api-client/package.json b/packages/api-client/package.json index ecf17087bc..8499f90bc4 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -8,10 +8,12 @@ "fix": "eslint . --fix && prettier --write ." }, "dependencies": { - "ofetch": "^1.4.1" + "ofetch": "^1.4.1", + "papaparse": "^5.4.1" }, "devDependencies": { - "@modrinth/tooling-config": "workspace:*" + "@modrinth/tooling-config": "workspace:*", + "@types/papaparse": "^5.3.15" }, "peerDependencies": { "@tauri-apps/plugin-http": "^2.0.0" diff --git a/packages/api-client/src/core/abstract-client.ts b/packages/api-client/src/core/abstract-client.ts index 5e7557127a..a20ac5cd07 100644 --- a/packages/api-client/src/core/abstract-client.ts +++ b/packages/api-client/src/core/abstract-client.ts @@ -26,6 +26,7 @@ export abstract class AbstractModrinthClient { public readonly labrinth!: InferredClientModules['labrinth'] public readonly archon!: InferredClientModules['archon'] public readonly kyros!: InferredClientModules['kyros'] + public readonly iso3166!: InferredClientModules['iso3166'] constructor(config: ClientConfig) { this.config = { diff --git a/packages/api-client/src/modules/index.ts b/packages/api-client/src/modules/index.ts index 431f65f1d8..9727d38180 100644 --- a/packages/api-client/src/modules/index.ts +++ b/packages/api-client/src/modules/index.ts @@ -2,10 +2,12 @@ import type { AbstractModrinthClient } from '../core/abstract-client' import type { AbstractModule } from '../core/abstract-module' import { ArchonServersV0Module } from './archon/servers/v0' import { ArchonServersV1Module } from './archon/servers/v1' +import { ISO3166Module } from './iso3166' import { KyrosFilesV0Module } from './kyros/files/v0' import { LabrinthBillingInternalModule } from './labrinth/billing/internal' import { LabrinthProjectsV2Module } from './labrinth/projects/v2' import { LabrinthProjectsV3Module } from './labrinth/projects/v3' +import { LabrinthStateModule } from './labrinth/state' type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule @@ -21,10 +23,12 @@ type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule export const MODULE_REGISTRY = { archon_servers_v0: ArchonServersV0Module, archon_servers_v1: ArchonServersV1Module, + iso3166_data: ISO3166Module, kyros_files_v0: KyrosFilesV0Module, labrinth_billing_internal: LabrinthBillingInternalModule, labrinth_projects_v2: LabrinthProjectsV2Module, labrinth_projects_v3: LabrinthProjectsV3Module, + labrinth_state: LabrinthStateModule, } as const satisfies Record export type ModuleID = keyof typeof MODULE_REGISTRY diff --git a/packages/api-client/src/modules/iso3166/index.ts b/packages/api-client/src/modules/iso3166/index.ts new file mode 100644 index 0000000000..c1ed03c37e --- /dev/null +++ b/packages/api-client/src/modules/iso3166/index.ts @@ -0,0 +1,103 @@ +import { $fetch } from 'ofetch' +import Papa from 'papaparse' +import { AbstractModule } from '../../core/abstract-module' +import type { ISO3166 } from './types' + +export type { ISO3166 } from './types' + +const ISO3166_REPO = 'https://raw.githubusercontent.com/ipregistry/iso3166/master' + +/** + * Module for fetching ISO 3166 country and subdivision data + * Data from https://github.com/ipregistry/iso3166 (Licensed under CC BY-SA 4.0) + * @platform Not for use in Tauri or Nuxt environments, only node. + */ +export class ISO3166Module extends AbstractModule { + public getModuleID(): string { + return 'iso3166_data' + } + + /** + * Build ISO 3166 country and subdivision data from the ipregistry repository + * + * @returns Promise resolving to countries and subdivisions data + * + * @example + * ```typescript + * const data = await client.iso3166.data.build() + * console.log(data.countries) // Array of country data + * console.log(data.subdivisions['US']) // Array of US state data + * ``` + */ + public async build(): Promise { + try { + // Fetch CSV files in parallel + const [countriesCSV, subdivisionsCSV] = await Promise.all([ + $fetch(`${ISO3166_REPO}/countries.csv`, { + // @ts-expect-error supports text + responseType: 'text', + }), + $fetch(`${ISO3166_REPO}/subdivisions.csv`, { + // @ts-expect-error supports text + responseType: 'text', + }), + ]) + + const countriesData = Papa.parse>(countriesCSV, { + header: true, + skipEmptyLines: true, + transformHeader: (header) => (header.startsWith('#') ? header.slice(1) : header), + }).data + + const subdivisionsData = Papa.parse>(subdivisionsCSV, { + header: true, + skipEmptyLines: true, + transformHeader: (header) => (header.startsWith('#') ? header.slice(1) : header), + }).data + + const countries: ISO3166.Country[] = countriesData.map((c) => ({ + alpha2: c.country_code_alpha2, + alpha3: c.country_code_alpha3, + numeric: c.numeric_code, + nameShort: c.name_short, + nameLong: c.name_long, + })) + + // Group subdivisions by country code + const subdivisions: Record = subdivisionsData.reduce( + (acc, sub) => { + const countryCode = sub.country_code_alpha2 + + if (!countryCode || typeof countryCode !== 'string' || countryCode.trim() === '') { + return acc + } + + if (!acc[countryCode]) acc[countryCode] = [] + + acc[countryCode].push({ + code: sub['subdivision_code_iso3166-2'], + name: sub.subdivision_name, + localVariant: sub.localVariant || null, + category: sub.category, + parent: sub.parent_subdivision || null, + language: sub.language_code, + }) + + return acc + }, + {} as Record, + ) + + return { + countries, + subdivisions, + } + } catch (err) { + console.error('Error fetching ISO3166 data:', err) + return { + countries: [], + subdivisions: {}, + } + } + } +} diff --git a/packages/api-client/src/modules/iso3166/types.ts b/packages/api-client/src/modules/iso3166/types.ts new file mode 100644 index 0000000000..78d7655566 --- /dev/null +++ b/packages/api-client/src/modules/iso3166/types.ts @@ -0,0 +1,23 @@ +export namespace ISO3166 { + export interface Country { + alpha2: string + alpha3: string + numeric: string + nameShort: string + nameLong: string + } + + export interface Subdivision { + code: string // Full ISO 3166-2 code (e.g., "US-NY") + name: string // Official name in local language + localVariant: string | null // English variant if different + category: string // STATE, PROVINCE, REGION, etc. + parent: string | null // Parent subdivision code + language: string // Language code + } + + export interface State { + countries: Country[] + subdivisions: Record + } +} diff --git a/packages/api-client/src/modules/labrinth/index.ts b/packages/api-client/src/modules/labrinth/index.ts index 986d949802..fd99f54b16 100644 --- a/packages/api-client/src/modules/labrinth/index.ts +++ b/packages/api-client/src/modules/labrinth/index.ts @@ -1,3 +1,4 @@ export * from './billing/internal' export * from './projects/v2' export * from './projects/v3' +export * from './state' diff --git a/packages/api-client/src/modules/labrinth/state/index.ts b/packages/api-client/src/modules/labrinth/state/index.ts new file mode 100644 index 0000000000..c7c85ceaa1 --- /dev/null +++ b/packages/api-client/src/modules/labrinth/state/index.ts @@ -0,0 +1,131 @@ +import { AbstractModule } from '../../../core/abstract-module' +import type { Labrinth } from '../types' + +export class LabrinthStateModule extends AbstractModule { + public getModuleID(): string { + return 'labrinth_state' + } + + /** + * Build the complete generated state by fetching from multiple endpoints + * + * @returns Promise resolving to the generated state containing categories, loaders, products, etc. + * + * @example + * ```typescript + * const state = await client.labrinth.state.build() + * console.log(state.products) // Available billing products + * ``` + */ + public async build(): Promise { + const handleError = (err: any, defaultValue: any) => { + console.error('Error fetching state data:', err) + return defaultValue + } + + // TODO: as we add new modules, move these raw requests to actual + // abstractions + const [ + categories, + loaders, + gameVersions, + donationPlatforms, + reportTypes, + homePageProjects, + homePageSearch, + homePageNotifs, + products, + muralBankDetails, + iso3166Data, + ] = await Promise.all([ + // Tag endpoints + this.client + .request('/tag/category', { + api: 'labrinth', + version: 2, + method: 'GET', + }) + .catch((err) => handleError(err, [])), + this.client + .request('/tag/loader', { + api: 'labrinth', + version: 2, + method: 'GET', + }) + .catch((err) => handleError(err, [])), + this.client + .request('/tag/game_version', { + api: 'labrinth', + version: 2, + method: 'GET', + }) + .catch((err) => handleError(err, [])), + this.client + .request('/tag/donation_platform', { + api: 'labrinth', + version: 2, + method: 'GET', + }) + .catch((err) => handleError(err, [])), + this.client + .request('/tag/report_type', { api: 'labrinth', version: 2, method: 'GET' }) + .catch((err) => handleError(err, [])), + + // Homepage data + this.client + .request('/projects_random', { + api: 'labrinth', + version: 2, + method: 'GET', + params: { count: '60' }, + }) + .catch((err) => handleError(err, [])), + this.client + .request('/search', { + api: 'labrinth', + version: 2, + method: 'GET', + params: { limit: '3', query: 'leave', index: 'relevance' }, + }) + .catch((err) => handleError(err, {} as Labrinth.Search.v2.SearchResults)), + this.client + .request('/search', { + api: 'labrinth', + version: 2, + method: 'GET', + params: { limit: '3', query: '', index: 'updated' }, + }) + .catch((err) => handleError(err, {} as Labrinth.Search.v2.SearchResults)), + + // Internal billing/mural endpoints + this.client.labrinth.billing_internal.getProducts().catch((err) => handleError(err, [])), + this.client + .request<{ bankDetails: Record }>('/mural/bank-details', { + api: 'labrinth', + version: 'internal', + method: 'GET', + }) + .catch((err) => handleError(err, null)), + + // ISO3166 country and subdivision data + this.client.iso3166.data + .build() + .catch((err) => handleError(err, { countries: [], subdivisions: {} })), + ]) + + return { + categories, + loaders, + gameVersions, + donationPlatforms, + reportTypes, + homePageProjects, + homePageSearch, + homePageNotifs, + products, + muralBankDetails: muralBankDetails?.bankDetails, + countries: iso3166Data.countries, + subdivisions: iso3166Data.subdivisions, + } + } +} diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index d185b1cfa0..33a93ba4f4 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -1,3 +1,5 @@ +import type { ISO3166 } from '../iso3166/types' + export namespace Labrinth { export namespace Billing { export namespace Internal { @@ -358,4 +360,92 @@ export namespace Labrinth { } } } + + export namespace Tags { + export namespace v2 { + export interface Category { + icon: string + name: string + project_type: string + header: string + } + + export interface Loader { + icon: string + name: string + supported_project_types: string[] + } + + export interface GameVersion { + version: string + version_type: string + date: string // RFC 3339 DateTime + major: boolean + } + + export interface DonationPlatform { + short: string + name: string + } + } + } + + export namespace Search { + export namespace v2 { + export interface ResultSearchProject { + project_id: string + project_type: string + slug: string | null + author: string + title: string + description: string + categories: string[] + display_categories: string[] + versions: string[] + downloads: number + follows: number + icon_url: string + date_created: string + date_modified: string + latest_version: string + license: string + client_side: string + server_side: string + gallery: string[] + featured_gallery: string | null + color: number | null + } + + export interface SearchResults { + hits: ResultSearchProject[] + offset: number + limit: number + total_hits: number + } + } + } + + export namespace State { + export interface GeneratedState { + categories: Tags.v2.Category[] + loaders: Tags.v2.Loader[] + gameVersions: Tags.v2.GameVersion[] + donationPlatforms: Tags.v2.DonationPlatform[] + reportTypes: string[] + muralBankDetails?: Record< + string, + { + bankNames: string[] + } + > + + homePageProjects?: Projects.v2.Project[] + homePageSearch?: Search.v2.SearchResults + homePageNotifs?: Search.v2.SearchResults + products?: Billing.Internal.Product[] + + countries: ISO3166.Country[] + subdivisions: Record + } + } } diff --git a/packages/api-client/src/modules/types.ts b/packages/api-client/src/modules/types.ts index c54668b23b..f0aff3b3c7 100644 --- a/packages/api-client/src/modules/types.ts +++ b/packages/api-client/src/modules/types.ts @@ -1,3 +1,4 @@ export * from './archon/types' +export * from './iso3166/types' export * from './kyros/types' export * from './labrinth/types' diff --git a/apps/frontend/src/assets/images/servers/minecraft_server_icon.png b/packages/assets/external/illustrations/minecraft_server_icon.png similarity index 100% rename from apps/frontend/src/assets/images/servers/minecraft_server_icon.png rename to packages/assets/external/illustrations/minecraft_server_icon.png diff --git a/packages/assets/index.ts b/packages/assets/index.ts index 7803c884ab..b04a369de9 100644 --- a/packages/assets/index.ts +++ b/packages/assets/index.ts @@ -40,6 +40,7 @@ import _CurseForgeIcon from './external/curseforge.svg?component' import _DiscordIcon from './external/discord.svg?component' import _FacebookIcon from './external/facebook.svg?component' import _GithubIcon from './external/github.svg?component' +import _MinecraftServerIcon from './external/illustrations/minecraft_server_icon.png?url' import _InstagramIcon from './external/instagram.svg?component' import _KoFiIcon from './external/kofi.svg?component' import _MastodonIcon from './external/mastodon.svg?component' @@ -113,6 +114,7 @@ export const VenmoIcon = _VenmoIcon export const PolygonIcon = _PolygonIcon export const USDCColorIcon = _USDCColorIcon export const VisaIcon = _VisaIcon +export const MinecraftServerIcon = _MinecraftServerIcon export * from './generated-icons' export { default as ClassicPlayerModel } from './models/classic-player.gltf?url' diff --git a/packages/ui/src/components/billing/ServersUpgradeModalWrapper.vue b/packages/ui/src/components/billing/ServersUpgradeModalWrapper.vue new file mode 100644 index 0000000000..c5cb1b2d54 --- /dev/null +++ b/packages/ui/src/components/billing/ServersUpgradeModalWrapper.vue @@ -0,0 +1,329 @@ + + + diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index a5c39655d3..33282787b0 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -108,6 +108,7 @@ export { default as AffiliateLinkCreateModal } from './affiliate/AffiliateLinkCr export { default as AddPaymentMethodModal } from './billing/AddPaymentMethodModal.vue' export { default as ModrinthServersPurchaseModal } from './billing/ModrinthServersPurchaseModal.vue' export { default as PurchaseModal } from './billing/PurchaseModal.vue' +export { default as ServersUpgradeModalWrapper } from './billing/ServersUpgradeModalWrapper.vue' // Skins export { default as CapeButton } from './skin/CapeButton.vue' diff --git a/packages/ui/src/components/servers/icons/ServerIcon.vue b/packages/ui/src/components/servers/icons/ServerIcon.vue index c0eae785a4..58d7cb3a03 100644 --- a/packages/ui/src/components/servers/icons/ServerIcon.vue +++ b/packages/ui/src/components/servers/icons/ServerIcon.vue @@ -13,13 +13,15 @@ v-else class="h-full w-full select-none object-fill" alt="Server Icon" - src="~/assets/images/servers/minecraft_server_icon.png" + :src="MinecraftServerIcon" />
diff --git a/apps/frontend/src/pages/settings/billing/index.vue b/apps/frontend/src/pages/settings/billing/index.vue index b5dcb3d69a..7c6357ebb9 100644 --- a/apps/frontend/src/pages/settings/billing/index.vue +++ b/apps/frontend/src/pages/settings/billing/index.vue @@ -626,11 +626,11 @@ import { OverflowMenu, PurchaseModal, } from '@modrinth/ui' +import ServerListing from '@modrinth/ui/src/components/servers/ServerListing.vue' import { calculateSavings, formatPrice, getCurrency } from '@modrinth/utils' import { computed, ref } from 'vue' import { useBaseFetch } from '@/composables/fetch.js' -import ServerListing from '@modrinth/ui/src/components/servers/ServerListing.vue' import ModrinthServersIcon from '~/components/ui/servers/ModrinthServersIcon.vue' import ServersUpgradeModalWrapper from '~/components/ui/servers/ServersUpgradeModalWrapper.vue' import { useServersFetch } from '~/composables/servers/servers-fetch.ts' diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts index 1e6ecabfc4..106c7e4f92 100644 --- a/packages/api-client/src/index.ts +++ b/packages/api-client/src/index.ts @@ -1,21 +1,21 @@ export { AbstractModrinthClient } from './core/abstract-client' export { AbstractFeature, type FeatureConfig } from './core/abstract-feature' export { ModrinthApiError, ModrinthServerError } from './core/errors' -export { AuthFeature, type AuthConfig } from './features/auth' +export { type AuthConfig, AuthFeature } from './features/auth' export { - CircuitBreakerFeature, - InMemoryCircuitBreakerStorage, type CircuitBreakerConfig, + CircuitBreakerFeature, type CircuitBreakerState, type CircuitBreakerStorage, + InMemoryCircuitBreakerStorage, } from './features/circuit-breaker' -export { RetryFeature, type BackoffStrategy, type RetryConfig } from './features/retry' -export { VerboseLoggingFeature, type VerboseLoggingConfig } from './features/verbose-logging' +export { type BackoffStrategy, type RetryConfig, RetryFeature } from './features/retry' +export { type VerboseLoggingConfig, VerboseLoggingFeature } from './features/verbose-logging' export type { InferredClientModules } from './modules' export * from './modules/types' export { GenericModrinthClient } from './platform/generic' -export { NuxtCircuitBreakerStorage, NuxtModrinthClient } from './platform/nuxt' export type { NuxtClientConfig } from './platform/nuxt' -export { TauriModrinthClient } from './platform/tauri' +export { NuxtCircuitBreakerStorage, NuxtModrinthClient } from './platform/nuxt' export type { TauriClientConfig } from './platform/tauri' +export { TauriModrinthClient } from './platform/tauri' export * from './types' diff --git a/packages/api-client/src/modules/iso3166/index.ts b/packages/api-client/src/modules/iso3166/index.ts index c1ed03c37e..cb64ca141e 100644 --- a/packages/api-client/src/modules/iso3166/index.ts +++ b/packages/api-client/src/modules/iso3166/index.ts @@ -1,5 +1,6 @@ import { $fetch } from 'ofetch' import Papa from 'papaparse' + import { AbstractModule } from '../../core/abstract-module' import type { ISO3166 } from './types' @@ -43,12 +44,14 @@ export class ISO3166Module extends AbstractModule { }), ]) + // eslint-disable-next-line @typescript-eslint/no-explicit-any const countriesData = Papa.parse>(countriesCSV, { header: true, skipEmptyLines: true, transformHeader: (header) => (header.startsWith('#') ? header.slice(1) : header), }).data + // eslint-disable-next-line @typescript-eslint/no-explicit-any const subdivisionsData = Papa.parse>(subdivisionsCSV, { header: true, skipEmptyLines: true, diff --git a/packages/api-client/src/modules/labrinth/state/index.ts b/packages/api-client/src/modules/labrinth/state/index.ts index c7c85ceaa1..8d9d0f1980 100644 --- a/packages/api-client/src/modules/labrinth/state/index.ts +++ b/packages/api-client/src/modules/labrinth/state/index.ts @@ -18,8 +18,11 @@ export class LabrinthStateModule extends AbstractModule { * ``` */ public async build(): Promise { + const errors: unknown[] = [] + // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleError = (err: any, defaultValue: any) => { console.error('Error fetching state data:', err) + errors.push(err) return defaultValue } @@ -126,6 +129,7 @@ export class LabrinthStateModule extends AbstractModule { muralBankDetails: muralBankDetails?.bankDetails, countries: iso3166Data.countries, subdivisions: iso3166Data.subdivisions, + errors, } } } diff --git a/packages/api-client/src/modules/labrinth/types.ts b/packages/api-client/src/modules/labrinth/types.ts index 33a93ba4f4..f5e6476226 100644 --- a/packages/api-client/src/modules/labrinth/types.ts +++ b/packages/api-client/src/modules/labrinth/types.ts @@ -446,6 +446,8 @@ export namespace Labrinth { countries: ISO3166.Country[] subdivisions: Record + + errors: unknown[] } } } diff --git a/packages/ui/src/pages/servers/manage/index.vue b/packages/ui/src/pages/servers/manage/index.vue index 83e05c15cc..e7bf952e10 100644 --- a/packages/ui/src/pages/servers/manage/index.vue +++ b/packages/ui/src/pages/servers/manage/index.vue @@ -4,7 +4,7 @@ class="experimental-styles-within relative mx-auto mb-6 flex min-h-screen w-full max-w-[1280px] flex-col px-6" >

Servers

-
+
-
- - - + + + New server - +
@@ -178,7 +172,7 @@