diff --git a/.vscode/settings.json b/.vscode/settings.json index d4d338f6dc..7522198819 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -29,5 +29,7 @@ }, "[rust]": { "editor.defaultFormatter": "rust-lang.rust-analyzer" - } + }, + "css.lint.unknownAtRules": "ignore", + "scss.lint.unknownAtRules": "ignore" } diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index a4bc69aafc..06e392bf3d 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -970,13 +970,6 @@ provideAppUpdateDownloadProgress(appUpdateDownload) - - - + + +
diff --git a/apps/app-frontend/src/assets/stylesheets/global.scss b/apps/app-frontend/src/assets/stylesheets/global.scss index 346cb2abb8..a17170dcca 100644 --- a/apps/app-frontend/src/assets/stylesheets/global.scss +++ b/apps/app-frontend/src/assets/stylesheets/global.scss @@ -77,12 +77,8 @@ body { } a { - color: var(--color-link); + color: inherit; text-decoration: none; - - &:hover { - text-decoration: none; - } } .badge { @@ -174,4 +170,11 @@ img { } } +button, +input[type='button'] { + cursor: pointer; + border: none; + outline: 2px solid transparent; +} + @import '@modrinth/assets/omorphia.scss'; diff --git a/apps/app-frontend/src/config.ts b/apps/app-frontend/src/config.ts new file mode 100644 index 0000000000..bc98d33109 --- /dev/null +++ b/apps/app-frontend/src/config.ts @@ -0,0 +1,7 @@ +// src/config.ts +export const config = { + siteUrl: import.meta.env.VITE_SITE_URL, + stripePublishableKey: + import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY || + 'pk_test_51JbFxJJygY5LJFfKV50mnXzz3YLvBVe2Gd1jn7ljWAkaBlRz3VQdxN9mXcPSrFbSqxwAb0svte9yhnsmm7qHfcWn00R611Ce7b', +} diff --git a/apps/app-frontend/src/pages/Servers.vue b/apps/app-frontend/src/pages/Servers.vue new file mode 100644 index 0000000000..d2cc730f73 --- /dev/null +++ b/apps/app-frontend/src/pages/Servers.vue @@ -0,0 +1,26 @@ + + + diff --git a/apps/app-frontend/src/pages/index.js b/apps/app-frontend/src/pages/index.js index d08a3fbeb6..5a4294937a 100644 --- a/apps/app-frontend/src/pages/index.js +++ b/apps/app-frontend/src/pages/index.js @@ -1,6 +1,7 @@ import Browse from './Browse.vue' import Index from './Index.vue' +import Servers from './Servers.vue' import Skins from './Skins.vue' import Worlds from './Worlds.vue' -export { Browse, Index, Skins, Worlds } +export { Browse, Index, Servers, Skins, Worlds } diff --git a/apps/app-frontend/src/routes.js b/apps/app-frontend/src/routes.js index 0c7b641e54..11b8842dfd 100644 --- a/apps/app-frontend/src/routes.js +++ b/apps/app-frontend/src/routes.js @@ -1,4 +1,3 @@ -import { ServersManagePageIndex } from '@modrinth/ui' import { createRouter, createWebHistory } from 'vue-router' import * as Pages from '@/pages' @@ -31,7 +30,7 @@ export default new createRouter({ { path: '/hosting/manage/', name: 'Servers', - component: ServersManagePageIndex, + component: Pages.Servers, meta: { breadcrumb: [{ name: 'Servers' }], }, diff --git a/apps/app-frontend/tsconfig.app.json b/apps/app-frontend/tsconfig.app.json index 8d5b455fe5..f723e2026f 100644 --- a/apps/app-frontend/tsconfig.app.json +++ b/apps/app-frontend/tsconfig.app.json @@ -16,6 +16,8 @@ "strict": true, + "types": ["vite/client"], + "paths": { "@/*": ["./src/*"] } diff --git a/apps/app/tauri.conf.json b/apps/app/tauri.conf.json index 986347dc30..bc06fa6405 100644 --- a/apps/app/tauri.conf.json +++ b/apps/app/tauri.conf.json @@ -87,12 +87,12 @@ "capabilities": ["ads", "core", "plugins"], "csp": { "default-src": "'self' customprotocol: asset:", - "connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net 'self' data: blob:", + "connect-src": "ipc: http://ipc.localhost https://modrinth.com https://*.modrinth.com https://*.posthog.com https://posthog.modrinth.com https://*.sentry.io https://api.mclo.gs http://textures.minecraft.net https://textures.minecraft.net https://js.stripe.com https://*.stripe.com wss://*.stripe.com wss://*.nodes.modrinth.com 'self' data: blob:", "font-src": ["https://cdn-raw.modrinth.com/fonts/"], "img-src": "https: 'unsafe-inline' 'self' asset: http://asset.localhost http://textures.minecraft.net blob: data:", "style-src": "'unsafe-inline' 'self'", - "script-src": "https://*.posthog.com https://tally.so/widgets/embed.js 'self'", - "frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ 'self'", + "script-src": "https://*.posthog.com https://posthog.modrinth.com https://js.stripe.com https://tally.so/widgets/embed.js 'self'", + "frame-src": "https://www.youtube.com https://www.youtube-nocookie.com https://discord.com https://tally.so/popup/ https://js.stripe.com https://hooks.stripe.com 'self'", "media-src": "https://*.githubusercontent.com" } } diff --git a/apps/frontend/src/assets/styles/global.scss b/apps/frontend/src/assets/styles/global.scss index b003bb72a9..3f9817684d 100644 --- a/apps/frontend/src/assets/styles/global.scss +++ b/apps/frontend/src/assets/styles/global.scss @@ -463,9 +463,9 @@ kbd { font-size: 0.85em !important; } -@import '~/assets/styles/layout.scss'; -@import '~/assets/styles/utils.scss'; -@import '~/assets/styles/components.scss'; +@import './layout.scss'; +@import './utils.scss'; +@import './components.scss'; // OMORPHIA FIXES .card { diff --git a/apps/frontend/src/pages/hosting/manage/[id].vue b/apps/frontend/src/pages/hosting/manage/[id].vue index bcf3667fcf..87e799c124 100644 --- a/apps/frontend/src/pages/hosting/manage/[id].vue +++ b/apps/frontend/src/pages/hosting/manage/[id].vue @@ -160,7 +160,7 @@ :show-loader-label="showLoaderLabel" :uptime-seconds="uptimeSeconds" :linked="true" - class="server-action-buttons-anim flex min-w-0 flex-col flex-wrap items-center gap-4 text-secondary *:hidden sm:flex-row sm:*:flex" + class="server-action-buttons-anim flex min-w-0 flex-col flex-wrap items-center gap-2 text-primary *:hidden sm:flex-row sm:*:flex" /> @@ -1135,7 +1135,8 @@ const handleInstallationResult = async (data: Archon.Websocket.v0.WSInstallation } const updateStats = (currentStats: Stats['current']) => { - isConnected.value = true + if (!isMounted.value) return + if (!isConnected.value) isConnected.value = true stats.value = { current: currentStats, past: { ...stats.value.current }, diff --git a/apps/frontend/src/pages/settings/billing/index.vue b/apps/frontend/src/pages/settings/billing/index.vue index 8cb481bc77..3a2f87f043 100644 --- a/apps/frontend/src/pages/settings/billing/index.vue +++ b/apps/frontend/src/pages/settings/billing/index.vue @@ -233,6 +233,8 @@ v-if="subscription.serverInfo" v-bind="subscription.serverInfo" :pending-change="getPendingChange(subscription)" + :cancellation-date="getCancellationDate(subscription)" + :on-download-backup="getLatestBackupDownload(subscription.serverInfo)" />

@@ -764,6 +766,11 @@ const { data: serversData } = useQuery({ queryFn: () => client.archon.servers_v0.list(), }) +const { data: serverFullList } = useQuery({ + queryKey: ['servers', 'v1'], + queryFn: () => client.archon.servers_v1.list(), +}) + const midasProduct = ref(products.find((x) => x.metadata?.type === 'midas')) const midasSubscription = computed(() => subscriptions.value?.find( @@ -919,6 +926,13 @@ const getPyroCharge = (subscription) => { ) } +const getCancellationDate = (subscription) => { + const charge = getPyroCharge(subscription) + if (!charge) return null + if (charge.status === 'cancelled') return charge.due + return null +} + const getProductSize = (product) => { if (!product || !product.metadata) return 'Unknown' const ramSize = product.metadata.ram @@ -982,6 +996,46 @@ const resubscribePyro = async (subscriptionId, wasSuspended) => { } } +function getLatestBackupDownload(serverInfo) { + const serverFull = serverFullList.value?.find((s) => s.id === serverInfo.server_id) + if (!serverFull) return null + + const activeWorld = serverFull.worlds.find((w) => w.is_active) ?? serverFull.worlds[0] + if (!activeWorld?.backups?.length) return null + + const latestBackup = activeWorld.backups + .filter((b) => b.status === 'done') + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0] + if (!latestBackup) return null + + return async () => { + try { + const server = await client.archon.servers_v0.get(serverInfo.server_id) + const kyrosUrl = server.node?.instance + const jwt = server.node?.token + if (!kyrosUrl || !jwt) { + addNotification({ + title: 'Download unavailable', + text: 'Server connection info is not available. Please contact support.', + type: 'error', + }) + return + } + + window.open( + `https://${kyrosUrl}/modrinth/v0/backups/${latestBackup.id}/download?auth=${jwt}`, + '_blank', + ) + } catch { + addNotification({ + title: 'Download failed', + text: 'An error occurred while trying to download the backup.', + type: 'error', + }) + } + } +} + const refresh = async () => { await Promise.all([ queryClient.invalidateQueries({ queryKey: ['billing'] }), diff --git a/packages/api-client/src/platform/tauri.ts b/packages/api-client/src/platform/tauri.ts index 357a73ab7e..7e57fc2a53 100644 --- a/packages/api-client/src/platform/tauri.ts +++ b/packages/api-client/src/platform/tauri.ts @@ -103,8 +103,11 @@ export class TauriModrinthClient extends XHRUploadClient { throw error } - const data = await response.json() - return data as T + const text = await response.text() + if (!text) { + return undefined as T + } + return JSON.parse(text) as T } catch (error) { throw this.normalizeError(error) } diff --git a/packages/ui/.storybook/preview.ts b/packages/ui/.storybook/preview.ts index d775c85df0..9b340008eb 100644 --- a/packages/ui/.storybook/preview.ts +++ b/packages/ui/.storybook/preview.ts @@ -1,15 +1,22 @@ -import '@modrinth/assets/omorphia.scss' import 'floating-vue/dist/style.css' -import '../src/styles/tailwind.css' +import '../../assets/styles/defaults.scss' +// frontend css imports +// import '../../../apps/frontend/src/assets/styles/global.scss' +// import '../../../apps/frontend/src/assets/styles/tailwind.css' +// --- +// app-frontend css imports +import '../../../apps/app-frontend/src/assets/stylesheets/global.scss' import type { Labrinth } from '@modrinth/api-client' import { GenericModrinthClient } from '@modrinth/api-client' import { withThemeByClassName } from '@storybook/addon-themes' import type { Preview } from '@storybook/vue3-vite' import { setup } from '@storybook/vue3-vite' +import { QueryClient, VueQueryPlugin } from '@tanstack/vue-query' import FloatingVue from 'floating-vue' -import { defineComponent, ref } from 'vue' +import { computed, defineComponent, h, ref } from 'vue' import { createI18n } from 'vue-i18n' +import { createMemoryHistory, createRouter } from 'vue-router' import NotificationPanel from '../src/components/nav/NotificationPanel.vue' import PopupNotificationPanel from '../src/components/nav/PopupNotificationPanel.vue' @@ -109,9 +116,68 @@ class StorybookPopupNotificationManager extends AbstractPopupNotificationManager } } +const StorybookLink = defineComponent({ + name: 'StorybookLink', + inheritAttrs: false, + props: { + to: { + type: [String, Object], + default: '', + }, + }, + setup(props, { attrs, slots }) { + const href = computed(() => { + if (typeof props.to === 'string') return props.to || '#' + if (props.to && typeof props.to === 'object' && 'path' in props.to) { + const path = props.to.path + return typeof path === 'string' ? path : '#' + } + return '#' + }) + + return () => + h( + 'a', + { + ...attrs, + href: href.value, + }, + slots.default?.(), + ) + }, +}) + +const StorybookClientOnly = defineComponent({ + name: 'StorybookClientOnly', + setup(_, { slots }) { + return () => slots.default?.() + }, +}) + setup((app) => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }) + app.use(VueQueryPlugin, { queryClient }) app.use(i18n) + const router = createRouter({ + history: createMemoryHistory(), + routes: [{ path: '/:pathMatch(.*)*', component: { render: () => null } }], + }) + app.use(router) + + app.component('NuxtLink', StorybookLink) + app.component('RouterLink', StorybookLink) + app.component('ClientOnly', StorybookClientOnly) + // Provide the custom I18nContext for components using injectI18n() const i18nContext: I18nContext = { locale: i18n.global.locale, diff --git a/packages/ui/src/components/base/CopyCode.vue b/packages/ui/src/components/base/CopyCode.vue index f046b5f12c..7e3edb33f6 100644 --- a/packages/ui/src/components/base/CopyCode.vue +++ b/packages/ui/src/components/base/CopyCode.vue @@ -1,5 +1,9 @@