mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +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
+411
-268
No files matched your search
@@ -48,6 +48,18 @@ module.exports = {
|
||||
'no-use-before-define': '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
|
||||
'import/extensions': [
|
||||
|
||||
+1
-1
@@ -8,6 +8,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
+2
-1
@@ -2,7 +2,6 @@
|
||||
"name": "app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev:docker": "API_TARGET=http://backend:3000 vite",
|
||||
@@ -18,6 +17,7 @@
|
||||
"@linux-odyssey/constants": "*",
|
||||
"@linux-odyssey/file-graph": "*",
|
||||
"@linux-odyssey/utils": "*",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"axios": "^1.6.0",
|
||||
"dompurify": "^3.0.6",
|
||||
"echarts": "^5.4.3",
|
||||
@@ -33,6 +33,7 @@
|
||||
"xterm-addon-fit": "^0.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/validator": "^13.11.9",
|
||||
"@vitejs/plugin-vue": "^4.5.2",
|
||||
"@vue/compiler-sfc": "^3.3.13",
|
||||
"autoprefixer": "^10.4.14",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default {
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
@@ -27,7 +27,7 @@ const password = ref('')
|
||||
const errorMessage = ref('')
|
||||
const toast = useToast()
|
||||
|
||||
const availableMethods = ref({})
|
||||
const availableMethods = ref<{ [key: string]: boolean }>({})
|
||||
onMounted(async () => {
|
||||
const res = await api.get('/auth/available-methods')
|
||||
availableMethods.value = res.data
|
||||
@@ -44,7 +44,7 @@ const success = () => {
|
||||
router.push({ name: 'map' })
|
||||
}
|
||||
|
||||
const error = (err) => {
|
||||
const error = (err: string) => {
|
||||
errorMessage.value = err
|
||||
}
|
||||
|
||||
@@ -114,10 +114,7 @@ const handleChange = () => {
|
||||
:icon="['fab', 'google']"
|
||||
class="text-text-secondary px-3 text-lg"
|
||||
/>
|
||||
<span class="font-medium">
|
||||
<!-- Continue with Google -->
|
||||
以 Google 繼續
|
||||
</span>
|
||||
<span class="font-medium"> 以 Google 繼續 </span>
|
||||
</a>
|
||||
<a
|
||||
id="GitHubLogin"
|
||||
@@ -129,32 +126,17 @@ const handleChange = () => {
|
||||
:icon="['fab', 'github']"
|
||||
class="text-text-secondary px-3 text-lg"
|
||||
/>
|
||||
<span class="font-medium">
|
||||
<!-- Continue with GitHub -->
|
||||
以 GitHub 繼續
|
||||
</span>
|
||||
<span class="font-medium"> 以 GitHub 繼續 </span>
|
||||
</a>
|
||||
<p
|
||||
id="or"
|
||||
class="text-text-secondary flex justify-center mt-3"
|
||||
v-if="type === 'login' || type === 'register'"
|
||||
>
|
||||
<!-- or -->
|
||||
或
|
||||
</p>
|
||||
</div>
|
||||
<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
|
||||
type="text"
|
||||
id="username"
|
||||
@@ -176,17 +158,6 @@ const handleChange = () => {
|
||||
</label>
|
||||
</div>
|
||||
<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
|
||||
type="email"
|
||||
id="email"
|
||||
@@ -200,15 +171,6 @@ const handleChange = () => {
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
type="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"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="type === 'login'">
|
||||
<!-- Log In -->
|
||||
登入
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- Sign Up -->
|
||||
註冊
|
||||
</span>
|
||||
<span v-if="type === 'login'"> 登入 </span>
|
||||
<span v-else> 註冊 </span>
|
||||
</button>
|
||||
|
||||
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
|
||||
<span id="CheckRegistered" v-if="type === 'register'">
|
||||
<!-- Already have an account? -->
|
||||
已經有帳號了嗎?
|
||||
<RouterLink
|
||||
id="SwitchtoLogin"
|
||||
class="text-text-primary font-bold"
|
||||
to="/login"
|
||||
>
|
||||
<u>
|
||||
<!-- Log in -->
|
||||
登入
|
||||
</u>
|
||||
<u> 登入 </u>
|
||||
</RouterLink></span
|
||||
>
|
||||
<span id="CheckRegistered" v-else-if="type === 'login'">
|
||||
<!-- Don't have an account? -->
|
||||
還沒有帳號嗎?
|
||||
<RouterLink
|
||||
id="SwitchtoRegister"
|
||||
class="text-text-primary font-bold"
|
||||
to="/register"
|
||||
>
|
||||
<u>
|
||||
<!-- Sign up -->
|
||||
註冊
|
||||
</u>
|
||||
<u> 註冊 </u>
|
||||
</RouterLink></span
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
<template>
|
||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">
|
||||
<!-- Command Cheatsheets -->
|
||||
指令之書
|
||||
</h1>
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
|
||||
<div id="cheatsheets">
|
||||
<p class="text-text-secondary">
|
||||
<!-- --Command List-- -->
|
||||
-- --指令列表-- --
|
||||
</p>
|
||||
<p class="text-text-secondary">--指令列表--</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ref } from 'vue'
|
||||
@@ -47,7 +47,6 @@ setTimeout(() => {
|
||||
id="QuestCompleted"
|
||||
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||
>
|
||||
<!-- Quest Completed! -->
|
||||
關卡完成!
|
||||
</h2>
|
||||
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
|
||||
|
||||
@@ -1,55 +1,4 @@
|
||||
<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">
|
||||
<!-- 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>
|
||||
<script setup lang="ts">
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { createSession } from '../store/session'
|
||||
@@ -64,9 +13,47 @@ const continuePlay = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
// useToast().error('Failed to load map')
|
||||
useToast().error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
</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'
|
||||
|
||||
const cw = 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 maxColums = ref(0)
|
||||
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)
|
||||
}
|
||||
|
||||
const randomFloat = (min, max) => {
|
||||
const randomFloat = (min: number, max: number) => {
|
||||
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()
|
||||
if (point.y < 0) point.speed = randomFloat(10, 20)
|
||||
|
||||
if (ctx.value === null) return
|
||||
ctx.value.fillStyle = '#0F0'
|
||||
ctx.value.font = `${fontSize.value}px san-serif`
|
||||
ctx.value.fillText(point.value, point.x, point.y)
|
||||
@@ -33,6 +41,7 @@ const draw = (point) => {
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
if (ctx.value === null) return
|
||||
ctx.value.fillStyle = 'rgba(0,0,0,0.05)'
|
||||
ctx.value.fillRect(0, 0, cw.value, ch.value)
|
||||
|
||||
@@ -48,7 +57,7 @@ onMounted(() => {
|
||||
ch.value = document.documentElement.clientHeight
|
||||
maxColums.value = cw.value / fontSize.value
|
||||
|
||||
const canvas = document.querySelector('canvas')
|
||||
const canvas = document.querySelector('canvas') as HTMLCanvasElement
|
||||
ctx.value = canvas.getContext('2d')
|
||||
|
||||
canvas.width = cw.value
|
||||
@@ -58,6 +67,8 @@ onMounted(() => {
|
||||
fallingCharArr.value.push({
|
||||
x: i * fontSize.value,
|
||||
y: randomFloat(-500, 0),
|
||||
value: '',
|
||||
speed: 0,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { defineProps, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
@@ -21,7 +21,6 @@ const handleLogout = async () => {
|
||||
reset()
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
// toast.error('Failed to logout')
|
||||
toast.error('登出失敗')
|
||||
console.error(err)
|
||||
}
|
||||
@@ -38,7 +37,6 @@ onMounted(async () => {
|
||||
try {
|
||||
await loadUserProfile()
|
||||
} catch (err) {
|
||||
// toast.error('Failed to load user profile')
|
||||
toast.error('無法讀取使用者資料')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import sessionStore from '../store/session'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed, defineProps } from 'vue'
|
||||
import markdown2HTML from '../utils/markdown'
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- eslint-disable no-console -->
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
@@ -50,7 +50,7 @@ async function getProgress() {
|
||||
}
|
||||
}
|
||||
|
||||
const genOption = (nodes, edges) => ({
|
||||
const genOption = (nodes: any, edges: any) => ({
|
||||
renderer: 'svg',
|
||||
tooltip: {},
|
||||
animationDurationUpdate: 1500,
|
||||
@@ -94,7 +94,7 @@ const genOption = (nodes, edges) => ({
|
||||
curveness: 0.08,
|
||||
},
|
||||
itemStyle: {
|
||||
color: (params) => {
|
||||
color: (params: any) => {
|
||||
const {
|
||||
data: { completed, unlocked },
|
||||
} = 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 nodes = dag.getNodes().map((node) => ({
|
||||
@@ -120,9 +120,9 @@ function getOption(quests, progress) {
|
||||
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
|
||||
x: marginY * node.layer * 2,
|
||||
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 {
|
||||
const { unlocked } = node
|
||||
return {
|
||||
@@ -134,7 +134,7 @@ function getOption(quests, progress) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
const edges = dag.getEdgesArray().map((edge) => ({
|
||||
const edges = dag.getEdgesArray().map((edge: any) => ({
|
||||
source: edge[0],
|
||||
target: edge[1],
|
||||
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
|
||||
@@ -145,10 +145,10 @@ function getOption(quests, progress) {
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
|
||||
function initChart(option) {
|
||||
function initChart(option: any) {
|
||||
chartInstance = init(chartContainer.value)
|
||||
chartInstance.setOption(option)
|
||||
chartInstance.on('click', (params) => {
|
||||
chartInstance.on('click', (params: any) => {
|
||||
const {
|
||||
data: { id, unlocked },
|
||||
} = params
|
||||
@@ -156,7 +156,6 @@ function initChart(option) {
|
||||
if (unlocked) {
|
||||
router.push({ name: 'game', params: { questId: id } })
|
||||
} else {
|
||||
// toast.warning('You have not completed the previous quest yet!')
|
||||
toast.warning('你還沒完成前一個關卡!')
|
||||
}
|
||||
})
|
||||
@@ -181,7 +180,6 @@ onMounted(async () => {
|
||||
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"
|
||||
>
|
||||
<!-- Get through your linux journey! -->
|
||||
踏上你的Linux冒險之旅吧!
|
||||
</h1>
|
||||
<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 MarkdownText from './MarkdownText.vue'
|
||||
import sessionStore from '../store/session'
|
||||
import { Task } from '../types'
|
||||
|
||||
const tasks = computed(() => {
|
||||
return sessionStore.session.tasks.map((task) => {
|
||||
return sessionStore.session.tasks.map((task: Task) => {
|
||||
const prefix = task.completed ? '✓' : '➤'
|
||||
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||
return {
|
||||
@@ -29,10 +30,7 @@ const tasks = computed(() => {
|
||||
:content="sessionStore.quest.instruction"
|
||||
/>
|
||||
<br />
|
||||
<p id="tasks" class="text-text">
|
||||
<!-- Tasks: -->
|
||||
任務:
|
||||
</p>
|
||||
<p id="tasks" class="text-text">任務:</p>
|
||||
<ul v-if="sessionStore.session">
|
||||
<li v-for="task in tasks" :key="task.id">
|
||||
<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>
|
||||
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
|
||||
<button id="Terminal" class="text-text h-[5%]">
|
||||
<!-- Terminal -->
|
||||
終端機
|
||||
</button>
|
||||
<button id="Terminal" class="text-text h-[5%]">終端機</button>
|
||||
<div
|
||||
id="terminal"
|
||||
ref="terminal"
|
||||
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
|
||||
></div>
|
||||
</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 sessionStore from '../store/session.js'
|
||||
import sessionStore from '../store/session'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from '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 }
|
||||
|
||||
function mouseMoveHandler(e) {
|
||||
function mouseMoveHandler(e: MouseEvent) {
|
||||
const dx = e.clientX - pos.x
|
||||
const dy = e.clientY - pos.y
|
||||
|
||||
if (treeRef.value === null) return
|
||||
treeRef.value.scrollTop = pos.top - dy
|
||||
treeRef.value.scrollLeft = pos.left - dx
|
||||
}
|
||||
|
||||
function mouseUpHandler() {
|
||||
if (treeRef.value === null) return
|
||||
treeRef.value.style.cursor = 'grab'
|
||||
treeRef.value.style.removeProperty('user-select')
|
||||
|
||||
@@ -21,7 +23,8 @@ function 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.userSelect = 'none'
|
||||
|
||||
@@ -37,6 +40,7 @@ function mouseDownHandler(e) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (treeRef.value === null) return
|
||||
treeRef.value.style.cursor = 'grab'
|
||||
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,
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const loggedIn = await isLoggedIn()
|
||||
// Check if the route requires authentication
|
||||
if (to.meta.requiresAuth && !loggedIn) {
|
||||
@@ -1,10 +1,13 @@
|
||||
import { reactive } from 'vue'
|
||||
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 Socket from '../utils/socket'
|
||||
import SocketTerminal from '../utils/terminal'
|
||||
import { LoadQuestError, LoadSessionError } from '../utils/errors'
|
||||
import { Session, StageResponse } from '../types'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
@@ -15,6 +18,7 @@ function newSession() {
|
||||
discovered: false,
|
||||
})
|
||||
return {
|
||||
_id: '',
|
||||
status: 'inactive',
|
||||
graph,
|
||||
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(),
|
||||
questId: '',
|
||||
quest: null,
|
||||
@@ -33,14 +43,14 @@ const store = reactive({
|
||||
const socket = new Socket()
|
||||
const term = new SocketTerminal(40, 80)
|
||||
|
||||
function setQuest(questId) {
|
||||
function setQuest(questId: string) {
|
||||
return api.get(`/quests/${questId}`).then((res) => {
|
||||
store.questId = res.data._id
|
||||
store.quest = res.data
|
||||
})
|
||||
}
|
||||
|
||||
async function setSession(session) {
|
||||
async function setSession(session: Session) {
|
||||
try {
|
||||
console.log('Setting session...', session)
|
||||
store.session = session
|
||||
@@ -48,9 +58,13 @@ async function setSession(session) {
|
||||
term.reset()
|
||||
await socket.connect(session)
|
||||
term.focus()
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -67,7 +81,7 @@ export async function createSession() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getActiveSession(questId) {
|
||||
async function getActiveSession(questId: string) {
|
||||
try {
|
||||
const res = await api.post('/sessions/active', { questId })
|
||||
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) {
|
||||
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)
|
||||
store.session.responses.push(response.responses)
|
||||
store.session.hints.push(response.hints)
|
||||
store.session.responses.push(...response.responses)
|
||||
store.session.hints.push(...response.hints)
|
||||
store.session.tasks = response.tasks
|
||||
store.session.status = response.status
|
||||
}
|
||||
@@ -106,7 +120,7 @@ export function reset() {
|
||||
store.quest = null
|
||||
}
|
||||
|
||||
export async function init(questId) {
|
||||
export async function init(questId: string) {
|
||||
if (!questId) throw new Error('No quest ID provided')
|
||||
reset()
|
||||
try {
|
||||
@@ -124,16 +138,16 @@ export async function init(questId) {
|
||||
}
|
||||
|
||||
function setup() {
|
||||
socket.on('terminal', (data) => {
|
||||
socket.on('terminal', (data: string) => {
|
||||
term.write(data)
|
||||
})
|
||||
term.onData((data) => {
|
||||
term.onData((data: string) => {
|
||||
socket.emit('terminal', data)
|
||||
})
|
||||
socket.on('graph', (event) => {
|
||||
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
|
||||
updateGraph(event)
|
||||
})
|
||||
socket.on('response', (response) => {
|
||||
socket.on('response', (response: StageResponse) => {
|
||||
newResponse(response)
|
||||
})
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export async function loadUserProfile() {
|
||||
store.email = email
|
||||
store.progress = progress
|
||||
return store
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
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
|
||||
}
|
||||
|
||||
function handleRequestError(err) {
|
||||
function handleRequestError(err: {
|
||||
response: {
|
||||
data?: any
|
||||
status?: number
|
||||
}
|
||||
}) {
|
||||
if (!err.response) throw err // Not an axios error
|
||||
const { status } = err.response
|
||||
if (status === 429) {
|
||||
@@ -41,14 +46,14 @@ function handleRequestError(err) {
|
||||
throw err
|
||||
}
|
||||
|
||||
export const login = (username, password) => {
|
||||
export const login = (username: string, password: string) => {
|
||||
return api
|
||||
.post('/auth/login', { username, password })
|
||||
.then(() => true)
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
export const register = (username, email, password) => {
|
||||
export const register = (username: string, email: string, password: string) => {
|
||||
return api
|
||||
.post('/auth/register', {
|
||||
username,
|
||||
@@ -59,7 +64,7 @@ export const register = (username, email, password) => {
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
export const checkUsername = async (username) => {
|
||||
export const checkUsername = async (username: string) => {
|
||||
return api
|
||||
.get('/auth/check-username', {
|
||||
params: { username },
|
||||
@@ -68,7 +73,7 @@ export const checkUsername = async (username) => {
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
export const chooseUsername = async (username) => {
|
||||
export const chooseUsername = async (username: string) => {
|
||||
return api
|
||||
.post('/auth/register-from-session', { username })
|
||||
.then(() => true)
|
||||
@@ -1,27 +1,28 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
export class TooManyRequestsError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'TooManyRequestsError'
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'ValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends Error {
|
||||
constructor(message) {
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
this.name = 'UnauthorizedError'
|
||||
}
|
||||
}
|
||||
|
||||
export class LoadQuestError extends Error {
|
||||
constructor(message, questId) {
|
||||
questId: string
|
||||
constructor(message: string, questId: string) {
|
||||
super(message)
|
||||
this.name = 'LoadQuestError'
|
||||
this.questId = questId
|
||||
@@ -29,7 +30,8 @@ export class LoadQuestError extends Error {
|
||||
}
|
||||
|
||||
export class LoadSessionError extends Error {
|
||||
constructor(message, questId) {
|
||||
questId: string
|
||||
constructor(message: string, questId: string) {
|
||||
super(message)
|
||||
this.name = 'LoadSessionError'
|
||||
this.questId = questId
|
||||
@@ -1,7 +1,7 @@
|
||||
import { marked } from 'marked'
|
||||
import DOMPurify from 'dompurify'
|
||||
|
||||
export default function markdown2HTML(markdown) {
|
||||
export default function markdown2HTML(markdown: string) {
|
||||
const html = marked.parse(markdown)
|
||||
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 {
|
||||
socket: Socket | null
|
||||
listeners: { event: string; callback: SocketCallback }[]
|
||||
constructor() {
|
||||
this.socket = null
|
||||
this.listeners = []
|
||||
}
|
||||
|
||||
connect(session) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connect(session: Session) {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.disconnect()
|
||||
|
||||
this.socket = io('', {
|
||||
@@ -28,7 +33,7 @@ class SocketWrapper {
|
||||
})
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
on(event: string, callback: SocketCallback) {
|
||||
this.listeners.push({ event, callback })
|
||||
if (this.socket) {
|
||||
this.socket.on(event, callback)
|
||||
@@ -36,12 +41,14 @@ class SocketWrapper {
|
||||
}
|
||||
|
||||
bindListeners() {
|
||||
this.listeners.forEach(({ event, callback }) => {
|
||||
this.socket.on(event, callback)
|
||||
})
|
||||
this.listeners.forEach(
|
||||
({ 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) {
|
||||
console.warn('Socket is not connected!')
|
||||
return
|
||||
@@ -3,7 +3,10 @@ import { FitAddon } from 'xterm-addon-fit'
|
||||
import 'xterm/css/xterm.css'
|
||||
|
||||
class SocketTerminal {
|
||||
constructor(rows, cols) {
|
||||
private term: Terminal
|
||||
private fitAddon: FitAddon
|
||||
|
||||
constructor(rows: number, cols: number) {
|
||||
this.term = new Terminal({
|
||||
rendererType: 'canvas', // 渲染类型
|
||||
rows, // 行数
|
||||
@@ -24,29 +27,29 @@ class SocketTerminal {
|
||||
this.fitAddon.fit()
|
||||
}
|
||||
|
||||
write(message) {
|
||||
write(message: string): void {
|
||||
this.term.write(message)
|
||||
}
|
||||
|
||||
onData(callback) {
|
||||
onData(callback: any): void {
|
||||
this.term.onData(callback)
|
||||
}
|
||||
|
||||
focus() {
|
||||
focus(): void {
|
||||
this.term.focus()
|
||||
}
|
||||
|
||||
resizeScreen() {
|
||||
resizeScreen(): void {
|
||||
this.fitAddon.fit()
|
||||
}
|
||||
|
||||
mount(reference) {
|
||||
mount(reference: HTMLElement): void {
|
||||
this.term.open(reference)
|
||||
this.term.focus()
|
||||
this.resizeScreen()
|
||||
}
|
||||
|
||||
reset() {
|
||||
reset(): void {
|
||||
this.term.reset()
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,20 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { isValidUsername } from '@linux-odyssey/utils'
|
||||
import AuthForm from '../components/AuthForm.vue'
|
||||
import Background from '../components/DynamicBackground.vue'
|
||||
import { TooManyRequestsError, ValidationError } from '../utils/errors'
|
||||
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 (!isValidUsername(username)) {
|
||||
// error('Invalid username.')
|
||||
error('無效的帳號名稱')
|
||||
return
|
||||
}
|
||||
@@ -16,7 +22,6 @@ async function check({ username, error }) {
|
||||
await checkUsername(username)
|
||||
} catch (err) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
@@ -24,19 +29,26 @@ async function check({ username, error }) {
|
||||
error(err.message)
|
||||
return
|
||||
}
|
||||
// error('Something went wrong. Please try again later.')
|
||||
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 {
|
||||
await chooseUsername(username)
|
||||
success()
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import sessionStore, { init } from '../store/session'
|
||||
@@ -28,16 +28,12 @@ const toast = useToast()
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await init(props.questId)
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
if (err instanceof LoadQuestError) {
|
||||
// toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
|
||||
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
|
||||
return
|
||||
}
|
||||
if (err instanceof LoadSessionError) {
|
||||
// toast.error(
|
||||
// `Failed to create session for quest: ${err.questId}. Please re-login and try again.`
|
||||
// )
|
||||
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,18 +1,24 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||
import api from '../utils/api'
|
||||
|
||||
const leaderboard = ref([])
|
||||
const leaderboard = ref<
|
||||
{
|
||||
username: string
|
||||
score: number
|
||||
completedQuests: string[]
|
||||
}[]
|
||||
>([])
|
||||
|
||||
const toast = useToast()
|
||||
async function getLeaderboard() {
|
||||
try {
|
||||
const res = await api.get('/leaderboard')
|
||||
leaderboard.value = res.data
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
toast.error('讀取排行榜失敗')
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import AuthForm from '../components/AuthForm.vue'
|
||||
import Background from '../components/DynamicBackground.vue'
|
||||
|
||||
@@ -9,28 +9,35 @@ import {
|
||||
} from '../utils/errors'
|
||||
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 {
|
||||
const isSuccess = await login(username, password)
|
||||
if (isSuccess) success()
|
||||
} catch (err) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
if (err instanceof UnauthorizedError) {
|
||||
// error('Wrong username or password.')
|
||||
error('錯誤的帳號名稱或密碼。')
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
// error('Invalid username or password.')
|
||||
error('無效的帳號名稱或密碼。')
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
// error('Something went wrong.')
|
||||
error('出了點問題。')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import QuestMap from '../components/QuestMap.vue'
|
||||
import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||
</script>
|
||||
@@ -6,13 +6,6 @@ import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||
<template>
|
||||
<div class="h-[6vh] w-full">
|
||||
<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 class="flex flex-col justify-center items-center w-full h-[94vh]">
|
||||
<QuestMap />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import validator from 'validator'
|
||||
import { isValidUsername } from '@linux-odyssey/utils'
|
||||
import { passwordPolicy } from '@linux-odyssey/constants'
|
||||
@@ -12,17 +12,28 @@ import {
|
||||
ValidationError,
|
||||
} 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)
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
if (err instanceof UnauthorizedError) {
|
||||
// error('Invalid username or password.')
|
||||
error('無效的帳號名稱或密碼。')
|
||||
return
|
||||
}
|
||||
@@ -31,26 +42,31 @@ function handleRegister({ username, email, password, success, error }) {
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
// error('Something went wrong.')
|
||||
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)) {
|
||||
// error('Invalid username.')
|
||||
error('無效的帳號名稱')
|
||||
return
|
||||
}
|
||||
if (email && !validator.isEmail(email)) {
|
||||
// error('Invalid email.')
|
||||
error('無效的電子郵件')
|
||||
return
|
||||
}
|
||||
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個字元,至少包含一個數字、一個大寫及一個小寫。')
|
||||
return
|
||||
}
|
||||
@@ -62,12 +78,10 @@ async function check({ username, email, password, error }) {
|
||||
return
|
||||
}
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
// error('Something went wrong.')
|
||||
error('出了點問題。')
|
||||
}
|
||||
}
|
||||
|
||||
File renamed without changes.
@@ -51,5 +51,8 @@
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"typescript-eslint": "^7.4.0"
|
||||
}
|
||||
}
|
||||
@@ -1043,6 +1043,13 @@
|
||||
"@types/docker-modem" "*"
|
||||
"@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":
|
||||
version "8.2.0"
|
||||
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"
|
||||
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":
|
||||
version "13.11.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.11.9.tgz#adfe96520b437a0eaa798a475877bf2f75ee402d"
|
||||
@@ -1343,6 +1355,23 @@
|
||||
dependencies:
|
||||
"@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":
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3"
|
||||
@@ -1360,6 +1389,17 @@
|
||||
semver "^7.5.4"
|
||||
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":
|
||||
version "6.21.0"
|
||||
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/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":
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e"
|
||||
@@ -1389,11 +1437,26 @@
|
||||
debug "^4.3.4"
|
||||
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":
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d"
|
||||
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":
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46"
|
||||
@@ -1408,6 +1471,20 @@
|
||||
semver "^7.5.4"
|
||||
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":
|
||||
version "6.21.0"
|
||||
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"
|
||||
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":
|
||||
version "6.21.0"
|
||||
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"
|
||||
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":
|
||||
version "1.2.0"
|
||||
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"
|
||||
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:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
|
||||
Reference in new issue
Block a user