mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 13:36:48 +00:00
start new website + layouts lib
This commit is contained in:
@@ -184,7 +184,7 @@ img {
|
||||
}
|
||||
}
|
||||
|
||||
@import '@modrinth/assets/omorphia.scss';
|
||||
@import '@modrinth/assets/omorphia.css';
|
||||
|
||||
input {
|
||||
border-radius: var(--size-rounded-sm);
|
||||
|
||||
@@ -25,8 +25,8 @@ export default defineConfig({
|
||||
replacesTitle: true,
|
||||
},
|
||||
customCss: [
|
||||
'@modrinth/assets/styles/variables.scss',
|
||||
'@modrinth/assets/styles/inter.scss',
|
||||
'@modrinth/assets/styles/variables.css',
|
||||
'@modrinth/assets/styles/inter.css',
|
||||
'./src/styles/modrinth.css',
|
||||
],
|
||||
plugins: [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@modrinth/assets/styles/reset.scss';
|
||||
@import '@modrinth/assets/styles/reset.css';
|
||||
|
||||
html {
|
||||
--dark-color-text: #b0bac5;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@@ -0,0 +1,7 @@
|
||||
**/.nuxt
|
||||
**/dist
|
||||
**/.output
|
||||
**/.data
|
||||
**/.wrangler
|
||||
src/locales/**
|
||||
src/assets/**/*.svg
|
||||
@@ -0,0 +1,75 @@
|
||||
# Nuxt Minimal Starter
|
||||
|
||||
Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||
|
||||
## Setup
|
||||
|
||||
Make sure to install dependencies:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm install
|
||||
|
||||
# pnpm
|
||||
pnpm install
|
||||
|
||||
# yarn
|
||||
yarn install
|
||||
|
||||
# bun
|
||||
bun install
|
||||
```
|
||||
|
||||
## Development Server
|
||||
|
||||
Start the development server on `http://localhost:3000`:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run dev
|
||||
|
||||
# pnpm
|
||||
pnpm dev
|
||||
|
||||
# yarn
|
||||
yarn dev
|
||||
|
||||
# bun
|
||||
bun run dev
|
||||
```
|
||||
|
||||
## Production
|
||||
|
||||
Build the application for production:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run build
|
||||
|
||||
# pnpm
|
||||
pnpm build
|
||||
|
||||
# yarn
|
||||
yarn build
|
||||
|
||||
# bun
|
||||
bun run build
|
||||
```
|
||||
|
||||
Locally preview production build:
|
||||
|
||||
```bash
|
||||
# npm
|
||||
npm run preview
|
||||
|
||||
# pnpm
|
||||
pnpm preview
|
||||
|
||||
# yarn
|
||||
yarn preview
|
||||
|
||||
# bun
|
||||
bun run preview
|
||||
```
|
||||
|
||||
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||
@@ -0,0 +1,9 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config.append([
|
||||
{
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -0,0 +1,28 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
srcDir: 'src/',
|
||||
compatibilityDate: '2025-07-15',
|
||||
devtools: { enabled: true },
|
||||
devServer: {
|
||||
port: 42069,
|
||||
},
|
||||
app: {
|
||||
head: {
|
||||
title: 'Modrinth 2',
|
||||
htmlAttrs: {
|
||||
lang: 'en',
|
||||
},
|
||||
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
|
||||
},
|
||||
},
|
||||
future: {
|
||||
compatibilityVersion: 5,
|
||||
},
|
||||
css: ['~/assets/styles/styles.css'],
|
||||
postcss: {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@modrinth/website",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write .",
|
||||
"intl:extract": "formatjs extract \"src/**/*.{vue,ts,tsx,js,jsx,mts,cts,mjs,cjs}\" --ignore \"**/*.d.ts\" --ignore node_modules --out-file src/locales/en-US/index.json --format crowdin --preserve-whitespace",
|
||||
"intl:prune-local": "pnpm -w scripts i18n-icu-contract prune-local --scope apps/website"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modrinth/assets": "workspace:*",
|
||||
"@modrinth/layouts": "workspace:*",
|
||||
"@modrinth/ui": "workspace:*",
|
||||
"intl-messageformat": "^10.7.7",
|
||||
"lru-cache": "^11.2.4",
|
||||
"nuxt": "^4.5.2",
|
||||
"vue": "^3.5.41",
|
||||
"vue-router": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@formatjs/cli": "^6.2.12",
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"autoprefixer": "^10.5.4",
|
||||
"postcss": "^8.5.26",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"vite-tsconfig-paths": "^6.1.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
import { NuxtModrinthClient } from '@modrinth/api-client'
|
||||
import { provideModrinthClient, providePageContext } from '@modrinth/ui'
|
||||
|
||||
provideModrinthClient(
|
||||
new NuxtModrinthClient({
|
||||
labrinthBaseUrl: 'https://api.modrinth.com',
|
||||
}),
|
||||
)
|
||||
|
||||
providePageContext({
|
||||
hierarchicalSidebarAvailable: ref(false),
|
||||
showAds: ref(false),
|
||||
adConsentAvailable: ref(false),
|
||||
floatingActionBarOffsets: {
|
||||
left: ref(0),
|
||||
right: ref(0),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<div class="page-layout">
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.page-layout {
|
||||
@apply max-w-[min(80rem,100%-1.5rem)] mx-auto h-[100dvh] pt-6;
|
||||
|
||||
&:has(#page-sidebar:not(:empty)) {
|
||||
@apply grid grid-cols-[1fr_300px];
|
||||
grid-template-areas: 'anything sidebar';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
#page-sidebar {
|
||||
@apply pl-4 border-l border-surface-5;
|
||||
grid-area: sidebar;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
@import '@modrinth/ui/src/styles/tailwind-utilities.css';
|
||||
@import '@modrinth/assets/omorphia.css';
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: var(--font-standard, sans-serif), sans-serif;
|
||||
color-scheme: dark;
|
||||
--view-width: calc(100% - 5rem);
|
||||
--expanded-view-width: calc(100% - 13rem);
|
||||
--medal-promotion-bg: #000;
|
||||
--medal-promotion-bg-orange: rgba(208, 246, 255, 0.25);
|
||||
--medal-promotion-text-orange: #42abff;
|
||||
--medal-promotion-bg-gradient: linear-gradient(
|
||||
90deg,
|
||||
rgba(66, 170, 255, 0.15),
|
||||
rgba(0, 0, 0, 0) 100%
|
||||
);
|
||||
}
|
||||
|
||||
html.light-mode {
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
background-color: var(--color-button-bg);
|
||||
border: none;
|
||||
color: var(--color-button-bg);
|
||||
height: 1px;
|
||||
margin: var(--gap-sm) 0;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
will-change: filter;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: flex;
|
||||
border-radius: var(--radius-md);
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg);
|
||||
padding-block: var(--gap-sm);
|
||||
padding-inline: var(--gap-lg);
|
||||
width: min-content;
|
||||
|
||||
svg {
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&.featured {
|
||||
background-color: var(--color-brand-highlight);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
scrollbar-width: auto;
|
||||
scrollbar-color: var(--color-scrollbar) var(--color-bg);
|
||||
}
|
||||
|
||||
/* Chrome, Edge, and Safari */
|
||||
*::-webkit-scrollbar {
|
||||
width: 16px;
|
||||
border: 3px solid transparent;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-scrollbar);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 5px solid transparent;
|
||||
background-clip: content-box;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
box-shadow: 0 0 1rem var(--color-brand) !important;
|
||||
}
|
||||
|
||||
.gecko {
|
||||
background-color: var(--color-raised-bg);
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
img {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.card-shadow {
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: var(--size-rounded-sm);
|
||||
box-sizing: border-box;
|
||||
border: 2px solid transparent;
|
||||
/* safari iOS rounds inputs by default. set the appearance to none to prevent this */
|
||||
appearance: none !important;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-text);
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: var(--font-weight-medium);
|
||||
border: none;
|
||||
outline: 2px solid transparent;
|
||||
box-shadow:
|
||||
var(--shadow-inset-sm),
|
||||
0 0 0 0 transparent;
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
min-height: 36px;
|
||||
|
||||
&:focus,
|
||||
&:focus-visible {
|
||||
box-shadow:
|
||||
inset 0 0 0 transparent,
|
||||
0 0 0 0.25rem var(--color-brand-shadow);
|
||||
color: var(--color-button-text-active);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&[disabled='true'] {
|
||||
opacity: 0.6;
|
||||
pointer-events: none;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus::placeholder {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-button-text);
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template>
|
||||
all the fun stuff:
|
||||
<ul>
|
||||
<li>
|
||||
<NuxtLink to="/user/Prospector">User page</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { UserPage } from '@modrinth/layouts'
|
||||
|
||||
const { id } = useRoute().params
|
||||
</script>
|
||||
<template>
|
||||
<UserPage :user-id="id" />
|
||||
</template>
|
||||
@@ -0,0 +1,237 @@
|
||||
import { layoutsLocaleModules } from '@modrinth/layouts'
|
||||
import {
|
||||
type CrowdinMessages,
|
||||
I18N_INJECTION_KEY,
|
||||
type I18nContext,
|
||||
LOCALES,
|
||||
transformCrowdinMessages,
|
||||
uiLocaleModules,
|
||||
useDebugLogger,
|
||||
} from '@modrinth/ui'
|
||||
import IntlMessageFormat from 'intl-messageformat'
|
||||
import { LRUCache } from 'lru-cache'
|
||||
|
||||
const debug = useDebugLogger('i18n')
|
||||
const DEFAULT_LOCALE = 'en-US'
|
||||
|
||||
const websiteLocaleModules = import.meta.glob<{ default: CrowdinMessages }>(
|
||||
'../locales/*/index.json',
|
||||
{ eager: false },
|
||||
)
|
||||
|
||||
const messageCache = new LRUCache<string, Record<string, string>>({ max: 10 })
|
||||
const formatterCache = new LRUCache<string, IntlMessageFormat>({ max: 1000 })
|
||||
const loadingPromises = new Map<string, Promise<void>>()
|
||||
|
||||
type LocaleModules = Record<string, () => Promise<{ default: CrowdinMessages }>>
|
||||
|
||||
function findLocaleLoader(modules: LocaleModules, code: string) {
|
||||
for (const [path, loader] of Object.entries(modules)) {
|
||||
if (path.endsWith(`/${code}/index.json`)) {
|
||||
return loader
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function formatIcuMessage(msg: string, locale: string, values: Record<string, unknown>) {
|
||||
const cacheKey = `${locale}:${msg}`
|
||||
let formatter = formatterCache.get(cacheKey)
|
||||
|
||||
try {
|
||||
if (!formatter) {
|
||||
formatter = new IntlMessageFormat(msg, locale)
|
||||
formatterCache.set(cacheKey, formatter)
|
||||
}
|
||||
const result = formatter.format(values)
|
||||
if (import.meta.dev && typeof result !== 'string') {
|
||||
debug('formatIcuMessage: format returned non-string', typeof result)
|
||||
}
|
||||
return result as string
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLocale(code: string): Promise<void> {
|
||||
if (messageCache.has(code)) {
|
||||
debug('loadLocale: already cached', code)
|
||||
return
|
||||
}
|
||||
|
||||
const existing = loadingPromises.get(code)
|
||||
if (existing) {
|
||||
debug('loadLocale: already loading', code)
|
||||
return existing
|
||||
}
|
||||
|
||||
debug('loadLocale: starting', code)
|
||||
|
||||
const promise = (async () => {
|
||||
const websiteLoader = findLocaleLoader(websiteLocaleModules, code)
|
||||
const uiLoader = findLocaleLoader(uiLocaleModules, code)
|
||||
const layoutsLoader = findLocaleLoader(layoutsLocaleModules, code)
|
||||
|
||||
debug('loadLocale: loaders found', {
|
||||
code,
|
||||
website: !!websiteLoader,
|
||||
ui: !!uiLoader,
|
||||
layouts: !!layoutsLoader,
|
||||
})
|
||||
|
||||
const [uiData, layoutsData, websiteData] = await Promise.all([
|
||||
uiLoader?.().catch((e) => {
|
||||
debug('loadLocale: ui loader failed', code, e)
|
||||
return null
|
||||
}),
|
||||
layoutsLoader?.().catch((e) => {
|
||||
debug('loadLocale: layouts loader failed', code, e)
|
||||
return null
|
||||
}),
|
||||
websiteLoader?.().catch((e) => {
|
||||
debug('loadLocale: website loader failed', code, e)
|
||||
return null
|
||||
}),
|
||||
])
|
||||
|
||||
debug('loadLocale: data loaded', {
|
||||
code,
|
||||
uiKeys: uiData ? Object.keys(uiData.default).length : 0,
|
||||
layoutsKeys: layoutsData ? Object.keys(layoutsData.default).length : 0,
|
||||
websiteKeys: websiteData ? Object.keys(websiteData.default).length : 0,
|
||||
})
|
||||
|
||||
const mergedMessages: Record<string, string> = {}
|
||||
if (uiData) Object.assign(mergedMessages, transformCrowdinMessages(uiData.default))
|
||||
if (layoutsData) Object.assign(mergedMessages, transformCrowdinMessages(layoutsData.default))
|
||||
if (websiteData) Object.assign(mergedMessages, transformCrowdinMessages(websiteData.default))
|
||||
|
||||
debug('loadLocale: merged', code, 'total keys:', Object.keys(mergedMessages).length)
|
||||
|
||||
if (Object.keys(mergedMessages).length > 0) {
|
||||
messageCache.set(code, mergedMessages)
|
||||
}
|
||||
})()
|
||||
|
||||
loadingPromises.set(code, promise)
|
||||
try {
|
||||
await promise
|
||||
} finally {
|
||||
loadingPromises.delete(code)
|
||||
}
|
||||
}
|
||||
|
||||
function parseAcceptLanguage(header: string): string | null {
|
||||
try {
|
||||
for (const lang of header
|
||||
.split(',')
|
||||
.map((l) => l.split(';')[0]?.trim())
|
||||
.filter(Boolean)) {
|
||||
const exact = LOCALES.find((loc) => loc.code === lang)
|
||||
if (exact) return exact.code
|
||||
const prefix = LOCALES.find((loc) => loc.code.startsWith(lang.split('-')[0] + '-'))
|
||||
if (prefix) return prefix.code
|
||||
}
|
||||
} catch {
|
||||
// Malformed header, ignore
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'i18n',
|
||||
enforce: 'pre',
|
||||
async setup(nuxtApp) {
|
||||
const locale = useState<string>('i18n-locale', () => DEFAULT_LOCALE)
|
||||
const localeDir = computed(
|
||||
() => LOCALES.find((entry) => entry.code === locale.value)?.dir ?? 'ltr',
|
||||
)
|
||||
|
||||
function t(key: string, values?: Record<string, unknown>): string {
|
||||
const currentLocale = locale.value
|
||||
const localeMessages = messageCache.get(currentLocale)
|
||||
const fallbackMessages = messageCache.get(DEFAULT_LOCALE)
|
||||
const msg = localeMessages?.[key] ?? fallbackMessages?.[key]
|
||||
|
||||
if (!msg) {
|
||||
debug('t: key not found', {
|
||||
key,
|
||||
locale: currentLocale,
|
||||
hasLocaleMessages: !!localeMessages,
|
||||
hasFallbackMessages: !!fallbackMessages,
|
||||
})
|
||||
return key
|
||||
}
|
||||
|
||||
if (!values || Object.keys(values).length === 0) return msg
|
||||
|
||||
const formatted = formatIcuMessage(msg, currentLocale, values)
|
||||
if (formatted !== null) return formatted
|
||||
|
||||
const fallbackMsg = fallbackMessages?.[key]
|
||||
if (fallbackMsg && fallbackMsg !== msg) {
|
||||
return formatIcuMessage(fallbackMsg, DEFAULT_LOCALE, values) ?? fallbackMsg
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
||||
async function setLocale(
|
||||
newLocale: string,
|
||||
{ persist = true }: { persist?: boolean } = {},
|
||||
): Promise<void> {
|
||||
debug('setLocale: called', { newLocale, currentLocale: locale.value })
|
||||
|
||||
if (!LOCALES.some((l) => l.code === newLocale)) {
|
||||
debug('setLocale: invalid locale', newLocale)
|
||||
return
|
||||
}
|
||||
|
||||
await loadLocale(newLocale)
|
||||
|
||||
debug('setLocale: loaded', {
|
||||
newLocale,
|
||||
cacheHas: messageCache.has(newLocale),
|
||||
cacheKeys: messageCache.get(newLocale)
|
||||
? Object.keys(messageCache.get(newLocale)!).length
|
||||
: 0,
|
||||
})
|
||||
|
||||
locale.value = newLocale
|
||||
if (persist) {
|
||||
useCookie('locale', { maxAge: 31536000, path: '/' }).value = newLocale
|
||||
}
|
||||
}
|
||||
|
||||
const cookieLocale = useCookie('locale').value
|
||||
let detectedLocale = DEFAULT_LOCALE
|
||||
if (cookieLocale && LOCALES.some((l) => l.code === cookieLocale)) {
|
||||
detectedLocale = cookieLocale
|
||||
debug('init: locale from cookie', detectedLocale)
|
||||
} else if (import.meta.server) {
|
||||
const acceptLang = useRequestHeaders(['accept-language'])['accept-language']
|
||||
if (acceptLang) {
|
||||
detectedLocale = parseAcceptLanguage(acceptLang) ?? DEFAULT_LOCALE
|
||||
debug('init: locale from Accept-Language', detectedLocale)
|
||||
}
|
||||
}
|
||||
|
||||
debug('init: detected locale', { detectedLocale, cookieLocale, isServer: import.meta.server })
|
||||
|
||||
await loadLocale(DEFAULT_LOCALE)
|
||||
if (detectedLocale !== DEFAULT_LOCALE) await loadLocale(detectedLocale)
|
||||
locale.value = detectedLocale
|
||||
|
||||
debug('init: complete', { locale: locale.value })
|
||||
|
||||
useHead({
|
||||
htmlAttrs: {
|
||||
lang: locale,
|
||||
dir: localeDir,
|
||||
},
|
||||
})
|
||||
|
||||
const context: I18nContext = { locale, t, setLocale }
|
||||
nuxtApp.vueApp.provide(I18N_INJECTION_KEY, context)
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
// https://tanstack.com/query/v5/docs/framework/vue/examples/nuxt3
|
||||
import type { DehydratedState, VueQueryPluginOptions } from '@tanstack/vue-query'
|
||||
import { dehydrate, hydrate, QueryClient, VueQueryPlugin } from '@tanstack/vue-query'
|
||||
|
||||
import { defineNuxtPlugin, useState } from '#imports'
|
||||
|
||||
export default defineNuxtPlugin((nuxt) => {
|
||||
const vueQueryState = useState<DehydratedState | null>('vue-query')
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 10000,
|
||||
retry: (failureCount, error) => {
|
||||
const status = (error as any)?.statusCode ?? (error as any)?.status
|
||||
if (status !== undefined && status >= 400 && status < 500 && status !== 429) return false
|
||||
return failureCount < 3
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const options: VueQueryPluginOptions = { queryClient }
|
||||
|
||||
nuxt.vueApp.use(VueQueryPlugin, options)
|
||||
|
||||
// Expose queryClient for middleware and composables
|
||||
nuxt.provide('queryClient', queryClient)
|
||||
|
||||
if (import.meta.server) {
|
||||
nuxt.hooks.hook('app:rendered', () => {
|
||||
vueQueryState.value = dehydrate(queryClient)
|
||||
})
|
||||
}
|
||||
|
||||
if (import.meta.client) {
|
||||
hydrate(queryClient, vueQueryState.value)
|
||||
}
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow:
|
||||
@@ -0,0 +1,14 @@
|
||||
import preset from '@modrinth/tooling-config/tailwind/tailwind-preset.ts'
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./src/**/*.{js,vue,ts,mdx}',
|
||||
'./src/app.vue',
|
||||
'../../packages/**/*.{js,vue,ts}',
|
||||
'!../../packages/**/node_modules/**',
|
||||
],
|
||||
presets: [preset],
|
||||
}
|
||||
|
||||
export default config
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.server.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.shared.json"
|
||||
},
|
||||
{
|
||||
"path": "./.nuxt/tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user