Migrate to Turborepo (#1251)

This commit is contained in:
Evan Song
2024-07-04 21:46:29 -07:00
committed by GitHub
parent 6fa1acc461
commit 0f2ddb452c
811 changed files with 5623 additions and 7832 deletions

View File

@@ -0,0 +1,129 @@
export const useAuth = async (oldToken = null) => {
const auth = useState('auth', () => ({
user: null,
token: '',
headers: {},
}))
if (!auth.value.user || oldToken) {
auth.value = await initAuth(oldToken)
}
return auth
}
export const initAuth = async (oldToken = null) => {
const auth = {
user: null,
token: '',
}
if (oldToken === 'none') {
return auth
}
const route = useRoute()
const authCookie = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
})
if (oldToken) {
authCookie.value = oldToken
}
if (route.query.code && !route.fullPath.includes('new_account=true')) {
authCookie.value = route.query.code
}
if (authCookie.value) {
auth.token = authCookie.value
if (!auth.token || !auth.token.startsWith('mra_')) {
return auth
}
try {
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
},
},
true
)
} catch {}
}
if (!auth.user && auth.token) {
try {
const session = await useBaseFetch(
'session/refresh',
{
method: 'POST',
headers: {
Authorization: auth.token,
},
},
true
)
auth.token = session.session
authCookie.value = auth.token
auth.user = await useBaseFetch(
'user',
{
headers: {
Authorization: auth.token,
},
},
true
)
} catch {
authCookie.value = null
}
}
return auth
}
export const getAuthUrl = (provider, redirect = '') => {
const config = useRuntimeConfig()
const route = useNativeRoute()
if (redirect === '') {
redirect = route.path
}
const fullURL = `${config.public.siteUrl}${redirect}`
return `${config.public.apiBaseUrl}auth/init?url=${fullURL}&provider=${provider}`
}
export const removeAuthProvider = async (provider) => {
startLoading()
try {
const auth = await useAuth()
await useBaseFetch('auth/provider', {
method: 'DELETE',
body: {
provider,
},
})
await useAuth(auth.value.token)
} catch (err) {
const data = useNuxtApp()
data.$notify({
group: 'main',
title: 'An error occurred',
text: err.data.description,
type: 'error',
})
}
stopLoading()
}

View File

@@ -0,0 +1,675 @@
export const scopeMessages = defineMessages({
userReadEmailLabel: {
id: 'scopes.userReadEmail.label',
defaultMessage: 'Read user email',
},
userReadEmailDescription: {
id: 'scopes.userReadEmail.description',
defaultMessage: 'Read your email',
},
userReadLabel: {
id: 'scopes.userRead.label',
defaultMessage: 'Read user data',
},
userReadDescription: {
id: 'scopes.userRead.description',
defaultMessage: 'Access your public profile information',
},
userWriteLabel: {
id: 'scopes.userWrite.label',
defaultMessage: 'Write user data',
},
userWriteDescription: {
id: 'scopes.userWrite.description',
defaultMessage: 'Write to your profile',
},
userDeleteLabel: {
id: 'scopes.userDelete.label',
defaultMessage: 'Delete your account',
},
userDeleteDescription: {
id: 'scopes.userDelete.description',
defaultMessage: 'Delete your account',
},
userAuthWriteLabel: {
id: 'scopes.userAuthWrite.label',
defaultMessage: 'Write auth data',
},
userAuthWriteDescription: {
id: 'scopes.userAuthWrite.description',
defaultMessage: 'Modify your authentication data',
},
notificationReadLabel: {
id: 'scopes.notificationRead.label',
defaultMessage: 'Read notifications',
},
notificationReadDescription: {
id: 'scopes.notificationRead.description',
defaultMessage: 'Read your notifications',
},
notificationWriteLabel: {
id: 'scopes.notificationWrite.label',
defaultMessage: 'Write notifications',
},
notificationWriteDescription: {
id: 'scopes.notificationWrite.description',
defaultMessage: 'Delete/View your notifications',
},
payoutsReadLabel: {
id: 'scopes.payoutsRead.label',
defaultMessage: 'Read payouts',
},
payoutsReadDescription: {
id: 'scopes.payoutsRead.description',
defaultMessage: 'Read your payouts data',
},
payoutsWriteLabel: {
id: 'scopes.payoutsWrite.label',
defaultMessage: 'Write payouts',
},
payoutsWriteDescription: {
id: 'scopes.payoutsWrite.description',
defaultMessage: 'Withdraw money',
},
analyticsLabel: {
id: 'scopes.analytics.label',
defaultMessage: 'Read analytics',
},
analyticsDescription: {
id: 'scopes.analytics.description',
defaultMessage: 'Access your analytics data',
},
projectCreateLabel: {
id: 'scopes.projectCreate.label',
defaultMessage: 'Create projects',
},
projectCreateDescription: {
id: 'scopes.projectCreate.description',
defaultMessage: 'Create new projects',
},
projectReadLabel: {
id: 'scopes.projectRead.label',
defaultMessage: 'Read projects',
},
projectReadDescription: {
id: 'scopes.projectRead.description',
defaultMessage: 'Read all your projects',
},
projectWriteLabel: {
id: 'scopes.projectWrite.label',
defaultMessage: 'Write projects',
},
projectWriteDescription: {
id: 'scopes.projectWrite.description',
defaultMessage: 'Write to project data',
},
projectDeleteLabel: {
id: 'scopes.projectDelete.label',
defaultMessage: 'Delete projects',
},
projectDeleteDescription: {
id: 'scopes.projectDelete.description',
defaultMessage: 'Delete your projects',
},
versionCreateLabel: {
id: 'scopes.versionCreate.label',
defaultMessage: 'Create versions',
},
versionCreateDescription: {
id: 'scopes.versionCreate.description',
defaultMessage: 'Create new versions',
},
versionReadLabel: {
id: 'scopes.versionRead.label',
defaultMessage: 'Read versions',
},
versionReadDescription: {
id: 'scopes.versionRead.description',
defaultMessage: 'Read all versions',
},
versionWriteLabel: {
id: 'scopes.versionWrite.label',
defaultMessage: 'Write versions',
},
versionWriteDescription: {
id: 'scopes.versionWrite.description',
defaultMessage: 'Write to version data',
},
versionDeleteLabel: {
id: 'scopes.versionDelete.label',
defaultMessage: 'Delete versions',
},
versionDeleteDescription: {
id: 'scopes.versionDelete.description',
defaultMessage: 'Delete a version',
},
reportCreateLabel: {
id: 'scopes.reportCreate.label',
defaultMessage: 'Create reports',
},
reportCreateDescription: {
id: 'scopes.reportCreate.description',
defaultMessage: 'Create reports',
},
reportReadLabel: {
id: 'scopes.reportRead.label',
defaultMessage: 'Read reports',
},
reportReadDescription: {
id: 'scopes.reportRead.description',
defaultMessage: 'Read reports',
},
reportWriteLabel: {
id: 'scopes.reportWrite.label',
defaultMessage: 'Write reports',
},
reportWriteDescription: {
id: 'scopes.reportWrite.description',
defaultMessage: 'Edit reports',
},
reportDeleteLabel: {
id: 'scopes.reportDelete.label',
defaultMessage: 'Delete reports',
},
reportDeleteDescription: {
id: 'scopes.reportDelete.description',
defaultMessage: 'Delete reports',
},
threadReadLabel: {
id: 'scopes.threadRead.label',
defaultMessage: 'Read threads',
},
threadReadDescription: {
id: 'scopes.threadRead.description',
defaultMessage: 'Read threads',
},
threadWriteLabel: {
id: 'scopes.threadWrite.label',
defaultMessage: 'Write threads',
},
threadWriteDescription: {
id: 'scopes.threadWrite.description',
defaultMessage: 'Write to threads',
},
patCreateLabel: {
id: 'scopes.patCreate.label',
defaultMessage: 'Create PATs',
},
patCreateDescription: {
id: 'scopes.patCreate.description',
defaultMessage: 'Create personal API tokens',
},
patReadLabel: {
id: 'scopes.patRead.label',
defaultMessage: 'Read PATs',
},
patReadDescription: {
id: 'scopes.patRead.description',
defaultMessage: 'View created API tokens',
},
patWriteLabel: {
id: 'scopes.patWrite.label',
defaultMessage: 'Write PATs',
},
patWriteDescription: {
id: 'scopes.patWrite.description',
defaultMessage: 'Edit personal API tokens',
},
patDeleteLabel: {
id: 'scopes.patDelete.label',
defaultMessage: 'Delete PATs',
},
patDeleteDescription: {
id: 'scopes.patDelete.description',
defaultMessage: 'Delete your personal API tokens',
},
sessionReadLabel: {
id: 'scopes.sessionRead.label',
defaultMessage: 'Read sessions',
},
sessionReadDescription: {
id: 'scopes.sessionRead.description',
defaultMessage: 'Read active sessions',
},
sessionDeleteLabel: {
id: 'scopes.sessionDelete.label',
defaultMessage: 'Delete sessions',
},
sessionDeleteDescription: {
id: 'scopes.sessionDelete.description',
defaultMessage: 'Delete sessions',
},
performAnalyticsLabel: {
id: 'scopes.performAnalytics.label',
defaultMessage: 'Perform analytics',
},
performAnalyticsDescription: {
id: 'scopes.performAnalytics.description',
defaultMessage: 'Perform analytics actions',
},
collectionCreateLabel: {
id: 'scopes.collectionCreate.label',
defaultMessage: 'Create collections',
},
collectionCreateDescription: {
id: 'scopes.collectionCreate.description',
defaultMessage: 'Create collections',
},
collectionReadLabel: {
id: 'scopes.collectionRead.label',
defaultMessage: 'Read collections',
},
collectionReadDescription: {
id: 'scopes.collectionRead.description',
defaultMessage: 'Read collections',
},
collectionWriteLabel: {
id: 'scopes.collectionWrite.label',
defaultMessage: 'Write collections',
},
collectionWriteDescription: {
id: 'scopes.collectionWrite.description',
defaultMessage: 'Write to collections',
},
collectionDeleteLabel: {
id: 'scopes.collectionDelete.label',
defaultMessage: 'Delete collections',
},
collectionDeleteDescription: {
id: 'scopes.collectionDelete.description',
defaultMessage: 'Delete collections',
},
organizationCreateLabel: {
id: 'scopes.organizationCreate.label',
defaultMessage: 'Create organizations',
},
organizationCreateDescription: {
id: 'scopes.organizationCreate.description',
defaultMessage: 'Create organizations',
},
organizationReadLabel: {
id: 'scopes.organizationRead.label',
defaultMessage: 'Read organizations',
},
organizationReadDescription: {
id: 'scopes.organizationRead.description',
defaultMessage: 'Read organizations',
},
organizationWriteLabel: {
id: 'scopes.organizationWrite.label',
defaultMessage: 'Write organizations',
},
organizationWriteDescription: {
id: 'scopes.organizationWrite.description',
defaultMessage: 'Write to organizations',
},
organizationDeleteLabel: {
id: 'scopes.organizationDelete.label',
defaultMessage: 'Delete organizations',
},
organizationDeleteDescription: {
id: 'scopes.organizationDelete.description',
defaultMessage: 'Delete organizations',
},
sessionAccessLabel: {
id: 'scopes.sessionAccess.label',
defaultMessage: 'Access sessions',
},
sessionAccessDescription: {
id: 'scopes.sessionAccess.description',
defaultMessage: 'Access modrinth-issued sessions',
},
})
const scopeDefinitions = [
{
id: 'USER_READ_EMAIL',
value: BigInt(1) << BigInt(0),
label: scopeMessages.userReadEmailLabel,
desc: scopeMessages.userReadEmailDescription,
},
{
id: 'USER_READ',
value: BigInt(1) << BigInt(1),
label: scopeMessages.userReadLabel,
desc: scopeMessages.userReadDescription,
},
{
id: 'USER_WRITE',
value: BigInt(1) << BigInt(2),
label: scopeMessages.userWriteLabel,
desc: scopeMessages.userWriteDescription,
},
{
id: 'USER_DELETE',
value: BigInt(1) << BigInt(3),
label: scopeMessages.userDeleteLabel,
desc: scopeMessages.userDeleteDescription,
},
{
id: 'USER_AUTH_WRITE',
value: BigInt(1) << BigInt(4),
label: scopeMessages.userAuthWriteLabel,
desc: scopeMessages.userAuthWriteDescription,
},
{
id: 'NOTIFICATION_READ',
value: BigInt(1) << BigInt(5),
label: scopeMessages.notificationReadLabel,
desc: scopeMessages.notificationReadDescription,
},
{
id: 'NOTIFICATION_WRITE',
value: BigInt(1) << BigInt(6),
label: scopeMessages.notificationWriteLabel,
desc: scopeMessages.notificationWriteDescription,
},
{
id: 'PAYOUTS_READ',
value: BigInt(1) << BigInt(7),
label: scopeMessages.payoutsReadLabel,
desc: scopeMessages.payoutsReadDescription,
},
{
id: 'PAYOUTS_WRITE',
value: BigInt(1) << BigInt(8),
label: scopeMessages.payoutsWriteLabel,
desc: scopeMessages.payoutsWriteDescription,
},
{
id: 'ANALYTICS',
value: BigInt(1) << BigInt(9),
label: scopeMessages.analyticsLabel,
desc: scopeMessages.analyticsDescription,
},
{
id: 'PROJECT_CREATE',
value: BigInt(1) << BigInt(10),
label: scopeMessages.projectCreateLabel,
desc: scopeMessages.projectCreateDescription,
},
{
id: 'PROJECT_READ',
value: BigInt(1) << BigInt(11),
label: scopeMessages.projectReadLabel,
desc: scopeMessages.projectReadDescription,
},
{
id: 'PROJECT_WRITE',
value: BigInt(1) << BigInt(12),
label: scopeMessages.projectWriteLabel,
desc: scopeMessages.projectWriteDescription,
},
{
id: 'PROJECT_DELETE',
value: BigInt(1) << BigInt(13),
label: scopeMessages.projectDeleteLabel,
desc: scopeMessages.projectDeleteDescription,
},
{
id: 'VERSION_CREATE',
value: BigInt(1) << BigInt(14),
label: scopeMessages.versionCreateLabel,
desc: scopeMessages.versionCreateDescription,
},
{
id: 'VERSION_READ',
value: BigInt(1) << BigInt(15),
label: scopeMessages.versionReadLabel,
desc: scopeMessages.versionReadDescription,
},
{
id: 'VERSION_WRITE',
value: BigInt(1) << BigInt(16),
label: scopeMessages.versionWriteLabel,
desc: scopeMessages.versionWriteDescription,
},
{
id: 'VERSION_DELETE',
value: BigInt(1) << BigInt(17),
label: scopeMessages.versionDeleteLabel,
desc: scopeMessages.versionDeleteDescription,
},
{
id: 'REPORT_CREATE',
value: BigInt(1) << BigInt(18),
label: scopeMessages.reportCreateLabel,
desc: scopeMessages.reportCreateDescription,
},
{
id: 'REPORT_READ',
value: BigInt(1) << BigInt(19),
label: scopeMessages.reportReadLabel,
desc: scopeMessages.reportReadDescription,
},
{
id: 'REPORT_WRITE',
value: BigInt(1) << BigInt(20),
label: scopeMessages.reportWriteLabel,
desc: scopeMessages.reportWriteDescription,
},
{
id: 'REPORT_DELETE',
value: BigInt(1) << BigInt(21),
label: scopeMessages.reportDeleteLabel,
desc: scopeMessages.reportDeleteDescription,
},
{
id: 'THREAD_READ',
value: BigInt(1) << BigInt(22),
label: scopeMessages.threadReadLabel,
desc: scopeMessages.threadReadDescription,
},
{
id: 'THREAD_WRITE',
value: BigInt(1) << BigInt(23),
label: scopeMessages.threadWriteLabel,
desc: scopeMessages.threadWriteDescription,
},
{
id: 'PAT_CREATE',
value: BigInt(1) << BigInt(24),
label: scopeMessages.patCreateLabel,
desc: scopeMessages.patCreateDescription,
},
{
id: 'PAT_READ',
value: BigInt(1) << BigInt(25),
label: scopeMessages.patReadLabel,
desc: scopeMessages.patReadDescription,
},
{
id: 'PAT_WRITE',
value: BigInt(1) << BigInt(26),
label: scopeMessages.patWriteLabel,
desc: scopeMessages.patWriteDescription,
},
{
id: 'PAT_DELETE',
value: BigInt(1) << BigInt(27),
label: scopeMessages.patDeleteLabel,
desc: scopeMessages.patDeleteDescription,
},
{
id: 'SESSION_READ',
value: BigInt(1) << BigInt(28),
label: scopeMessages.sessionReadLabel,
desc: scopeMessages.sessionReadDescription,
},
{
id: 'SESSION_DELETE',
value: BigInt(1) << BigInt(29),
label: scopeMessages.sessionDeleteLabel,
desc: scopeMessages.sessionDeleteDescription,
},
{
id: 'PERFORM_ANALYTICS',
value: BigInt(1) << BigInt(30),
label: scopeMessages.performAnalyticsLabel,
desc: scopeMessages.performAnalyticsDescription,
},
{
id: 'COLLECTION_CREATE',
value: BigInt(1) << BigInt(31),
label: scopeMessages.collectionCreateLabel,
desc: scopeMessages.collectionCreateDescription,
},
{
id: 'COLLECTION_READ',
value: BigInt(1) << BigInt(32),
label: scopeMessages.collectionReadLabel,
desc: scopeMessages.collectionReadDescription,
},
{
id: 'COLLECTION_WRITE',
value: BigInt(1) << BigInt(33),
label: scopeMessages.collectionWriteLabel,
desc: scopeMessages.collectionWriteDescription,
},
{
id: 'COLLECTION_DELETE',
value: BigInt(1) << BigInt(34),
label: scopeMessages.collectionDeleteLabel,
desc: scopeMessages.collectionDeleteDescription,
},
{
id: 'ORGANIZATION_CREATE',
value: BigInt(1) << BigInt(35),
label: scopeMessages.organizationCreateLabel,
desc: scopeMessages.organizationCreateDescription,
},
{
id: 'ORGANIZATION_READ',
value: BigInt(1) << BigInt(36),
label: scopeMessages.organizationReadLabel,
desc: scopeMessages.organizationReadDescription,
},
{
id: 'ORGANIZATION_WRITE',
value: BigInt(1) << BigInt(37),
label: scopeMessages.organizationWriteLabel,
desc: scopeMessages.organizationWriteDescription,
},
{
id: 'ORGANIZATION_DELETE',
value: BigInt(1) << BigInt(38),
label: scopeMessages.organizationDeleteLabel,
desc: scopeMessages.organizationDeleteDescription,
},
{
id: 'SESSION_ACCESS',
value: BigInt(1) << BigInt(39),
label: scopeMessages.sessionAccessLabel,
desc: scopeMessages.sessionAccessDescription,
},
]
const Scopes = scopeDefinitions.reduce((acc, scope) => {
acc[scope.id] = scope.value
return acc
}, {} as Record<string, bigint>)
export const restrictedScopes = [
Scopes.PAT_READ,
Scopes.PAT_CREATE,
Scopes.PAT_WRITE,
Scopes.PAT_DELETE,
Scopes.SESSION_READ,
Scopes.SESSION_DELETE,
Scopes.SESSION_ACCESS,
Scopes.USER_AUTH_WRITE,
Scopes.USER_DELETE,
Scopes.PERFORM_ANALYTICS,
]
export const scopeList = Object.entries(Scopes)
.filter(([_, value]) => !restrictedScopes.includes(value))
.map(([key, _]) => key)
export const getScopeValue = (scope: string) => {
return Scopes[scope]
}
export const encodeScopes = (scopes: string[]) => {
let scopeFlag = BigInt(0)
// We iterate over the provided scopes
for (const scope of scopes) {
// We iterate over the entries of the Scopes object
for (const [scopeName, scopeFlagValue] of Object.entries(Scopes)) {
// If the scope name is the same as the provided scope, add the scope flag to the scopeFlag variable
if (scopeName === scope) {
scopeFlag = scopeFlag | scopeFlagValue
}
}
}
return scopeFlag
}
export const decodeScopes = (scopes: bigint | number) => {
if (typeof scopes === 'number') {
scopes = BigInt(scopes)
}
const authorizedScopes = []
// We iterate over the entries of the Scopes object
for (const [scopeName, scopeFlag] of Object.entries(Scopes)) {
// If the scope flag is present in the provided number, add the scope name to the list
if ((scopes & scopeFlag) === scopeFlag) {
authorizedScopes.push(scopeName)
}
}
return authorizedScopes
}
export const hasScope = (scopes: bigint, scope: string) => {
const authorizedScopes = decodeScopes(scopes)
return authorizedScopes.includes(scope)
}
export const toggleScope = (scopes: bigint, scope: string) => {
const authorizedScopes = decodeScopes(scopes)
if (authorizedScopes.includes(scope)) {
return encodeScopes(authorizedScopes.filter((authorizedScope) => authorizedScope !== scope))
} else {
return encodeScopes([...authorizedScopes, scope])
}
}
export const useScopes = () => {
const { formatMessage } = useVIntl()
const scopesToDefinitions = (scopes: bigint) => {
const authorizedScopes = decodeScopes(scopes)
return authorizedScopes.map((scope) => {
const scopeDefinition = scopeDefinitions.find(
(scopeDefinition) => scopeDefinition.id === scope
)
if (!scopeDefinition) {
throw new Error(`Scope ${scope} not found`)
}
return formatMessage(scopeDefinition.desc)
})
}
const scopesToLabels = (scopes: bigint) => {
const authorizedScopes = decodeScopes(scopes)
return authorizedScopes.map((scope) => {
const scopeDefinition = scopeDefinitions.find(
(scopeDefinition) => scopeDefinition.id === scope
)
if (!scopeDefinition) {
throw new Error(`Scope ${scope} not found`)
}
return formatMessage(scopeDefinition.label)
})
}
return {
scopesToDefinitions,
scopesToLabels,
}
}

View File

@@ -0,0 +1,13 @@
export type AutoRef<T> = [T] extends [(...args: any[]) => any]
? Ref<T> | (() => T)
: T | Ref<T> | (() => T)
/**
* Accepts a value directly, a ref with the value or a getter function and returns a Vue ref.
* @param value The value to use.
* @returns Either the original or newly created ref.
*/
export function useAutoRef<T>(value: AutoRef<T>): Ref<T> {
if (typeof value === 'function') return computed(() => value())
return isRef(value) ? value : ref(value as any)
}

View File

@@ -0,0 +1,18 @@
import { createFormatter, type Formatter } from '@vintl/compact-number'
import type { IntlController } from '@vintl/vintl/controller'
const formatters = new WeakMap<IntlController<any>, Formatter>()
export function useCompactNumber(): Formatter {
const vintl = useVIntl()
let formatter = formatters.get(vintl)
if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl))
formatter = (value, options) => formatterRef.value(value, options)
formatters.set(vintl, formatter)
}
return formatter
}

View File

@@ -0,0 +1,52 @@
export const useCosmetics = () =>
useState('cosmetics', () => {
const cosmetics = useCookie('cosmetics', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
})
if (!cosmetics.value) {
cosmetics.value = {
searchLayout: false,
projectLayout: false,
advancedRendering: true,
externalLinksNewTab: true,
notUsingBlockers: false,
hideModrinthAppPromos: false,
preferredDarkTheme: 'dark',
searchDisplayMode: {
mod: 'list',
plugin: 'list',
resourcepack: 'gallery',
modpack: 'list',
shader: 'gallery',
datapack: 'list',
user: 'list',
collection: 'list',
},
hideStagingBanner: false,
}
}
return cosmetics.value
})
export const saveCosmetics = () => {
const cosmetics = useCosmetics()
console.log('SAVING COSMETICS:')
console.log(cosmetics)
const cosmeticsCookie = useCookie('cosmetics', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
})
cosmeticsCookie.value = cosmetics.value
}

View File

@@ -0,0 +1,17 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
dayjs.extend(relativeTime)
export const useCurrentDate = () => useState('currentDate', () => Date.now())
export const updateCurrentDate = () => {
const currentDate = useCurrentDate()
currentDate.value = Date.now()
}
export const fromNow = (date) => {
const currentDate = useCurrentDate()
return dayjs(date).from(currentDate.value)
}

View File

@@ -0,0 +1,91 @@
import { useAutoRef, type AutoRef } from './auto-ref.ts'
const safeTags = new Map<string, string>()
function safeTagFor(locale: string) {
let safeTag = safeTags.get(locale)
if (safeTag == null) {
safeTag = new Intl.Locale(locale).baseName
safeTags.set(locale, safeTag)
}
return safeTag
}
type DisplayNamesWrapper = Intl.DisplayNames & {
of(tag: string): string | undefined
}
const displayNamesDicts = new Map<string, DisplayNamesWrapper>()
function getWrapperKey(locale: string, options: Intl.DisplayNamesOptions) {
return JSON.stringify({ ...options, locale })
}
export function createDisplayNames(
locale: string,
options: Intl.DisplayNamesOptions = { type: 'language' }
) {
const wrapperKey = getWrapperKey(locale, options)
let wrapper = displayNamesDicts.get(wrapperKey)
if (wrapper == null) {
const dict = new Intl.DisplayNames(locale, options)
const badTags: string[] = []
wrapper = {
resolvedOptions() {
return dict.resolvedOptions()
},
of(tag: string) {
let attempt = 0
// eslint-disable-next-line no-labels
lookupLoop: do {
let lookup: string
switch (attempt) {
case 0:
lookup = tag
break
case 1:
lookup = safeTagFor(tag)
break
default:
// eslint-disable-next-line no-labels
break lookupLoop
}
if (badTags.includes(lookup)) continue
try {
return dict.of(lookup)
} catch (err) {
console.warn(
`Failed to get display name for ${lookup} using dictionary for ${
this.resolvedOptions().locale
}`
)
badTags.push(lookup)
continue
}
} while (++attempt < 5)
return undefined
},
}
displayNamesDicts.set(wrapperKey, wrapper)
}
return wrapper
}
export function useDisplayNames(
locale: AutoRef<string>,
options?: AutoRef<Intl.DisplayNamesOptions | undefined>
) {
const $locale = useAutoRef(locale)
const $options = useAutoRef(options)
return computed(() => createDisplayNames($locale.value, $options.value))
}

View File

@@ -0,0 +1,105 @@
import type { CookieOptions } from '#app'
export type ProjectDisplayMode = 'list' | 'grid' | 'gallery'
export type DarkColorTheme = 'dark' | 'oled' | 'retro'
export interface NumberFlag {
min: number
max: number
}
export type BooleanFlag = boolean
export type RadioFlag = ProjectDisplayMode | DarkColorTheme
export type FlagValue = BooleanFlag /* | NumberFlag | RadioFlag */
const validateValues = <K extends PropertyKey>(flags: Record<K, FlagValue>) => flags
export const DEFAULT_FEATURE_FLAGS = validateValues({
// Developer flags
developerMode: false,
// In-development features, flags will be removed over time
newProjectLinks: true,
newProjectMembers: false,
newProjectDetails: true,
projectCompatibility: false,
removeFeaturedVersions: false,
// Alt layouts
// searchSidebarRight: false,
// projectSidebarRight: false,
// Feature toggles
// advancedRendering: true,
// externalLinksNewTab: true,
// notUsingBlockers: false,
// hideModrinthAppPromos: false,
// preferredDarkTheme: 'dark',
// hideStagingBanner: false,
// Project display modes
// modSearchDisplayMode: 'list',
// pluginSearchDisplayMode: 'list',
// resourcePackSearchDisplayMode: 'gallery',
// modpackSearchDisplayMode: 'list',
// shaderSearchDisplayMode: 'gallery',
// dataPackSearchDisplayMode: 'list',
// userProjectDisplayMode: 'list',
// collectionProjectDisplayMode: 'list',
} as const)
export type FeatureFlag = keyof typeof DEFAULT_FEATURE_FLAGS
export type AllFeatureFlags = {
[key in FeatureFlag]: (typeof DEFAULT_FEATURE_FLAGS)[key]
}
export type PartialFeatureFlags = Partial<AllFeatureFlags>
const COOKIE_OPTIONS = {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
} satisfies CookieOptions<PartialFeatureFlags>
export const useFeatureFlags = () =>
useState<AllFeatureFlags>('featureFlags', () => {
const config = useRuntimeConfig()
const savedFlags = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
if (!savedFlags.value) {
savedFlags.value = {}
}
const flags: AllFeatureFlags = JSON.parse(JSON.stringify(DEFAULT_FEATURE_FLAGS))
const overrides = config.public.featureFlagOverrides as PartialFeatureFlags
for (const key in overrides) {
if (key in flags) {
const flag = key as FeatureFlag
const value = overrides[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
}
}
for (const key in savedFlags.value) {
if (key in flags) {
const flag = key as FeatureFlag
const value = savedFlags.value[flag] as (typeof flags)[FeatureFlag]
flags[flag] = value
}
}
return flags
})
export const saveFeatureFlags = () => {
const flags = useFeatureFlags()
const cookie = useCookie<PartialFeatureFlags>('featureFlags', COOKIE_OPTIONS)
cookie.value = flags.value
}

View File

@@ -0,0 +1,36 @@
export const useBaseFetch = async (url, options = {}, skipAuth = false) => {
const config = useRuntimeConfig()
let base = process.server ? config.apiBaseUrl : config.public.apiBaseUrl
if (!options.headers) {
options.headers = {}
}
if (process.server) {
options.headers['x-ratelimit-key'] = config.rateLimitKey
}
if (!skipAuth) {
const auth = await useAuth()
options.headers.Authorization = auth.value.token
}
if (options.apiVersion || options.internal) {
// Base may end in /vD/ or /vD. We would need to replace the digit with the new version number
// and keep the trailing slash if it exists
const baseVersion = base.match(/\/v\d\//)
const replaceStr = options.internal ? `/_internal/` : `/v${options.apiVersion}/`
if (baseVersion) {
base = base.replace(baseVersion[0], replaceStr)
} else {
base = base.replace(/\/v\d$/, replaceStr)
}
delete options.apiVersion
}
return await $fetch(`${base}${url}`, options)
}

View File

@@ -0,0 +1,18 @@
import { createFormatter, type Formatter } from '@vintl/how-ago'
import type { IntlController } from '@vintl/vintl/controller'
const formatters = new WeakMap<IntlController<any>, Formatter>()
export function useRelativeTime(): Formatter {
const vintl = useVIntl()
let formatter = formatters.get(vintl)
if (formatter == null) {
const formatterRef = computed(() => createFormatter(vintl.intl))
formatter = (value, options) => formatterRef.value(value, options)
formatters.set(vintl, formatter)
}
return formatter
}

View File

@@ -0,0 +1,46 @@
type ImageUploadContext = {
projectID?: string
context: 'project' | 'version' | 'thread_message' | 'report'
}
interface ImageUploadResponse {
id: string
url: string
}
export const useImageUpload = async (file: File, ctx: ImageUploadContext) => {
// Make sure file is of type image/png, image/jpeg, image/gif, or image/webp
if (
!file.type.startsWith('image/') ||
!['png', 'jpeg', 'gif', 'webp'].includes(file.type.split('/')[1])
) {
throw new Error('File is not an accepted image type')
}
// Make sure file is less than 1MB
if (file.size > 1024 * 1024) {
throw new Error('File is too large')
}
const qs = new URLSearchParams()
if (ctx.projectID) qs.set('project_id', ctx.projectID)
qs.set('context', ctx.context)
qs.set('ext', file.type.split('/')[1])
const url = `image?${qs.toString()}`
const response = (await useBaseFetch(url, {
method: 'POST',
body: file,
apiVersion: 3,
})) as ImageUploadResponse
// Type check to see if response has a url property and an id property
if (!response?.id || typeof response.id !== 'string') {
throw new Error('Unexpected response from server')
}
if (!response?.url || typeof response.url !== 'string') {
throw new Error('Unexpected response from server')
}
return response
}

View File

@@ -0,0 +1,13 @@
export const useLoading = () => useState('loading', () => false)
export const startLoading = () => {
const loading = useLoading()
loading.value = true
}
export const stopLoading = () => {
const loading = useLoading()
loading.value = false
}

View File

@@ -0,0 +1,34 @@
export const useNotifications = () => useState('notifications', () => [])
export const addNotification = (notification) => {
const notifications = useNotifications()
const existingNotif = notifications.value.find(
(x) =>
x.text === notification.text && x.title === notification.title && x.type === notification.type
)
if (existingNotif) {
setNotificationTimer(existingNotif)
return
}
notification.id = new Date()
setNotificationTimer(notification)
notifications.value.push(notification)
}
export const setNotificationTimer = (notification) => {
if (!notification) return
const notifications = useNotifications()
if (notification.timer) {
clearTimeout(notification.timer)
}
notification.timer = setTimeout(() => {
notifications.value.splice(notifications.value.indexOf(notification), 1)
}, 30000)
}

View File

@@ -0,0 +1 @@
export { useRoute as useNativeRoute, useRouter as useNativeRouter } from 'vue-router'

View File

@@ -0,0 +1,7 @@
export const getArrayOrString = (x) => {
if (typeof x === 'string' || x instanceof String) {
return [x]
} else {
return x
}
}

View File

@@ -0,0 +1,10 @@
/**
* Extracts the [id] from the route params and returns it as a ref.
*
* @param {string?} key The key of the route param to extract.
* @returns {import('vue').Ref<string | string[] | undefined>}
*/
export const useRouteId = (key = 'id') => {
const route = useNativeRoute()
return route.params?.[key] || undefined
}

View File

@@ -0,0 +1,64 @@
import tags from '~/generated/state.json'
export const useTags = () =>
useState('tags', () => ({
categories: tags.categories,
loaders: tags.loaders,
gameVersions: tags.gameVersions,
donationPlatforms: tags.donationPlatforms,
reportTypes: tags.reportTypes,
projectTypes: [
{
actual: 'mod',
id: 'mod',
display: 'mod',
},
{
actual: 'mod',
id: 'plugin',
display: 'plugin',
},
{
actual: 'mod',
id: 'datapack',
display: 'data pack',
},
{
actual: 'shader',
id: 'shader',
display: 'shader',
},
{
actual: 'resourcepack',
id: 'resourcepack',
display: 'resource pack',
},
{
actual: 'modpack',
id: 'modpack',
display: 'modpack',
},
],
loaderData: {
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge', 'folia'],
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
allPluginLoaders: [
'bukkit',
'spigot',
'paper',
'purpur',
'sponge',
'bungeecord',
'waterfall',
'velocity',
'folia',
],
dataPackLoaders: ['datapack'],
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift', 'neoforge'],
hiddenModLoaders: ['liteloader', 'modloader', 'rift'],
},
projectViewModes: ['list', 'grid', 'gallery'],
approvedStatuses: ['approved', 'archived', 'unlisted', 'private'],
rejectedStatuses: ['rejected', 'withheld'],
staffRoles: ['moderator', 'admin'],
}))

View File

@@ -0,0 +1,58 @@
export const useTheme = () =>
useState('theme', () => {
const colorMode = useCookie('color-mode', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
})
if (!colorMode.value) {
colorMode.value = {
value: 'dark',
preference: 'system',
}
}
if (colorMode.value.preference !== 'system') {
colorMode.value.value = colorMode.value.preference
}
return colorMode.value
})
export const updateTheme = (value, updatePreference = false) => {
const theme = useTheme()
const cosmetics = useCosmetics()
const themeCookie = useCookie('color-mode', {
maxAge: 60 * 60 * 24 * 365 * 10,
sameSite: 'lax',
secure: true,
httpOnly: false,
path: '/',
})
if (value === 'system') {
theme.value.preference = 'system'
const colorSchemeQueryList = window.matchMedia('(prefers-color-scheme: light)')
if (colorSchemeQueryList.matches) {
theme.value.value = 'light'
} else {
theme.value.value = cosmetics.value.preferredDarkTheme
}
} else {
theme.value.value = value
if (updatePreference) theme.value.preference = value
}
if (process.client) {
document.documentElement.className = `${theme.value.value}-mode`
}
themeCookie.value = theme.value
}
export const DARK_THEMES = ['dark', 'oled', 'retro']

View File

@@ -0,0 +1,36 @@
type AsyncFunction<TArgs extends any[], TResult> = (...args: TArgs) => Promise<TResult>
type ErrorFunction = (err: any) => void | Promise<void>
type VoidFunction = () => void | Promise<void>
type useClientTry = <TArgs extends any[], TResult>(
fn: AsyncFunction<TArgs, TResult>,
onFail?: ErrorFunction,
onFinish?: VoidFunction
) => (...args: TArgs) => Promise<TResult | undefined>
const defaultOnError: ErrorFunction = (error) => {
addNotification({
group: 'main',
title: 'An error occurred',
text: error?.data?.description || error.message || error || 'Unknown error',
type: 'error',
})
}
export const useClientTry: useClientTry =
(fn, onFail = defaultOnError, onFinish) =>
async (...args) => {
startLoading()
try {
return await fn(...args)
} catch (err) {
if (onFail) {
await onFail(err)
} else {
console.error('[CLIENT TRY ERROR]', err)
}
} finally {
if (onFinish) await onFinish()
stopLoading()
}
}

View File

@@ -0,0 +1,173 @@
export const useUser = async (force = false) => {
const user = useState('user', () => {})
if (!user.value || force || (user.value && Date.now() - user.value.lastUpdated > 300000)) {
user.value = await initUser()
}
return user
}
export const initUser = async () => {
const auth = (await useAuth()).value
const user = {
notifications: [],
follows: [],
lastUpdated: 0,
}
if (auth.user && auth.user.id) {
try {
const [follows, collections] = await Promise.all([
useBaseFetch(`user/${auth.user.id}/follows`),
useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 }),
])
user.collections = collections
user.follows = follows
user.lastUpdated = Date.now()
} catch (err) {
console.error(err)
}
}
return user
}
export const initUserCollections = async () => {
const auth = (await useAuth()).value
const user = (await useUser()).value
if (auth.user && auth.user.id) {
try {
user.collections = await useBaseFetch(`user/${auth.user.id}/collections`, { apiVersion: 3 })
} catch (err) {
console.error(err)
}
}
}
export const initUserFollows = async () => {
const auth = (await useAuth()).value
const user = (await useUser()).value
if (auth.user && auth.user.id) {
try {
user.follows = await useBaseFetch(`user/${auth.user.id}/follows`)
} catch (err) {
console.error(err)
}
}
}
export const initUserProjects = async () => {
const auth = (await useAuth()).value
const user = (await useUser()).value
if (auth.user && auth.user.id) {
try {
user.projects = await useBaseFetch(`user/${auth.user.id}/projects`)
} catch (err) {
console.error(err)
}
}
}
export const userCollectProject = async (collection, projectId) => {
const user = (await useUser()).value
await initUserCollections()
const collectionId = collection.id
const latestCollection = user.collections.find((x) => x.id === collectionId)
if (!latestCollection) {
throw new Error('This collection was not found. Has it been deleted?')
}
const add = !latestCollection.projects.includes(projectId)
const projects = add
? [...latestCollection.projects, projectId]
: [...latestCollection.projects].filter((x) => x !== projectId)
const idx = user.collections.findIndex((x) => x.id === latestCollection.id)
if (idx >= 0) {
user.collections[idx].projects = projects
}
await useBaseFetch(`collection/${collection.id}`, {
method: 'PATCH',
body: {
new_projects: projects,
},
apiVersion: 3,
})
}
export const userFollowProject = async (project) => {
const user = (await useUser()).value
user.follows = user.follows.concat(project)
project.followers++
setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, {
method: 'POST',
})
})
}
export const userUnfollowProject = async (project) => {
const user = (await useUser()).value
user.follows = user.follows.filter((x) => x.id !== project.id)
project.followers--
setTimeout(() => {
useBaseFetch(`project/${project.id}/follow`, {
method: 'DELETE',
})
})
}
export const resendVerifyEmail = async () => {
const app = useNuxtApp()
startLoading()
try {
await useBaseFetch('auth/email/resend_verify', {
method: 'POST',
})
const auth = await useAuth()
app.$notify({
group: 'main',
title: 'Email sent',
text: `An email with a link to verify your account has been sent to ${auth.value.user.email}.`,
type: 'success',
})
} catch (err) {
app.$notify({
group: 'main',
title: 'An error occurred',
text: err.data.description,
type: 'error',
})
}
stopLoading()
}
export const logout = async () => {
startLoading()
const auth = await useAuth()
try {
await useBaseFetch(`session/${auth.value.token}`, {
method: 'DELETE',
})
} catch {}
await useAuth('none')
useCookie('auth-token').value = null
await navigateTo('/')
stopLoading()
}