Feat typescript frontend (#107)

There is still tons of errors in e2e. Fix it after merging.
This commit is contained in:
Lin, Hung-Shin authored and GitHub committed 2024-03-28 21:22:42 +08:00
commit 2fb1315c5a
40 files changed
+411 -268

No files matched your search

+12
View File
@@ -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
View File
@@ -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
View File
@@ -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: {},
+9 -61
View File
@@ -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>
+2 -8
View File
@@ -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 -2
View File
@@ -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">
+40 -53
View File
@@ -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>
+18 -7
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
<script setup> <script setup lang="ts">
const props = defineProps({ const props = defineProps({
node: { node: {
type: Object, type: Object,
+1 -3
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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'
+9 -11
View File
@@ -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">
+4 -6
View File
@@ -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" />
+15 -17
View File
@@ -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>
+2 -2
View File
@@ -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>
+8 -4
View File
@@ -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.
View File
File renamed without changes.
+1 -1
View File
@@ -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)
toast.error(err.message) if (err instanceof Error) {
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}`)
} }
+26
View File
@@ -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()
} }
} }
+20 -8
View File
@@ -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
} }
+2 -6
View File
@@ -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
} }
+9 -3
View File
@@ -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('讀取排行榜失敗')
} }
+13 -6
View File
@@ -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 -8
View File
@@ -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 />
+27 -13
View File
@@ -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.
+3
View File
@@ -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"
} }
} }
+107
View File
@@ -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"