mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 11:30:12 +08:00
Feat typescript frontend (#107)
There is still tons of errors in e2e. Fix it after merging.
This commit is contained in:
40 files changed
+410
-267
No files matched your search
@@ -48,6 +48,18 @@ module.exports = {
|
|||||||
'no-use-before-define': 'off',
|
'no-use-before-define': 'off',
|
||||||
|
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
args: 'all',
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
caughtErrors: 'all',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
ignoreRestSiblings: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
// Allow missing file extensions
|
// Allow missing file extensions
|
||||||
'import/extensions': [
|
'import/extensions': [
|
||||||
|
|||||||
+1
-1
@@ -8,6 +8,6 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+2
-1
@@ -2,7 +2,6 @@
|
|||||||
"name": "app",
|
"name": "app",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
||||||
@@ -18,6 +17,7 @@
|
|||||||
"@linux-odyssey/constants": "*",
|
"@linux-odyssey/constants": "*",
|
||||||
"@linux-odyssey/file-graph": "*",
|
"@linux-odyssey/file-graph": "*",
|
||||||
"@linux-odyssey/utils": "*",
|
"@linux-odyssey/utils": "*",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"dompurify": "^3.0.6",
|
"dompurify": "^3.0.6",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"xterm-addon-fit": "^0.7.0"
|
"xterm-addon-fit": "^0.7.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/validator": "^13.11.9",
|
||||||
"@vitejs/plugin-vue": "^4.5.2",
|
"@vitejs/plugin-vue": "^4.5.2",
|
||||||
"@vue/compiler-sfc": "^3.3.13",
|
"@vue/compiler-sfc": "^3.3.13",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { RouterLink, useRouter } from 'vue-router'
|
import { RouterLink, useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
@@ -27,7 +27,7 @@ const password = ref('')
|
|||||||
const errorMessage = ref('')
|
const errorMessage = ref('')
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const availableMethods = ref({})
|
const availableMethods = ref<{ [key: string]: boolean }>({})
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const res = await api.get('/auth/available-methods')
|
const res = await api.get('/auth/available-methods')
|
||||||
availableMethods.value = res.data
|
availableMethods.value = res.data
|
||||||
@@ -44,7 +44,7 @@ const success = () => {
|
|||||||
router.push({ name: 'map' })
|
router.push({ name: 'map' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const error = (err) => {
|
const error = (err: string) => {
|
||||||
errorMessage.value = err
|
errorMessage.value = err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,10 +114,7 @@ const handleChange = () => {
|
|||||||
:icon="['fab', 'google']"
|
:icon="['fab', 'google']"
|
||||||
class="text-text-secondary px-3 text-lg"
|
class="text-text-secondary px-3 text-lg"
|
||||||
/>
|
/>
|
||||||
<span class="font-medium">
|
<span class="font-medium"> 以 Google 繼續 </span>
|
||||||
<!-- Continue with Google -->
|
|
||||||
以 Google 繼續
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
id="GitHubLogin"
|
id="GitHubLogin"
|
||||||
@@ -129,32 +126,17 @@ const handleChange = () => {
|
|||||||
:icon="['fab', 'github']"
|
:icon="['fab', 'github']"
|
||||||
class="text-text-secondary px-3 text-lg"
|
class="text-text-secondary px-3 text-lg"
|
||||||
/>
|
/>
|
||||||
<span class="font-medium">
|
<span class="font-medium"> 以 GitHub 繼續 </span>
|
||||||
<!-- Continue with GitHub -->
|
|
||||||
以 GitHub 繼續
|
|
||||||
</span>
|
|
||||||
</a>
|
</a>
|
||||||
<p
|
<p
|
||||||
id="or"
|
id="or"
|
||||||
class="text-text-secondary flex justify-center mt-3"
|
class="text-text-secondary flex justify-center mt-3"
|
||||||
v-if="type === 'login' || type === 'register'"
|
v-if="type === 'login' || type === 'register'"
|
||||||
>
|
>
|
||||||
<!-- or -->
|
|
||||||
或
|
或
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<!-- <input
|
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
ref="usernameInput"
|
|
||||||
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
|
|
||||||
:placeholder="type === 'login' ? 'Email / Username' : 'Username'"
|
|
||||||
v-model="username"
|
|
||||||
required
|
|
||||||
@input="handleChange()"
|
|
||||||
autocomplete="username"
|
|
||||||
/> -->
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="username"
|
id="username"
|
||||||
@@ -176,17 +158,6 @@ const handleChange = () => {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-6" v-if="type === 'register'">
|
<div class="mb-6" v-if="type === 'register'">
|
||||||
<!-- <input
|
|
||||||
type="email"
|
|
||||||
id="email"
|
|
||||||
ref="emailInput"
|
|
||||||
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
|
|
||||||
placeholder="Email"
|
|
||||||
v-model="email"
|
|
||||||
required
|
|
||||||
@input="handleChange()"
|
|
||||||
autocomplete="email"
|
|
||||||
/> -->
|
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
id="email"
|
id="email"
|
||||||
@@ -200,15 +171,6 @@ const handleChange = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-6" v-if="type === 'login' || type === 'register'">
|
<div class="mb-6" v-if="type === 'login' || type === 'register'">
|
||||||
<!-- <input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
|
|
||||||
placeholder="Password"
|
|
||||||
v-model="password"
|
|
||||||
required
|
|
||||||
@input="handleChange()"
|
|
||||||
/> -->
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
id="password"
|
id="password"
|
||||||
@@ -231,43 +193,29 @@ const handleChange = () => {
|
|||||||
class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full"
|
class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<span v-if="type === 'login'">
|
<span v-if="type === 'login'"> 登入 </span>
|
||||||
<!-- Log In -->
|
<span v-else> 註冊 </span>
|
||||||
登入
|
|
||||||
</span>
|
|
||||||
<span v-else>
|
|
||||||
<!-- Sign Up -->
|
|
||||||
註冊
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
|
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
|
||||||
<span id="CheckRegistered" v-if="type === 'register'">
|
<span id="CheckRegistered" v-if="type === 'register'">
|
||||||
<!-- Already have an account? -->
|
|
||||||
已經有帳號了嗎?
|
已經有帳號了嗎?
|
||||||
<RouterLink
|
<RouterLink
|
||||||
id="SwitchtoLogin"
|
id="SwitchtoLogin"
|
||||||
class="text-text-primary font-bold"
|
class="text-text-primary font-bold"
|
||||||
to="/login"
|
to="/login"
|
||||||
>
|
>
|
||||||
<u>
|
<u> 登入 </u>
|
||||||
<!-- Log in -->
|
|
||||||
登入
|
|
||||||
</u>
|
|
||||||
</RouterLink></span
|
</RouterLink></span
|
||||||
>
|
>
|
||||||
<span id="CheckRegistered" v-else-if="type === 'login'">
|
<span id="CheckRegistered" v-else-if="type === 'login'">
|
||||||
<!-- Don't have an account? -->
|
|
||||||
還沒有帳號嗎?
|
還沒有帳號嗎?
|
||||||
<RouterLink
|
<RouterLink
|
||||||
id="SwitchtoRegister"
|
id="SwitchtoRegister"
|
||||||
class="text-text-primary font-bold"
|
class="text-text-primary font-bold"
|
||||||
to="/register"
|
to="/register"
|
||||||
>
|
>
|
||||||
<u>
|
<u> 註冊 </u>
|
||||||
<!-- Sign up -->
|
|
||||||
註冊
|
|
||||||
</u>
|
|
||||||
</RouterLink></span
|
</RouterLink></span
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">
|
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
|
||||||
<!-- Command Cheatsheets -->
|
|
||||||
指令之書
|
|
||||||
</h1>
|
|
||||||
<div id="cheatsheets">
|
<div id="cheatsheets">
|
||||||
<p class="text-text-secondary">
|
<p class="text-text-secondary">--指令列表--</p>
|
||||||
<!-- --Command List-- -->
|
|
||||||
-- --指令列表-- --
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
@@ -47,7 +47,6 @@ setTimeout(() => {
|
|||||||
id="QuestCompleted"
|
id="QuestCompleted"
|
||||||
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||||
>
|
>
|
||||||
<!-- Quest Completed! -->
|
|
||||||
關卡完成!
|
關卡完成!
|
||||||
</h2>
|
</h2>
|
||||||
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
|
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
|
||||||
|
|||||||
@@ -1,55 +1,4 @@
|
|||||||
<template>
|
<script setup lang="ts">
|
||||||
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
|
|
||||||
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
|
||||||
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'file-invoice']"
|
|
||||||
class="text-text-primary"
|
|
||||||
/>
|
|
||||||
<p class="text-text-primary inline ml-1">
|
|
||||||
<!-- Survey -->
|
|
||||||
問卷
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</button>
|
|
||||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['far', 'circle-question']"
|
|
||||||
class="text-text-disabled"
|
|
||||||
/>
|
|
||||||
<p class="text-text-disabled inline ml-1">
|
|
||||||
<!-- Solution -->
|
|
||||||
解答
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', 'arrow-rotate-left']"
|
|
||||||
class="text-text-primary"
|
|
||||||
/>
|
|
||||||
<p class="text-text-primary inline ml-1">
|
|
||||||
<!-- Reset -->
|
|
||||||
重來
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id="continue"
|
|
||||||
class="bg-bg-secondary rounded-lg p-2"
|
|
||||||
@click="continuePlay"
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['far', 'circle-right']"
|
|
||||||
class="text-text-primary"
|
|
||||||
/>
|
|
||||||
<p class="text-text-primary inline ml-1">
|
|
||||||
<!-- Continue -->
|
|
||||||
繼續
|
|
||||||
</p>
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { createSession } from '../store/session'
|
import { createSession } from '../store/session'
|
||||||
@@ -64,9 +13,47 @@ const continuePlay = async () => {
|
|||||||
try {
|
try {
|
||||||
router.push({ name: 'map' })
|
router.push({ name: 'map' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// useToast().error('Failed to load map')
|
|
||||||
useToast().error('無法讀取地圖')
|
useToast().error('無法讀取地圖')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
|
||||||
|
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
|
||||||
|
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'file-invoice']"
|
||||||
|
class="text-text-primary"
|
||||||
|
/>
|
||||||
|
<p class="text-text-primary inline ml-1">問卷</p>
|
||||||
|
</a>
|
||||||
|
</button>
|
||||||
|
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['far', 'circle-question']"
|
||||||
|
class="text-text-disabled"
|
||||||
|
/>
|
||||||
|
<p class="text-text-disabled inline ml-1">解答</p>
|
||||||
|
</button>
|
||||||
|
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'arrow-rotate-left']"
|
||||||
|
class="text-text-primary"
|
||||||
|
/>
|
||||||
|
<p class="text-text-primary inline ml-1">重來</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="continue"
|
||||||
|
class="bg-bg-secondary rounded-lg p-2"
|
||||||
|
@click="continuePlay"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['far', 'circle-right']"
|
||||||
|
class="text-text-primary"
|
||||||
|
/>
|
||||||
|
<p class="text-text-primary inline ml-1">繼續</p>
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
@@ -1,26 +1,34 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
|
|
||||||
const cw = ref(0)
|
const cw = ref(0)
|
||||||
const ch = ref(0)
|
const ch = ref(0)
|
||||||
const fallingCharArr = ref([])
|
const fallingCharArr = ref<
|
||||||
|
{ value: string; x: number; y: number; speed: number }[]
|
||||||
|
>([])
|
||||||
const fontSize = ref(16)
|
const fontSize = ref(16)
|
||||||
const maxColums = ref(0)
|
const maxColums = ref(0)
|
||||||
const charArr = ['0', '1', '', '', '']
|
const charArr = ['0', '1', '', '', '']
|
||||||
const ctx = ref(null)
|
const ctx = ref<CanvasRenderingContext2D | null>(null)
|
||||||
|
|
||||||
const randomInt = (min, max) => {
|
const randomInt = (min: number, max: number) => {
|
||||||
return Math.floor(Math.random() * (max - min) + min)
|
return Math.floor(Math.random() * (max - min) + min)
|
||||||
}
|
}
|
||||||
|
|
||||||
const randomFloat = (min, max) => {
|
const randomFloat = (min: number, max: number) => {
|
||||||
return Math.random() * (max - min) + min
|
return Math.random() * (max - min) + min
|
||||||
}
|
}
|
||||||
|
|
||||||
const draw = (point) => {
|
const draw = (point: {
|
||||||
|
value: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
speed: number
|
||||||
|
}) => {
|
||||||
point.value = charArr[randomInt(0, charArr.length)].toUpperCase()
|
point.value = charArr[randomInt(0, charArr.length)].toUpperCase()
|
||||||
if (point.y < 0) point.speed = randomFloat(10, 20)
|
if (point.y < 0) point.speed = randomFloat(10, 20)
|
||||||
|
|
||||||
|
if (ctx.value === null) return
|
||||||
ctx.value.fillStyle = '#0F0'
|
ctx.value.fillStyle = '#0F0'
|
||||||
ctx.value.font = `${fontSize.value}px san-serif`
|
ctx.value.font = `${fontSize.value}px san-serif`
|
||||||
ctx.value.fillText(point.value, point.x, point.y)
|
ctx.value.fillText(point.value, point.x, point.y)
|
||||||
@@ -33,6 +41,7 @@ const draw = (point) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
|
if (ctx.value === null) return
|
||||||
ctx.value.fillStyle = 'rgba(0,0,0,0.05)'
|
ctx.value.fillStyle = 'rgba(0,0,0,0.05)'
|
||||||
ctx.value.fillRect(0, 0, cw.value, ch.value)
|
ctx.value.fillRect(0, 0, cw.value, ch.value)
|
||||||
|
|
||||||
@@ -48,7 +57,7 @@ onMounted(() => {
|
|||||||
ch.value = document.documentElement.clientHeight
|
ch.value = document.documentElement.clientHeight
|
||||||
maxColums.value = cw.value / fontSize.value
|
maxColums.value = cw.value / fontSize.value
|
||||||
|
|
||||||
const canvas = document.querySelector('canvas')
|
const canvas = document.querySelector('canvas') as HTMLCanvasElement
|
||||||
ctx.value = canvas.getContext('2d')
|
ctx.value = canvas.getContext('2d')
|
||||||
|
|
||||||
canvas.width = cw.value
|
canvas.width = cw.value
|
||||||
@@ -58,6 +67,8 @@ onMounted(() => {
|
|||||||
fallingCharArr.value.push({
|
fallingCharArr.value.push({
|
||||||
x: i * fontSize.value,
|
x: i * fontSize.value,
|
||||||
y: randomFloat(-500, 0),
|
y: randomFloat(-500, 0),
|
||||||
|
value: '',
|
||||||
|
speed: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
node: {
|
node: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { defineProps, onMounted } from 'vue'
|
import { defineProps, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
@@ -21,7 +21,6 @@ const handleLogout = async () => {
|
|||||||
reset()
|
reset()
|
||||||
router.push({ name: 'login' })
|
router.push({ name: 'login' })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// toast.error('Failed to logout')
|
|
||||||
toast.error('登出失敗')
|
toast.error('登出失敗')
|
||||||
console.error(err)
|
console.error(err)
|
||||||
}
|
}
|
||||||
@@ -38,7 +37,6 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
await loadUserProfile()
|
await loadUserProfile()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// toast.error('Failed to load user profile')
|
|
||||||
toast.error('無法讀取使用者資料')
|
toast.error('無法讀取使用者資料')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import sessionStore from '../store/session'
|
import sessionStore from '../store/session'
|
||||||
import MarkdownText from './MarkdownText.vue'
|
import MarkdownText from './MarkdownText.vue'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { computed, defineProps } from 'vue'
|
import { computed, defineProps } from 'vue'
|
||||||
import markdown2HTML from '../utils/markdown'
|
import markdown2HTML from '../utils/markdown'
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- eslint-disable no-console -->
|
<!-- eslint-disable no-console -->
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
@@ -50,7 +50,7 @@ async function getProgress() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const genOption = (nodes, edges) => ({
|
const genOption = (nodes: any, edges: any) => ({
|
||||||
renderer: 'svg',
|
renderer: 'svg',
|
||||||
tooltip: {},
|
tooltip: {},
|
||||||
animationDurationUpdate: 1500,
|
animationDurationUpdate: 1500,
|
||||||
@@ -94,7 +94,7 @@ const genOption = (nodes, edges) => ({
|
|||||||
curveness: 0.08,
|
curveness: 0.08,
|
||||||
},
|
},
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: (params) => {
|
color: (params: any) => {
|
||||||
const {
|
const {
|
||||||
data: { completed, unlocked },
|
data: { completed, unlocked },
|
||||||
} = params
|
} = params
|
||||||
@@ -111,7 +111,7 @@ const genOption = (nodes, edges) => ({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
function getOption(quests, progress) {
|
function getOption(quests: any, progress: any) {
|
||||||
const dag = new DAG(quests)
|
const dag = new DAG(quests)
|
||||||
|
|
||||||
const nodes = dag.getNodes().map((node) => ({
|
const nodes = dag.getNodes().map((node) => ({
|
||||||
@@ -120,9 +120,9 @@ function getOption(quests, progress) {
|
|||||||
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
||||||
x: marginY * node.layer * 2,
|
x: marginY * node.layer * 2,
|
||||||
completed: progress[node._id]?.completed || false,
|
completed: progress[node._id]?.completed || false,
|
||||||
unlocked: node.requirements.every((req) => progress[req]?.completed),
|
unlocked: node.requirements.every((req: any) => progress[req]?.completed),
|
||||||
}))
|
}))
|
||||||
const lineAppearence = (node) => {
|
const lineAppearence = (node: any) => {
|
||||||
try {
|
try {
|
||||||
const { unlocked } = node
|
const { unlocked } = node
|
||||||
return {
|
return {
|
||||||
@@ -134,7 +134,7 @@ function getOption(quests, progress) {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const edges = dag.getEdgesArray().map((edge) => ({
|
const edges = dag.getEdgesArray().map((edge: any) => ({
|
||||||
source: edge[0],
|
source: edge[0],
|
||||||
target: edge[1],
|
target: edge[1],
|
||||||
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
|
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
|
||||||
@@ -145,10 +145,10 @@ function getOption(quests, progress) {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
function initChart(option) {
|
function initChart(option: any) {
|
||||||
chartInstance = init(chartContainer.value)
|
chartInstance = init(chartContainer.value)
|
||||||
chartInstance.setOption(option)
|
chartInstance.setOption(option)
|
||||||
chartInstance.on('click', (params) => {
|
chartInstance.on('click', (params: any) => {
|
||||||
const {
|
const {
|
||||||
data: { id, unlocked },
|
data: { id, unlocked },
|
||||||
} = params
|
} = params
|
||||||
@@ -156,7 +156,6 @@ function initChart(option) {
|
|||||||
if (unlocked) {
|
if (unlocked) {
|
||||||
router.push({ name: 'game', params: { questId: id } })
|
router.push({ name: 'game', params: { questId: id } })
|
||||||
} else {
|
} else {
|
||||||
// toast.warning('You have not completed the previous quest yet!')
|
|
||||||
toast.warning('你還沒完成前一個關卡!')
|
toast.warning('你還沒完成前一個關卡!')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -181,7 +180,6 @@ onMounted(async () => {
|
|||||||
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
||||||
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
||||||
>
|
>
|
||||||
<!-- Get through your linux journey! -->
|
|
||||||
踏上你的Linux冒險之旅吧!
|
踏上你的Linux冒險之旅吧!
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex flex-wrap absolute w-full h-full z-1">
|
<div class="flex flex-wrap absolute w-full h-full z-1">
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import MarkdownText from './MarkdownText.vue'
|
import MarkdownText from './MarkdownText.vue'
|
||||||
import sessionStore from '../store/session'
|
import sessionStore from '../store/session'
|
||||||
|
import { Task } from '../types'
|
||||||
|
|
||||||
const tasks = computed(() => {
|
const tasks = computed(() => {
|
||||||
return sessionStore.session.tasks.map((task) => {
|
return sessionStore.session.tasks.map((task: Task) => {
|
||||||
const prefix = task.completed ? '✓' : '➤'
|
const prefix = task.completed ? '✓' : '➤'
|
||||||
const color = task.completed ? 'text-text-primary' : 'text-text'
|
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||||
return {
|
return {
|
||||||
@@ -29,10 +30,7 @@ const tasks = computed(() => {
|
|||||||
:content="sessionStore.quest.instruction"
|
:content="sessionStore.quest.instruction"
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<p id="tasks" class="text-text">
|
<p id="tasks" class="text-text">任務:</p>
|
||||||
<!-- Tasks: -->
|
|
||||||
任務:
|
|
||||||
</p>
|
|
||||||
<ul v-if="sessionStore.session">
|
<ul v-if="sessionStore.session">
|
||||||
<li v-for="task in tasks" :key="task.id">
|
<li v-for="task in tasks" :key="task.id">
|
||||||
<MarkdownText :class="task.class" :content="task.content" />
|
<MarkdownText :class="task.class" :content="task.content" />
|
||||||
|
|||||||
@@ -1,25 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import { useTerminal } from '../store/session'
|
||||||
|
|
||||||
|
const terminal = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const term = useTerminal()
|
||||||
|
if (!terminal.value) return
|
||||||
|
term.mount(terminal.value)
|
||||||
|
window.addEventListener('resize', () => term.resizeScreen())
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
|
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
|
||||||
<button id="Terminal" class="text-text h-[5%]">
|
<button id="Terminal" class="text-text h-[5%]">終端機</button>
|
||||||
<!-- Terminal -->
|
|
||||||
終端機
|
|
||||||
</button>
|
|
||||||
<div
|
<div
|
||||||
id="terminal"
|
id="terminal"
|
||||||
ref="terminal"
|
ref="terminal"
|
||||||
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
|
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
|
||||||
></div>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
import { useTerminal } from '../store/session'
|
|
||||||
|
|
||||||
const terminal = ref(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
const term = useTerminal()
|
|
||||||
term.mount(terminal.value)
|
|
||||||
window.addEventListener('resize', () => term.resizeScreen())
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import FileNode from './FileNode.vue'
|
import FileNode from './FileNode.vue'
|
||||||
import sessionStore from '../store/session.js'
|
import sessionStore from '../store/session'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted } from 'vue'
|
||||||
import TreeChart from './TreeChart.vue'
|
import TreeChart from './TreeChart.vue'
|
||||||
|
|
||||||
const treeRef = ref(null)
|
const treeRef = ref<HTMLElement | null>(null)
|
||||||
let pos = { top: 0, left: 0, x: 0, y: 0 }
|
let pos = { top: 0, left: 0, x: 0, y: 0 }
|
||||||
|
|
||||||
function mouseMoveHandler(e) {
|
function mouseMoveHandler(e: MouseEvent) {
|
||||||
const dx = e.clientX - pos.x
|
const dx = e.clientX - pos.x
|
||||||
const dy = e.clientY - pos.y
|
const dy = e.clientY - pos.y
|
||||||
|
|
||||||
|
if (treeRef.value === null) return
|
||||||
treeRef.value.scrollTop = pos.top - dy
|
treeRef.value.scrollTop = pos.top - dy
|
||||||
treeRef.value.scrollLeft = pos.left - dx
|
treeRef.value.scrollLeft = pos.left - dx
|
||||||
}
|
}
|
||||||
|
|
||||||
function mouseUpHandler() {
|
function mouseUpHandler() {
|
||||||
|
if (treeRef.value === null) return
|
||||||
treeRef.value.style.cursor = 'grab'
|
treeRef.value.style.cursor = 'grab'
|
||||||
treeRef.value.style.removeProperty('user-select')
|
treeRef.value.style.removeProperty('user-select')
|
||||||
|
|
||||||
@@ -21,7 +23,8 @@ function mouseUpHandler() {
|
|||||||
document.removeEventListener('mouseup', mouseUpHandler)
|
document.removeEventListener('mouseup', mouseUpHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
function mouseDownHandler(e) {
|
function mouseDownHandler(e: MouseEvent) {
|
||||||
|
if (treeRef.value === null) return
|
||||||
treeRef.value.style.cursor = 'grabbing'
|
treeRef.value.style.cursor = 'grabbing'
|
||||||
treeRef.value.style.userSelect = 'none'
|
treeRef.value.style.userSelect = 'none'
|
||||||
|
|
||||||
@@ -37,6 +40,7 @@ function mouseDownHandler(e) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (treeRef.value === null) return
|
||||||
treeRef.value.style.cursor = 'grab'
|
treeRef.value.style.cursor = 'grab'
|
||||||
treeRef.value.addEventListener('mousedown', mouseDownHandler)
|
treeRef.value.addEventListener('mousedown', mouseDownHandler)
|
||||||
})
|
})
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -57,7 +57,7 @@ const router = createRouter({
|
|||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, _from, next) => {
|
||||||
const loggedIn = await isLoggedIn()
|
const loggedIn = await isLoggedIn()
|
||||||
// Check if the route requires authentication
|
// Check if the route requires authentication
|
||||||
if (to.meta.requiresAuth && !loggedIn) {
|
if (to.meta.requiresAuth && !loggedIn) {
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import { FileGraph } from '@linux-odyssey/file-graph'
|
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
|
||||||
|
// eslint-disable-next-line import/no-relative-packages
|
||||||
|
import { IQuest } from '../../../packages/models/src/quest'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import Socket from '../utils/socket'
|
import Socket from '../utils/socket'
|
||||||
import SocketTerminal from '../utils/terminal'
|
import SocketTerminal from '../utils/terminal'
|
||||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
||||||
|
import { Session, StageResponse } from '../types'
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
@@ -15,6 +18,7 @@ function newSession() {
|
|||||||
discovered: false,
|
discovered: false,
|
||||||
})
|
})
|
||||||
return {
|
return {
|
||||||
|
_id: '',
|
||||||
status: 'inactive',
|
status: 'inactive',
|
||||||
graph,
|
graph,
|
||||||
pwd: '',
|
pwd: '',
|
||||||
@@ -24,7 +28,13 @@ function newSession() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = reactive({
|
interface Store {
|
||||||
|
session: Session
|
||||||
|
questId: string
|
||||||
|
quest: IQuest | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = reactive<Store>({
|
||||||
session: newSession(),
|
session: newSession(),
|
||||||
questId: '',
|
questId: '',
|
||||||
quest: null,
|
quest: null,
|
||||||
@@ -33,14 +43,14 @@ const store = reactive({
|
|||||||
const socket = new Socket()
|
const socket = new Socket()
|
||||||
const term = new SocketTerminal(40, 80)
|
const term = new SocketTerminal(40, 80)
|
||||||
|
|
||||||
function setQuest(questId) {
|
function setQuest(questId: string) {
|
||||||
return api.get(`/quests/${questId}`).then((res) => {
|
return api.get(`/quests/${questId}`).then((res) => {
|
||||||
store.questId = res.data._id
|
store.questId = res.data._id
|
||||||
store.quest = res.data
|
store.quest = res.data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setSession(session) {
|
async function setSession(session: Session) {
|
||||||
try {
|
try {
|
||||||
console.log('Setting session...', session)
|
console.log('Setting session...', session)
|
||||||
store.session = session
|
store.session = session
|
||||||
@@ -48,9 +58,13 @@ async function setSession(session) {
|
|||||||
term.reset()
|
term.reset()
|
||||||
await socket.connect(session)
|
await socket.connect(session)
|
||||||
term.focus()
|
term.focus()
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
if (err instanceof Error) {
|
||||||
toast.error(err.message)
|
toast.error(err.message)
|
||||||
|
} else {
|
||||||
|
toast.error('Failed to connect to the session. Please try again later.')
|
||||||
|
}
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +81,7 @@ export async function createSession() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getActiveSession(questId) {
|
async function getActiveSession(questId: string) {
|
||||||
try {
|
try {
|
||||||
const res = await api.post('/sessions/active', { questId })
|
const res = await api.post('/sessions/active', { questId })
|
||||||
await setSession(res.data)
|
await setSession(res.data)
|
||||||
@@ -78,7 +92,7 @@ async function getActiveSession(questId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateGraph(event) {
|
function updateGraph(event: { discover: FileObject[]; pwd: string }) {
|
||||||
if (event.discover) {
|
if (event.discover) {
|
||||||
store.session.graph.discover(event.discover)
|
store.session.graph.discover(event.discover)
|
||||||
}
|
}
|
||||||
@@ -87,10 +101,10 @@ function updateGraph(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function newResponse(response) {
|
function newResponse(response: StageResponse) {
|
||||||
console.log('newResponse', response, 'session', store.session)
|
console.log('newResponse', response, 'session', store.session)
|
||||||
store.session.responses.push(response.responses)
|
store.session.responses.push(...response.responses)
|
||||||
store.session.hints.push(response.hints)
|
store.session.hints.push(...response.hints)
|
||||||
store.session.tasks = response.tasks
|
store.session.tasks = response.tasks
|
||||||
store.session.status = response.status
|
store.session.status = response.status
|
||||||
}
|
}
|
||||||
@@ -106,7 +120,7 @@ export function reset() {
|
|||||||
store.quest = null
|
store.quest = null
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function init(questId) {
|
export async function init(questId: string) {
|
||||||
if (!questId) throw new Error('No quest ID provided')
|
if (!questId) throw new Error('No quest ID provided')
|
||||||
reset()
|
reset()
|
||||||
try {
|
try {
|
||||||
@@ -124,16 +138,16 @@ export async function init(questId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setup() {
|
function setup() {
|
||||||
socket.on('terminal', (data) => {
|
socket.on('terminal', (data: string) => {
|
||||||
term.write(data)
|
term.write(data)
|
||||||
})
|
})
|
||||||
term.onData((data) => {
|
term.onData((data: string) => {
|
||||||
socket.emit('terminal', data)
|
socket.emit('terminal', data)
|
||||||
})
|
})
|
||||||
socket.on('graph', (event) => {
|
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
||||||
updateGraph(event)
|
updateGraph(event)
|
||||||
})
|
})
|
||||||
socket.on('response', (response) => {
|
socket.on('response', (response: StageResponse) => {
|
||||||
newResponse(response)
|
newResponse(response)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ export async function loadUserProfile() {
|
|||||||
store.email = email
|
store.email = email
|
||||||
store.progress = progress
|
store.progress = progress
|
||||||
return store
|
return store
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
throw new Error(`Failed to load user profile: ${err.message}`)
|
throw new Error(`Failed to load user profile: ${err.message}`)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { FileGraph } from '@linux-odyssey/file-graph'
|
||||||
|
// eslint-disable-next-line import/no-relative-packages
|
||||||
|
import { IResponse } from '../../packages/models/src/response'
|
||||||
|
|
||||||
|
export interface Task {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
completed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StageResponse {
|
||||||
|
status: string
|
||||||
|
hints: string[]
|
||||||
|
tasks: Task[]
|
||||||
|
responses: IResponse[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Session {
|
||||||
|
_id: string
|
||||||
|
status: string
|
||||||
|
graph: FileGraph
|
||||||
|
pwd: string
|
||||||
|
hints: string[]
|
||||||
|
tasks: Task[]
|
||||||
|
responses: IResponse[]
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -12,7 +12,12 @@ export async function isLoggedIn() {
|
|||||||
return loggedIn
|
return loggedIn
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleRequestError(err) {
|
function handleRequestError(err: {
|
||||||
|
response: {
|
||||||
|
data?: any
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
}) {
|
||||||
if (!err.response) throw err // Not an axios error
|
if (!err.response) throw err // Not an axios error
|
||||||
const { status } = err.response
|
const { status } = err.response
|
||||||
if (status === 429) {
|
if (status === 429) {
|
||||||
@@ -41,14 +46,14 @@ function handleRequestError(err) {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
export const login = (username, password) => {
|
export const login = (username: string, password: string) => {
|
||||||
return api
|
return api
|
||||||
.post('/auth/login', { username, password })
|
.post('/auth/login', { username, password })
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(handleRequestError)
|
.catch(handleRequestError)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const register = (username, email, password) => {
|
export const register = (username: string, email: string, password: string) => {
|
||||||
return api
|
return api
|
||||||
.post('/auth/register', {
|
.post('/auth/register', {
|
||||||
username,
|
username,
|
||||||
@@ -59,7 +64,7 @@ export const register = (username, email, password) => {
|
|||||||
.catch(handleRequestError)
|
.catch(handleRequestError)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkUsername = async (username) => {
|
export const checkUsername = async (username: string) => {
|
||||||
return api
|
return api
|
||||||
.get('/auth/check-username', {
|
.get('/auth/check-username', {
|
||||||
params: { username },
|
params: { username },
|
||||||
@@ -68,7 +73,7 @@ export const checkUsername = async (username) => {
|
|||||||
.catch(handleRequestError)
|
.catch(handleRequestError)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const chooseUsername = async (username) => {
|
export const chooseUsername = async (username: string) => {
|
||||||
return api
|
return api
|
||||||
.post('/auth/register-from-session', { username })
|
.post('/auth/register-from-session', { username })
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
@@ -1,27 +1,28 @@
|
|||||||
/* eslint-disable max-classes-per-file */
|
/* eslint-disable max-classes-per-file */
|
||||||
export class TooManyRequestsError extends Error {
|
export class TooManyRequestsError extends Error {
|
||||||
constructor(message) {
|
constructor(message: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'TooManyRequestsError'
|
this.name = 'TooManyRequestsError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationError extends Error {
|
export class ValidationError extends Error {
|
||||||
constructor(message) {
|
constructor(message: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'ValidationError'
|
this.name = 'ValidationError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnauthorizedError extends Error {
|
export class UnauthorizedError extends Error {
|
||||||
constructor(message) {
|
constructor(message: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'UnauthorizedError'
|
this.name = 'UnauthorizedError'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoadQuestError extends Error {
|
export class LoadQuestError extends Error {
|
||||||
constructor(message, questId) {
|
questId: string
|
||||||
|
constructor(message: string, questId: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'LoadQuestError'
|
this.name = 'LoadQuestError'
|
||||||
this.questId = questId
|
this.questId = questId
|
||||||
@@ -29,7 +30,8 @@ export class LoadQuestError extends Error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class LoadSessionError extends Error {
|
export class LoadSessionError extends Error {
|
||||||
constructor(message, questId) {
|
questId: string
|
||||||
|
constructor(message: string, questId: string) {
|
||||||
super(message)
|
super(message)
|
||||||
this.name = 'LoadSessionError'
|
this.name = 'LoadSessionError'
|
||||||
this.questId = questId
|
this.questId = questId
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import DOMPurify from 'dompurify'
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
export default function markdown2HTML(markdown) {
|
export default function markdown2HTML(markdown: string) {
|
||||||
const html = marked.parse(markdown)
|
const html = marked.parse(markdown)
|
||||||
return DOMPurify.sanitize(html)
|
return DOMPurify.sanitize(html)
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
import io from 'socket.io-client'
|
import { io, Socket } from 'socket.io-client'
|
||||||
|
import { Session } from '../types'
|
||||||
|
|
||||||
|
type SocketCallback = (data: any) => void
|
||||||
|
|
||||||
class SocketWrapper {
|
class SocketWrapper {
|
||||||
|
socket: Socket | null
|
||||||
|
listeners: { event: string; callback: SocketCallback }[]
|
||||||
constructor() {
|
constructor() {
|
||||||
this.socket = null
|
this.socket = null
|
||||||
this.listeners = []
|
this.listeners = []
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(session) {
|
connect(session: Session) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
this.disconnect()
|
this.disconnect()
|
||||||
|
|
||||||
this.socket = io('', {
|
this.socket = io('', {
|
||||||
@@ -28,7 +33,7 @@ class SocketWrapper {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
on(event, callback) {
|
on(event: string, callback: SocketCallback) {
|
||||||
this.listeners.push({ event, callback })
|
this.listeners.push({ event, callback })
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
this.socket.on(event, callback)
|
this.socket.on(event, callback)
|
||||||
@@ -36,12 +41,14 @@ class SocketWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bindListeners() {
|
bindListeners() {
|
||||||
this.listeners.forEach(({ event, callback }) => {
|
this.listeners.forEach(
|
||||||
this.socket.on(event, callback)
|
({ event, callback }: { event: string; callback: SocketCallback }) => {
|
||||||
})
|
if (this.socket !== null) this.socket.on(event, callback)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(event, data) {
|
emit(event: string, data: any) {
|
||||||
if (!this.socket) {
|
if (!this.socket) {
|
||||||
console.warn('Socket is not connected!')
|
console.warn('Socket is not connected!')
|
||||||
return
|
return
|
||||||
@@ -3,7 +3,10 @@ import { FitAddon } from 'xterm-addon-fit'
|
|||||||
import 'xterm/css/xterm.css'
|
import 'xterm/css/xterm.css'
|
||||||
|
|
||||||
class SocketTerminal {
|
class SocketTerminal {
|
||||||
constructor(rows, cols) {
|
private term: Terminal
|
||||||
|
private fitAddon: FitAddon
|
||||||
|
|
||||||
|
constructor(rows: number, cols: number) {
|
||||||
this.term = new Terminal({
|
this.term = new Terminal({
|
||||||
rendererType: 'canvas', // 渲染类型
|
rendererType: 'canvas', // 渲染类型
|
||||||
rows, // 行数
|
rows, // 行数
|
||||||
@@ -24,29 +27,29 @@ class SocketTerminal {
|
|||||||
this.fitAddon.fit()
|
this.fitAddon.fit()
|
||||||
}
|
}
|
||||||
|
|
||||||
write(message) {
|
write(message: string): void {
|
||||||
this.term.write(message)
|
this.term.write(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
onData(callback) {
|
onData(callback: any): void {
|
||||||
this.term.onData(callback)
|
this.term.onData(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
focus() {
|
focus(): void {
|
||||||
this.term.focus()
|
this.term.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeScreen() {
|
resizeScreen(): void {
|
||||||
this.fitAddon.fit()
|
this.fitAddon.fit()
|
||||||
}
|
}
|
||||||
|
|
||||||
mount(reference) {
|
mount(reference: HTMLElement): void {
|
||||||
this.term.open(reference)
|
this.term.open(reference)
|
||||||
this.term.focus()
|
this.term.focus()
|
||||||
this.resizeScreen()
|
this.resizeScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
reset() {
|
reset(): void {
|
||||||
this.term.reset()
|
this.term.reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { isValidUsername } from '@linux-odyssey/utils'
|
import { isValidUsername } from '@linux-odyssey/utils'
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import Background from '../components/DynamicBackground.vue'
|
import Background from '../components/DynamicBackground.vue'
|
||||||
import { TooManyRequestsError, ValidationError } from '../utils/errors'
|
import { TooManyRequestsError, ValidationError } from '../utils/errors'
|
||||||
import { checkUsername, chooseUsername } from '../utils/auth'
|
import { checkUsername, chooseUsername } from '../utils/auth'
|
||||||
|
|
||||||
async function check({ username, error }) {
|
async function check({
|
||||||
|
username,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
username: string
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
error: (msg: string) => void
|
||||||
|
}) {
|
||||||
if (username) {
|
if (username) {
|
||||||
if (!isValidUsername(username)) {
|
if (!isValidUsername(username)) {
|
||||||
// error('Invalid username.')
|
|
||||||
error('無效的帳號名稱')
|
error('無效的帳號名稱')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -16,7 +22,6 @@ async function check({ username, error }) {
|
|||||||
await checkUsername(username)
|
await checkUsername(username)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TooManyRequestsError) {
|
if (err instanceof TooManyRequestsError) {
|
||||||
// error('Too many requests. Try again in 2 minutes.')
|
|
||||||
error('太多請求,兩分鐘後再試一次。')
|
error('太多請求,兩分鐘後再試一次。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -24,19 +29,26 @@ async function check({ username, error }) {
|
|||||||
error(err.message)
|
error(err.message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// error('Something went wrong. Please try again later.')
|
|
||||||
error('出了點問題,請再試一次。')
|
error('出了點問題,請再試一次。')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit({ username, success, error }) {
|
async function handleSubmit({
|
||||||
|
username,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
username: string
|
||||||
|
success: () => void
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
error: (msg: string) => void
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
await chooseUsername(username)
|
await chooseUsername(username)
|
||||||
success()
|
success()
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
if (err instanceof TooManyRequestsError) {
|
if (err instanceof TooManyRequestsError) {
|
||||||
// error('Too many requests. Try again in 2 minutes.')
|
|
||||||
error('太多請求,兩分鐘後再試一次。')
|
error('太多請求,兩分鐘後再試一次。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import sessionStore, { init } from '../store/session'
|
import sessionStore, { init } from '../store/session'
|
||||||
@@ -28,16 +28,12 @@ const toast = useToast()
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
await init(props.questId)
|
await init(props.questId)
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
if (err instanceof LoadQuestError) {
|
if (err instanceof LoadQuestError) {
|
||||||
// toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
|
|
||||||
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
|
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (err instanceof LoadSessionError) {
|
if (err instanceof LoadSessionError) {
|
||||||
// toast.error(
|
|
||||||
// `Failed to create session for quest: ${err.questId}. Please re-login and try again.`
|
|
||||||
// )
|
|
||||||
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
|
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,24 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useToast } from 'vue-toastification'
|
import { useToast } from 'vue-toastification'
|
||||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
|
|
||||||
const leaderboard = ref([])
|
const leaderboard = ref<
|
||||||
|
{
|
||||||
|
username: string
|
||||||
|
score: number
|
||||||
|
completedQuests: string[]
|
||||||
|
}[]
|
||||||
|
>([])
|
||||||
|
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
async function getLeaderboard() {
|
async function getLeaderboard() {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/leaderboard')
|
const res = await api.get('/leaderboard')
|
||||||
leaderboard.value = res.data
|
leaderboard.value = res.data
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
toast.error('讀取排行榜失敗')
|
toast.error('讀取排行榜失敗')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import Background from '../components/DynamicBackground.vue'
|
import Background from '../components/DynamicBackground.vue'
|
||||||
|
|
||||||
@@ -9,28 +9,35 @@ import {
|
|||||||
} from '../utils/errors'
|
} from '../utils/errors'
|
||||||
import { login } from '../utils/auth'
|
import { login } from '../utils/auth'
|
||||||
|
|
||||||
const handleLogin = async ({ username, password, success, error }) => {
|
const handleLogin = async ({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
success: () => void
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
error: (msg: string) => void
|
||||||
|
}) => {
|
||||||
try {
|
try {
|
||||||
const isSuccess = await login(username, password)
|
const isSuccess = await login(username, password)
|
||||||
if (isSuccess) success()
|
if (isSuccess) success()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof TooManyRequestsError) {
|
if (err instanceof TooManyRequestsError) {
|
||||||
// error('Too many requests. Try again in 2 minutes.')
|
|
||||||
error('太多請求,兩分鐘後再試一次。')
|
error('太多請求,兩分鐘後再試一次。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (err instanceof UnauthorizedError) {
|
if (err instanceof UnauthorizedError) {
|
||||||
// error('Wrong username or password.')
|
|
||||||
error('錯誤的帳號名稱或密碼。')
|
error('錯誤的帳號名稱或密碼。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (err instanceof ValidationError) {
|
if (err instanceof ValidationError) {
|
||||||
// error('Invalid username or password.')
|
|
||||||
error('無效的帳號名稱或密碼。')
|
error('無效的帳號名稱或密碼。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
// error('Something went wrong.')
|
|
||||||
error('出了點問題。')
|
error('出了點問題。')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import QuestMap from '../components/QuestMap.vue'
|
import QuestMap from '../components/QuestMap.vue'
|
||||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||||
</script>
|
</script>
|
||||||
@@ -6,13 +6,6 @@ import GameHeaderPart from '../components/GameHeaderPart.vue'
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-[6vh] w-full">
|
<div class="h-[6vh] w-full">
|
||||||
<GameHeaderPart title="關卡地圖" />
|
<GameHeaderPart title="關卡地圖" />
|
||||||
<!-- <p
|
|
||||||
class="text-text-primary absolute z-1 font-bold inline-block tracking-widest"
|
|
||||||
style="left: 1.2%; top: 1.5%; height: 100%; font-size: 2vh"
|
|
||||||
>
|
|
||||||
Quest Map
|
|
||||||
Unix編年史
|
|
||||||
</p> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col justify-center items-center w-full h-[94vh]">
|
<div class="flex flex-col justify-center items-center w-full h-[94vh]">
|
||||||
<QuestMap />
|
<QuestMap />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
import { isValidUsername } from '@linux-odyssey/utils'
|
import { isValidUsername } from '@linux-odyssey/utils'
|
||||||
import { passwordPolicy } from '@linux-odyssey/constants'
|
import { passwordPolicy } from '@linux-odyssey/constants'
|
||||||
@@ -12,17 +12,28 @@ import {
|
|||||||
ValidationError,
|
ValidationError,
|
||||||
} from '../utils/errors'
|
} from '../utils/errors'
|
||||||
|
|
||||||
function handleRegister({ username, email, password, success, error }) {
|
function handleRegister({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
success,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
success: () => void
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
error: (msg: string) => void
|
||||||
|
}) {
|
||||||
register(username, email, password)
|
register(username, email, password)
|
||||||
.then(success)
|
.then(success)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err instanceof TooManyRequestsError) {
|
if (err instanceof TooManyRequestsError) {
|
||||||
// error('Too many requests. Try again in 2 minutes.')
|
|
||||||
error('太多請求,兩分鐘後再試一次。')
|
error('太多請求,兩分鐘後再試一次。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (err instanceof UnauthorizedError) {
|
if (err instanceof UnauthorizedError) {
|
||||||
// error('Invalid username or password.')
|
|
||||||
error('無效的帳號名稱或密碼。')
|
error('無效的帳號名稱或密碼。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -31,26 +42,31 @@ function handleRegister({ username, email, password, success, error }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
// error('Something went wrong.')
|
|
||||||
error('出了點問題。')
|
error('出了點問題。')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function check({ username, email, password, error }) {
|
async function check({
|
||||||
|
username,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
error,
|
||||||
|
}: {
|
||||||
|
username: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
error: (msg: string) => void
|
||||||
|
}) {
|
||||||
if (username && !isValidUsername(username)) {
|
if (username && !isValidUsername(username)) {
|
||||||
// error('Invalid username.')
|
|
||||||
error('無效的帳號名稱')
|
error('無效的帳號名稱')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (email && !validator.isEmail(email)) {
|
if (email && !validator.isEmail(email)) {
|
||||||
// error('Invalid email.')
|
|
||||||
error('無效的電子郵件')
|
error('無效的電子郵件')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (password && !validator.isStrongPassword(password, passwordPolicy)) {
|
if (password && !validator.isStrongPassword(password, passwordPolicy)) {
|
||||||
// error(
|
|
||||||
// 'Your password must be 8+ characters with at least one number, one upper and one lower case letter.'
|
|
||||||
// )
|
|
||||||
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
|
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -62,12 +78,10 @@ async function check({ username, email, password, error }) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (err instanceof TooManyRequestsError) {
|
if (err instanceof TooManyRequestsError) {
|
||||||
// error('Too many requests. Try again in 2 minutes.')
|
|
||||||
error('太多請求,兩分鐘後再試一次。')
|
error('太多請求,兩分鐘後再試一次。')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
// error('Something went wrong.')
|
|
||||||
error('出了點問題。')
|
error('出了點問題。')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File renamed without changes.
@@ -51,5 +51,8 @@
|
|||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"typescript-eslint": "^7.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1043,6 +1043,13 @@
|
|||||||
"@types/docker-modem" "*"
|
"@types/docker-modem" "*"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@types/dompurify@^3.0.5":
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-3.0.5.tgz#02069a2fcb89a163bacf1a788f73cb415dd75cb7"
|
||||||
|
integrity sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==
|
||||||
|
dependencies:
|
||||||
|
"@types/trusted-types" "*"
|
||||||
|
|
||||||
"@types/dotenv@^8.2.0":
|
"@types/dotenv@^8.2.0":
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053"
|
resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-8.2.0.tgz#5cd64710c3c98e82d9d15844375a33bf1b45d053"
|
||||||
@@ -1307,6 +1314,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
|
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
|
||||||
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
|
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
|
||||||
|
|
||||||
|
"@types/trusted-types@*":
|
||||||
|
version "2.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11"
|
||||||
|
integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
|
||||||
|
|
||||||
"@types/validator@^13.11.9":
|
"@types/validator@^13.11.9":
|
||||||
version "13.11.9"
|
version "13.11.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.9.tgz#adfe96520b437a0eaa798a475877bf2f75ee402d"
|
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.9.tgz#adfe96520b437a0eaa798a475877bf2f75ee402d"
|
||||||
@@ -1343,6 +1355,23 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
|
|
||||||
|
"@typescript-eslint/eslint-plugin@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz#de61c3083842fc6ac889d2fc83c9a96b55ab8328"
|
||||||
|
integrity sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==
|
||||||
|
dependencies:
|
||||||
|
"@eslint-community/regexpp" "^4.5.1"
|
||||||
|
"@typescript-eslint/scope-manager" "7.4.0"
|
||||||
|
"@typescript-eslint/type-utils" "7.4.0"
|
||||||
|
"@typescript-eslint/utils" "7.4.0"
|
||||||
|
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||||
|
debug "^4.3.4"
|
||||||
|
graphemer "^1.4.0"
|
||||||
|
ignore "^5.2.4"
|
||||||
|
natural-compare "^1.4.0"
|
||||||
|
semver "^7.5.4"
|
||||||
|
ts-api-utils "^1.0.1"
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin@^6.14.0":
|
"@typescript-eslint/eslint-plugin@^6.14.0":
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"
|
||||||
@@ -1360,6 +1389,17 @@
|
|||||||
semver "^7.5.4"
|
semver "^7.5.4"
|
||||||
ts-api-utils "^1.0.1"
|
ts-api-utils "^1.0.1"
|
||||||
|
|
||||||
|
"@typescript-eslint/parser@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.4.0.tgz#540f4321de1e52b886c0fa68628af1459954c1f1"
|
||||||
|
integrity sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/scope-manager" "7.4.0"
|
||||||
|
"@typescript-eslint/types" "7.4.0"
|
||||||
|
"@typescript-eslint/typescript-estree" "7.4.0"
|
||||||
|
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||||
|
debug "^4.3.4"
|
||||||
|
|
||||||
"@typescript-eslint/parser@^6.14.0":
|
"@typescript-eslint/parser@^6.14.0":
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b"
|
||||||
@@ -1379,6 +1419,14 @@
|
|||||||
"@typescript-eslint/types" "6.21.0"
|
"@typescript-eslint/types" "6.21.0"
|
||||||
"@typescript-eslint/visitor-keys" "6.21.0"
|
"@typescript-eslint/visitor-keys" "6.21.0"
|
||||||
|
|
||||||
|
"@typescript-eslint/scope-manager@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz#acfc69261f10ece7bf7ece1734f1713392c3655f"
|
||||||
|
integrity sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types" "7.4.0"
|
||||||
|
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||||
|
|
||||||
"@typescript-eslint/type-utils@6.21.0":
|
"@typescript-eslint/type-utils@6.21.0":
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e"
|
||||||
@@ -1389,11 +1437,26 @@
|
|||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
ts-api-utils "^1.0.1"
|
ts-api-utils "^1.0.1"
|
||||||
|
|
||||||
|
"@typescript-eslint/type-utils@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz#cfcaab21bcca441c57da5d3a1153555e39028cbd"
|
||||||
|
integrity sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/typescript-estree" "7.4.0"
|
||||||
|
"@typescript-eslint/utils" "7.4.0"
|
||||||
|
debug "^4.3.4"
|
||||||
|
ts-api-utils "^1.0.1"
|
||||||
|
|
||||||
"@typescript-eslint/types@6.21.0":
|
"@typescript-eslint/types@6.21.0":
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d"
|
||||||
integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==
|
integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==
|
||||||
|
|
||||||
|
"@typescript-eslint/types@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.4.0.tgz#ee9dafa75c99eaee49de6dcc9348b45d354419b6"
|
||||||
|
integrity sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree@6.21.0":
|
"@typescript-eslint/typescript-estree@6.21.0":
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46"
|
||||||
@@ -1408,6 +1471,20 @@
|
|||||||
semver "^7.5.4"
|
semver "^7.5.4"
|
||||||
ts-api-utils "^1.0.1"
|
ts-api-utils "^1.0.1"
|
||||||
|
|
||||||
|
"@typescript-eslint/typescript-estree@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz#12dbcb4624d952f72c10a9f4431284fca24624f4"
|
||||||
|
integrity sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types" "7.4.0"
|
||||||
|
"@typescript-eslint/visitor-keys" "7.4.0"
|
||||||
|
debug "^4.3.4"
|
||||||
|
globby "^11.1.0"
|
||||||
|
is-glob "^4.0.3"
|
||||||
|
minimatch "9.0.3"
|
||||||
|
semver "^7.5.4"
|
||||||
|
ts-api-utils "^1.0.1"
|
||||||
|
|
||||||
"@typescript-eslint/utils@6.21.0":
|
"@typescript-eslint/utils@6.21.0":
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134"
|
||||||
@@ -1421,6 +1498,19 @@
|
|||||||
"@typescript-eslint/typescript-estree" "6.21.0"
|
"@typescript-eslint/typescript-estree" "6.21.0"
|
||||||
semver "^7.5.4"
|
semver "^7.5.4"
|
||||||
|
|
||||||
|
"@typescript-eslint/utils@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.4.0.tgz#d889a0630cab88bddedaf7c845c64a00576257bd"
|
||||||
|
integrity sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==
|
||||||
|
dependencies:
|
||||||
|
"@eslint-community/eslint-utils" "^4.4.0"
|
||||||
|
"@types/json-schema" "^7.0.12"
|
||||||
|
"@types/semver" "^7.5.0"
|
||||||
|
"@typescript-eslint/scope-manager" "7.4.0"
|
||||||
|
"@typescript-eslint/types" "7.4.0"
|
||||||
|
"@typescript-eslint/typescript-estree" "7.4.0"
|
||||||
|
semver "^7.5.4"
|
||||||
|
|
||||||
"@typescript-eslint/visitor-keys@6.21.0":
|
"@typescript-eslint/visitor-keys@6.21.0":
|
||||||
version "6.21.0"
|
version "6.21.0"
|
||||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47"
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47"
|
||||||
@@ -1429,6 +1519,14 @@
|
|||||||
"@typescript-eslint/types" "6.21.0"
|
"@typescript-eslint/types" "6.21.0"
|
||||||
eslint-visitor-keys "^3.4.1"
|
eslint-visitor-keys "^3.4.1"
|
||||||
|
|
||||||
|
"@typescript-eslint/visitor-keys@7.4.0":
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz#0c8ff2c1f8a6fe8d7d1a57ebbd4a638e86a60a94"
|
||||||
|
integrity sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/types" "7.4.0"
|
||||||
|
eslint-visitor-keys "^3.4.1"
|
||||||
|
|
||||||
"@ungap/structured-clone@^1.2.0":
|
"@ungap/structured-clone@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
|
resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406"
|
||||||
@@ -6936,6 +7034,15 @@ typed-array-length@^1.0.4:
|
|||||||
is-typed-array "^1.1.13"
|
is-typed-array "^1.1.13"
|
||||||
possible-typed-array-names "^1.0.0"
|
possible-typed-array-names "^1.0.0"
|
||||||
|
|
||||||
|
typescript-eslint@^7.4.0:
|
||||||
|
version "7.4.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-7.4.0.tgz#2a9b35e99a6292e59ff72bce3ef56e10d8b20b40"
|
||||||
|
integrity sha512-8GYQsb/joknlAZEAs/kqonfrsAc98C5DoellmwHREPqKwSTKSY2YB93IwmvNuX6+WE5QkKc31X9wHo/UcpYXpw==
|
||||||
|
dependencies:
|
||||||
|
"@typescript-eslint/eslint-plugin" "7.4.0"
|
||||||
|
"@typescript-eslint/parser" "7.4.0"
|
||||||
|
"@typescript-eslint/utils" "7.4.0"
|
||||||
|
|
||||||
typescript@^5.3.3:
|
typescript@^5.3.3:
|
||||||
version "5.3.3"
|
version "5.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||||
|
|||||||
Reference in new issue
Block a user