mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-05 19:20:12 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4de8bbfee0 | ||
|
|
5897e562d1 | ||
|
|
733e02ddd8 | ||
|
|
85dda9486e | ||
|
|
54f73504ec | ||
|
|
b47e52406f | ||
|
|
1f8d4162f8 | ||
|
|
4c3b7434c9 | ||
|
|
218c4cce44 | ||
|
|
3d6eaf6b64 | ||
|
|
accfbc620c | ||
|
|
994618f71c | ||
|
|
b5024631f7 | ||
|
|
c5b91cf201 | ||
|
|
8665add560 | ||
|
|
119f8c552a | ||
|
|
22c71d367c | ||
|
|
c3893a9ca3 | ||
|
|
4260e742df | ||
|
|
a7039714aa | ||
|
|
174bab7494 | ||
|
|
9f0804a194 | ||
|
|
de0eb1b33b | ||
|
|
5e8d3f275d | ||
|
|
6a097c02ed | ||
|
|
9165a4ecf6 | ||
|
|
4bb27a2cbd | ||
|
|
66b0b7ba68 | ||
|
|
6e780811a2 | ||
|
|
68d1beea69 | ||
|
|
08fc200e47 | ||
|
|
eb9e68953b | ||
|
|
9b8d7109cf | ||
|
|
fae9a012aa | ||
|
|
2dc3546c98 | ||
|
|
a74a9d71f2 | ||
|
|
34ce90323a | ||
|
|
604f1ef4b6 | ||
|
|
43a76c4952 | ||
|
|
492f27cf29 | ||
|
|
cf5e8a1f24 | ||
|
|
7b16722e55 | ||
|
|
a037c7d512 | ||
|
|
c9b04682c9 | ||
|
|
03fbae616d | ||
|
|
87df12ac85 | ||
|
|
10c8dd5f0e | ||
|
|
4a43df9dd9 | ||
|
|
013a5acaa7 | ||
|
|
bd1d5af699 | ||
|
|
e4da258a8d | ||
|
|
4aeba17a1d | ||
|
|
0fddc05443 | ||
|
|
a3c69764c8 | ||
|
|
f0a279fa83 |
No files matched your search
@@ -0,0 +1,103 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "dev" ]
|
||||
pull_request:
|
||||
branches: [ "main", "dev" ]
|
||||
schedule:
|
||||
- cron: '25 14 * * 4'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: go
|
||||
build-mode: autobuild
|
||||
- language: javascript-typescript
|
||||
build-mode: none
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- name: Run manual build steps
|
||||
if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
+6
-5
@@ -18,14 +18,15 @@
|
||||
"@fortawesome/vue-fontawesome": "^3.0.3",
|
||||
"@trpc/client": "^11.1.0",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"axios": "^1.8.2",
|
||||
"dompurify": "^3.2.4",
|
||||
"axios": "^1.20.0",
|
||||
"dompurify": "^3.4.16",
|
||||
"marked": "^10.0.0",
|
||||
"pinia": "^2.1.7",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"socket.io-client": "^4.7.1",
|
||||
"validator": "^13.11.0",
|
||||
"validator": "^13.15.22",
|
||||
"vue": "^3.3.4",
|
||||
"vue-i18n": "11.3.0",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-toastification": "^2.0.0-rc.5",
|
||||
"vue3-lottie": "^3.2.0",
|
||||
@@ -39,10 +40,10 @@
|
||||
"@vue/compiler-sfc": "^3.3.13",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss": "^8.5.23",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "~5.7.2",
|
||||
"vite": "^6.3.1",
|
||||
"vite": "^6.4.3",
|
||||
"vue-tsc": "^2.2.8"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { RouterLink, useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import api from '../utils/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits(['onSubmit', 'onChange'])
|
||||
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
@@ -110,7 +113,7 @@ const registerGuest = async () => {
|
||||
:icon="['fas', 'circle-exclamation']"
|
||||
class="px-1 text-sm"
|
||||
/>
|
||||
<span>建議使用電腦全螢幕執行</span>
|
||||
<span>{{ t('authform.recommand_pc_fullscreen') }}</span>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit()" class="w-full">
|
||||
<div v-if="socialLogin && hasSocialLogins">
|
||||
@@ -124,7 +127,7 @@ const registerGuest = async () => {
|
||||
:icon="['fab', 'google']"
|
||||
class="text-text-secondary px-3 text-lg"
|
||||
/>
|
||||
<span class="font-medium"> 以 Google 繼續 </span>
|
||||
<span class="font-medium"> {{ t('authform.google_login') }} </span>
|
||||
</a>
|
||||
<a
|
||||
id="GitHubLogin"
|
||||
@@ -136,14 +139,14 @@ const registerGuest = async () => {
|
||||
:icon="['fab', 'github']"
|
||||
class="text-text-secondary px-3 text-lg"
|
||||
/>
|
||||
<span class="font-medium"> 以 GitHub 繼續 </span>
|
||||
<span class="font-medium"> {{ t('authform.github_login') }} </span>
|
||||
</a>
|
||||
<p
|
||||
id="or"
|
||||
class="text-text-secondary flex justify-center mt-3"
|
||||
v-if="type === 'login' || type === 'register'"
|
||||
>
|
||||
或
|
||||
{{ t('authform.or') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
@@ -152,20 +155,22 @@ const registerGuest = async () => {
|
||||
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' ? '電子郵件 / 帳號名稱' : '帳號名稱'"
|
||||
:placeholder="
|
||||
t('authform.username') +
|
||||
(type === 'login' ? ' / ' + t('authform.email') : '')
|
||||
"
|
||||
v-model="username"
|
||||
required
|
||||
@input="handleChange()"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<label
|
||||
class="text-sm font-normal text-text-secondary"
|
||||
<p
|
||||
class="mx-1 text-xs font-normal text-text-secondary"
|
||||
id="NameRules"
|
||||
v-if="type === 'username' || type === 'register'"
|
||||
v-if="type === 'register'"
|
||||
>
|
||||
<p>可用小寫字母、數字、"_"和"-"</p>
|
||||
<p>小寫字母開頭,至多32字元</p>
|
||||
</label>
|
||||
{{ t('authform.username_rules') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-6" v-if="type === 'register'">
|
||||
<input
|
||||
@@ -173,7 +178,7 @@ const registerGuest = async () => {
|
||||
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="電子郵件"
|
||||
:placeholder="t('authform.email')"
|
||||
v-model="email"
|
||||
required
|
||||
@input="handleChange()"
|
||||
@@ -185,11 +190,18 @@ const registerGuest = async () => {
|
||||
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="密碼"
|
||||
:placeholder="t('authform.password')"
|
||||
v-model="password"
|
||||
required
|
||||
@input="handleChange()"
|
||||
/>
|
||||
<p
|
||||
class="mx-1 text-xs font-normal text-text-secondary"
|
||||
id="PasswordRules"
|
||||
v-if="type === 'register'"
|
||||
>
|
||||
{{ t('authform.password_rules') }}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
id="ErrorDisplay"
|
||||
@@ -203,29 +215,29 @@ const registerGuest = async () => {
|
||||
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'"> 登入 </span>
|
||||
<span v-else> 註冊 </span>
|
||||
<span v-if="type === 'login'"> {{ t('authform.login') }} </span>
|
||||
<span v-else> {{ t('authform.register') }} </span>
|
||||
</button>
|
||||
|
||||
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
|
||||
<span id="CheckRegistered" v-if="type === 'register'">
|
||||
已經有帳號了嗎?
|
||||
{{ t('authform.already_registered') }}
|
||||
<RouterLink
|
||||
id="SwitchtoLogin"
|
||||
class="text-text-primary font-bold"
|
||||
to="/login"
|
||||
>
|
||||
<u> 登入 </u>
|
||||
<u> {{ t('authform.login') }} </u>
|
||||
</RouterLink></span
|
||||
>
|
||||
<span id="CheckRegistered" v-else-if="type === 'login'">
|
||||
還沒有帳號嗎?
|
||||
{{ t('authform.not_registered') }}
|
||||
<RouterLink
|
||||
id="SwitchtoRegister"
|
||||
class="text-text-primary font-bold"
|
||||
to="/register"
|
||||
>
|
||||
<u> 註冊 </u>
|
||||
<u> {{ t('authform.register') }} </u>
|
||||
</RouterLink></span
|
||||
>
|
||||
</p>
|
||||
@@ -234,7 +246,7 @@ const registerGuest = async () => {
|
||||
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
||||
@click="registerGuest()"
|
||||
>
|
||||
以訪客身分繼續
|
||||
{{ t('authform.continue_as_guest') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ref, defineProps, onMounted } from 'vue'
|
||||
import MarkdownText from './MarkdownText.vue'
|
||||
import { trpc } from '../utils/trpc'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
questTitle: {
|
||||
type: String,
|
||||
@@ -36,7 +39,7 @@ const handleQuests = (id: string) => {
|
||||
if (questUnlocked) {
|
||||
router.push({ name: 'game', params: { questId: id } })
|
||||
} else {
|
||||
toast.warning('你還沒完成前一個關卡!')
|
||||
toast.warning(t('quest.locked'))
|
||||
}
|
||||
}
|
||||
const handleColour = () => {
|
||||
@@ -49,8 +52,15 @@ const handleColour = () => {
|
||||
}
|
||||
}
|
||||
onMounted(async () => {
|
||||
const quest = await trpc.quests.getQuestDetail.query(questId)
|
||||
questInstruction.value = quest.instruction
|
||||
try {
|
||||
const quest = await trpc.quests.getQuestDetail.query({
|
||||
questId,
|
||||
locale: locale.value,
|
||||
})
|
||||
questInstruction.value = quest.instruction
|
||||
} catch (error) {
|
||||
toast.error(t('quest.load_quest_instruction_failed'), error)
|
||||
}
|
||||
handleColour()
|
||||
})
|
||||
</script>
|
||||
@@ -61,7 +71,6 @@ onMounted(async () => {
|
||||
<span class="close text-text content-between" @click="closeModal"
|
||||
>×</span
|
||||
>
|
||||
|
||||
<h2
|
||||
id="QuestTitle"
|
||||
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||
@@ -81,9 +90,10 @@ onMounted(async () => {
|
||||
target="_blank"
|
||||
@click="handleQuests(questId)"
|
||||
:style="{ backgroundColor: questColor, color: questTextColor }"
|
||||
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full text-lg"
|
||||
>開始冒險</a
|
||||
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full"
|
||||
>
|
||||
{{ t('quest.start') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { DAG } from '../../../packages/utils'
|
||||
@@ -6,17 +7,16 @@ import { trpc } from '../utils/trpc'
|
||||
import useUserProfile from '../store/userProfile'
|
||||
import QuestIntro from './QuestIntro.vue'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const store = useUserProfile()
|
||||
const toast = useToast()
|
||||
|
||||
const svgWidth = ref(window.innerWidth)
|
||||
const svgHeight = ref(window.innerHeight)
|
||||
|
||||
const marginX = 120
|
||||
const marginY = 120
|
||||
|
||||
const nodeWidth = 160
|
||||
const nodeHeight = 50
|
||||
|
||||
const opened = ref<Node | null>(null)
|
||||
type Node = {
|
||||
id: string
|
||||
@@ -44,14 +44,18 @@ const dragStartY = ref(0)
|
||||
const offsetX = ref(0)
|
||||
const offsetY = ref(0)
|
||||
|
||||
function startDrag(event: MouseEvent) {
|
||||
function startDrag(event: PointerEvent) {
|
||||
isDragging.value = true
|
||||
dragStartX.value = event.clientX - offsetX.value
|
||||
dragStartY.value = event.clientY - offsetY.value
|
||||
}
|
||||
|
||||
function drag(event: MouseEvent) {
|
||||
function drag(event: PointerEvent) {
|
||||
if (!isDragging.value) return
|
||||
// Capture only once dragging starts so plain clicks still reach the node buttons
|
||||
const el = event.currentTarget as HTMLElement
|
||||
if (!el.hasPointerCapture(event.pointerId))
|
||||
el.setPointerCapture(event.pointerId)
|
||||
offsetX.value = event.clientX - dragStartX.value
|
||||
offsetY.value = event.clientY - dragStartY.value
|
||||
}
|
||||
@@ -64,7 +68,8 @@ async function computeGraphData() {
|
||||
if (!store.progress) return
|
||||
|
||||
try {
|
||||
const quests = await trpc.quests.getQuests.query()
|
||||
const quests = await trpc.quests.getQuests.query({ locale: locale.value })
|
||||
|
||||
const dag = new DAG(quests)
|
||||
const nodesValues = dag.getNodes()
|
||||
|
||||
@@ -72,10 +77,8 @@ async function computeGraphData() {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
x: marginX * node.layer * 2,
|
||||
y:
|
||||
marginY * node.index -
|
||||
(marginX * dag.getLayer(node.id)) / 2 +
|
||||
svgHeight.value / 2,
|
||||
// Centered on 0; the world container is anchored at the viewport's vertical center
|
||||
y: (node.index - (dag.getLayer(node.id) + 1) / 2) * marginY,
|
||||
completed: store.progress[node.id]?.completed || false,
|
||||
unlocked: node.requirements.every(
|
||||
(req: string) => store.progress[req]?.completed
|
||||
@@ -96,8 +99,8 @@ async function computeGraphData() {
|
||||
|
||||
graphData.value = { nodes, edges }
|
||||
} catch (error) {
|
||||
console.error('Error computing graph data:', error)
|
||||
toast.error('Failed to load quest data')
|
||||
console.error(t('quest_map.compute_graph_failed'), error)
|
||||
toast.error(t('quest_map.load_quest_data_failed'))
|
||||
}
|
||||
}
|
||||
function handleNodeClick(node: Node) {
|
||||
@@ -115,15 +118,15 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
watch(() => store.progress, computeGraphData, { deep: true })
|
||||
watch(locale, () => {
|
||||
opened.value = null
|
||||
computeGraphData()
|
||||
})
|
||||
|
||||
const curvedPath = computed(() => {
|
||||
const edgePath = computed(() => {
|
||||
return (source: Node, target: Node) => {
|
||||
// const dx = target.x - source.x
|
||||
// const dy = target.y - source.y
|
||||
// const dr = Math.sqrt(dx * dx + dy * dy)
|
||||
const dr = 0
|
||||
const sweep = target.index > source.index ? 0 : 1 // Determines if the arc should curve up or down based on node index
|
||||
return `M${source.x},${source.y}A${dr},${dr} 0 0,${sweep} ${target.x},${target.y}`
|
||||
const mx = (source.x + target.x) / 2
|
||||
return `M${source.x},${source.y}C${mx},${source.y} ${mx},${target.y} ${target.x},${target.y}`
|
||||
}
|
||||
})
|
||||
|
||||
@@ -151,54 +154,46 @@ const edgeStyle = computed(() => {
|
||||
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
||||
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
||||
>
|
||||
踏上你的Linux冒險之旅吧!
|
||||
{{ t('quest_map.intro') }}
|
||||
</h1>
|
||||
<svg
|
||||
:width="svgWidth"
|
||||
:height="svgHeight"
|
||||
class="absolute"
|
||||
@mousedown="startDrag"
|
||||
@mousemove="drag"
|
||||
@mouseup="endDrag"
|
||||
@mouseleave="endDrag"
|
||||
<div
|
||||
class="absolute inset-0 overflow-hidden select-none"
|
||||
:class="isDragging ? 'cursor-grabbing' : 'cursor-grab'"
|
||||
style="touch-action: none"
|
||||
@pointerdown="startDrag"
|
||||
@pointermove="drag"
|
||||
@pointerup="endDrag"
|
||||
@pointercancel="endDrag"
|
||||
>
|
||||
<g :transform="`translate(${offsetX}, ${offsetY})`">
|
||||
<g
|
||||
v-for="edge in graphData.edges"
|
||||
:key="`${edge.source.id}-${edge.target.id}`"
|
||||
>
|
||||
<div
|
||||
class="absolute top-1/2 left-0"
|
||||
:style="{ transform: `translate(${offsetX}px, ${offsetY}px)` }"
|
||||
>
|
||||
<svg class="absolute overflow-visible pointer-events-none">
|
||||
<path
|
||||
:d="curvedPath(edge.source, edge.target)"
|
||||
v-for="edge in graphData.edges"
|
||||
:key="`${edge.source.id}-${edge.target.id}`"
|
||||
:d="edgePath(edge.source, edge.target)"
|
||||
:class="['edge', edgeStyle(edge)]"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
</svg>
|
||||
<button
|
||||
v-for="node in graphData.nodes"
|
||||
:key="node.id"
|
||||
type="button"
|
||||
:class="['node', nodeStyle(node)]"
|
||||
:style="{
|
||||
left: `${node.x - nodeWidth / 2}px`,
|
||||
top: `${node.y - nodeHeight / 2}px`,
|
||||
width: `${nodeWidth}px`,
|
||||
height: `${nodeHeight}px`,
|
||||
}"
|
||||
@click="handleNodeClick(node)"
|
||||
>
|
||||
<rect
|
||||
:x="node.x - nodeWidth / 2"
|
||||
:y="node.y - nodeHeight / 2"
|
||||
:width="nodeWidth"
|
||||
:height="nodeHeight"
|
||||
rx="10"
|
||||
ry="10"
|
||||
:class="['node', nodeStyle(node)]"
|
||||
/>
|
||||
<text
|
||||
:x="node.x"
|
||||
:y="node.y"
|
||||
text-anchor="middle"
|
||||
alignment-baseline="middle"
|
||||
:class="['node-text', nodeStyle(node)]"
|
||||
font-size="18"
|
||||
>
|
||||
{{ node.title }}
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<span class="line-clamp-2">{{ node.title }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<QuestIntro
|
||||
:questTitle="opened.title"
|
||||
:questId="opened.id"
|
||||
@@ -211,44 +206,49 @@ const edgeStyle = computed(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.node {
|
||||
&.completed {
|
||||
fill: #6cf76c;
|
||||
}
|
||||
&.unlocked {
|
||||
fill: #8c8c92;
|
||||
}
|
||||
&.locked {
|
||||
fill: #505050;
|
||||
}
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 0.5rem;
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
filter 0.15s,
|
||||
box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.node-text {
|
||||
&.completed {
|
||||
fill: #1d1d1d;
|
||||
}
|
||||
&.unlocked {
|
||||
fill: #ffffff;
|
||||
}
|
||||
&.locked {
|
||||
fill: #a0a0a0;
|
||||
}
|
||||
.node:hover {
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
.node:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px #00ff00;
|
||||
}
|
||||
.node.completed {
|
||||
background-color: #6cf76c;
|
||||
color: #1d1d1d;
|
||||
}
|
||||
.node.unlocked {
|
||||
background-color: #8c8c92;
|
||||
color: #ffffff;
|
||||
}
|
||||
.node.locked {
|
||||
background-color: #505050;
|
||||
color: #a0a0a0;
|
||||
}
|
||||
|
||||
.edge {
|
||||
stroke-width: 3;
|
||||
fill: none;
|
||||
&.unlocked {
|
||||
stroke: #adadb5;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
&.locked {
|
||||
stroke: #454552;
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
}
|
||||
.edge.unlocked {
|
||||
stroke: #adadb5;
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
.edge.locked {
|
||||
stroke: #454552;
|
||||
stroke-dasharray: 5, 5;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">
|
||||
{{ t('game.command_book') }}
|
||||
</h1>
|
||||
<div id="cheatsheets">
|
||||
<p class="text-text-secondary">-- --指令列表-- --</p>
|
||||
<p class="text-text-secondary">-- --{{ t('game.cheatsheets') }}-- --</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { ref } from 'vue'
|
||||
@@ -6,6 +7,8 @@ import { Vue3Lottie } from 'vue3-lottie'
|
||||
// import { surveyUrl } from '../config'
|
||||
import CompleteAnimation from '../../img/complete-animation.json'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const showModal = ref(true) // Controls whether the modal is visible or not
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
@@ -29,7 +32,7 @@ const backtoMap = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
toast.error('無法讀取地圖')
|
||||
toast.error(t('quest_map.load_quest_map_failed'))
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +52,7 @@ setTimeout(() => {
|
||||
id="QuestCompleted"
|
||||
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||
>
|
||||
關卡完成!
|
||||
{{ t('game.quest_completed') }}
|
||||
</h2>
|
||||
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
|
||||
<Vue3Lottie
|
||||
@@ -58,16 +61,18 @@ setTimeout(() => {
|
||||
:width="200"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-base text-text text-center">恭喜你完成這一關!</p>
|
||||
<p class="text-base text-text text-center">
|
||||
接下來回到地圖,迎接新的挑戰吧!
|
||||
{{ t('game.quest_completed_desc_1') }}
|
||||
</p>
|
||||
<p class="text-base text-text text-center">
|
||||
{{ t('game.quest_completed_desc_2') }}
|
||||
</p>
|
||||
<a
|
||||
id="BacktoMap"
|
||||
target="_blank"
|
||||
@click="backtoMap"
|
||||
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
|
||||
>回到地圖</a
|
||||
>{{ t('game.back_to_map') }}</a
|
||||
>
|
||||
</div>
|
||||
<div style="position: relative; height: 100vh; overflow: auto">
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { useSession } from '../../store/session'
|
||||
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
|
||||
import { trpc } from '../../utils/trpc'
|
||||
import type { QuestLocale } from '../../../../packages/constants'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const router = useRouter()
|
||||
const sessionStore = useSession()
|
||||
const reset = async () => {
|
||||
console.log('resetting...')
|
||||
await sessionStore.createSession()
|
||||
const selectedLocale = locale.value as QuestLocale
|
||||
const questId = sessionStore.session?.quest
|
||||
|
||||
if (!questId) return
|
||||
|
||||
try {
|
||||
const quests = await trpc.quests.getQuests.query({
|
||||
locale: selectedLocale,
|
||||
})
|
||||
|
||||
console.log({
|
||||
selectedLocale,
|
||||
questId,
|
||||
availableQuestIds: quests.map((quest) => quest.id),
|
||||
})
|
||||
|
||||
if (!quests.some((quest) => quest.id === questId)) {
|
||||
toast.warning(t('game.language_unavailable'))
|
||||
return
|
||||
}
|
||||
|
||||
await sessionStore.createSession()
|
||||
} catch (error) {
|
||||
console.error('Failed to reset quest:', error)
|
||||
toast.error(t('game.reset_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
const continuePlay = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
useToast().error('無法讀取地圖')
|
||||
useToast().error(t('quest_map.load_quest_map_failed'))
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="mt-2 flex flex-row gap-3 justify-center">
|
||||
<section class="mt-2 flex flex-row gap-2 justify-center">
|
||||
<button
|
||||
id="survey"
|
||||
:title="t('game_nav.survey')"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="openQuestSurvey"
|
||||
>
|
||||
@@ -31,15 +64,18 @@ const continuePlay = async () => {
|
||||
:icon="['fas', 'file-invoice']"
|
||||
class="text-text-primary px-1"
|
||||
/>
|
||||
<p class="text-text-primary inline md:flex">問卷</p>
|
||||
<p class="text-text-primary text-xs">{{ t('game_nav.survey') }}</p>
|
||||
</button>
|
||||
<button
|
||||
title="Bug Report"
|
||||
id="bug-report"
|
||||
:title="t('game_nav.bug_report')"
|
||||
@click="openBugReport"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||
<p class="text-text-primary inline ml-1 md:flex">回報</p>
|
||||
<p class="text-text-primary text-xs">
|
||||
{{ t('game_nav.bug_report') }}
|
||||
</p>
|
||||
</button>
|
||||
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||
<font-awesome-icon
|
||||
@@ -48,15 +84,23 @@ const continuePlay = async () => {
|
||||
/>
|
||||
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
|
||||
</button> -->
|
||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||
<button
|
||||
id="reset"
|
||||
:title="t('game.reset')"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="reset"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-rotate-left']"
|
||||
class="text-text-primary px-1"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1 md:flex">重來</p>
|
||||
<p class="text-text-primary text-xs">
|
||||
{{ t('game.reset') }}
|
||||
</p>
|
||||
</button>
|
||||
<button
|
||||
id="continue"
|
||||
:title="t('game.continue')"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="continuePlay"
|
||||
>
|
||||
@@ -64,7 +108,9 @@ const continuePlay = async () => {
|
||||
:icon="['far', 'circle-right']"
|
||||
class="text-text-primary px-1"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
|
||||
<p class="text-text-primary text-xs">
|
||||
{{ t('game.continue') }}
|
||||
</p>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useSession } from '../../store/session'
|
||||
import MarkdownText from '../MarkdownText.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const sessionStore = useSession()
|
||||
const current = ref(-1)
|
||||
const hintDetails = ref<HTMLDetailsElement | null>(null)
|
||||
@@ -52,7 +55,9 @@ watch(response, () => {
|
||||
:icon="['far', 'lightbulb']"
|
||||
class="text-yellow-200 p-2 content-center"
|
||||
/>
|
||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">說明</h1>
|
||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
|
||||
{{ t('game.description') }}
|
||||
</h1>
|
||||
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
||||
<button
|
||||
class="p-2 m-1 w-1/8"
|
||||
@@ -98,7 +103,7 @@ watch(response, () => {
|
||||
class="text-text-primary font-xl whitespace-pre-wrap"
|
||||
>
|
||||
<details ref="hintDetails">
|
||||
<summary class="cursor-pointer">提示</summary>
|
||||
<summary class="cursor-pointer">{{ t('game.hint') }}</summary>
|
||||
<MarkdownText :content="response.hint" />
|
||||
</details>
|
||||
</li>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed } from 'vue'
|
||||
import MarkdownText from '../MarkdownText.vue'
|
||||
import { useSession } from '../../store/session'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const sessionStore = useSession()
|
||||
const tasks = computed(() => {
|
||||
console.log('session', sessionStore.session)
|
||||
@@ -31,14 +34,14 @@ const tasks = computed(() => {
|
||||
:content="sessionStore.quest.instruction"
|
||||
/>
|
||||
<br />
|
||||
<p id="tasks" class="text-text">任務:</p>
|
||||
<p id="tasks" class="text-text font-bold">{{ t('game.task') }}</p>
|
||||
<ul v-if="sessionStore.session">
|
||||
<li v-for="task in tasks" :key="task.id">
|
||||
<MarkdownText :class="task.class" :content="task.content" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="text-text" v-else>Loading...</p>
|
||||
<p class="text-text" v-else>{{ t('game.loading') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { computed } from 'vue'
|
||||
import { useSession } from '../../store/session'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const toast = useToast()
|
||||
const sessionStore = useSession()
|
||||
|
||||
@@ -31,7 +34,7 @@ const startSession = async () => {
|
||||
:icon="['fas', 'play']"
|
||||
class="text-text-primary h-full px-3"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1 md:flex">開始</p>
|
||||
<p class="text-text-primary inline ml-1 md:flex">{{ t('game.start') }}</p>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,7 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useSession, useTerminal } from '../../store/session'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const sessionStore = useSession()
|
||||
|
||||
const terminal = ref<HTMLElement | null>(null)
|
||||
@@ -24,14 +27,14 @@ onMounted(() => {
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex items-center gap-2 p-1">
|
||||
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text" />
|
||||
<button id="Terminal" class="text-text">終端機</button>
|
||||
<p id="Terminal" class="text-text">{{ t('game.terminal') }}</p>
|
||||
|
||||
<a
|
||||
class="text-sm font-normal text-gray-200 ml-10"
|
||||
class="text-xs font-normal text-gray-200 ml-10 bg-bg-primary rounded-lg px-2 py-1 hover:bg-bg-primary-hover transition-colors duration-300"
|
||||
:href="`/terminal/${sessionStore.session?.containerName}?token=${sessionStore.session?.token}`"
|
||||
target="_blank"
|
||||
>
|
||||
Open Terminal in New Tab
|
||||
{{ t('game.open_terminal_in_new_tab') }}
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
@@ -8,6 +9,7 @@ import { openBugReport } from '../../utils/formbricks'
|
||||
import { useSession } from '../../store/session'
|
||||
import useUserProfile from '../../store/userProfile'
|
||||
|
||||
const { t } = useI18n()
|
||||
const userStore = useUserProfile()
|
||||
const sessionStore = useSession()
|
||||
const router = useRouter()
|
||||
@@ -21,7 +23,7 @@ const handleLogout = async () => {
|
||||
sessionStore.reset()
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
toast.error('登出失敗')
|
||||
toast.error(t('game_nav.sign_out_failed'))
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +37,7 @@ onMounted(async () => {
|
||||
try {
|
||||
await userStore.loadUserProfile()
|
||||
} catch (err) {
|
||||
toast.error('無法讀取使用者資料')
|
||||
toast.error(t('game_nav.load_user_profile_failed'))
|
||||
console.error(err)
|
||||
}
|
||||
})
|
||||
@@ -67,7 +69,7 @@ onMounted(async () => {
|
||||
{{ userStore.username }}
|
||||
</p>
|
||||
<button
|
||||
title="Survey"
|
||||
:title="t('game_nav.survey')"
|
||||
target="_blank"
|
||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||
@click="openSurvey"
|
||||
@@ -76,37 +78,36 @@ onMounted(async () => {
|
||||
:icon="['fas', 'file-invoice']"
|
||||
class="text-text-primary inline"
|
||||
/>
|
||||
<span class="text-text sm:hidden px-2">Survey</span>
|
||||
</button>
|
||||
<RouterLink
|
||||
title="LeaderBoard"
|
||||
:title="t('game_nav.leaderboard')"
|
||||
to="/leaderboard"
|
||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'trophy']" class="text-text-primary" />
|
||||
<span class="text-text sm:hidden px-2">Leader Board</span>
|
||||
</RouterLink>
|
||||
<RouterLink title="Map" to="/map" class="md:h-5 md:w-5 px-1.5 w-auto">
|
||||
<RouterLink
|
||||
:title="t('game_nav.map')"
|
||||
to="/map"
|
||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
|
||||
<span class="text-text sm:hidden px-2">Map</span>
|
||||
</RouterLink>
|
||||
<button
|
||||
title="Bug Report"
|
||||
:title="t('game_nav.bug_report')"
|
||||
@click="openBugReport"
|
||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||
>
|
||||
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||
<span class="text-text sm:hidden px-2">Bug Report</span>
|
||||
</button>
|
||||
<button
|
||||
title="Sign Out"
|
||||
:title="t('game_nav.sign_out')"
|
||||
@click="handleLogout"
|
||||
class="md:h-5 md:w-5 px-1.5 w-auto"
|
||||
class="md:h-5 md:w-5 px-1.5 mr-2 w-auto"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-right-from-bracket']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<span class="text-text sm:hidden px-2">Sign Out</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -1,5 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { defineProps, ref } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import LanguageSelector from './LanguageSelector.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
@@ -19,7 +24,7 @@ const toggleMenu = () => {
|
||||
</script>
|
||||
<template>
|
||||
<div
|
||||
class="w-full top-0 px-5 fixed flex flex-row flex-wrap bg-bg-secondary justify-between items-center sm:justify-between"
|
||||
class="w-full top-0 px-2 fixed flex flex-row flex-wrap bg-bg-secondary justify-between items-center sm:justify-between"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-3" :class="{ hidden: menuOpen }">
|
||||
<img src="../../img/icon_totem.svg" class="h-8 justify-self-start" />
|
||||
@@ -44,6 +49,7 @@ const toggleMenu = () => {
|
||||
:class="{ hidden: !menuOpen }"
|
||||
>
|
||||
<component :is="headerComponent" />
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
<div
|
||||
v-if="windowWidth"
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { bugReportUrl } from '../../config'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const scrollToSection = (sectionId: string) => {
|
||||
const section = document.getElementById(sectionId)
|
||||
if (section) {
|
||||
@@ -21,22 +24,25 @@ const scrollToSection = (sectionId: string) => {
|
||||
@click="scrollToSection('intro')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
前言
|
||||
{{ t('landing_nav.intro') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="Feature"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
@click="scrollToSection('feature')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
特色</button
|
||||
><button
|
||||
{{ t('landing_nav.features') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
id="Info"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
@click="scrollToSection('about')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
關於
|
||||
{{ t('landing_nav.about') }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -45,18 +51,27 @@ const scrollToSection = (sectionId: string) => {
|
||||
@click="scrollToSection('contact')"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
聯絡我們
|
||||
{{ t('landing_nav.contact') }}
|
||||
</button>
|
||||
|
||||
<RouterLink
|
||||
title="Sign In"
|
||||
id="SignIn"
|
||||
to="/register"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
登入
|
||||
{{ t('landing_nav.login') }}
|
||||
</RouterLink>
|
||||
<a title="Bug Report" :href="bugReportUrl" target="_blank" class="h-5 w-5">
|
||||
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
|
||||
|
||||
<a
|
||||
:title="t('landing_nav.report_bug')"
|
||||
:href="bugReportUrl"
|
||||
target="_blank"
|
||||
class="h-5 w-5"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'bug']"
|
||||
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
|
||||
/>
|
||||
</a>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { setLocale } from '../../i18n'
|
||||
import type { QuestLocale } from '../../../../packages/constants'
|
||||
import {
|
||||
SUPPORTED_LOCALES,
|
||||
LOCALE_NAME_MAP,
|
||||
} from '../../../../packages/constants'
|
||||
|
||||
const { t, locale } = useI18n({ useScope: 'global' })
|
||||
|
||||
function changeLanguage(event: Event) {
|
||||
const selectedLocale = (event.target as HTMLSelectElement)
|
||||
.value as QuestLocale
|
||||
|
||||
setLocale(selectedLocale)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<select
|
||||
:value="locale"
|
||||
:title="t('language.change')"
|
||||
class="bg-bg-secondary px-3 py-2 text-sm text-text shadow-md outline-none"
|
||||
aria-label="Language"
|
||||
@change="changeLanguage"
|
||||
style="font-size: 2vh"
|
||||
>
|
||||
<option v-for="locale in SUPPORTED_LOCALES" :key="locale" :value="locale">
|
||||
{{ LOCALE_NAME_MAP[locale] }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import type { QuestLocale } from '../../../packages/constants'
|
||||
import { SUPPORTED_LOCALES } from '../../../packages/constants'
|
||||
import en from './locales/en.json'
|
||||
import zhTW from './locales/zh-TW.json'
|
||||
|
||||
function isSupportedLocale(locale: string): locale is QuestLocale {
|
||||
return SUPPORTED_LOCALES.includes(locale as QuestLocale)
|
||||
}
|
||||
|
||||
function getInitialLocale(): QuestLocale {
|
||||
const savedLocale = localStorage.getItem('locale')
|
||||
|
||||
if (savedLocale && isSupportedLocale(savedLocale)) {
|
||||
return savedLocale
|
||||
}
|
||||
|
||||
return navigator.language.startsWith('zh') ? 'zh-TW' : 'en'
|
||||
}
|
||||
|
||||
const initialLocale = getInitialLocale()
|
||||
|
||||
document.documentElement.lang = initialLocale
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: initialLocale,
|
||||
fallbackLocale: 'zh-TW',
|
||||
messages: {
|
||||
'zh-TW': zhTW,
|
||||
en,
|
||||
},
|
||||
})
|
||||
|
||||
export function setLocale(locale: QuestLocale) {
|
||||
i18n.global.locale.value = locale
|
||||
localStorage.setItem('locale', locale)
|
||||
document.documentElement.lang = locale
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"language": {
|
||||
"change": "Change language"
|
||||
},
|
||||
"landing_nav": {
|
||||
"intro": "Introduction",
|
||||
"features": "Features",
|
||||
"about": "About",
|
||||
"contact": "Contact",
|
||||
"login": "Sign in",
|
||||
"report_bug": "Report a bug"
|
||||
},
|
||||
"landing": {
|
||||
"intro": "Programming is about more than writing code - it also means knowing how to interact effectively with your computer. Yet programming education often overlooks the importance of the development environment, leaving many learners frustrated. A development environment includes all the tools needed to write and run software, many of which are used through terminal commands. While most learners use Windows or macOS, much of the software world runs on Linux. Linux Odyssey is an interactive online learning platform designed to bridge that gap.",
|
||||
"feature1": "Easy for Beginners",
|
||||
"feature1_desc": "Setting up and using development tools can be a major hurdle for beginners. With Linux Odyssey, there is nothing to install - learners can access a real Linux environment directly from their browser.",
|
||||
"feature2": "Learn Through Play",
|
||||
"feature2_desc": "Linux Odyssey combines the fun of gaming with practical learning. Through carefully designed levels, learners take on challenges, solve puzzles, and master Linux commands along the way.",
|
||||
"feature3": "Hands-on Linux Experience",
|
||||
"feature3_desc": "Linux Odyssey goes beyond theory by providing real hands-on experience. Every command runs in a real Linux environment, giving learners an authentic introduction to the world of software development.",
|
||||
"start_journey": "Grab your keyboard and begin your Linux adventure!",
|
||||
"team_intro": "Meet the Team",
|
||||
"team_desc": "NTUT Intelligent Automation Engineering, 2nd Cohort",
|
||||
"team_member1": "Wancat",
|
||||
"team_member2": "Neil",
|
||||
"team_member3": "A.H.",
|
||||
"team_member4": "Ray Gur",
|
||||
"awards": "Awards",
|
||||
"award_desc": "2023 InnoServe Awards - 2nd Place, Titansoft Agile Development Award",
|
||||
"player_feedback": "What Players Say",
|
||||
"feedback1": "\"It's really fun - just like a puzzle game.\"",
|
||||
"feedback2": "\"I never knew free software existed. Thank you!\"",
|
||||
"feedback3": "\"It's pretty fun. It makes programming enjoyable.\"",
|
||||
"feedback4": "\"I used to dislike computer class, but now I like it a little more.\"",
|
||||
"feedback5": "\"The program is really beginner-friendly, and I learned a lot.\"",
|
||||
"feedback6": "\"It's very well made, clear, and easy to understand. A lot of thought went into the level design.\"",
|
||||
"feedback7": "\"I found the experience really interesting. I got to try programming in a way I never had before, and the student mentors were very helpful. Thank you for explaining everything so carefully. I hope I can join an activity like this again.\"",
|
||||
"feedback8": "\"I used to think programming was difficult, but now I find it interesting.\"",
|
||||
"feedback9": "\"I used to think programming was difficult and had nothing to do with my life. After this class, it feels much more approachable.\"",
|
||||
"contact_us": "Contact Us"
|
||||
},
|
||||
"authform": {
|
||||
"google_login": "Continue with Google",
|
||||
"github_login": "Continue with GitHub",
|
||||
"or": "or",
|
||||
"username": "Username",
|
||||
"username_rules": "At most 32 characters, lowercase first, use lowercase letters, numbers, _, and -",
|
||||
"email": "Email",
|
||||
"password": "Password",
|
||||
"login": "Login",
|
||||
"register": "Register",
|
||||
"already_registered": "Already have an account?",
|
||||
"not_registered": "Don't have an account?",
|
||||
"continue_as_guest": "Continue as Guest",
|
||||
"password_rules": "At least 8 characters, including uppercase and lowercase letters and numbers",
|
||||
"recommand_pc_fullscreen": "Recommended to use desktop version in full-screen mode"
|
||||
},
|
||||
"authform_error": {
|
||||
"too_many_requests": "Too many requests, please try again in 2 minutes",
|
||||
"unauthorized": "Invalid username or password",
|
||||
"auth_failed": "Something went wrong",
|
||||
"invalid_username": "Invalid username",
|
||||
"invalid_email": "Invalid email",
|
||||
"invalid_password": "Invalid password",
|
||||
"username_taken": "Username is already taken",
|
||||
"email_taken": "Email is already taken"
|
||||
},
|
||||
"game_nav": {
|
||||
"survey": "Survey",
|
||||
"leaderboard": "Leaderboard",
|
||||
"map": "Map",
|
||||
"bug_report": "Bug Report",
|
||||
"sign_out": "Sign Out",
|
||||
"sign_out_failed": "Sign Out Failed",
|
||||
"load_user_profile_failed": "Failed to Load User Profile"
|
||||
},
|
||||
"quest_map": {
|
||||
"title": "Quest Map",
|
||||
"intro": "Embark on your Linux adventure!",
|
||||
"load_quest_map_failed": "Failed to Load Quest Map",
|
||||
"compute_graph_failed": "Error computing graph data: ",
|
||||
"load_quest_data_failed": "Failed to Load Quest Data"
|
||||
},
|
||||
"quest": {
|
||||
"start": "Start Adventure",
|
||||
"locked": "Quest not unlocked",
|
||||
"load_quest_instruction_failed": "Failed to Load Quest Instruction: "
|
||||
},
|
||||
"leaderboard": {
|
||||
"title": "Leaderboard",
|
||||
"username": "Username",
|
||||
"score": "Score",
|
||||
"completed_quests": "Completed Quests",
|
||||
"load_leaderboard_failed": "Failed to Load Leaderboard"
|
||||
},
|
||||
"game": {
|
||||
"start": "Start",
|
||||
"task": "Task:",
|
||||
"loading": "Loading...",
|
||||
"description": "Description",
|
||||
"hint": "Hint",
|
||||
"terminal": "Terminal",
|
||||
"open_terminal_in_new_tab": "Open Terminal in New Tab",
|
||||
"reset": "Reset",
|
||||
"continue": "Continue",
|
||||
"command_book": "Command Book",
|
||||
"cheatsheets": "Cheat Sheets",
|
||||
"reset_to_apply_language": "Reset quest to apply language changes",
|
||||
"reset_failed": "Reset failed",
|
||||
"language_unavailable": "This quest does not support the selected language, please choose another language",
|
||||
"quest_completed": "Quest Completed",
|
||||
"quest_completed_desc_1": "Congratulations! You have completed this quest!",
|
||||
"quest_completed_desc_2": "Return to the map to face new challenges!",
|
||||
"back_to_map": "Back to Map"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"language": {
|
||||
"change": "切換語言"
|
||||
},
|
||||
"landing_nav": {
|
||||
"intro": "前言",
|
||||
"features": "特色",
|
||||
"about": "關於",
|
||||
"contact": "聯絡我們",
|
||||
"login": "登入",
|
||||
"report_bug": "回報錯誤"
|
||||
},
|
||||
"landing": {
|
||||
"intro": "程式設計不僅僅是寫程式碼,還涉及到如何有效地與你的電腦互動。程式教育卻往往忽略背後「開發環境」的重要性,使得許多學生在過程中充滿挫折。「開發環境」是寫程式、執行程式過程所需要的一切工具。而這些工具需要在終端機輸入指令來使用。大部分的程式要用來運行的環境,並非 Windows 或 Mac OS,而是 Linux 作業系統。 Linux Odyssey 即是為此設計的線上 Linux 互動教學網站。",
|
||||
"feature1": "初學者也能立即上手",
|
||||
"feature1_desc": "我們明白對於初學者來說,設定和使用開發工具可能是一大障礙。學習者無需安裝任何軟體,僅需通過瀏覽器即可連上一個真實的 Linux 環境。",
|
||||
"feature2": "遊戲般的學習體驗",
|
||||
"feature2_desc": "Linux Odyssey 融合了遊戲的樂趣與學習的實用性。透過精心設計的遊戲關卡,學習者可以一邊接受挑戰、破解謎題,一邊掌握 Linux 指令的運用。",
|
||||
"feature3": "真實的 Linux 操作之旅",
|
||||
"feature3_desc": "Linux Odyssey 提供的不僅是理論知識,更是真實的操作體驗。每一個指令都是在真實的 Linux 環境中執行,讓學習者能夠真正深入軟體開發的世界。",
|
||||
"start_journey": "準備好鍵盤,跟我們一起開啟你的 Linux 冒險吧!",
|
||||
"team_intro": "團隊介紹",
|
||||
"team_desc": "國立臺北科技大學 智慧自動化工程科 第二屆",
|
||||
"team_member1": "Wancat",
|
||||
"team_member2": "Neil",
|
||||
"team_member3": "A.H.",
|
||||
"team_member4": "Ray Gur",
|
||||
"awards": "獲獎紀錄",
|
||||
"award_desc": "2023大專校院資訊應用服務創新競賽 鈦坦敏捷開發特別獎 第二名",
|
||||
"player_feedback": "玩家感想",
|
||||
"feedback1": "\"很好玩很像益智遊戲\"",
|
||||
"feedback2": "\"我從來不知道自由軟體的存在,謝謝你們。\"",
|
||||
"feedback3": "\"還蠻好玩的,讓程式變好玩\"",
|
||||
"feedback4": "\"以前不喜歡電腦課現在覺得喜歡一點點\"",
|
||||
"feedback5": "\"程式真的對新手很友善,讓我學到了很多東西\"",
|
||||
"feedback6": "\"感覺做得很好很簡明易懂關卡設計很用心\"",
|
||||
"feedback7": "\"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動\"",
|
||||
"feedback8": "\"以前我覺得寫程式很難不過現在覺得很有趣\"",
|
||||
"feedback9": "\"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近\"",
|
||||
"contact_us": "聯絡我們"
|
||||
},
|
||||
"authform": {
|
||||
"google_login": "以 Google 繼續",
|
||||
"github_login": "以 GitHub 繼續",
|
||||
"or": "或",
|
||||
"username": "帳號名稱",
|
||||
"username_rules": "至多 32 字元,小寫字母開頭,可用小寫字母、數字、_ 和 -",
|
||||
"email": "電子郵件",
|
||||
"password": "密碼",
|
||||
"login": "登入",
|
||||
"register": "註冊",
|
||||
"already_registered": "已經有帳號了嗎?",
|
||||
"not_registered": "還沒有帳號嗎?",
|
||||
"continue_as_guest": "以訪客身分繼續",
|
||||
"password_rules": "至少 8 個字元,包含數字及大小寫字母",
|
||||
"recommand_pc_fullscreen": "建議使用電腦版全螢幕操作"
|
||||
},
|
||||
"authform_error": {
|
||||
"too_many_requests": "太多請求,兩分鐘後再試一次",
|
||||
"unauthorized": "無效的帳號名稱或密碼",
|
||||
"auth_failed": "出了點問題",
|
||||
"invalid_username": "無效的帳號名稱",
|
||||
"invalid_email": "無效的電子郵件",
|
||||
"invalid_password": "無效的密碼",
|
||||
"username_taken": "帳號名稱已被使用",
|
||||
"email_taken": "電子郵件已被使用"
|
||||
},
|
||||
"game_nav": {
|
||||
"survey": "問卷",
|
||||
"leaderboard": "排行榜",
|
||||
"map": "地圖",
|
||||
"bug_report": "回報錯誤",
|
||||
"sign_out": "登出",
|
||||
"sign_out_failed": "登出失敗",
|
||||
"load_user_profile_failed": "無法讀取使用者資料"
|
||||
},
|
||||
"quest_map": {
|
||||
"title": "關卡地圖",
|
||||
"intro": "踏上你的Linux冒險之旅吧!",
|
||||
"load_quest_map_failed": "無法讀取關卡地圖",
|
||||
"compute_graph_failed": "計算關卡地圖時出錯:",
|
||||
"load_quest_data_failed": "無法讀取關卡"
|
||||
},
|
||||
"quest": {
|
||||
"start": "開始冒險",
|
||||
"locked": "關卡尚未解鎖",
|
||||
"load_quest_instruction_failed": "無法讀取關卡說明:"
|
||||
},
|
||||
"leaderboard": {
|
||||
"title": "排行榜",
|
||||
"username": "使用者名稱",
|
||||
"score": "分數",
|
||||
"completed_quests": "完成關卡",
|
||||
"load_leaderboard_failed": "無法讀取排行榜"
|
||||
},
|
||||
"game": {
|
||||
"start": "開始",
|
||||
"task": "任務:",
|
||||
"loading": "載入中...",
|
||||
"description": "說明",
|
||||
"hint": "提示",
|
||||
"terminal": "終端機",
|
||||
"open_terminal_in_new_tab": "在新分頁開啟終端機",
|
||||
"reset": "重置",
|
||||
"continue": "繼續",
|
||||
"command_book": "指令之書",
|
||||
"cheatsheets": "指令列表",
|
||||
"reset_to_apply_language": "重置關卡以套用語言變更",
|
||||
"reset_failed": "重置失敗",
|
||||
"language_unavailable": "此關卡不支援所選語言,請選擇其他語言",
|
||||
"quest_completed": "關卡完成",
|
||||
"quest_completed_desc_1": "恭喜你完成了這個關卡!",
|
||||
"quest_completed_desc_2": "回到地圖,迎接新的挑戰吧!",
|
||||
"back_to_map": "回到地圖"
|
||||
}
|
||||
}
|
||||
+7
-9
@@ -1,18 +1,15 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import './style.css'
|
||||
/* import font awesome icon component */
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import './library'
|
||||
/* import Vue Toastification */
|
||||
import Toast, { POSITION } from 'vue-toastification'
|
||||
/* import Vue Lottie */
|
||||
import Vue3Lottie from 'vue3-lottie'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' // import font awesome icon component
|
||||
import { i18n } from './i18n'
|
||||
import Toast, { POSITION } from 'vue-toastification' // import Vue Toastification
|
||||
import Vue3Lottie from 'vue3-lottie' // import Vue Lottie
|
||||
import 'vue-toastification/dist/index.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import formbricks from './utils/formbricks'
|
||||
import './style.css'
|
||||
import './library'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
@@ -21,6 +18,7 @@ app.component('font-awesome-icon', FontAwesomeIcon)
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
||||
router.afterEach(() => {
|
||||
if (typeof formbricks !== 'undefined') {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { QuestLocale } from '../../../packages/constants'
|
||||
import {
|
||||
FileGraph,
|
||||
type FileGraphUpdateEvent,
|
||||
@@ -9,6 +10,7 @@ import Socket from '../utils/socket'
|
||||
import SocketTerminal from '../utils/terminal'
|
||||
import type { Session } from '../types'
|
||||
import { trpc } from '../utils/trpc'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
const socket = new Socket()
|
||||
const term = new SocketTerminal()
|
||||
@@ -41,11 +43,16 @@ export const useSession = defineStore('session', {
|
||||
quest: null,
|
||||
}),
|
||||
actions: {
|
||||
async setQuest(questId: string) {
|
||||
this.quest = await trpc.quests.getQuestDetail.query(questId)
|
||||
async setQuest(questId: string, locale: QuestLocale) {
|
||||
this.quest = await trpc.quests.getQuestDetail.query({
|
||||
questId,
|
||||
locale,
|
||||
})
|
||||
this.questId = questId
|
||||
},
|
||||
async setSession(session: SessionDetail) {
|
||||
await this.setQuest(session.quest, session.locale)
|
||||
|
||||
this.session = {
|
||||
...session,
|
||||
graph: new FileGraph(session.graph),
|
||||
@@ -60,18 +67,18 @@ export const useSession = defineStore('session', {
|
||||
term.focus()
|
||||
},
|
||||
async createSession() {
|
||||
const locale = i18n.global.locale.value
|
||||
const session = await trpc.session.createSession.mutate({
|
||||
questId: this.questId,
|
||||
locale,
|
||||
})
|
||||
await this.setSession(session)
|
||||
term.send('echo start\n')
|
||||
},
|
||||
async getActiveSession() {
|
||||
const session = await trpc.session.getActiveSession.query({
|
||||
questId: this.questId,
|
||||
})
|
||||
async getActiveSession(questId: string) {
|
||||
const session = await trpc.session.getActiveSession.query({ questId })
|
||||
if (session) {
|
||||
this.setSession(session)
|
||||
await this.setSession(session)
|
||||
}
|
||||
},
|
||||
newUpdate(update: SessionUpdate) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import api from './api'
|
||||
import { i18n } from '../i18n'
|
||||
|
||||
import {
|
||||
TooManyRequestsError,
|
||||
@@ -32,11 +33,15 @@ function handleRequestError(err: {
|
||||
const { path, msg } = errors[0]
|
||||
switch (path) {
|
||||
case 'username':
|
||||
throw new ValidationError('Username is already taken')
|
||||
throw new ValidationError(
|
||||
i18n.global.t('authform_error.username_taken')
|
||||
)
|
||||
case 'email':
|
||||
throw new ValidationError('Email is already taken')
|
||||
throw new ValidationError(i18n.global.t('authform_error.email_taken'))
|
||||
case 'password':
|
||||
throw new ValidationError('Password is too weak')
|
||||
throw new ValidationError(
|
||||
i18n.global.t('authform_error.invalid_password')
|
||||
)
|
||||
default:
|
||||
throw new ValidationError(msg)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { isValidUsername } from '../../../packages/utils'
|
||||
import AuthForm from '../components/AuthForm.vue'
|
||||
import Background from '../components/DynamicBackground.vue'
|
||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||
import { TooManyRequestsError, ValidationError } from '../utils/errors'
|
||||
import { checkUsername, chooseUsername } from '../utils/auth'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
async function check({
|
||||
username,
|
||||
error,
|
||||
@@ -15,21 +19,21 @@ async function check({
|
||||
}) {
|
||||
if (username) {
|
||||
if (!isValidUsername(username)) {
|
||||
error('無效的帳號名稱')
|
||||
error(t('authform_error.invalid_username'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
await checkUsername(username)
|
||||
} catch (err) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
error(t('authform_error.too_many_requests'))
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
error(err.message)
|
||||
return
|
||||
}
|
||||
error('出了點問題,請再試一次。')
|
||||
error(t('authform_error.auth_failed'))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +53,7 @@ async function handleSubmit({
|
||||
success()
|
||||
} catch (err: any) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
error(t('authform_error.too_many_requests'))
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
@@ -63,10 +67,11 @@ async function handleSubmit({
|
||||
|
||||
<template>
|
||||
<div class="w-screen h-screen">
|
||||
<Background class="w-full h-full" />
|
||||
<DynamicBackground class="w-full h-full" />
|
||||
<div
|
||||
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
||||
>
|
||||
<HeaderPart />
|
||||
<div class="w-fit">
|
||||
<AuthForm
|
||||
@onSubmit="handleSubmit"
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
import { useSession } from '../store/session'
|
||||
import useUserProfile from '../store/userProfile'
|
||||
@@ -13,6 +15,8 @@ import CompleteModal from '../components/game/CompleteModal.vue'
|
||||
import StartButton from '../components/game/StartButton.vue'
|
||||
import { openQuestSurvey } from '../utils/formbricks'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const toast = useToast()
|
||||
const sessionStore = useSession()
|
||||
const userStore = useUserProfile()
|
||||
|
||||
@@ -27,6 +31,13 @@ watch(completed, (newValue, oldValue) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(locale, (newLocale) => {
|
||||
const { session } = sessionStore
|
||||
if (session && newLocale !== session.locale) {
|
||||
toast.info(t('game.reset_to_apply_language'))
|
||||
}
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
questId: {
|
||||
type: String,
|
||||
@@ -37,8 +48,12 @@ const props = defineProps({
|
||||
onMounted(async () => {
|
||||
sessionStore.reset()
|
||||
sessionStore.setup()
|
||||
await sessionStore.setQuest(props.questId)
|
||||
await sessionStore.getActiveSession()
|
||||
|
||||
await sessionStore.getActiveSession(props.questId)
|
||||
|
||||
if (!sessionStore.session) {
|
||||
await sessionStore.setQuest(props.questId, locale.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ContactButton from '../components/ContactButton.vue'
|
||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||
import LandingHeaderComponents from '../components/header/LandingHeaderComponents.vue'
|
||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||
|
||||
import { facebookUrl, githubUrl } from '../config'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
<template>
|
||||
<div class="w-screen h-screen bg-scroll bg-bg">
|
||||
@@ -18,13 +21,9 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
<div
|
||||
class="flex flex-col md:flex-row mt-10 justify-around gap-5 items-center my-10"
|
||||
>
|
||||
<h1 class="text-text-primary text-7xl font-extrabold font-mono">
|
||||
Linux
|
||||
</h1>
|
||||
<h1 class="text-text-primary text-7xl font-extrabold">Linux</h1>
|
||||
<img src="../img/icon_totem.svg" class="h-[30vh]" />
|
||||
<h1 class="text-text-primary text-7xl font-extrabold font-mono">
|
||||
Odyssey
|
||||
</h1>
|
||||
<h1 class="text-text-primary text-7xl font-extrabold">Odyssey</h1>
|
||||
</div>
|
||||
<!-- intro -->
|
||||
<div class="w-full md:w-2/3 px-5">
|
||||
@@ -36,13 +35,7 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
class="text-text inline-block bg-bg-primary p-5 rounded-xl opacity-90"
|
||||
style="font-size: 2.5vh"
|
||||
>
|
||||
程式設計不僅僅是寫程式碼,還涉及到如何有效地與你的電腦互動。
|
||||
程式教育卻往往忽略背後「開發環境」的重要性,
|
||||
使得許多學生在過程中充滿挫折。
|
||||
「開發環境」是寫程式、執行程式過程所需要的一切工具。
|
||||
而這些工具需要在終端機輸入指令來使用。
|
||||
大部分的程式要用來運行的環境,並非 Windows 或 Mac OS,而是 Linux
|
||||
作業系統。 Linux Odyssey 即是為此設計的線上 Linux 互動教學網站。
|
||||
{{ t('landing.intro') }}
|
||||
</h1>
|
||||
</section>
|
||||
<section
|
||||
@@ -57,31 +50,27 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
class="h-[18vh] row-span-2"
|
||||
/>
|
||||
<h1
|
||||
class="text-text-primary p-5 font-extrabold font-mono col-span-2"
|
||||
class="text-text-primary p-5 font-extrabold col-span-2"
|
||||
style="font-size: 3.5vh"
|
||||
>
|
||||
初學者也能立即上手
|
||||
{{ t('landing.feature1') }}
|
||||
</h1>
|
||||
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
|
||||
我們明白對於初學者來說,設定和使用開發工具可能是一大障礙。
|
||||
學習者無需安裝任何軟體,僅需通過瀏覽器即可連上一個真實的 Linux
|
||||
環境。
|
||||
{{ t('landing.feature1_desc') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
class="bg-bg-primary p-5 rounded-xl opacity-90 grid grid-cols-3 grid-rows-2 justify-items-center items-center"
|
||||
>
|
||||
<h1
|
||||
class="text-text-primary p-5 font-extrabold font-mono col-span-2"
|
||||
class="text-text-primary p-5 font-extrabold col-span-2"
|
||||
style="font-size: 3.5vh"
|
||||
>
|
||||
遊戲般的學習體驗
|
||||
{{ t('landing.feature2') }}
|
||||
</h1>
|
||||
<img src="../img/icon_sword.svg" class="h-[18vh] row-span-2" />
|
||||
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
|
||||
Linux Odyssey 融合了遊戲的樂趣與學習的實用性。
|
||||
透過精心設計的遊戲關卡,學習者可以一邊接受挑戰、破解謎題,
|
||||
一邊掌握 Linux 指令的運用。
|
||||
{{ t('landing.feature2_desc') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
@@ -92,15 +81,13 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
class="h-[18vh] row-span-2"
|
||||
/>
|
||||
<h1
|
||||
class="text-text-primary p-5 font-extrabold font-mono col-span-2 pt-8"
|
||||
class="text-text-primary p-5 font-extrabold col-span-2 pt-8"
|
||||
style="font-size: 3.5vh"
|
||||
>
|
||||
真實的 Linux 操作之旅
|
||||
{{ t('landing.feature3') }}
|
||||
</h1>
|
||||
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
|
||||
Linux Odyssey 提供的不僅是理論知識,更是真實的操作體驗。
|
||||
每一個指令都是在真實的 Linux 環境中執行,
|
||||
讓學習者能夠真正深入軟體開發的世界。
|
||||
{{ t('landing.feature3_desc') }}
|
||||
</h1>
|
||||
</div>
|
||||
</section>
|
||||
@@ -120,7 +107,7 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
>
|
||||
<RouterLink title="StartGame" id="StartGame" to="/register">
|
||||
<div class="text-text text-xl p-2 w-full text-center">
|
||||
準備好鍵盤,跟我們一起開啟你的 Linux 冒險吧!
|
||||
{{ t('landing.start_journey') }}
|
||||
</div>
|
||||
</RouterLink>
|
||||
</section>
|
||||
@@ -129,24 +116,25 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
class="text-text flex flex-col items-center mt-10 gap-10"
|
||||
>
|
||||
<div class="w-full flex flex-col items-center gap-2">
|
||||
<h1 class="text-text-primary text-3xl font-extrabold font-mono">
|
||||
團隊介紹
|
||||
<h1 class="text-text-primary text-3xl font-extrabold">
|
||||
{{ t('landing.team_intro') }}
|
||||
</h1>
|
||||
<p class="text-lg">國立臺北科技大學 智慧自動化工程科 第二屆</p>
|
||||
<p class="text-lg">
|
||||
{{ t('landing.team_desc') }}
|
||||
</p>
|
||||
<div class="flex flex-row gap-5 justify-center">
|
||||
<h1 class="font-bold">Wancat</h1>
|
||||
<h1 class="font-bold">Neil</h1>
|
||||
<h1 class="font-bold">A.H.</h1>
|
||||
<h1 class="font-bold">Ray Gur</h1>
|
||||
<h1 class="font-bold">{{ t('landing.team_member1') }}</h1>
|
||||
<h1 class="font-bold">{{ t('landing.team_member2') }}</h1>
|
||||
<h1 class="font-bold">{{ t('landing.team_member3') }}</h1>
|
||||
<h1 class="font-bold">{{ t('landing.team_member4') }}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex flex-col items-center gap-2">
|
||||
<h1 class="text-text-primary text-3xl font-extrabold font-mono">
|
||||
得獎肯定
|
||||
<h1 class="text-text-primary text-3xl font-extrabold">
|
||||
{{ t('landing.awards') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
大專校院資訊應用服務創新競賽<br />
|
||||
鈦坦敏捷開發特別獎 第二名
|
||||
{{ t('landing.award_desc') }}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="container mx-auto">
|
||||
@@ -154,35 +142,37 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
class="justify-items-center grid grid-cols-1 sm:grid-cols-3 gap-5"
|
||||
>
|
||||
<h1
|
||||
class="text-text-primary text-3xl font-extrabold font-mono sm:col-span-3 gap-6"
|
||||
class="text-text-primary text-3xl font-extrabold sm:col-span-3 gap-6"
|
||||
>
|
||||
玩家感想
|
||||
{{ t('landing.player_feedback') }}
|
||||
<!--可以做成翻頁卡牌一次一個-->
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">"很好玩很像益智遊戲"</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"我從來不知道自由軟體的存在,謝謝你們。"
|
||||
{{ t('landing.feedback1') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"還蠻好玩的,讓程式變好玩"
|
||||
{{ t('landing.feedback2') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"以前不喜歡電腦課現在覺得喜歡一點點"
|
||||
{{ t('landing.feedback3') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"程式真的對新手很友善,讓我學到了很多東西"
|
||||
{{ t('landing.feedback4') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"感覺做得很好很簡明易懂關卡設計很用心"
|
||||
{{ t('landing.feedback5') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
|
||||
{{ t('landing.feedback6') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"以前我覺得寫程式很難不過現在覺得很有趣"
|
||||
{{ t('landing.feedback7') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
|
||||
{{ t('landing.feedback8') }}
|
||||
</h1>
|
||||
<h1 class="text-xl text-center">
|
||||
{{ t('landing.feedback9') }}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
@@ -192,8 +182,8 @@ import { facebookUrl, githubUrl } from '../config'
|
||||
id="contact"
|
||||
class="text-text mt-10 flex flex-col items-center"
|
||||
>
|
||||
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
|
||||
聯絡我們
|
||||
<h1 class="text-text-primary font-extrabold text-3xl">
|
||||
{{ t('landing.contact_us') }}
|
||||
</h1>
|
||||
<div
|
||||
class="flex flex-row items-center justify-around mt-5 gap-5 mb-5"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||
@@ -6,6 +7,8 @@ import HeaderPart from '../components/header/HeaderPart.vue'
|
||||
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||
import api from '../utils/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const leaderboard = ref<
|
||||
{
|
||||
username: string
|
||||
@@ -21,7 +24,7 @@ async function getLeaderboard() {
|
||||
leaderboard.value = res.data
|
||||
} catch (err: any) {
|
||||
console.error(err)
|
||||
toast.error('讀取排行榜失敗')
|
||||
toast.error(t('leaderboard.load_leaderboard_failed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,22 +40,33 @@ onMounted(async () => {
|
||||
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
|
||||
>
|
||||
<div class="w-full">
|
||||
<HeaderPart title="排行榜" :headerComponent="GameHeaderComponents" />
|
||||
<HeaderPart
|
||||
:title="t('leaderboard.title')"
|
||||
:headerComponent="GameHeaderComponents"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-fit h-[94vh] m-3">
|
||||
<div
|
||||
class="h-full bg-bg border-8 border-bg-primary flex flex-col items-center rounded-3xl p-10"
|
||||
>
|
||||
<h1 class="text-text-primary text-3xl mb-5">排行榜</h1>
|
||||
<h1 class="text-text-primary text-3xl mb-5">
|
||||
{{ t('leaderboard.title') }}
|
||||
</h1>
|
||||
<div class="max-h-300 overflow-auto">
|
||||
<table class="w-full text-sm text-left text-text">
|
||||
<thead
|
||||
class="text-xs text-text-secondary uppercase bg-bg-secondary"
|
||||
>
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3">使用者名稱</th>
|
||||
<th scope="col" class="px-6 py-3">分數</th>
|
||||
<th scope="col" class="px-6 py-3">完成關卡</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
{{ t('leaderboard.username') }}
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
{{ t('leaderboard.score') }}
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3">
|
||||
{{ t('leaderboard.completed_quests') }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AuthForm from '../components/AuthForm.vue'
|
||||
import DynamicBackground from '../components/DynamicBackground.vue'
|
||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||
@@ -10,6 +11,8 @@ import {
|
||||
} from '../utils/errors'
|
||||
import { login } from '../utils/auth'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const handleLogin = async ({
|
||||
username,
|
||||
password,
|
||||
@@ -27,19 +30,19 @@ const handleLogin = async ({
|
||||
if (isSuccess) success()
|
||||
} catch (err) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
error(t('authform_error.too_many_requests'))
|
||||
return
|
||||
}
|
||||
if (err instanceof UnauthorizedError) {
|
||||
error('錯誤的帳號名稱或密碼。')
|
||||
error(t('authform_error.unauthorized'))
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
error('無效的帳號名稱或密碼。')
|
||||
error(err.message)
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
error('出了點問題。')
|
||||
error(t('authform_error.auth_failed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import QuestMap from '../components/QuestMap.vue'
|
||||
import HeaderPart from '../components/header/HeaderPart.vue'
|
||||
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-fit">
|
||||
<HeaderPart
|
||||
title="關卡地圖"
|
||||
:title="t('quest_map.title')"
|
||||
:headerComponent="GameHeaderComponents"
|
||||
class="z-50"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import validator from 'validator'
|
||||
import { isValidUsername } from '../../../packages/utils'
|
||||
import { passwordPolicy } from '../../../packages/constants'
|
||||
@@ -12,6 +13,8 @@ import {
|
||||
ValidationError,
|
||||
} from '../utils/errors'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function handleRegister({
|
||||
username,
|
||||
email,
|
||||
@@ -30,11 +33,11 @@ function handleRegister({
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
error(t('authform_error.too_many_requests'))
|
||||
return
|
||||
}
|
||||
if (err instanceof UnauthorizedError) {
|
||||
error('無效的帳號名稱或密碼。')
|
||||
error(t('authform_error.unauthorized'))
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
@@ -42,7 +45,7 @@ function handleRegister({
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
error('出了點問題。')
|
||||
error(t('authform_error.auth_failed'))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,15 +62,15 @@ async function check({
|
||||
error: (msg: string) => void
|
||||
}) {
|
||||
if (username && !isValidUsername(username)) {
|
||||
error('無效的帳號名稱')
|
||||
error(t('authform_error.invalid_username'))
|
||||
return
|
||||
}
|
||||
if (email && !validator.isEmail(email)) {
|
||||
error('無效的電子郵件')
|
||||
error(t('authform_error.invalid_email'))
|
||||
return
|
||||
}
|
||||
if (password && !validator.isStrongPassword(password, passwordPolicy)) {
|
||||
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
|
||||
error(t('authform_error.invalid_password'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -78,11 +81,11 @@ async function check({
|
||||
return
|
||||
}
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
error(t('authform_error.too_many_requests'))
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
error('出了點問題。')
|
||||
error(t('authform_error.auth_failed'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
+149
-92
@@ -1,141 +1,198 @@
|
||||
# Windows Environment Setup
|
||||
|
||||
## Install WSL and Ubuntu
|
||||
## 1. Install WSL and Ubuntu
|
||||
|
||||
Please refer to the Official [Setup WSL](https://learn.microsoft.com/en-us/windows/wsl/setup/environment) on Microsoft's website.
|
||||
Refer to Microsoft’s official [WSL installation guide](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
|
||||
Run the following command in PowerShell as Administrator:
|
||||
Open PowerShell as Administrator and run:
|
||||
|
||||
wsl --install
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
|
||||
Then restart the computer. After restarting, you can open Ubuntu terminal by searching "Ubuntu" in Start menu. You will be prompted to create a new user account. Once you have done that, you can continue with the following steps.
|
||||
Restart your computer after the installation finishes.
|
||||
|
||||
## Setup Environment (In Ubuntu terminal)
|
||||
Open Ubuntu from the Start menu. The first time Ubuntu starts, it will ask you to create a Linux username and password.
|
||||
|
||||
1. Update and upgrade
|
||||
Verify that Ubuntu is using WSL 2 by running this command in PowerShell:
|
||||
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get upgrade
|
||||
```
|
||||
```powershell
|
||||
wsl --update
|
||||
wsl --list --verbose
|
||||
```
|
||||
|
||||
1. Install Git
|
||||
Ubuntu should show `2` in the `VERSION` column.
|
||||
|
||||
```
|
||||
sudo apt-get install git
|
||||
```
|
||||
If it is using WSL 1, run:
|
||||
|
||||
1. Install Node.js (Version 20.x)
|
||||
```powershell
|
||||
wsl --set-version Ubuntu 2
|
||||
```
|
||||
|
||||
```
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install nodejs
|
||||
sudo apt install build-essential
|
||||
sudo npm install -g yarn
|
||||
```
|
||||
## 2. Install Docker Desktop
|
||||
|
||||
## Install Docker
|
||||
Install [Docker Desktop for Windows](https://docs.docker.com/desktop/setup/install/windows-install/).
|
||||
|
||||
(Refer to [get.docker.com](https://get.docker.com/))
|
||||
Start Docker Desktop after installation.
|
||||
|
||||
To install the latest stable versions of Docker CLI, Docker Engine, and their
|
||||
dependencies:
|
||||
Docker Desktop normally uses the WSL 2 engine automatically. If the `Use the WSL 2 based engine` option appears under `Settings > General`, ensure it is enabled.
|
||||
|
||||
1. Download the script
|
||||
Go to:
|
||||
|
||||
```
|
||||
curl -fsSL https://get.docker.com -o install-docker.sh
|
||||
```
|
||||
```text
|
||||
Settings > Resources > WSL Integration
|
||||
```
|
||||
|
||||
1. verify the script's content
|
||||
Enable integration with your Ubuntu distribution, then select **Apply & restart**.
|
||||
|
||||
```
|
||||
cat install-docker.sh
|
||||
```
|
||||
If the WSL Integration section is unavailable, ensure Docker Desktop is using Linux containers.
|
||||
|
||||
1. run the script with --dry-run to verify the steps it executes
|
||||
> [!IMPORTANT]
|
||||
> Do not separately install Docker Engine inside Ubuntu when using Docker Desktop. Having both installations may cause conflicts.
|
||||
|
||||
```
|
||||
sh install-docker.sh --dry-run
|
||||
```
|
||||
Open the Ubuntu terminal and verify that Docker is available:
|
||||
|
||||
**NOTE: the script will recommend you to use Docker Desktop instead, but we need to use Docker inside WSL for the backend to work.** Just ignore it and wait for the installation to finish.
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
docker run --rm hello-world
|
||||
```
|
||||
|
||||
1. run the script either as root, or using sudo to perform the installation.
|
||||
## 3. Set Up the Ubuntu Environment
|
||||
|
||||
```
|
||||
sudo sh install-docker.sh
|
||||
```
|
||||
Run the following commands in the Ubuntu terminal.
|
||||
|
||||
1. Verify the installation
|
||||
### Update the Package List
|
||||
|
||||
```
|
||||
docker --version
|
||||
```
|
||||
```bash
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
1. Add your user to the docker group
|
||||
### Install the Required Tools
|
||||
|
||||
```
|
||||
sudo usermod -aG docker $USER
|
||||
```
|
||||
```bash
|
||||
sudo apt install -y git curl ca-certificates build-essential
|
||||
```
|
||||
|
||||
**NOTE: You need to create a new WSL terminal to apply the changes.**
|
||||
### Install Node.js 20
|
||||
|
||||
1. Verify the installation
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt install -y nodejs
|
||||
```
|
||||
|
||||
```
|
||||
docker ps
|
||||
```
|
||||
This should return something like:
|
||||
### Install Yarn 1.22
|
||||
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
```bash
|
||||
sudo npm install -g yarn@1.22.22
|
||||
```
|
||||
|
||||
## Run the Project (In Ubuntu terminal)
|
||||
Verify the installations:
|
||||
|
||||
1. Clone the repository
|
||||
```bash
|
||||
git --version
|
||||
node --version
|
||||
npm --version
|
||||
yarn --version
|
||||
```
|
||||
|
||||
```
|
||||
git clone https://github.com/lancatlin/linux-odyssey.git
|
||||
cd linux-odyssey
|
||||
```
|
||||
The Node.js version should be `20.x` or later, and the Yarn version should be `1.22.x`.
|
||||
|
||||
Optional: To open the project in VSCode, run the following command inside the project folder:
|
||||
## 4. Clone the Project
|
||||
|
||||
```
|
||||
code .
|
||||
```
|
||||
> [!WARNING]
|
||||
> Clone the repository into the WSL Linux filesystem, such as `~/projects`. Do not place it on a Windows-mounted drive such as `/mnt/c/...` or `/mnt/d/...`. The project uses a bind mount for MongoDB data, and Windows-mounted directories may cause permission and filesystem compatibility problems.
|
||||
|
||||
1. Install dependencies
|
||||
Create and enter a project directory:
|
||||
|
||||
```
|
||||
yarn install
|
||||
```
|
||||
It is ok if there are some warnings and `gyp: ERR` errors, just verify there is `done` at the end.
|
||||
```bash
|
||||
mkdir -p <path-to-projects-directory>
|
||||
cd <path-to-projects-directory>
|
||||
```
|
||||
|
||||
1. Copy the environment variables
|
||||
Clone the repository:
|
||||
|
||||
```
|
||||
cp .env.sample .env
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/linux-odyssey/linux-odyssey.git
|
||||
cd linux-odyssey
|
||||
```
|
||||
|
||||
1. Docker setup
|
||||
### Optional: Open the Project in VS Code
|
||||
|
||||
```
|
||||
docker compose pull
|
||||
docker compose build
|
||||
docker compose up -d db
|
||||
```
|
||||
Install [Visual Studio Code](https://code.visualstudio.com/) and the **WSL** extension first. Then run:
|
||||
|
||||
<!-- Check folder ./config exist and is empty -->
|
||||
<!-- Check .env file exist -->
|
||||
```bash
|
||||
code .
|
||||
```
|
||||
|
||||
1. Run the project
|
||||
## 5. Install Project Dependencies
|
||||
|
||||
```
|
||||
yarn build
|
||||
yarn dev
|
||||
```
|
||||
From the project root, run:
|
||||
|
||||
<!-- Check ./config/ssh_key.pub is a file not a folder -->
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
Enjoy!
|
||||
Warnings during installation may be harmless. The installation succeeded if Yarn finishes with a `Done` message and does not display `error Command failed`.
|
||||
|
||||
If a `gyp` error appears, confirm that it belongs to an optional dependency before ignoring it.
|
||||
|
||||
## 6. Create the Environment File
|
||||
|
||||
Copy the sample environment file:
|
||||
|
||||
```bash
|
||||
cp .env.sample .env
|
||||
```
|
||||
|
||||
Confirm that the file exists:
|
||||
|
||||
```bash
|
||||
ls -la .env
|
||||
```
|
||||
|
||||
The default values are suitable for local development. Do not use the sample secret key in a production environment.
|
||||
|
||||
## 7. Start the Docker Services
|
||||
|
||||
Ensure Docker Desktop is running, then execute:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose build
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Check that the containers are running:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
To stop the Docker services later:
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## 8. Run the Project
|
||||
|
||||
Start the frontend and backend development servers:
|
||||
|
||||
```bash
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Keep the terminal running and open the following address in your browser:
|
||||
|
||||
[http://localhost:8000](http://localhost:8000)
|
||||
|
||||
To stop the development servers, press `Ctrl+C`.
|
||||
|
||||
### Optional: Verify the Production Build
|
||||
|
||||
You can verify that the project builds successfully by running:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
"build:quests": "yarn workspace server build:quests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/core": "^7.29.6",
|
||||
"@babel/preset-env": "^7.24.0",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@jest/globals": "^29.7.0",
|
||||
|
||||
@@ -16,3 +16,10 @@ export const getSessionsRequestSchema = z.object({
|
||||
})
|
||||
|
||||
export type GetSessionsRequest = z.infer<typeof getSessionsRequestSchema>
|
||||
|
||||
export const SUPPORTED_LOCALES = ['zh-TW', 'en'] as const
|
||||
export const LOCALE_NAME_MAP: Record<string, string> = {
|
||||
'zh-TW': '繁體中文',
|
||||
en: 'English',
|
||||
}
|
||||
export type QuestLocale = (typeof SUPPORTED_LOCALES)[number]
|
||||
@@ -1,4 +1,6 @@
|
||||
import { model, Schema, Types } from 'mongoose'
|
||||
import type { QuestLocale } from '../constants'
|
||||
import { SUPPORTED_LOCALES } from '../constants'
|
||||
|
||||
export interface INode {
|
||||
path: string
|
||||
@@ -27,6 +29,7 @@ export interface ISession {
|
||||
_id: Types.ObjectId
|
||||
user: Types.ObjectId
|
||||
quest: string
|
||||
locale: QuestLocale
|
||||
containerId: string | null
|
||||
containerName: string | null
|
||||
status: 'active' | 'finished' | 'inactive'
|
||||
@@ -52,6 +55,11 @@ export const Session = model<ISession>(
|
||||
ref: 'Quest',
|
||||
required: true,
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
enum: SUPPORTED_LOCALES,
|
||||
required: true,
|
||||
},
|
||||
containerId: String,
|
||||
containerName: String,
|
||||
status: {
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,92 @@
|
||||
id: checkpoint-01
|
||||
title: Checkpoint 1
|
||||
image: base
|
||||
instruction: |
|
||||
This quest tests your understanding of:
|
||||
1. `echo`: print text
|
||||
2. `ls`: list the contents of a folder
|
||||
3. `cd`: move between folders
|
||||
4. `pwd`: show your current path
|
||||
|
||||
requirements:
|
||||
- path
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
name: Click `Start` to begin
|
||||
condition:
|
||||
command: echo start
|
||||
response:
|
||||
content: |
|
||||
This quest checks what you have learned in the previous lessons.
|
||||
You will need to choose the commands yourself.
|
||||
Think about which commands can help you complete each task.
|
||||
|
||||
To continue, make the terminal print `next`.
|
||||
|
||||
- id: next
|
||||
name: Make the terminal print `next`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
output: next
|
||||
response:
|
||||
content: |
|
||||
Now list the contents of your current folder.
|
||||
|
||||
- id: ls-home
|
||||
name: List the contents of the current folder
|
||||
requirements:
|
||||
- next
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: ls
|
||||
response:
|
||||
content: |
|
||||
Enter the `folder03` folder.
|
||||
|
||||
- id: cd-folder03
|
||||
name: Enter `folder03`
|
||||
requirements:
|
||||
- ls-home
|
||||
condition:
|
||||
pwd: /home/commander/folder03
|
||||
response:
|
||||
content: |
|
||||
Show your current path in the terminal.
|
||||
|
||||
- id: pwd-folder03
|
||||
name: Show your current path
|
||||
requirements:
|
||||
- cd-folder03
|
||||
condition:
|
||||
command: pwd
|
||||
output: /home/commander/folder03
|
||||
response:
|
||||
content: |
|
||||
Now enter the `folder02` folder
|
||||
and list its contents.
|
||||
|
||||
- id: ls-folder02
|
||||
name: Enter `folder02` and list its contents
|
||||
requirements:
|
||||
- pwd-folder03
|
||||
condition:
|
||||
pwd: /home/commander/folder02
|
||||
command: ls
|
||||
response:
|
||||
content: |
|
||||
Finally, stay in `folder02`
|
||||
and list the contents of `folder01`.
|
||||
|
||||
- id: finish
|
||||
name: Stay in `folder02` and list the contents of `folder01`
|
||||
requirements:
|
||||
- ls-folder02
|
||||
condition:
|
||||
pwd: /home/commander/folder02
|
||||
output: file1.txt
|
||||
response:
|
||||
content: |
|
||||
Congratulations! You passed this checkpoint!
|
||||
You are ready to move on to the next lessons.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,7 @@
|
||||
echo start
|
||||
echo next
|
||||
ls
|
||||
cd folder03
|
||||
pwd
|
||||
cd ../folder02
|
||||
ls ../folder01
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,97 @@
|
||||
id: checkpoint-02
|
||||
title: Checkpoint 2
|
||||
image: base
|
||||
instruction: |
|
||||
This quest tests your understanding of:
|
||||
1. `cat`: display the contents of a file
|
||||
2. `touch`: create a file
|
||||
3. `mkdir`: create a folder
|
||||
4. Use the `Tab` key for autocompletion
|
||||
5. Topics covered in the previous checkpoint
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
name: Click `Start` to begin
|
||||
condition:
|
||||
command: echo start
|
||||
response:
|
||||
content: |
|
||||
This quest checks what you have learned in the previous lessons.
|
||||
You will need to choose the commands yourself.
|
||||
Think about which commands can help you complete each task.
|
||||
|
||||
To continue, make the terminal print `next`.
|
||||
|
||||
- id: next
|
||||
name: Make the terminal print `next`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
output: next
|
||||
response:
|
||||
content: |
|
||||
Now list the contents of your current folder.
|
||||
|
||||
- id: ls-home
|
||||
name: List the contents of the current folder
|
||||
requirements:
|
||||
- next
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: ls
|
||||
response:
|
||||
content: |
|
||||
Display the contents of `hello.txt`.
|
||||
|
||||
- id: cat-hello
|
||||
name: Display the contents of `hello.txt`
|
||||
requirements:
|
||||
- ls-home
|
||||
condition:
|
||||
command: cat hello.txt
|
||||
output: This is `hello.txt`.
|
||||
response:
|
||||
content: |
|
||||
Display the contents of the other file, which has a very long name.
|
||||
|
||||
- id: cat-long-filename
|
||||
name: Display the contents of the other file
|
||||
requirements:
|
||||
- cat-hello
|
||||
condition:
|
||||
command: cat pneumonoultramicroscopicsilicovolcanoconiosis.txt
|
||||
output: A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.
|
||||
response:
|
||||
content: |
|
||||
Now create a folder named `folder`.
|
||||
|
||||
- id: mkdir-folder
|
||||
name: Create a folder named `folder`
|
||||
requirements:
|
||||
- cat-long-filename
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder
|
||||
type: directory
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
Finally, inside the `folder` folder,
|
||||
create a file named `file.txt`.
|
||||
|
||||
- id: finish
|
||||
name: Create `file.txt` inside `folder`
|
||||
requirements:
|
||||
- mkdir-folder
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder/file.txt
|
||||
type: file
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
Congratulations! You passed this checkpoint!
|
||||
You are ready to move on to the next lessons.
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,7 @@
|
||||
echo start
|
||||
echo next
|
||||
ls
|
||||
cat hello.txt
|
||||
cat pneumonoultramicroscopicsilicovolcanoconiosis.txt
|
||||
mkdir folder
|
||||
touch folder/file.txt
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1 @@
|
||||
A name that has been invented for a lung disease caused by breathing in very small pieces of ash or dust.
|
||||
File renamed without changes.
@@ -0,0 +1,140 @@
|
||||
id: checkpoint-03
|
||||
title: Checkpoint 3
|
||||
image: base
|
||||
instruction: |
|
||||
This quest tests your understanding of:
|
||||
1. `cp`: copy files and folders
|
||||
2. `mv`: move or rename files and folders
|
||||
3. `rm`: delete files and folders
|
||||
4. `rmdir`: delete empty folders
|
||||
5. Use options to change a command's behavior, such as `rm -r`
|
||||
6. Topics covered in the previous checkpoints
|
||||
|
||||
requirements:
|
||||
- remove
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
name: Click `Start` to begin
|
||||
condition:
|
||||
command: echo start
|
||||
response:
|
||||
content: |
|
||||
This quest checks what you have learned in the previous lessons.
|
||||
You will need to choose the commands yourself.
|
||||
Think about which commands can help you complete each task.
|
||||
|
||||
To continue, make the terminal print `next`.
|
||||
|
||||
- id: next
|
||||
name: Make the terminal print `next`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
output: next
|
||||
response:
|
||||
content: |
|
||||
Now list the contents of your current folder.
|
||||
|
||||
- id: ls-home
|
||||
name: List the contents of the current folder
|
||||
requirements:
|
||||
- next
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: ls
|
||||
response:
|
||||
content: |
|
||||
Copy the file inside `folder` to a new file named `world.txt` in the same folder.
|
||||
|
||||
- id: cp-hello-world
|
||||
name: Copy the file inside `folder` as `world.txt`
|
||||
requirements:
|
||||
- ls-home
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder/hello.txt
|
||||
type: file
|
||||
exists: true
|
||||
- path: /home/commander/folder/world.txt
|
||||
type: file
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
Delete the `hello.txt` file inside `folder`.
|
||||
|
||||
- id: rm-hello
|
||||
name: Delete `hello.txt` inside `folder`
|
||||
requirements:
|
||||
- cp-hello-world
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder/hello.txt
|
||||
type: file
|
||||
exists: false
|
||||
response:
|
||||
content: |
|
||||
Now move `world.txt` out of `folder`.
|
||||
(Move it into your home folder, `/home/commander`.)
|
||||
|
||||
- id: mv-world-out
|
||||
name: Move `world.txt` out of `folder`
|
||||
requirements:
|
||||
- rm-hello
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder/world.txt
|
||||
type: file
|
||||
exists: false
|
||||
- path: /home/commander/world.txt
|
||||
type: file
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
Rename `world.txt` back to `hello.txt`
|
||||
and display its contents.
|
||||
|
||||
- id: cat-hello
|
||||
name: Rename `world.txt` to `hello.txt` and display its contents
|
||||
requirements:
|
||||
- mv-world-out
|
||||
condition:
|
||||
command: cat hello.txt
|
||||
files:
|
||||
- path: /home/commander/world.txt
|
||||
type: file
|
||||
exists: false
|
||||
- path: /home/commander/hello.txt
|
||||
type: file
|
||||
exists: true
|
||||
output: This is `hello.txt`.
|
||||
response:
|
||||
content: |
|
||||
Delete the `folder` folder.
|
||||
|
||||
- id: rmdir-folder
|
||||
name: Delete `folder`
|
||||
requirements:
|
||||
- cat-hello
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/folder
|
||||
type: directory
|
||||
exists: false
|
||||
response:
|
||||
content: |
|
||||
Delete the `trash_can` folder.
|
||||
|
||||
- id: finish
|
||||
name: Delete `trash_can`
|
||||
requirements:
|
||||
- rmdir-folder
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/trash_can
|
||||
type: directory
|
||||
exists: false
|
||||
response:
|
||||
content: |
|
||||
Congratulations! You passed this checkpoint!
|
||||
You are ready to move on to the next lessons.
|
||||
@@ -0,0 +1 @@
|
||||
This is `hello.txt`.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,12 @@
|
||||
echo start
|
||||
echo next
|
||||
ls
|
||||
cd folder
|
||||
cp hello.txt world.txt
|
||||
rm hello.txt
|
||||
mv world.txt ..
|
||||
cd ..
|
||||
mv world.txt hello.txt
|
||||
cat hello.txt
|
||||
rmdir folder
|
||||
rm -r trash_can
|
||||
File renamed without changes.
@@ -0,0 +1 @@
|
||||
This is `hello.txt`.
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1 @@
|
||||
aHR0cHM6Ly9yZXVybC5jYy9YUjZZMDA=
|
||||
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,348 @@
|
||||
id: copy
|
||||
title: Copying Files
|
||||
image: base
|
||||
instruction: |
|
||||
In this quest, you will learn:
|
||||
1. How to copy files with `cp`
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
name: Click `Start` to begin
|
||||
condition:
|
||||
command: echo start
|
||||
response:
|
||||
content: |
|
||||
Today we will learn how to copy files.
|
||||
First, use `ls` to see which files are in your current folder.
|
||||
hint: Use `ls` to list the files in your current folder
|
||||
|
||||
- id: ls
|
||||
name: List the files in the current folder
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: ls
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
response:
|
||||
content: |
|
||||
You can see `hello.txt` and `world.txt` in this folder.
|
||||
Use `cat` to display their contents.
|
||||
When you have finished, enter `echo next` to continue!
|
||||
hint: |
|
||||
Use `cat hello.txt` and `cat world.txt` to display the file contents
|
||||
If a command is keeping the terminal busy, press `Ctrl + C` to cancel it
|
||||
|
||||
- id: cat-hello
|
||||
name: Display the contents of `hello.txt`
|
||||
requirements:
|
||||
- ls
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cat hello.txt
|
||||
output: Hello
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`
|
||||
- id: missing_command
|
||||
condition:
|
||||
command: cat
|
||||
response:
|
||||
content: |
|
||||
To display this file, add its filename after `cat`.
|
||||
If `cat` is waiting for input, press `Ctrl + C` to cancel it.
|
||||
Enter `cat hello.txt` to display the contents of `hello.txt`
|
||||
- id: missing_extension
|
||||
condition:
|
||||
command: cat hello
|
||||
response:
|
||||
content: |
|
||||
Did you forget the `.txt` extension?
|
||||
Enter `cat hello.txt` to display the contents of `hello.txt`
|
||||
response:
|
||||
content: |
|
||||
You have displayed the contents of `hello.txt`!
|
||||
Here is what it contains:
|
||||
```
|
||||
Hello
|
||||
```
|
||||
|
||||
- id: cat-world
|
||||
name: Display the contents of `world.txt`
|
||||
requirements:
|
||||
- ls
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cat world.txt
|
||||
output: World
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`
|
||||
- id: missing_command
|
||||
condition:
|
||||
command: cat
|
||||
response:
|
||||
content: |
|
||||
To display this file, add its filename after `cat`.
|
||||
If `cat` is waiting for input, press `Ctrl + C` to cancel it.
|
||||
Enter `cat world.txt` to display the contents of `world.txt`
|
||||
- id: missing_extension
|
||||
condition:
|
||||
command: cat world
|
||||
response:
|
||||
content: |
|
||||
Did you forget the `.txt` extension?
|
||||
Enter `cat world.txt` to display the contents of `world.txt`
|
||||
response:
|
||||
content: |
|
||||
You have displayed the contents of `world.txt`!
|
||||
Here is what it contains:
|
||||
```
|
||||
World
|
||||
```
|
||||
|
||||
- id: cat-next
|
||||
name: Enter `echo next` to continue
|
||||
requirements:
|
||||
- cat-hello
|
||||
- cat-world
|
||||
condition:
|
||||
command: echo next
|
||||
response:
|
||||
content: |
|
||||
Next, let's learn how to copy files.
|
||||
`cp` is short for copy.
|
||||
To copy a file, use `cp [source file] [destination file]`.
|
||||
Use `cp` to copy `hello.txt` to `hello2.txt`.
|
||||
hint: Use `cp hello.txt hello2.txt` to copy `hello.txt` to `hello2.txt`
|
||||
|
||||
- id: cp-hello
|
||||
name: Copy `hello.txt` to `hello2.txt`
|
||||
requirements:
|
||||
- cat-next
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cp hello.txt hello2.txt
|
||||
files:
|
||||
- path: /home/commander/hello2.txt
|
||||
type: file
|
||||
exists: true
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
response:
|
||||
content: |
|
||||
You have copied `hello.txt` to `hello2.txt`!
|
||||
Let's check the contents of `hello2.txt`.
|
||||
hint: Use `cat hello2.txt` to display the contents of `hello2.txt`
|
||||
|
||||
- id: cat-hello2
|
||||
name: Display the contents of `hello2.txt`
|
||||
requirements:
|
||||
- cp-hello
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cat hello2.txt
|
||||
output: Hello
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
response:
|
||||
content: |
|
||||
You have displayed the contents of `hello2.txt`!
|
||||
As you can see, `hello2.txt` contains exactly the same text as `hello.txt`.
|
||||
Now try copying `world.txt` to `world2.txt`!
|
||||
hint: Use `cp world.txt world2.txt` to copy `world.txt` to `world2.txt`
|
||||
|
||||
- id: cp-world
|
||||
name: Copy `world.txt` to `world2.txt`
|
||||
requirements:
|
||||
- cat-hello2
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cp world.txt world2.txt
|
||||
files:
|
||||
- path: /home/commander/world2.txt
|
||||
type: file
|
||||
exists: true
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
response:
|
||||
content: |
|
||||
You have copied `world.txt` to `world2.txt`!
|
||||
Let's check the contents of `world2.txt`.
|
||||
hint: Use `cat world2.txt` to display the contents of `world2.txt`
|
||||
|
||||
- id: cat-world2
|
||||
name: Display the contents of `world2.txt`
|
||||
requirements:
|
||||
- cp-world
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cat world2.txt
|
||||
output: World
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
response:
|
||||
content: |
|
||||
You have displayed the contents of `world2.txt`!
|
||||
`world2.txt` contains exactly the same text as `world.txt`.
|
||||
Next, let's learn how to copy folders.
|
||||
Your current folder contains a `notes` folder with a file named `note.txt` inside.
|
||||
We want to copy the entire folder, including its contents, to `notes2`.
|
||||
To copy a folder, add the `-r` option (recursive). This copies the folder and everything inside it.
|
||||
Options let you change how a command behaves.
|
||||
An option often starts with `-` followed by a letter, such as `-r`.
|
||||
Options usually go after the command name and before the filenames.
|
||||
Use `cp -r notes notes2` to copy `notes` to `notes2`.
|
||||
hint: |
|
||||
Use `cp -r notes notes2` to copy `notes` to `notes2`
|
||||
Remember to include `-r` to copy all the contents
|
||||
|
||||
- id: cp-notes
|
||||
name: Copy the `notes` folder to `notes2`
|
||||
requirements:
|
||||
- cat-world2
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cp -r notes notes2
|
||||
files:
|
||||
- path: /home/commander/notes2
|
||||
type: directory
|
||||
exists: true
|
||||
- path: /home/commander/notes2/note.txt
|
||||
type: file
|
||||
exists: true
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
- id: missing_recursive
|
||||
condition:
|
||||
command: cp notes notes2
|
||||
response:
|
||||
content: |
|
||||
Did you forget the `-r` option?
|
||||
Enter `cp -r notes notes2` to copy `notes` to `notes2`
|
||||
response:
|
||||
content: |
|
||||
You have copied the `notes` folder to `notes2`!
|
||||
The entire `notes` folder, including its files, has been copied to `notes2`.
|
||||
`cp -r` copies everything in a folder, including files inside any nested folders.
|
||||
|
||||
Finally, let's see what happens when you copy a file to a destination that already exists.
|
||||
Copy `hello.txt` to `world.txt` and see what happens.
|
||||
hint: Use `cp hello.txt world.txt` to copy `hello.txt` to `world.txt`
|
||||
|
||||
- id: cp-overwrite
|
||||
name: Copy `hello.txt` to `world.txt`
|
||||
requirements:
|
||||
- cp-notes
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cp hello.txt world.txt
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
response:
|
||||
content: |
|
||||
You have copied `hello.txt` to `world.txt`!
|
||||
Let's check the contents of `world.txt`.
|
||||
hint: Use `cat world.txt`
|
||||
|
||||
- id: check-world
|
||||
name: Check the contents of `world.txt`
|
||||
requirements:
|
||||
- cp-overwrite
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: cat world.txt
|
||||
output: Hello
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You are outside the `/home/commander` folder.
|
||||
Use `cd /home/commander` to return to `/home/commander`.
|
||||
response:
|
||||
content: |
|
||||
`world.txt` now contains `Hello`!
|
||||
When the destination file already exists, copying another file to it overwrites its contents.
|
||||
Check whether the destination already exists before copying, so you do not accidentally lose data.
|
||||
That covers the basics of copying files!
|
||||
Enter `echo finish` to complete this quest.
|
||||
|
||||
- id: finish
|
||||
name: Congratulations! All tasks completed!
|
||||
requirements:
|
||||
- check-world
|
||||
condition:
|
||||
command: echo finish
|
||||
response:
|
||||
content: |
|
||||
Congratulations! You have completed all the tasks!
|
||||
You now know how to copy files and folders.
|
||||
In the next quest, you will learn how to move files and folders.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,13 @@
|
||||
echo start
|
||||
ls
|
||||
cat hello.txt
|
||||
cat world.txt
|
||||
echo next
|
||||
cp hello.txt hello2.txt
|
||||
cat hello2.txt
|
||||
cp world.txt world2.txt
|
||||
cat world2.txt
|
||||
cp -r notes notes2
|
||||
ls notes2
|
||||
cp hello.txt world.txt
|
||||
cat world.txt
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,165 @@
|
||||
id: create
|
||||
title: Creating Files and Folders
|
||||
image: base
|
||||
instruction: |
|
||||
In this quest, you will learn:
|
||||
1. How to create files with `touch`
|
||||
2. How to create folders with `mkdir`
|
||||
|
||||
requirements:
|
||||
- tab
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
name: Click `Start` to begin
|
||||
condition:
|
||||
command: echo start
|
||||
response:
|
||||
content: |
|
||||
This time, we will learn about the `touch` command.
|
||||
First, use `ls` to list the files in your current folder.
|
||||
You should find that the folder has no files.
|
||||
hint: Enter `ls` to list the files in your current folder
|
||||
|
||||
- id: ls
|
||||
name: List the files in the folder
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command: ls
|
||||
response:
|
||||
content: |
|
||||
The terminal shows no output, which means there are no visible files in this folder.
|
||||
You can create a file with `touch [filename]`.
|
||||
Try creating a file named `hello.txt` in the terminal.
|
||||
hint: Enter `touch hello.txt` to create a file
|
||||
|
||||
- id: touch
|
||||
name: Create a file with `touch`
|
||||
requirements:
|
||||
- ls
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/hello.txt
|
||||
type: file
|
||||
exists: true
|
||||
exceptions:
|
||||
- id: missing_extension
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/hello
|
||||
type: file
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
It looks like you forgot the `.txt` extension.
|
||||
Try again and make sure the filename is `hello.txt`.
|
||||
response:
|
||||
content: |
|
||||
The terminal should show no output.
|
||||
The file tree on the right now shows the new `hello.txt` file.
|
||||
Let's look inside `hello.txt`!
|
||||
Use the `cat` command you learned earlier to display its contents.
|
||||
hint: Enter `cat hello.txt` to display the contents of `hello.txt`
|
||||
|
||||
- id: cat
|
||||
name: Display the contents of `hello.txt` with `cat`
|
||||
requirements:
|
||||
- touch
|
||||
condition:
|
||||
command: cat hello.txt
|
||||
response:
|
||||
content: |
|
||||
Huh? There is nothing there!
|
||||
That's right. A new file created with `touch` is empty.
|
||||
We will learn how to edit file contents in a future lesson.
|
||||
Let's create another file!
|
||||
Try creating another file named `world.txt` in the terminal.
|
||||
hint: Enter `touch world.txt` to create a file
|
||||
|
||||
- id: touch-2
|
||||
name: Create another file named `world.txt` with `touch`
|
||||
requirements:
|
||||
- cat
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/world.txt
|
||||
type: file
|
||||
exists: true
|
||||
exceptions:
|
||||
- id: missing_extension
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/world
|
||||
type: file
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
It looks like you forgot the `.txt` extension.
|
||||
Try again and make sure the filename is `world.txt`.
|
||||
response:
|
||||
content: |
|
||||
Great! You have created your second file.
|
||||
The file tree now shows both `hello.txt` and `world.txt`.
|
||||
Next, let's create a folder!
|
||||
The `mkdir` command creates an empty folder.
|
||||
Use `mkdir [folder name]`.
|
||||
Try creating a folder named `notes` in the terminal.
|
||||
hint: Enter `mkdir notes` to create a folder
|
||||
|
||||
- id: mkdir
|
||||
name: Create the `notes` folder with `mkdir`
|
||||
requirements:
|
||||
- touch-2
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/notes
|
||||
type: directory
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
Great! You have created the `notes` folder.
|
||||
Next, we will create a file named `note.txt` inside `notes`.
|
||||
First, enter the `notes` folder.
|
||||
Remember the `cd` command? Try using it to enter `notes`.
|
||||
hint: |
|
||||
Enter `cd notes` to enter the `notes` folder
|
||||
After typing `cd n`, press `Tab` to complete the folder name
|
||||
|
||||
- id: cd-notes
|
||||
name: Enter the `notes` folder
|
||||
requirements:
|
||||
- mkdir
|
||||
condition:
|
||||
pwd: /home/commander/notes
|
||||
response:
|
||||
content: |
|
||||
You have entered the `notes` folder.
|
||||
Now try creating a file named `note.txt` inside `notes`.
|
||||
hint: Enter `touch note.txt` to create a file
|
||||
|
||||
- id: touch-note
|
||||
name: Create `note.txt` inside `notes`
|
||||
requirements:
|
||||
- cd-notes
|
||||
condition:
|
||||
files:
|
||||
- path: /home/commander/notes/note.txt
|
||||
type: file
|
||||
exists: true
|
||||
response:
|
||||
content: |
|
||||
Great! You have created `note.txt`.
|
||||
Congratulations! You have completed this quest!
|
||||
In the next quest, you will learn how to copy files and folders.
|
||||
Enter `echo finish` to complete this quest.
|
||||
|
||||
- id: END
|
||||
name: Complete the quest
|
||||
requirements:
|
||||
- touch-note
|
||||
condition:
|
||||
command: echo finish
|
||||
response:
|
||||
content: |
|
||||
Congratulations! You have completed this quest!
|
||||
@@ -0,0 +1,12 @@
|
||||
echo start
|
||||
ls
|
||||
touch hello.txt
|
||||
ls
|
||||
cat hello.txt
|
||||
touch world.txt
|
||||
ls
|
||||
mkdir notes
|
||||
ls
|
||||
cd notes
|
||||
touch note.txt
|
||||
ls
|
||||
File renamed without changes.
@@ -0,0 +1,79 @@
|
||||
# Running Executable Files
|
||||
|
||||
## Outline
|
||||
|
||||
1. What an executable file is
|
||||
2. How to find an executable with `which`
|
||||
3. How to run an executable file
|
||||
|
||||
---
|
||||
|
||||
In earlier lessons, you used commands such as `ls`, `cat`, `cp`, `mv`, and `rm`.
|
||||
These commands are **executable files** that the operating system runs when you enter them.
|
||||
For most commands, you can find the corresponding file in the file system.
|
||||
Use `which` to find the file associated with a command.
|
||||
Enter `which [command name]` to display its path.
|
||||
|
||||
> Use `which ls` to find the path to `ls`
|
||||
|
||||
---
|
||||
|
||||
The terminal displays `/usr/bin/ls`, the location of the `ls` executable.
|
||||
`/usr/bin` is a system folder containing many standard programs, including commands such as `cat` and `cp`.
|
||||
|
||||
> Use `which cat` to find the path to `cat`
|
||||
|
||||
---
|
||||
|
||||
To run an executable file, enter its path directly.
|
||||
Let's try running a command using the path to its executable.
|
||||
|
||||
> Enter `/usr/bin/ls` to list the contents of your current folder
|
||||
|
||||
---
|
||||
|
||||
It lists the files in your folder, just as entering `ls` does. You can compare the results yourself.
|
||||
You can still pass arguments. Let's try this with `cat`.
|
||||
|
||||
> Enter `/usr/bin/cat hello.txt` to display the contents of `hello.txt`
|
||||
|
||||
---
|
||||
|
||||
You can see the contents of `hello.txt`, just as you would with `cat`.
|
||||
This makes sense: `cat` is located at `/usr/bin/cat`, so both commands run the same program.
|
||||
A command like this consists of **an executable** plus **arguments**.
|
||||
You can enter `ls` instead of `/usr/bin/ls` because the `PATH` environment variable tells the terminal which *folders* to search for programs.
|
||||
`PATH` and environment variables are beyond this lesson's scope. We will cover them in a future lesson.
|
||||
|
||||
You can also run custom programs, such as the `dog` file in your current folder.
|
||||
|
||||
> Use `cat dog` to display the contents of `dog`
|
||||
|
||||
---
|
||||
|
||||
This is a simple program built from `echo` and `sleep` commands.
|
||||
You do not need to understand every detail yet. It prints a few lines of text, with short pauses between them.
|
||||
|
||||
To run a file, specify its path.
|
||||
Since `dog` is in your home folder, its absolute path is `/home/commander/dog`. You can also use the shorter form `~/dog`.
|
||||
|
||||
> Enter `~/dog` to run the `dog` file
|
||||
|
||||
---
|
||||
|
||||
The terminal displays a few words from this dog with an attitude.
|
||||
When you are in the same folder as a program, a relative path is often more convenient than an absolute path.
|
||||
To run a program in your current folder, put `./` before its filename instead of entering just its name.
|
||||
In a path, `.` means the current folder.
|
||||
Now try running `dog` using a relative path.
|
||||
|
||||
> Enter `./dog` to run the `dog` file
|
||||
|
||||
---
|
||||
|
||||
The dog produces the same output as before.
|
||||
This is an example of a simple custom command.
|
||||
You can put a sequence of commands in a file and run them together to handle complex or repetitive tasks.
|
||||
This technique is also useful when running programs written by other people.
|
||||
|
||||
Here is a final challenge: enter the `commands` folder, then run the `lucky` file inside it using a relative path.
|
||||
@@ -0,0 +1,236 @@
|
||||
id: execute
|
||||
title: Running Executable Files
|
||||
image: base
|
||||
instruction: |
|
||||
In this quest, you will learn:
|
||||
1. What an executable file is
|
||||
2. How to find an executable with `which`
|
||||
3. How to run an executable file
|
||||
|
||||
requirements:
|
||||
- remove
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
name: Click Start
|
||||
condition:
|
||||
command: echo start
|
||||
response:
|
||||
content: |
|
||||
In earlier lessons, you used commands such as `ls`, `cat`, `cp`, `mv`, and `rm`.
|
||||
These commands are **executable files** that the operating system runs when you enter them.
|
||||
For most commands, you can find the corresponding file in the file system.
|
||||
Use `which` to find the file associated with a command.
|
||||
Enter `which [command name]` to display its path.
|
||||
hint: Use `which ls` to find the path to `ls`
|
||||
|
||||
- id: which-ls
|
||||
name: Find the path to `ls` with `which`
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command: which ls
|
||||
response:
|
||||
content: |
|
||||
The terminal displays `/usr/bin/ls`, the location of the `ls` executable.
|
||||
`/usr/bin` is a system folder containing many standard programs.
|
||||
|
||||
Next, let's find the path to `cat`.
|
||||
hint: Use `which cat` to find the path to `cat`
|
||||
|
||||
- id: which-cat
|
||||
name: Find the path to `cat` with `which`
|
||||
requirements:
|
||||
- which-ls
|
||||
condition:
|
||||
command: which cat
|
||||
response:
|
||||
content: |
|
||||
You can see `/usr/bin/cat`, which is also in the `/usr/bin` folder.
|
||||
To run an executable file, **enter its path directly**.
|
||||
Let's try running a command using the path to its executable.
|
||||
|
||||
First, let's see what happens when you run `ls` normally.
|
||||
hint: Enter `ls` to list the contents of your current folder
|
||||
|
||||
- id: ls
|
||||
name: List the current folder with `ls`
|
||||
requirements:
|
||||
- which-cat
|
||||
condition:
|
||||
command: ls
|
||||
response:
|
||||
content: |
|
||||
Your current folder contains the `commands` folder and two files, `dog` and `hello.txt`.
|
||||
Now try running `ls` using its absolute path, `/usr/bin/ls`.
|
||||
hint: Enter `/usr/bin/ls` to list the contents of your current folder
|
||||
|
||||
- id: bin-ls
|
||||
name: Run `ls` using its absolute path
|
||||
requirements:
|
||||
- ls
|
||||
condition:
|
||||
command: /usr/bin/ls
|
||||
exceptions:
|
||||
- id: exception-ls
|
||||
condition:
|
||||
command: ls
|
||||
response:
|
||||
content: |
|
||||
Here, we are practicing running `ls` using its absolute path.
|
||||
Enter `/usr/bin/ls` to list the contents of your current folder.
|
||||
response:
|
||||
content: |
|
||||
It lists the same files as `ls`. Both commands run the program at `/usr/bin/ls`, so the results are the same.
|
||||
|
||||
You can still pass arguments. Try using `/usr/bin/cat` to display the contents of `hello.txt`.
|
||||
hint: Enter `/usr/bin/cat hello.txt`
|
||||
|
||||
- id: bin-cat
|
||||
name: Run `cat` using its absolute path
|
||||
requirements:
|
||||
- bin-ls
|
||||
condition:
|
||||
command: /usr/bin/cat hello.txt
|
||||
response:
|
||||
content: |
|
||||
You can see the contents of `hello.txt`, just as you would with `cat`.
|
||||
Both commands run the program at `/usr/bin/cat`, so the results are the same.
|
||||
|
||||
A command usually has the form `command argument1 argument2 ...`.
|
||||
The `command` is the name or path of the program to run, such as `ls`, `cat`, or `which`.
|
||||
The `arguments` provide information to the program, such as `hello.txt` after `cat`.
|
||||
|
||||
You can enter `ls` instead of `/usr/bin/ls` because an environment variable called `PATH` tells the shell which folders to search for programs.
|
||||
We will learn more about `PATH` in a future lesson.
|
||||
|
||||
Now let's try running a custom command.
|
||||
The file named `dog` in this folder is a custom command provided for this lesson.
|
||||
First, let's see what is written inside it.
|
||||
hint: Use `cat dog` to display the contents of `dog`
|
||||
|
||||
- id: cat-dog
|
||||
name: Display the contents of `dog` with `cat`
|
||||
requirements:
|
||||
- bin-cat
|
||||
condition:
|
||||
command: cat dog
|
||||
response:
|
||||
content: |
|
||||
This is a simple program built from `echo` and `sleep` commands.
|
||||
You do not need to understand every detail yet. It just prints a few lines of text.
|
||||
|
||||
To run a file, specify its path.
|
||||
Here, the first part of the command is the path to `dog`. This example does not use any arguments.
|
||||
Since `dog` is in your home folder, its absolute path is `/home/commander/dog`.
|
||||
Using the format `executable arguments`, the command is:
|
||||
```
|
||||
/home/commander/dog
|
||||
```
|
||||
hint: Enter `/home/commander/dog` to run the `dog` file
|
||||
|
||||
- id: dog-abs
|
||||
name: Run `dog` using an absolute path
|
||||
requirements:
|
||||
- cat-dog
|
||||
condition:
|
||||
or:
|
||||
- command: /home/commander/dog
|
||||
- command: ~/dog
|
||||
exceptions:
|
||||
- id: exception-dog
|
||||
condition:
|
||||
command: dog
|
||||
error: 'command not found: dog'
|
||||
response:
|
||||
content: |
|
||||
`dog` cannot be run by name here because it is not in the command search path. Specify the path to its executable file.
|
||||
Here, we are practicing running `dog` using an absolute path.
|
||||
Enter `/home/commander/dog` to run the `dog` file.
|
||||
response:
|
||||
content: |
|
||||
The terminal displays a few words from this dog with an attitude.
|
||||
When you are in the same folder as a program, a relative path is often more convenient than an absolute path.
|
||||
To run a program in your current folder, put `./` before its filename instead of entering just its name.
|
||||
In a path, `.` means the current folder.
|
||||
Now try running `dog` using a relative path.
|
||||
hint: Enter `./dog` to run the `dog` file
|
||||
|
||||
- id: dog-rel
|
||||
name: Run `dog` using a relative path
|
||||
requirements:
|
||||
- dog-abs
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: ./dog
|
||||
exceptions:
|
||||
- id: exception-dog-2
|
||||
condition:
|
||||
command: dog
|
||||
error: 'command not found: dog'
|
||||
response:
|
||||
content: |
|
||||
`dog` cannot be run by name here because it is not in the command search path. Specify the path to its executable file.
|
||||
Here, we are practicing running `dog` using a relative path.
|
||||
Enter `./dog` to run the `dog` file.
|
||||
response:
|
||||
content: |
|
||||
The dog produces the same output as before.
|
||||
This is an example of a simple custom command.
|
||||
You can put a sequence of commands in a file and run them together to handle complex or repetitive tasks.
|
||||
This technique is also useful when running programs written by other people.
|
||||
|
||||
Here is a final challenge: enter the `commands` folder, then run the `lucky` file inside it using a relative path.
|
||||
hint: |
|
||||
Use `cd` to enter a folder
|
||||
Use `ls` to list the files
|
||||
Enter `./filename` to run a file
|
||||
|
||||
- id: execute-lucky
|
||||
name: Run `lucky` using a relative path
|
||||
requirements:
|
||||
- dog-rel
|
||||
condition:
|
||||
pwd: /home/commander/commands
|
||||
command: ./lucky
|
||||
response:
|
||||
content: |
|
||||
The terminal displays `I am SO LUCKY!!!`, the message printed by `lucky`.
|
||||
Congratulations! You have completed this lesson!
|
||||
Enter `echo finish` to complete this quest.
|
||||
exceptions:
|
||||
- id: exception-lucky
|
||||
condition:
|
||||
command: lucky
|
||||
error: 'command not found: lucky'
|
||||
response:
|
||||
content: |
|
||||
`lucky` cannot be run by name here because it is not in the command search path. Specify the path to its executable file.
|
||||
|
||||
- id: exception-lucky-2
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
command: ./lucky
|
||||
error: 'no such file or directory'
|
||||
response:
|
||||
content: |
|
||||
You are outside the `commands` folder, so `./lucky` does not point to the file yet.
|
||||
First, enter `cd commands` to move into `commands`. Then enter `./lucky` to run `lucky` using a relative path.
|
||||
- id: help
|
||||
condition:
|
||||
command: echo help
|
||||
response:
|
||||
content: |
|
||||
Enter the `commands` folder before running `lucky` using a relative path.
|
||||
First, enter `cd commands` to move into `commands`. Then enter `./lucky` to run `lucky` using a relative path.
|
||||
|
||||
- id: END
|
||||
name: Congratulations! All tasks completed!
|
||||
requirements:
|
||||
- execute-lucky
|
||||
condition:
|
||||
command: echo finish
|
||||
response:
|
||||
content: |
|
||||
Congratulations! You have completed this lesson!
|
||||
You now know how to run executable files.
|
||||
Executable → Regular
File renamed without changes.
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "You ran the dog file"
|
||||
echo "Woof woof woof"
|
||||
sleep 1
|
||||
echo "You know what?"
|
||||
sleep 1
|
||||
echo "I am better than a cat"
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "I am SO LUCKY!!!"
|
||||
File renamed without changes.
@@ -0,0 +1 @@
|
||||
Hello! This is a file hello.txt
|
||||
File renamed without changes.
@@ -0,0 +1,161 @@
|
||||
id: explore
|
||||
image: base
|
||||
title: Exploring Files
|
||||
instruction: |
|
||||
In this quest, you will learn:
|
||||
1. How to list folder contents with `ls`
|
||||
2. How to move between folders with `cd`
|
||||
|
||||
requirements:
|
||||
- get-started
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
name: Click `Start` to begin the lesson
|
||||
condition:
|
||||
command: ^echo start$
|
||||
response:
|
||||
content: |
|
||||
The terminal has no graphical interface for browsing folder contents.
|
||||
Instead, you can use `ls` to list what is inside a folder.
|
||||
`ls` is short for list. Its first character is a lowercase L.
|
||||
When you enter `ls`, the terminal lists the files and folders in your current folder.
|
||||
Now enter `ls` to see what is in this folder.
|
||||
hint: Enter `ls` to list the contents of the folder
|
||||
|
||||
- id: ls
|
||||
name: List the files in the folder
|
||||
requirements:
|
||||
- start
|
||||
condition:
|
||||
command: ^ls$
|
||||
exceptions:
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
type: narrative
|
||||
content: |
|
||||
First, return to `/home/commander/`
|
||||
Enter `cd ~` to return to `/home/commander/`
|
||||
response:
|
||||
content: |
|
||||
You should now see the contents of the folder.
|
||||
The file tree on the right has changed too.
|
||||
This folder contains two folders named `folder1` and `folder2`.
|
||||
|
||||
Next, let's learn how to enter a folder.
|
||||
`cd` is short for change directory, which means changing your current folder.
|
||||
Enter `cd [folder name]` to move into a folder.
|
||||
For example, enter `cd folder1` to move into `folder1`.
|
||||
Now enter the `folder1` folder.
|
||||
hint: Enter `cd folder1` to move into `folder1`
|
||||
|
||||
- id: cd-folder1
|
||||
name: Enter `folder1`
|
||||
requirements:
|
||||
- ls
|
||||
condition:
|
||||
pwd: /home/commander/folder1
|
||||
exceptions:
|
||||
- id: wrong_spelling
|
||||
condition:
|
||||
command: ^folderl$
|
||||
response:
|
||||
type: narrative
|
||||
content: |
|
||||
`folder1` ends with the number `1`, not the letter `l`.
|
||||
- id: wrong_directory
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
type: narrative
|
||||
content: Enter `cd folder1` to move into `folder1`
|
||||
- id: not_in_home
|
||||
condition:
|
||||
not:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
type: narrative
|
||||
content: |
|
||||
First, return to `/home/commander/`
|
||||
Enter `cd ~` to return to `/home/commander/`
|
||||
Then enter `cd folder1` to move into `folder1`
|
||||
response:
|
||||
content: |
|
||||
You have entered the `folder1` folder.
|
||||
Notice that the command prompt has changed from
|
||||
`commander:~ $` to
|
||||
`commander:~/folder1 $`.
|
||||
This tells you that you are now in `folder1`.
|
||||
Now list the contents of `folder1`.
|
||||
hint: Enter `ls` to list the contents of the folder
|
||||
|
||||
- id: ls-folder1
|
||||
name: List the contents of `folder1`
|
||||
requirements:
|
||||
- cd-folder1
|
||||
condition:
|
||||
pwd: /home/commander/folder1
|
||||
command: ^ls$
|
||||
response:
|
||||
content: |
|
||||
You can see two files in `folder1`, named `file1.txt` and `file2.txt`.
|
||||
Now let's leave `folder1` and return to the original folder.
|
||||
Use `cd ..` to move up to the parent folder.
|
||||
Here, `..` means the parent folder.
|
||||
hint: Enter `cd ..` to move up to the parent folder
|
||||
|
||||
- id: cd_up
|
||||
name: Return to the parent folder
|
||||
requirements:
|
||||
- ls-folder1
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You have returned to the parent folder.
|
||||
Use the commands you just learned to enter `folder2`, list its contents, and return to this folder.
|
||||
hint: |
|
||||
Use `cd` to move between folders
|
||||
Use `ls` to list the contents of a folder
|
||||
|
||||
- id: cd-folder2
|
||||
name: Enter `folder2`
|
||||
requirements:
|
||||
- cd_up
|
||||
condition:
|
||||
pwd: /home/commander/folder2
|
||||
response:
|
||||
content: |
|
||||
You have entered the `folder2` folder.
|
||||
Now list the contents of `folder2`.
|
||||
hint: Use `ls` to list the contents of the folder
|
||||
|
||||
- id: ls-folder2
|
||||
name: List the contents of `folder2`
|
||||
requirements:
|
||||
- cd-folder2
|
||||
condition:
|
||||
pwd: /home/commander/folder2
|
||||
command: ^ls$
|
||||
response:
|
||||
content: |
|
||||
You can see two files in `folder2`, named `file3.txt` and `file4.txt`.
|
||||
Now leave `folder2` and return to the original folder.
|
||||
hint: Use `cd ..` to move up to the parent folder
|
||||
|
||||
- id: END
|
||||
name: Return to the original folder
|
||||
requirements:
|
||||
- ls-folder2
|
||||
condition:
|
||||
pwd: /home/commander
|
||||
response:
|
||||
content: |
|
||||
You have returned to the original folder.
|
||||
Congratulations! You now know how to use `ls` and `cd`.
|
||||
hint: |
|
||||
Use `cd` to move between folders
|
||||
Use `ls` to list the contents of a folder
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,8 @@
|
||||
echo start
|
||||
ls
|
||||
cd folder1
|
||||
ls
|
||||
cd ..
|
||||
cd folder2
|
||||
ls
|
||||
cd ..
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
Whitespace-only changes.
Loaded 100 of 182 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user