diff --git a/.github/instructions/i18n-convert.instructions.md b/.github/instructions/i18n-convert.instructions.md index b8ce2218c1..91786f4fe3 100644 --- a/.github/instructions/i18n-convert.instructions.md +++ b/.github/instructions/i18n-convert.instructions.md @@ -81,6 +81,6 @@ Please follow these rules precisely: Use existing patterns from our codebase: - Variables/plurals: see `apps/frontend/src/pages/frog.vue` -- Rich-text link tags: see `apps/frontend/src/pages/auth/welcome.vue` and `apps/frontend/src/error.vue` +- Rich-text link tags: see `apps/frontend/src/error.vue` When you finish, there should be no hard-coded English strings left in the template—everything comes from `formatMessage` or ``. diff --git a/CLAUDE.md b/CLAUDE.md index e91b8f6083..a4658aa02d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,7 @@ The website and app `prepr` commands Each project may have its own `CLAUDE.md` with detailed instructions: -- [`apps/labrinth/CLAUDE.md`](apps/labrinth/CLAUDE.md) — Backend API +- [`apps/labrinth/AGENTS.md`](apps/labrinth/AGENTS.md) — Backend API - [`apps/frontend/CLAUDE.md`](apps/frontend/CLAUDE.md) - Frontend Website ## Code Guidelines diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index 5a889cd37f..b0572572b5 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -20,7 +20,7 @@ "@modrinth/utils": "workspace:*", "@sentry/vue": "^8.27.0", "@sfirew/minecraft-motd-parser": "^1.1.6", - "@tanstack/vue-query": "^5.90.7", + "@tanstack/vue-query": "5.90.7", "@tauri-apps/api": "^2.5.0", "@tauri-apps/plugin-dialog": "^2.2.1", "@tauri-apps/plugin-fs": "^2.4.5", diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 232305c092..b8ff688b7c 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -2,6 +2,7 @@ import { Intercom, shutdown as shutdownIntercom } from '@intercom/messenger-js-sdk' import { AuthFeature, + ModrinthApiError, NodeAuthFeature, nodeAuthState, PanelVersionFeature, @@ -98,6 +99,7 @@ import { command_listener, warning_listener } from '@/helpers/events.js' import { cancelLogin, get as getCreds, login, logout } from '@/helpers/mr_auth.ts' import { create_profile_and_install_from_file } from '@/helpers/pack' import { list } from '@/helpers/profile.js' +import { mergeUrlQuery, parseModrinthLink } from '@/helpers/project-links.ts' import { get as getSettings, set as setSettings } from '@/helpers/settings.ts' import { get_opening_command, initialize_state } from '@/helpers/state' import { @@ -1028,6 +1030,28 @@ async function installUpdate() { }, 250) } +async function openModrinthProjectLinkInApp(parsed) { + const { slug, pathSuffix, url } = parsed + const loadToken = loading.begin() + try { + const { id } = await tauriApiClient.labrinth.projects_v2.check(slug) + const query = mergeUrlQuery(route.query, url) + await router.push({ + path: `/project/${id}${pathSuffix}`, + query, + hash: url.hash || undefined, + }) + } catch (err) { + if (err instanceof ModrinthApiError && err.statusCode === 404) { + openUrl(url.href) + } else { + handleError(err) + } + } finally { + loading.end(loadToken) + } +} + function handleClick(e) { let target = e.target while (target != null) { @@ -1040,7 +1064,12 @@ function handleClick(e) { !target.href.startsWith('https://tauri.localhost') && !target.href.startsWith('http://tauri.localhost') ) { - openUrl(target.href) + const parsed = parseModrinthLink(target.href) + if (parsed) { + void openModrinthProjectLinkInApp(parsed) + } else { + openUrl(target.href) + } } e.preventDefault() break diff --git a/apps/app-frontend/src/helpers/project-links.ts b/apps/app-frontend/src/helpers/project-links.ts new file mode 100644 index 0000000000..3ce0c0c45b --- /dev/null +++ b/apps/app-frontend/src/helpers/project-links.ts @@ -0,0 +1,83 @@ +import type { LocationQuery, LocationQueryRaw } from 'vue-router' + +const MODRINTH_HOSTNAMES = new Set(['modrinth.com', 'www.modrinth.com']) + +const SUPPORTED_PROJECT_TYPES = new Set([ + 'mod', + 'modpack', + 'resourcepack', + 'datapack', + 'plugin', + 'shader', + 'server', + 'project', +]) + +export function parseModrinthLink( + href: string, +): { slug: string; pathSuffix: string; url: URL } | null { + let url: URL + try { + url = new URL(href) + } catch { + return null + } + + if (!MODRINTH_HOSTNAMES.has(url.hostname.toLowerCase())) { + return null + } + + const segments = url.pathname.split('/').filter((p) => p.length > 0) + if (segments.length < 2) { + return null + } + + if (SUPPORTED_PROJECT_TYPES.has(segments[0].toLowerCase())) { + const slug = segments[1] + if (!slug) { + return null + } + + const rest: string[] = segments.slice(2) + const pathSuffix = toValidAppSubpath(rest) + if (pathSuffix === null) { + return null + } + + return { slug, pathSuffix, url } + } else { + return null + } +} + +const SUPPORTED_SUBPATHS = ['versions', 'gallery'] + +function toValidAppSubpath(rest: string[]): string | null { + if (rest.length === 0) { + return '' + } + + const subroute = rest[0].toLowerCase() + if (rest.length === 1 && SUPPORTED_SUBPATHS.includes(subroute)) { + return `/${subroute}` + } + + if (rest.length === 2 && subroute === 'version') { + return `/version/${rest[1]}` + } + + return null +} + +export function mergeUrlQuery(routeQuery: LocationQuery, linkUrl: URL): LocationQueryRaw { + const newQuery: LocationQueryRaw = { ...routeQuery } + const keys = new Set() + linkUrl.searchParams.forEach((_value, key) => { + keys.add(key) + }) + for (const key of keys) { + const values = linkUrl.searchParams.getAll(key) + newQuery[key] = values.length === 1 ? values[0] : values + } + return newQuery +} diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 624c6c9431..4707b6a66e 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -47,7 +47,7 @@ "@modrinth/ui": "workspace:*", "@modrinth/utils": "workspace:*", "@sentry/nuxt": "^10.33.0", - "@tanstack/vue-query": "^5.90.7", + "@tanstack/vue-query": "5.90.7", "@types/three": "^0.172.0", "@vitejs/plugin-vue": "^6.0.3", "@vue-email/components": "^0.0.21", diff --git a/apps/frontend/src/app.vue b/apps/frontend/src/app.vue index 9cdb763dca..1c4585e3d3 100644 --- a/apps/frontend/src/app.vue +++ b/apps/frontend/src/app.vue @@ -14,6 +14,8 @@ import { I18nDebugPanel, LoadingBar, NotificationPanel } from '@modrinth/ui' import { setupProviders } from '~/providers/setup.ts' +import { useAuth } from './composables/auth' + const auth = await useAuth() setupProviders(auth) diff --git a/apps/frontend/src/components/ui/auth/CreateAccount.vue b/apps/frontend/src/components/ui/auth/CreateAccount.vue new file mode 100644 index 0000000000..1ab2c73366 --- /dev/null +++ b/apps/frontend/src/components/ui/auth/CreateAccount.vue @@ -0,0 +1,279 @@ + + + diff --git a/apps/frontend/src/components/ui/HCaptcha.vue b/apps/frontend/src/components/ui/auth/HCaptcha.vue similarity index 76% rename from apps/frontend/src/components/ui/HCaptcha.vue rename to apps/frontend/src/components/ui/auth/HCaptcha.vue index ebbec75f8f..c07a47a449 100644 --- a/apps/frontend/src/components/ui/HCaptcha.vue +++ b/apps/frontend/src/components/ui/auth/HCaptcha.vue @@ -47,17 +47,34 @@ defineExpose({ > - diff --git a/apps/frontend/src/components/ui/auth/SignIn.vue b/apps/frontend/src/components/ui/auth/SignIn.vue new file mode 100644 index 0000000000..38e03b4536 --- /dev/null +++ b/apps/frontend/src/components/ui/auth/SignIn.vue @@ -0,0 +1,377 @@ + + + + + diff --git a/apps/frontend/src/components/ui/auth/SignUp.vue b/apps/frontend/src/components/ui/auth/SignUp.vue new file mode 100644 index 0000000000..6fedd7d7fc --- /dev/null +++ b/apps/frontend/src/components/ui/auth/SignUp.vue @@ -0,0 +1,236 @@ + + + diff --git a/apps/frontend/src/components/ui/create-project-version/components/AddedDependencyRow.vue b/apps/frontend/src/components/ui/create-project-version/components/AddedDependencyRow.vue index 9ce2ffd62b..e6874350f2 100644 --- a/apps/frontend/src/components/ui/create-project-version/components/AddedDependencyRow.vue +++ b/apps/frontend/src/components/ui/create-project-version/components/AddedDependencyRow.vue @@ -2,28 +2,29 @@
-
+
- + {{ name || 'Unknown Project' }} + + {{ versionNumber }} + {{ dependencyType }}
- - {{ versionName }} - - -
+