Start a POC for a new frontend!

This commit is contained in:
MiniDigger 2022-02-12 19:24:05 +01:00 committed by MiniDigger | Martin
parent 40d611757d
commit d629e51f9d
48 changed files with 8040 additions and 0 deletions

View File

@ -0,0 +1,222 @@
{
"globals": {
"acceptHMRUpdate": true,
"asyncComputed": true,
"autoResetRef": true,
"axios": true,
"biSyncRef": true,
"computed": true,
"computedInject": true,
"controlledComputed": true,
"controlledRef": true,
"createApp": true,
"createEventHook": true,
"createGlobalState": true,
"createPinia": true,
"createReactiveFn": true,
"createSharedComposable": true,
"createUnrefFn": true,
"customRef": true,
"debouncedRef": true,
"debouncedWatch": true,
"defineAsyncComponent": true,
"defineComponent": true,
"defineStore": true,
"eagerComputed": true,
"effectScope": true,
"EffectScope": true,
"extendRef": true,
"getActivePinia": true,
"getCurrentInstance": true,
"getCurrentScope": true,
"h": true,
"ignorableWatch": true,
"inject": true,
"isDefined": true,
"isReadonly": true,
"isRef": true,
"makeDestructurable": true,
"mapActions": true,
"mapGetters": true,
"mapState": true,
"mapStores": true,
"mapWritableState": true,
"markRaw": true,
"nextTick": true,
"onActivated": true,
"onBeforeMount": true,
"onBeforeUnmount": true,
"onBeforeUpdate": true,
"onClickOutside": true,
"onDeactivated": true,
"onErrorCaptured": true,
"onKeyStroke": true,
"onMounted": true,
"onRenderTracked": true,
"onRenderTriggered": true,
"onScopeDispose": true,
"onServerPrefetch": true,
"onStartTyping": true,
"onUnmounted": true,
"onUpdated": true,
"pausableWatch": true,
"provide": true,
"reactify": true,
"reactifyObject": true,
"reactive": true,
"reactivePick": true,
"readonly": true,
"ref": true,
"refDefault": true,
"resolveComponent": true,
"setActivePinia": true,
"setMapStoreSuffix": true,
"shallowReactive": true,
"shallowReadonly": true,
"shallowRef": true,
"storeToRefs": true,
"syncRef": true,
"templateRef": true,
"throttledRef": true,
"throttledWatch": true,
"toRaw": true,
"toReactive": true,
"toRef": true,
"toRefs": true,
"triggerRef": true,
"tryOnBeforeUnmount": true,
"tryOnMounted": true,
"tryOnScopeDispose": true,
"tryOnUnmounted": true,
"unref": true,
"unrefElement": true,
"until": true,
"useActiveElement": true,
"useAsyncQueue": true,
"useAsyncState": true,
"useAttrs": true,
"useBase64": true,
"useBattery": true,
"useBreakpoints": true,
"useBroadcastChannel": true,
"useBrowserLocation": true,
"useClamp": true,
"useClipboard": true,
"useColorMode": true,
"useConfirmDialog": true,
"useCounter": true,
"useCssModule": true,
"useCssVar": true,
"useCssVars": true,
"useCycleList": true,
"useDark": true,
"useDebounce": true,
"useDebouncedRefHistory": true,
"useDebounceFn": true,
"useDeviceMotion": true,
"useDeviceOrientation": true,
"useDevicePixelRatio": true,
"useDevicesList": true,
"useDisplayMedia": true,
"useDocumentVisibility": true,
"useDraggable": true,
"useElementBounding": true,
"useElementByPoint": true,
"useElementHover": true,
"useElementSize": true,
"useElementVisibility": true,
"useEventBus": true,
"useEventListener": true,
"useEventSource": true,
"useEyeDropper": true,
"useFavicon": true,
"useFetch": true,
"useFocus": true,
"useFocusWithin": true,
"useFps": true,
"useFullscreen": true,
"useGeolocation": true,
"useHead": true,
"useI18n": true,
"useIdle": true,
"useIntersectionObserver": true,
"useInterval": true,
"useIntervalFn": true,
"useKeyModifier": true,
"useLastChanged": true,
"useLocalStorage": true,
"useMagicKeys": true,
"useManualRefHistory": true,
"useMediaControls": true,
"useMediaQuery": true,
"useMemoize": true,
"useMemory": true,
"useMounted": true,
"useMouse": true,
"useMouseInElement": true,
"useMousePressed": true,
"useMutationObserver": true,
"useNavigatorLanguage": true,
"useNetwork": true,
"useNow": true,
"useOnline": true,
"usePageLeave": true,
"useParallax": true,
"usePermission": true,
"usePointer": true,
"usePointerSwipe": true,
"usePreferredColorScheme": true,
"usePreferredDark": true,
"usePreferredLanguages": true,
"useRafFn": true,
"useRefHistory": true,
"useResizeObserver": true,
"useRoute": true,
"useRouter": true,
"useScreenSafeArea": true,
"useScriptTag": true,
"useScroll": true,
"useScrollLock": true,
"useSessionStorage": true,
"useShare": true,
"useSlots": true,
"useSpeechRecognition": true,
"useSpeechSynthesis": true,
"useStorage": true,
"useStorageAsync": true,
"useStyleTag": true,
"useSwipe": true,
"useTemplateRefsList": true,
"useTextSelection": true,
"useThrottle": true,
"useThrottledRefHistory": true,
"useThrottleFn": true,
"useTimeAgo": true,
"useTimeout": true,
"useTimeoutFn": true,
"useTimestamp": true,
"useTitle": true,
"useToggle": true,
"useTransition": true,
"useUrlSearchParams": true,
"useUserMedia": true,
"useVibrate": true,
"useVirtualList": true,
"useVModel": true,
"useVModels": true,
"useWakeLock": true,
"useWebNotification": true,
"useWebSocket": true,
"useWebWorker": true,
"useWebWorkerFn": true,
"useWindowFocus": true,
"useWindowScroll": true,
"useWindowSize": true,
"watch": true,
"watchAtMost": true,
"watchEffect": true,
"watchOnce": true,
"watchWithFilter": true,
"whenever": true
}
}

6
frontend-new/.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
.DS_Store
*.local
dist
dist-ssr
node_modules
**/_*

1
frontend-new/.npmrc Normal file
View File

@ -0,0 +1 @@
shamefully-hoist=true

View File

@ -0,0 +1,5 @@
/node_modules/**
/.cache/**
/dist/**
/tests/unit/coverage/**
**/.temp/**

7
frontend-new/.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"printWidth": 160,
"arrowParens": "always"
}

14
frontend-new/TODO.md Normal file
View File

@ -0,0 +1,14 @@
Stuff that needs to be done before I consider this a successful POC
- [x] initial setup (pages, layout, autoimport, basically the vitesse stuff)
- [x] windi/tailwind
- [x] store (with ssr)
- [x] async data replacement
- [ ] i18n
- [x] api calls
- [x] seo
- [ ] route perms
- [ ] error pages
- [ ] snackbar
- [ ] some basic pages
- [ ] maybe deployment alongside the existing frontend?

24
frontend-new/index.html Normal file
View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/pwa-192x192.png">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#00aba9">
<meta name="msapplication-TileColor" content="#00aba9">
<meta name="theme-color" content="#ffffff">
</head>
<body>
<div id="app"></div>
<script>
(function() {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('color-schema') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

76
frontend-new/package.json Normal file
View File

@ -0,0 +1,76 @@
{
"private": true,
"engines": {
"node": ">=16"
},
"scripts": {
"dev": "vite-ssr --port 3333",
"dev:spa": "vite --port 3333",
"build": "cross-env NODE_ENV=production vite-ssr build",
"lint": "eslint --ext \".js,.vue,.ts,.json\" --ignore-path .gitignore --fix .",
"format": "prettier . --write"
},
"dependencies": {
"@vueuse/core": "^7.5.5",
"@vueuse/head": "^0.7.5",
"@vueuse/integrations": "^7.5.5",
"axios": "^0.25.0",
"jwt-decode": "^3.1.2",
"nprogress": "^0.2.0",
"pinia": "^2.0.11",
"prism-theme-vars": "^0.2.2",
"qs": "^6.10.3",
"universal-cookie": "^4.0.4",
"vite-ssr": "^0.15.0",
"vue": "^3.2.29",
"vue-i18n": "^9.1.6",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@antfu/eslint-config": "^0.16.1",
"@iconify/json": "^2.0.33",
"@intlify/vite-plugin-vue-i18n": "^3.2.1",
"@types/markdown-it-link-attributes": "^3.0.1",
"@types/nprogress": "^0.2.0",
"@types/qs": "^6.9.7",
"@typescript-eslint/eslint-plugin": "^5.10.2",
"@vitejs/plugin-vue": "^2.1.0",
"@vue/compiler-sfc": "^3.2.29",
"@vue/server-renderer": "^3.2.29",
"cross-env": "^7.0.3",
"eslint": "^8.8.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-vue": "^8.4.1",
"markdown-it-link-attributes": "^4.0.0",
"markdown-it-prism": "^2.2.2",
"node-fetch": "^3.2.0",
"pnpm": "^6.29.1",
"prettier": "2.5.1",
"typescript": "^4.5.5",
"unplugin-auto-import": "^0.5.11",
"unplugin-icons": "^0.13.0",
"unplugin-vue-components": "^0.17.16",
"vite": "^2.7.13",
"vite-plugin-eslint": "^1.3.0",
"vite-plugin-md": "^0.11.7",
"vite-plugin-pages": "^0.20.1",
"vite-plugin-pwa": "^0.11.13",
"vite-plugin-vue-layouts": "^0.6.0",
"vite-plugin-windicss": "^1.6.3"
},
"//": "TODO steal from work",
"eslintConfig": {
"extends": [
"@antfu/eslint-config",
"prettier"
],
"env": {
"shared-node-browser": true
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": "off",
"no-console": "off"
}
}
}

5920
frontend-new/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

17
frontend-new/src/App.vue Normal file
View File

@ -0,0 +1,17 @@
<script setup lang="ts">
import { useHead } from '@vueuse/head';
import { useSeo } from '~/composables/useSeo';
const title = 'Hangar New Test';
const description = 'IDK WTF am doing';
useHead(useSeo(title, description, useRoute(), null));
</script>
<template>
<router-view v-slot="{ Component, route }">
<transition name="slide">
<component :is="Component" :key="route" />
</transition>
</router-view>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { Announcement } from 'hangar-api';
const props = defineProps<{ announcement: Announcement }>();
const { announcement } = toRefs(props);
</script>
<template>
<div :style="'background-color:' + announcement.color" class="mb-2 p-2 text-center">
{{ announcement.text }}
</div>
</template>

View File

@ -0,0 +1,7 @@
<script setup lang="ts">
</script>
<template>
<div>Footer</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
import type { Announcement as AnnouncementObject } from 'hangar-api';
import { useInitialState } from '~/composables/useInitialState';
import { useInternalApi } from '~/composables/useApi';
// not sure if they need to be part of the initial state, since we directly render them, would only save a request on page switch at most, but I guess its a good demonstration
const announcements = await useInitialState<AnnouncementObject[]>("announcements", async () => await useInternalApi<AnnouncementObject[]>("data/announcements", false));
</script>
<template>
<template v-if="announcements">
<Announcement v-for="(announcement, idx) in announcements" :key="idx" :announcement="announcement" />
</template>
</template>

View File

@ -0,0 +1,101 @@
import type { HangarApiException } from 'hangar-api';
import type { AxiosError, AxiosRequestConfig } from 'axios';
import qs from 'qs';
import { useCookies } from '@vueuse/integrations/useCookies';
import Cookies from 'universal-cookie';
import { useApiToken } from '~/composables/useApiToken';
import { useAxios } from '~/composables/useAxios';
interface StatCookie {
// eslint-disable-next-line camelcase
hangar_stats: string;
Path: string;
'Max-Age': string;
Expires: string;
SameSite: true | false | 'lax' | 'strict' | 'none' | undefined;
Secure?: boolean;
}
function request<T>(
url: string,
token: string | null,
method: AxiosRequestConfig['method'],
data: object,
header: Record<string, string> = {}
): Promise<T> {
const headers: Record<string, string> = token ? { ...header, Authorization: `HangarAuth ${token}` } : header;
return new Promise<T>((resolve, reject) => {
return useAxios
.request<T>({
method,
url: `/api/${url}`,
headers,
data: method?.toLowerCase() !== 'get' ? data : {},
params: method?.toLowerCase() === 'get' ? data : {},
paramsSerializer: (params) => {
return qs.stringify(params, {
arrayFormat: 'repeat'
});
}
})
.then(({ data, headers }) => {
// check for stats cookie
if (headers['set-cookie']) {
const statString = headers['set-cookie'].find((c: string) => c.startsWith('hangar_stats'));
if (statString) {
const statCookie: StatCookie = new Cookies("statString") as unknown as StatCookie;
const cookies = useCookies();
cookies.set('hangar_stats', statCookie.hangar_stats, {
path: statCookie.Path,
expires: new Date(statCookie.Expires),
sameSite: 'strict',
secure: statCookie.Secure
});
}
}
resolve(data);
})
.catch((error: AxiosError) => {
reject(error);
});
});
}
export async function useApi<T>(
url: string,
authed = true,
method: AxiosRequestConfig['method'] = 'get',
data: object = {},
headers: Record<string, string> = {}
): Promise<T> {
const token = await useApiToken(authed);
return request(`v1/${url}`, token, method, data, headers)
}
export async function useInternalApi<T>(
url: string,
authed = true,
method: AxiosRequestConfig['method'] = 'get',
data: object = {},
headers: Record<string, string> = {}
): Promise<T> {
const token = await useApiToken(authed);
if (authed && !token) {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({
isAxiosError: true,
response: {
data: {
isHangarApiException: true,
httpError: {
statusCode: 401,
},
message: 'You must be logged in',
} as HangarApiException,
},
});
} else {
return request(`internal/${url}`, token, method, data, headers);
}
}

View File

@ -0,0 +1,48 @@
import type { JwtPayload } from 'jwt-decode';
import jwtDecode from 'jwt-decode';
import { useCookies } from '@vueuse/integrations';
import { useAuthStore } from '~/store/auth';
import { useAxios } from '~/composables/useAxios';
function refreshToken(): Promise<string | null> {
return new Promise<string | null>((resolve) => {
return useAxios
.get<{ token: string; refreshToken: string; cookieName: string; expiresIn: number }>('/refresh')
.then((value) => {
useAuthStore().$patch({ token: value.data.token });
useCookies().set(value.data.cookieName, value.data.refreshToken, {
path: '/',
expires: new Date(Date.now() + value.data.expiresIn * 1000),
sameSite: 'strict',
secure: process.env.nodeEnv === 'production'
});
resolve(value.data.token);
})
.catch(() => {
resolve(null);
});
});
}
function validateToken(token: string): boolean {
const decodedToken = jwtDecode<JwtPayload>(token);
if (!decodedToken.exp) {
return false;
}
return decodedToken.exp * 1000 > Date.now() - 10 * 1000; // check against 10 seconds earlier to mitigate tokens expiring mid-request
}
export function useApiToken(forceFetch = true): Promise<string | null> {
const store = useAuthStore();
if (store.token) {
if (validateToken(store.token)) {
return Promise.resolve(store.token);
} else {
return refreshToken();
}
} else if (forceFetch) {
return refreshToken();
} else {
return Promise.resolve(null);
}
}

View File

@ -0,0 +1,6 @@
import type { AxiosInstance } from 'axios';
import axios from 'axios';
export const useAxios: AxiosInstance = axios.create({
baseURL: 'http://localhost:3333'
});

View File

@ -0,0 +1,39 @@
import type { Ref } from "vue";
import { onDeactivated, onMounted, onUnmounted, ref } from "vue";
import { useContext } from "vite-ssr/vue";
export async function useInitialState<T>(key: string, handler: (type: "server" | "client") => Promise<T>, blocking = false) {
const { initialState } = useContext();
const responseValue = ref(initialState[key] || null) as Ref<T | null>;
// remove data from initialState when component unmounts or deactivates
const removeState = () => {
if (!import.meta.env.SSR) {
initialState[key] = null;
}
};
onUnmounted(removeState);
onDeactivated(removeState);
if (import.meta.env.SSR) {
// if on server, block until data can be stored in initialState
const data = await handler("server");
initialState[key] = data;
responseValue.value = data;
} else {
// if on server, check if we already have data and use that
if (initialState[key]) {
responseValue.value = initialState[key];
} else {
// else do the request ourselves, blocking if needed
const fn = async () => await handler("client");
if (blocking) {
await fn();
} else {
onMounted(fn);
}
}
}
return responseValue;
}

View File

@ -0,0 +1,121 @@
import type { TranslateResult } from 'vue-i18n';
import type { HeadObject } from '@vueuse/head';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
export function useSeo(title: string | TranslateResult, description: string | TranslateResult | null, route: RouteLocationNormalizedLoaded, image: string | null): HeadObject {
description = description || 'Plugin repository for Paper plugins and more!';
const canonical = baseUrl() + (route.fullPath.endsWith('/') ? route.fullPath : `${route.fullPath}/`);
image = image || 'https://paper.readthedocs.io/en/latest/_images/papermc_logomark_500.png';
image = image.startsWith('http') ? image : baseUrl() + image;
const seo = {
title,
link: [{ rel: 'canonical', href: canonical }],
meta: [
{ hid: 'description', name: 'description', content: description },
{
property: 'og:description',
name: 'og:description',
vmid: 'og:description',
hid: 'og:description',
content: description
},
{
property: 'twitter:description',
name: 'twitter:description',
vmid: 'twitter:description',
hid: 'twitter:description',
content: description
},
{
property: 'og:title',
name: 'og:title',
hid: 'og:title',
template: (chunk: string) => (chunk ? `${chunk} | Hangar` : 'Hangar'),
content: title
},
{
property: 'twitter:title',
name: 'twitter:title',
hid: 'twitter:title',
template: (chunk: string) => (chunk ? `${chunk} | Hangar` : 'Hangar'),
content: title
},
{
property: 'og:url',
name: 'og:url',
hid: 'og:url',
content: canonical
},
{
property: 'twitter:url',
name: 'twitter:url',
hid: 'twitter:url',
content: canonical
},
{
property: 'og:image',
name: 'og:image',
hid: 'og:image',
content: image
}
],
script: [
{
type: 'application/ld+json',
json: {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: generateBreadcrumbs(route)
}
}
] as any[]
} as HeadObject;
// todo renenable crowdin integration
// if (context.app.i18n.locale === 'dum') {
// console.log('found crowdin language activated, lets inject the script');
// seo.script = seo.script ? seo.script : [];
// seo.script.push({
// type: 'text/javascript',
// innerHTML: 'var _jipt = []; _jipt.push([\'project\', \'0cbf58a3d76226e92659632533015495\']); _jipt.push([\'domain\', \'hangar\']);'
// });
// seo.script.push({
// type: 'text/javascript',
// src: 'https://cdn.crowdin.com/jipt/jipt.js'
// });
// seo.__dangerouslyDisableSanitizers = ['script'];
// }
return seo;
}
function generateBreadcrumbs(route: RouteLocationNormalizedLoaded) {
const arr = [];
const split = route.fullPath.split('/');
let curr = '';
for (let i = 0; i < split.length; i++) {
curr = `${curr + split[i]}/`;
arr.push({
'@type': 'ListItem',
position: i,
name: guessTitle(split[i]),
item: baseUrl() + curr
});
}
return arr;
}
function guessTitle(segment: string): string {
if (segment === '/' || segment === '') {
return 'Hangar';
} else {
return segment;
}
}
function baseUrl(): string {
// todo get this from somewhere
return 'https://hangar.benndorf.dev';
}

View File

@ -0,0 +1,7 @@
<template>
<main>
<Header />
<router-view v-bind="$attrs" />
<Footer />
</main>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
</script>
<template>
<main>
Error
</main>
</template>

38
frontend-new/src/main.ts Normal file
View File

@ -0,0 +1,38 @@
import 'windi.css';
import './styles/main.css';
import viteSSR, { ClientOnly } from 'vite-ssr';
import { createHead } from '@vueuse/head';
import generatedRoutes from 'virtual:generated-pages';
import { setupLayouts } from 'virtual:generated-layouts';
import { createPinia } from 'pinia';
import App from '~/App.vue';
const routes = setupLayouts(generatedRoutes);
const options: Parameters<typeof viteSSR>['1'] = {
routes
};
export default viteSSR( App, options, async (ctx) => {
// install all modules under `modules/`
Object.values(import.meta.globEager('./modules/*.ts')).map(i =>
i.install?.(ctx)
);
const { app, initialState } = ctx;
app.component(ClientOnly.name, ClientOnly);
const head = createHead();
const pinia = createPinia();
app.use(pinia).use(head);
if (import.meta.env.SSR) {
initialState.pinia = pinia.state.value;
} else {
pinia.state.value = initialState.pinia;
}
return { head };
}
);

View File

@ -0,0 +1,9 @@
import NProgress from 'nprogress'
import type { UserModule } from '~/types'
export const install: UserModule = ({ isClient, router }) => {
if (isClient) {
router.beforeEach(() => { NProgress.start() })
router.afterEach(() => { NProgress.done() })
}
}

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
</script>
<template>
<div>Index Page</div>
</template>
<route lang="yaml">
meta:
layout: default
</route>

12
frontend-new/src/shims.d.ts vendored Normal file
View File

@ -0,0 +1,12 @@
/* eslint-disable import/no-duplicates */
declare interface Window {
// extend the window
}
// with vite-plugin-md, markdowns can be treat as Vue components
declare module '*.md' {
import type { ComponentOptions } from 'vue'
const component: ComponentOptions
export default component
}

View File

@ -0,0 +1,12 @@
import type { HangarUser } from 'hangar-internal';
export const useAuthStore = defineStore('auth', {
state: () => {
return {
authenticated: false,
user: null as HangarUser | null,
token: null as string | null,
routePermissions: null as string | null
};
}
});

View File

@ -0,0 +1,18 @@
@import './markdown.css';
/* TODO do we need this? */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
@apply bg-teal-600 opacity-75;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 2px;
}

View File

@ -0,0 +1,48 @@
/* https://github.com/antfu/prism-theme-vars */
@import 'prism-theme-vars/base.css';
:root {
--prism-font-family: 'Input Mono', monospace;
}
html:not(.dark) {
--prism-foreground: #393a34;
--prism-background: #fbfbfb;
--prism-comment: #8e8f8e;
--prism-string: #a1644c;
--prism-literal: #3a9c9b;
--prism-keyword: #248358;
--prism-function: #7e8a42;
--prism-deleted: #a14f55;
--prism-class: #2b91af;
--prism-builtin: #a52727;
--prism-property: #ad502b;
--prism-namespace: #c96880;
--prism-punctuation: #8e8f8b;
--prism-decorator: #bd8f8f;
--prism-json-property: #698c96;
}
html.dark {
--prism-scheme: dark;
--prism-foreground: #d4cfbf;
--prism-background: #1e1e1e;
--prism-comment: #758575;
--prism-string: #ce9178;
--prism-literal: #4fb09d;
--prism-keyword: #4d9375;
--prism-function: #c2c275;
--prism-deleted: #a14f55;
--prism-class: #5ebaa8;
--prism-builtin: #cb7676;
--prism-property: #dd8e6e;
--prism-namespace: #c96880;
--prism-punctuation: #d4d4d4;
--prism-decorator: #bd8f8f;
--prism-regex: #ab5e3f;
--prism-json-property: #6b8b9e;
--prism-line-number: #888888;
--prism-line-number-gutter: #eeeeee;
--prism-line-highlight-background: #444444;
--prism-selection-background: #444444;
}

View File

@ -0,0 +1,9 @@
import type { Context } from 'vite-ssr/vue';
import type { App } from 'vue'
import type { RouteRecord, Router } from 'vue-router';
export type UserModule = (ctx: Context & {
app: App
router: Router
initialRoute: RouteRecord
}) => void;

42
frontend-new/src/types/api.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
declare module 'hangar-api' {
import type { Visibility } from '~/types/enums';
interface Model {
createdAt: string;
}
interface Named {
name: string;
}
interface TagColor {
background: string;
foreground: string;
}
interface Pagination {
limit: number;
offset: number;
count: number;
}
interface PaginatedResult<T extends Model> {
pagination: Pagination;
result: T[];
}
interface Announcement {
text: String;
color: String;
}
interface Sponsor {
image: String;
name: String;
link: String;
}
interface Visible {
visibility: Visibility;
}
}

View File

View File

@ -0,0 +1,42 @@
declare module 'hangar-api' {
interface HangarException {
message: string;
messageArgs: any[];
httpError: {
statusCode: number;
statusPhrase: string;
};
}
interface HangarApiException extends HangarException {
isHangarApiException: true;
isMultiException: false;
}
interface MultiHangarApiException {
isMultiException: true;
isHangarApiException: true;
exceptions: HangarApiException[];
}
interface ObjectError {
code: string;
errorMsg: string;
}
interface GlobalError extends ObjectError {
objectName: string;
}
interface FieldError extends ObjectError {
fieldName: string;
rejectedValue: string;
}
interface HangarValidationException extends HangarException {
isHangarValidationException: true;
object: string;
globalErrors: GlobalError[];
fieldErrors: FieldError[];
}
}

View File

@ -0,0 +1,20 @@
declare module 'hangar-api' {
import { NamedPermission, PermissionType } from '~/types/enums';
interface IPermission {
value: NamedPermission;
frontendName: string;
permission: bigint;
}
interface PermissionCheck {
type: PermissionType;
result: boolean;
}
interface UserPermissions {
type: PermissionType;
permissionBinString: string;
permissions: IPermission[];
}
}

View File

@ -0,0 +1,75 @@
declare module 'hangar-api' {
import { Model, Named, TagColor, Visible } from 'hangar-api';
import { ProjectCategory } from '~/types/enums';
interface ProjectNamespace {
owner: string;
slug: string;
}
interface ProjectStats {
views: number;
downloads: number;
recentViews: number;
recentDownloads: number;
stars: number;
watchers: number;
}
interface UserActions {
starred: boolean;
watching: boolean;
flagged: boolean;
}
interface License {
name: string | null;
url: string | null;
type: string | null;
}
interface ProjectSettings {
homepage: string | null;
issues: string | null;
source: string | null;
support: string | null;
license: License;
keywords: string[];
forumSync: boolean;
donation: {
enable: false;
email: string | null;
defaultAmount: number;
oneTimeAmounts: Array<number>;
monthlyAmounts: Array<number>;
};
}
interface PromotedVersionTag extends Named {
data: string;
displayData: string;
minecraftVersion: string;
color: TagColor;
}
interface PromotedVersion {
version: string;
tags: PromotedVersionTag[];
}
interface ProjectCompact extends Model, Named, Visible {
namespace: ProjectNamespace;
stats: ProjectStats;
category: ProjectCategory;
}
interface Project extends ProjectCompact {
description: string;
lastUpdated: Date;
userActions: UserActions;
settings: ProjectSettings;
postId: number;
topicId: number;
promotedVersions: PromotedVersion[];
}
}

30
frontend-new/src/types/api/users.d.ts vendored Normal file
View File

@ -0,0 +1,30 @@
declare module 'hangar-api' {
import { Model, Named } from 'hangar-api';
import { NamedPermission, RoleCategory } from '~/types/enums';
interface Role {
assignable: boolean;
rank?: number | null;
value: string;
roleId: number;
category: RoleCategory;
permissions: string;
title: string;
color: string;
}
interface User extends Model, Named {
tagline: string | null;
joinDate: string;
roles: Role[];
projectCount: number;
isOrganization: boolean;
locked: boolean;
}
interface ApiKey extends Model, Named {
token?: string;
tokenIdentifier?: string;
permissions: NamedPermission[];
}
}

View File

@ -0,0 +1,41 @@
declare module 'hangar-api' {
import { Model, Named, ProjectNamespace, TagColor, Visible } from 'hangar-api';
import { Platform, ReviewState } from '~/types/enums';
interface PluginDependency extends Named {
required: boolean;
namespace: ProjectNamespace | null;
externalUrl: string | null;
}
interface VersionStats {
downloads: number;
}
interface FileInfo {
name: string;
sizeBytes: number;
md5Hash: string;
}
interface Tag extends Named {
data: string;
color: TagColor;
}
interface DependencyVersion {
pluginDependencies: Record<Platform, PluginDependency[]>;
}
interface Version extends Model, Named, DependencyVersion, Visible {
platformDependencies: Record<Platform, string[]>;
description: string;
stats: VersionStats;
fileInfo: FileInfo;
externalUrl: string | null;
author: String;
reviewState: ReviewState;
tags: Tag[];
recommended: Platform[];
}
}

223
frontend-new/src/types/auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,223 @@
// Generated by 'unplugin-auto-import'
// We suggest you to commit this file into source control
declare global {
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
const axios: typeof import('axios')['default']
const biSyncRef: typeof import('@vueuse/core')['biSyncRef']
const computed: typeof import('vue')['computed']
const computedInject: typeof import('@vueuse/core')['computedInject']
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
const controlledRef: typeof import('@vueuse/core')['controlledRef']
const createApp: typeof import('vue')['createApp']
const createEventHook: typeof import('@vueuse/core')['createEventHook']
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
const createPinia: typeof import('pinia')['createPinia']
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const customRef: typeof import('vue')['customRef']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
const EffectScope: typeof import('vue')['EffectScope']
const extendRef: typeof import('@vueuse/core')['extendRef']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
const inject: typeof import('vue')['inject']
const isDefined: typeof import('@vueuse/core')['isDefined']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
const provide: typeof import('vue')['provide']
const reactify: typeof import('@vueuse/core')['reactify']
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
const reactive: typeof import('vue')['reactive']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const refDefault: typeof import('@vueuse/core')['refDefault']
const resolveComponent: typeof import('vue')['resolveComponent']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const syncRef: typeof import('@vueuse/core')['syncRef']
const templateRef: typeof import('@vueuse/core')['templateRef']
const throttledRef: typeof import('@vueuse/core')['throttledRef']
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
const toRaw: typeof import('vue')['toRaw']
const toReactive: typeof import('@vueuse/core')['toReactive']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
const unref: typeof import('vue')['unref']
const unrefElement: typeof import('@vueuse/core')['unrefElement']
const until: typeof import('@vueuse/core')['until']
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
const useAttrs: typeof import('vue')['useAttrs']
const useBase64: typeof import('@vueuse/core')['useBase64']
const useBattery: typeof import('@vueuse/core')['useBattery']
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
const useClamp: typeof import('@vueuse/core')['useClamp']
const useClipboard: typeof import('@vueuse/core')['useClipboard']
const useColorMode: typeof import('@vueuse/core')['useColorMode']
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
const useCounter: typeof import('@vueuse/core')['useCounter']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVar: typeof import('@vueuse/core')['useCssVar']
const useCssVars: typeof import('vue')['useCssVars']
const useCycleList: typeof import('@vueuse/core')['useCycleList']
const useDark: typeof import('@vueuse/core')['useDark']
const useDebounce: typeof import('@vueuse/core')['useDebounce']
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
const useDraggable: typeof import('@vueuse/core')['useDraggable']
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
const useElementHover: typeof import('@vueuse/core')['useElementHover']
const useElementSize: typeof import('@vueuse/core')['useElementSize']
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
const useEventBus: typeof import('@vueuse/core')['useEventBus']
const useEventListener: typeof import('@vueuse/core')['useEventListener']
const useEventSource: typeof import('@vueuse/core')['useEventSource']
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
const useFavicon: typeof import('@vueuse/core')['useFavicon']
const useFetch: typeof import('@vueuse/core')['useFetch']
const useFocus: typeof import('@vueuse/core')['useFocus']
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
const useFps: typeof import('@vueuse/core')['useFps']
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
const useHead: typeof import('@vueuse/head')['useHead']
const useI18n: typeof import('vue-i18n')['useI18n']
const useIdle: typeof import('@vueuse/core')['useIdle']
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
const useInterval: typeof import('@vueuse/core')['useInterval']
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
const useMemoize: typeof import('@vueuse/core')['useMemoize']
const useMemory: typeof import('@vueuse/core')['useMemory']
const useMounted: typeof import('@vueuse/core')['useMounted']
const useMouse: typeof import('@vueuse/core')['useMouse']
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
const useNetwork: typeof import('@vueuse/core')['useNetwork']
const useNow: typeof import('@vueuse/core')['useNow']
const useOnline: typeof import('@vueuse/core')['useOnline']
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
const useParallax: typeof import('@vueuse/core')['useParallax']
const usePermission: typeof import('@vueuse/core')['usePermission']
const usePointer: typeof import('@vueuse/core')['usePointer']
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
const useRafFn: typeof import('@vueuse/core')['useRafFn']
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
const useScroll: typeof import('@vueuse/core')['useScroll']
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
const useShare: typeof import('@vueuse/core')['useShare']
const useSlots: typeof import('vue')['useSlots']
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
const useStorage: typeof import('@vueuse/core')['useStorage']
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
const useSwipe: typeof import('@vueuse/core')['useSwipe']
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
const useThrottle: typeof import('@vueuse/core')['useThrottle']
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
const useTimeout: typeof import('@vueuse/core')['useTimeout']
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
const useTitle: typeof import('@vueuse/core')['useTitle']
const useToggle: typeof import('@vueuse/core')['useToggle']
const useTransition: typeof import('@vueuse/core')['useTransition']
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
const useVibrate: typeof import('@vueuse/core')['useVibrate']
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
const useVModel: typeof import('@vueuse/core')['useVModel']
const useVModels: typeof import('@vueuse/core')['useVModels']
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const watch: typeof import('vue')['watch']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
const watchEffect: typeof import('vue')['watchEffect']
const watchOnce: typeof import('@vueuse/core')['watchOnce']
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
const whenever: typeof import('@vueuse/core')['whenever']
}
export {}

13
frontend-new/src/types/components.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
// generated by unplugin-vue-components
// We suggest you to commit this file into source control
// Read more: https://github.com/vuejs/vue-next/pull/3399
declare module 'vue' {
export interface GlobalComponents {
Announcement: typeof import('./../components/Announcement.vue')['default']
Footer: typeof import('./../components/Footer.vue')['default']
Header: typeof import('./../components/Header.vue')['default']
}
}
export { }

View File

@ -0,0 +1,108 @@
/* eslint-disable no-unused-vars */
export enum RoleCategory {
GLOBAL = 'global',
PROJECT = 'project',
ORGANIZATION = 'organization',
}
export enum ProjectCategory {
ADMIN_TOOLS = 'admin_tools',
CHAT = 'chat',
DEV_TOOLS = 'dev_tools',
ECONOMY = 'economy',
GAMEPLAY = 'gameplay',
GAMES = 'games',
PROTECTION = 'protection',
ROLE_PLAYING = 'role_playing',
WORLD_MANAGEMENT = 'world_management',
MISC = 'misc',
UNDEFINED = 'undefined',
}
export enum Visibility {
PUBLIC = 'public',
NEW = 'new',
NEEDS_CHANGES = 'needsChanges',
NEEDS_APPROVAL = 'needsApproval',
SOFT_DELETE = 'softDelete',
}
export enum ReviewState {
UNREVIEWED = 'unreviewed',
REVIEWED = 'reviewed',
UNDER_REVIEW = 'under_review',
BACKLOG = 'backlog',
PARTIALLY_REVIEWED = 'partially_reviewed',
}
export enum PermissionType {
GLOBAL = 'global',
PROJECT = 'project',
ORGANIZATION = 'organization',
}
export enum NamedPermission {
VIEW_PUBLIC_INFO = 'view_public_info',
EDIT_OWN_USER_SETTINGS = 'edit_own_user_settings',
EDIT_API_KEYS = 'edit_api_keys',
EDIT_SUBJECT_SETTINGS = 'edit_subject_settings',
MANAGE_SUBJECT_MEMBERS = 'manage_subject_members',
IS_SUBJECT_OWNER = 'is_subject_owner',
IS_SUBJECT_MEMBER = 'is_subject_member',
CREATE_PROJECT = 'create_project',
EDIT_PAGE = 'edit_page',
DELETE_PROJECT = 'delete_project',
CREATE_VERSION = 'create_version',
EDIT_VERSION = 'edit_version',
DELETE_VERSION = 'delete_version',
EDIT_TAGS = 'edit_tags',
CREATE_ORGANIZATION = 'create_organization',
POST_AS_ORGANIZATION = 'post_as_organization',
MOD_NOTES_AND_FLAGS = 'mod_notes_and_flags',
SEE_HIDDEN = 'see_hidden',
IS_STAFF = 'is_staff',
REVIEWER = 'reviewer',
VIEW_HEALTH = 'view_health',
VIEW_IP = 'view_ip',
VIEW_STATS = 'view_stats',
VIEW_LOGS = 'view_logs',
MANUAL_VALUE_CHANGES = 'manual_value_changes',
HARD_DELETE_PROJECT = 'hard_delete_project',
HARD_DELETE_VERSION = 'hard_delete_version',
EDIT_ALL_USER_SETTINGS = 'edit_all_user_settings',
}
export enum Platform {
PAPER = 'PAPER',
WATERFALL = 'WATERFALL',
VELOCITY = 'VELOCITY',
}
export enum ReviewAction {
START = 'START',
MESSAGE = 'MESSAGE',
STOP = 'STOP',
REOPEN = 'REOPEN',
APPROVE = 'APPROVE',
PARTIALLY_APPROVE = 'PARTIALLY_APPROVE',
UNDO_APPROVAL = 'UNDO_APPROVAL',
}
export enum LogContext {
PROJECT = 'PROJECT',
VERSION = 'VERSION',
PAGE = 'PAGE',
USER = 'USER',
ORGANIZATION = 'ORGANIZATION',
}
export enum Prompt {
CHANGE_AVATAR = 'CHANGE_AVATAR',
}

View File

@ -0,0 +1,87 @@
declare module 'hangar-internal' {
import { Model, ProjectNamespace } from 'hangar-api';
import { LogContext, Platform, Visibility } from '~/types/enums';
interface LogProject {
id: number;
slug: string;
owner: string;
}
interface LogVersion {
id: number;
versionString: string;
platforms: Platform[];
}
interface LogPage {
id: number;
name: string;
slug: string;
}
interface LogSubject {
id: number;
name: string;
}
interface LoggedActionType {
pgLoggedAction: string;
name: string;
description: string;
}
interface LoggedAction extends Model {
userId: number | null;
userName: string | null;
address: string;
action: LoggedActionType;
contextType: LogContext;
newState: string;
oldState: string;
project: LogProject | null;
version: LogVersion | null;
page: LogPage | null;
subject: LogSubject | null;
}
interface Job extends Model {
jobType: string;
state: string;
lastError: string;
lastErrorDescriptor: string;
retryAt: string;
lastUpdated: string;
jobProperties: { [key: string]: string };
}
interface MissingFile {
platform: Platform;
versionString: string;
fileName: string;
namespace: ProjectNamespace;
name: string;
}
interface UnhealthyProject {
namespace: ProjectNamespace;
topicId: number | null;
postId: number | null;
lastUpdated: string;
visibility: Visibility;
}
interface Activity {
namespace: ProjectNamespace;
}
interface FlagActivity extends Activity {
resolvedAt: string;
}
interface ReviewActivity extends Activity {
endedAt: string;
versionString: string;
platforms: Platform[];
}
}

View File

@ -0,0 +1,49 @@
declare module 'hangar-internal' {
import { Model, TagColor } from 'hangar-api';
import { Platform, ProjectCategory, Prompt, Visibility } from '~/types/enums';
interface Table extends Model {
id: number;
}
interface IProjectCategory {
title: string;
icon: string;
apiName: ProjectCategory;
visible: boolean;
}
interface FlagReason {
type: string;
title: string;
}
interface Color {
name: string;
hex: string;
}
interface IPlatform {
name: string;
enumName: Platform;
category: 'Server' | 'Proxy';
url: string;
tagColor: TagColor;
possibleVersions: string[];
visible: boolean;
}
interface IVisibility {
name: Visibility;
showModal: boolean;
cssClass: string;
title: string;
}
interface IPrompt {
ordinal: number;
name: Prompt;
titleKey: string;
messageKey: string;
}
}

View File

@ -0,0 +1,100 @@
declare module 'hangar-internal' {
import { Joinable, Table } from 'hangar-internal';
import { Project, ProjectNamespace } from 'hangar-api';
import { ProjectCategory, Visibility } from '~/types/enums';
interface ProjectOwner {
id: number;
name: string;
userId: number;
isOrganization: boolean;
}
interface HangarProjectInfo {
publicVersions: number;
flagCount: number;
noteCount: number;
starCount: number;
watcherCount: number;
}
interface HangarProjectPage {
id: number;
name: string;
slug: string;
home: boolean;
children: HangarProjectPage[];
}
interface HangarProject extends Joinable, Project, Table {
lastVisibilityChangeComment: string;
lastVisibilityChangeUserName: string;
info: HangarProjectInfo;
pages: HangarProjectPage[];
recommendedVersions: {
[key: string]: string | null; // Platform: externalUrl (if any)
};
}
interface ProjectPage extends Table {
name: string;
slug: string;
contents: string;
deletable: boolean;
isHome: boolean;
}
interface Flag extends Table {
userId: number; //
reportedByName: string; //
reason: string; //
resolved: boolean; //
comment: string; //
resolvedAt: string | null; //
resolvedBy: number | null; //
resolvedByName: string | null; //
projectId: number; //
projectNamespace: ProjectNamespace;
projectVisibility: Visibility;
}
interface Note extends Table {
projectId: number;
message: string;
userId: number;
userName: string | null;
}
interface ProjectSettingsForm {
settings: {
homepage: string | null;
issues: string | null;
source: string | null;
support: string | null;
keywords: string[];
license: {
type?: string | null;
url: string | null;
name: string | null;
};
donation: {
enable: false;
email: string | null;
defaultAmount: number;
oneTimeAmounts: Array<string>;
monthlyAmounts: Array<string>;
};
forumSync: false;
};
category: ProjectCategory;
description: string;
}
interface ProjectApproval {
projectId: number;
namespace: ProjectNamespace;
visibility: Visibility;
comment: string;
changeRequester: string;
}
}

View File

@ -0,0 +1,36 @@
declare module 'hangar-internal' {
import { Model, ProjectNamespace } from 'hangar-api';
import { Platform, ReviewAction } from '~/types/enums';
interface HangarReviewMessage extends Model {
message: string;
args: Record<string, string>;
action: ReviewAction;
}
interface HangarReview extends Model {
endedAt: string | null;
userName: string;
userId: number;
messages: HangarReviewMessage[];
}
interface Review {
reviewerName: string;
reviewStarted: string;
reviewEnded: string | null;
lastAction: ReviewAction;
}
interface ReviewQueueEntry {
namespace: ProjectNamespace;
versionId: number;
versionString: string;
platforms: Platform[];
versionCreatedAt: string;
versionAuthor: string;
channelName: string;
channelColor: string;
reviews: Review[];
}
}

View File

@ -0,0 +1,75 @@
declare module 'hangar-internal' {
import { Named, Role, User } from 'hangar-api';
import { ProjectOwner, Table } from 'hangar-internal';
import { RoleCategory } from '~/types/enums';
interface HangarNotification {
id: number;
action: string;
message: string[];
read: boolean;
originUserName: string | null;
type: string;
}
interface Invite {
roleTableId: number;
role: Role;
type: 'project' | 'organization';
name: string;
url: string;
accepted?: boolean;
}
interface Invites {
project: Invite[];
organization: Invite[];
}
interface HeaderData {
globalPermission: string;
unreadNotifications: number;
unansweredInvites: number;
unresolvedFlags: number;
projectApprovals: number;
reviewQueueCount: number;
organizationCount: number;
}
interface HangarUser extends User, Table {
headerData: HeaderData;
readPrompts: number[];
language: string;
}
interface UserTable extends Table, Named {
tagline: string;
joinDate: string;
readPrompts: number[];
locked: boolean;
language: string;
}
interface RoleTable extends Table {
accepted: boolean;
principalId: number;
userId: number;
role: Role;
}
interface JoinableMember {
user: UserTable;
role: RoleTable;
hidden: boolean;
}
interface Joinable {
owner: ProjectOwner;
roleCategory: RoleCategory;
members: JoinableMember[];
}
interface Organization extends Joinable {
id: number;
}
}

View File

@ -0,0 +1,40 @@
declare module 'hangar-internal' {
import { DependencyVersion, FileInfo, Named, ProjectNamespace, Version } from 'hangar-api';
import { Table } from 'hangar-internal';
import { Platform } from '~/types/enums';
interface PlatformDependency {
name: string;
required: boolean;
namespace: ProjectNamespace | null;
externalUrl: string | null;
}
interface PendingVersion extends DependencyVersion {
platformDependencies: Record<Platform, string[]>;
versionString: string | null;
description: string | null;
fileInfo: FileInfo;
externalUrl: string | null;
channelName: string;
channelColor: string;
channelNonReviewed: boolean;
forumSync: boolean;
unstable: boolean;
recommended: boolean;
isFile: boolean;
}
interface ProjectChannel extends Named, Partial<Table> {
color: string;
nonReviewed: boolean;
editable: boolean;
temp?: boolean;
versionCount: number;
}
interface HangarVersion extends Version {
id: number;
approvedBy?: string;
}
}

View File

@ -0,0 +1,5 @@
declare module 'swagger-ui' {
import { SwaggerUIBundle } from 'swagger-ui-dist';
export default SwaggerUIBundle;
}

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "es2017",
"lib": ["DOM", "ESNext"],
"strict": true,
"esModuleInterop": true,
"incremental": true,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": true,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"types": [
"vite/client",
"vite-plugin-pages/client",
"vite-plugin-vue-layouts/client"
],
"paths": {
"~/*": ["src/*"]
}
},
"exclude": ["dist", "node_modules"]
}

173
frontend-new/vite.config.ts Normal file
View File

@ -0,0 +1,173 @@
import path from "path";
import { defineConfig } from "vite";
import Vue from "@vitejs/plugin-vue";
import Pages from "vite-plugin-pages";
import Layouts from "vite-plugin-vue-layouts";
import AutoImport from 'unplugin-auto-import/vite'
import Components from "unplugin-vue-components/vite";
import Icons from "unplugin-icons/vite";
import IconsResolver from "unplugin-icons/resolver";
import Markdown from "vite-plugin-md";
import WindiCSS from "vite-plugin-windicss";
import { VitePWA } from "vite-plugin-pwa";
import VueI18n from "@intlify/vite-plugin-vue-i18n";
import Prism from "markdown-it-prism";
import LinkAttributes from 'markdown-it-link-attributes'
import viteSSR from "vite-ssr/plugin";
import EslintPlugin from 'vite-plugin-eslint';
const proxyHost = process.env.proxyHost || 'http://localhost:8080';
const authHost = process.env.authHost || 'http://localhost:3001';
export default defineConfig({
resolve: {
alias: {
"~/": `${path.resolve(__dirname, "src")}/`,
},
},
plugins: [
viteSSR(),
Vue({
include: [/\.vue$/, /\.md$/],
}),
// https://github.com/hannoeru/vite-plugin-pages
Pages({
extensions: ["vue", "md"],
}),
// https://github.com/JohnCampionJr/vite-plugin-vue-layouts
Layouts(),
// https://github.com/antfu/vite-plugin-md
Markdown({
wrapperClasses: "prose prose-sm m-auto text-left",
headEnabled: true, // This relies on useHead
markdownItSetup(md) {
// https://prismjs.com/
md.use(Prism);
md.use(LinkAttributes, {
pattern: /^https?:\/\//,
attrs: {
target: '_blank',
rel: 'noopener',
},
});
},
}),
// https://github.com/antfu/unplugin-auto-import
AutoImport({
imports: [
'vue',
'vue-router',
'vue-i18n',
'@vueuse/head',
'@vueuse/core',
'pinia',
{
'axios': [['default', 'axios'] ],
},
],
dts: 'src/types/auto-imports.d.ts',
eslintrc: {
enabled: true,
},
}),
// https://github.com/antfu/vite-plugin-components
Components({
// allow auto load markdown components under `./src/components/`
extensions: ["vue", "md"],
// allow auto import and register components used in markdown
include: [/\.vue$/, /\.vue\?vue/, /\.md$/],
// auto import icons
resolvers: [
// https://github.com/antfu/vite-plugin-icons
IconsResolver({
componentPrefix: "",
// enabledCollections: ['carbon']
}),
],
dts: 'src/types/components.d.ts',
}),
// https://github.com/antfu/vite-plugin-icons
Icons({
autoInstall: true
}),
// https://github.com/antfu/vite-plugin-windicss
WindiCSS({
safelist: "prose prose-sm m-auto",
}),
// https://github.com/antfu/vite-plugin-pwa
VitePWA({
manifest: {
name: "Hangar | PaperMC",
short_name: "Hangar",
description: "Plugin repository for Paper plugins and more!",
theme_color: "#ffffff",
icons: [
{
src: "/pwa-192x192.png",
sizes: "192x192",
type: "image/png",
},
{
src: "/pwa-512x512.png",
sizes: "512x512",
type: "image/png",
},
{
src: "/pwa-512x512.png",
sizes: "512x512",
type: "image/png",
purpose: "any maskable",
},
],
},
}),
// https://github.com/intlify/vite-plugin-vue-i18n
VueI18n({
include: [path.resolve(__dirname, "src/i18n/translations/**")],
}),
EslintPlugin({
fix: true
}),
],
optimizeDeps: {
include: ['vue', 'vue-router', '@vueuse/core', '@vueuse/head'],
exclude: ['vue-demi'],
},
server: {
proxy: {
// backend
"/api/": proxyHost,
"/signup": proxyHost,
"/login": proxyHost,
"/logout": proxyHost,
"/handle-logout": proxyHost,
"/refresh": proxyHost,
"/invalidate": proxyHost,
"/v2/api-docs/": proxyHost,
"/robots.txt": proxyHost,
"/sitemap.xml": proxyHost,
"/global-sitemap.xml": proxyHost,
"/*/sitemap.xml": proxyHost,
"/statusz": proxyHost,
// auth
"/avatar": authHost,
"/oauth/logout": authHost,
"/oauth2": authHost,
},
}
});

View File

@ -0,0 +1,41 @@
import { defineConfig } from 'vite-plugin-windicss'
import colors from 'windicss/colors'
import typography from 'windicss/plugin/typography'
export default defineConfig({
darkMode: 'class',
attributify: true,
plugins: [
typography(),
],
theme: {
extend: {
typography: {
DEFAULT: {
css: {
maxWidth: '65ch',
color: 'inherit',
a: {
'color': 'inherit',
'opacity': 0.75,
'fontWeight': '500',
'textDecoration': 'underline',
'&:hover': {
opacity: 1,
color: colors.teal[600],
},
},
b: { color: 'inherit' },
strong: { color: 'inherit' },
em: { color: 'inherit' },
h1: { color: 'inherit' },
h2: { color: 'inherit' },
h3: { color: 'inherit' },
h4: { color: 'inherit' },
code: { color: 'inherit' },
},
},
},
},
},
})