diff --git a/app/src/components/ContactButton.vue b/app/src/components/ContactButton.vue new file mode 100644 index 0000000..c264443 --- /dev/null +++ b/app/src/components/ContactButton.vue @@ -0,0 +1,28 @@ + + diff --git a/app/src/components/GameHeaderPart.vue b/app/src/components/GameHeaderPart.vue index 2b9248b..0072a4c 100644 --- a/app/src/components/GameHeaderPart.vue +++ b/app/src/components/GameHeaderPart.vue @@ -26,24 +26,6 @@ const handleLogout = async () => { console.error(err) } } -const handleMap = async () => { - try { - router.push({ name: 'map' }) - } catch (err) { - // toast.error('Failed to load map') - toast.error('無法讀取地圖') - console.error(err) - } -} -const handleLeaderBoard = async () => { - try { - router.push({ name: 'leaderboard' }) - } catch (err) { - // toast.error('Failed to load map') - toast.error('無法讀取排行榜') - console.error(err) - } -} defineProps({ title: { @@ -126,18 +108,18 @@ onMounted(async () => { class="text-text-primary h-full w-full" /> - - + { diff --git a/app/src/config.js b/app/src/config.js index 2c6d374..73879c5 100644 --- a/app/src/config.js +++ b/app/src/config.js @@ -1,7 +1,4 @@ export const surveyUrl = '/api/v1/survey' export const bugReportUrl = '/api/v1/bug-report' - -export default { - surveyUrl, - bugReportUrl, -} +export const facebookUrl = 'https://www.facebook.com/LinuxOdyssey' +export const githubUrl = 'https://github.com/linux-odyssey/linux-odyssey' diff --git a/app/src/img/icon_settings.svg b/app/src/img/icon_settings.svg new file mode 100644 index 0000000..15c61e6 --- /dev/null +++ b/app/src/img/icon_settings.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/img/icon_shuttle.svg b/app/src/img/icon_shuttle.svg new file mode 100644 index 0000000..878edb9 --- /dev/null +++ b/app/src/img/icon_shuttle.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/img/icon_sword.svg b/app/src/img/icon_sword.svg new file mode 100644 index 0000000..22eda38 --- /dev/null +++ b/app/src/img/icon_sword.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/library.js b/app/src/library.js index 1142033..7f3c00c 100644 --- a/app/src/library.js +++ b/app/src/library.js @@ -18,13 +18,18 @@ import { faMap, faTrophy, faFileInvoice, + faEnvelope, } from '@fortawesome/free-solid-svg-icons' import { faLightbulb, faCircleQuestion, faCircleRight, } from '@fortawesome/free-regular-svg-icons' -import { faGithub, faGoogle } from '@fortawesome/free-brands-svg-icons' +import { + faFacebook, + faGithub, + faGoogle, +} from '@fortawesome/free-brands-svg-icons' /* add icons to the library */ library.add( @@ -47,5 +52,7 @@ library.add( faCircleRight, faGithub, faGoogle, - faFileInvoice + faFileInvoice, + faFacebook, + faEnvelope ) diff --git a/app/src/router.js b/app/src/router.js index 1d3b420..d6fd78d 100644 --- a/app/src/router.js +++ b/app/src/router.js @@ -5,15 +5,14 @@ import RegisterPage from './views/RegisterPage.vue' import ChooseUsernamePage from './views/ChooseUsernamePage.vue' import QuestMapPage from './views/QuestMapPage.vue' import LeaderboardPage from './views/LeaderboardPage.vue' +import LandingPage from './views/LandingPage.vue' import { isLoggedIn } from './utils/auth' const routes = [ { path: '/', - redirect: { - name: 'register', - }, - meta: { requiresGuest: true }, + name: 'landing-page', + component: LandingPage, }, { path: '/login', diff --git a/app/src/store/session.js b/app/src/store/session.js index 5464bd0..9ab8295 100644 --- a/app/src/store/session.js +++ b/app/src/store/session.js @@ -20,6 +20,7 @@ function newSession() { pwd: '', hints: [], tasks: [], + responses: [], } } @@ -41,6 +42,7 @@ function setQuest(questId) { async function setSession(session) { try { + console.log('Setting session...', session) store.session = session store.session.graph = new FileGraph(session.graph) term.reset() @@ -85,16 +87,12 @@ function updateGraph(event) { } } -function updateHints(hints) { - store.session.hints.push(hints) -} - -function updateStatus(status) { - store.session.status = status -} - -function setTasks(tasks) { - store.session.tasks = tasks +function newResponse(response) { + console.log('newResponse', response, 'session', store.session) + store.session.responses.push(response.responses) + store.session.hints.push(response.hints) + store.session.tasks = response.tasks + store.session.status = response.status } export function useTerminal() { @@ -135,14 +133,8 @@ function setup() { socket.on('graph', (event) => { updateGraph(event) }) - socket.on('hints', (event) => { - updateHints(event) - }) - socket.on('tasks', (tasks) => { - setTasks(tasks) - }) - socket.on('status', (event) => { - updateStatus(event) + socket.on('response', (response) => { + newResponse(response) }) } diff --git a/app/src/views/GamePage.vue b/app/src/views/GamePage.vue index a11e1ad..74db5ab 100644 --- a/app/src/views/GamePage.vue +++ b/app/src/views/GamePage.vue @@ -61,7 +61,7 @@ onMounted(async () => {
-
+
@@ -71,17 +71,17 @@ onMounted(async () => { -->
-
-
- +
+
+

-
- +
+
-
+
diff --git a/app/src/views/LandingPage.vue b/app/src/views/LandingPage.vue new file mode 100644 index 0000000..8db69bb --- /dev/null +++ b/app/src/views/LandingPage.vue @@ -0,0 +1,299 @@ + + diff --git a/packages/models/src/session.ts b/packages/models/src/session.ts index fdc749f..49185fd 100644 --- a/packages/models/src/session.ts +++ b/packages/models/src/session.ts @@ -1,5 +1,7 @@ import { model, Schema, Types } from 'mongoose' +import { IResponse, responseSchema } from './response.js' + export interface INode { path: string type: string @@ -46,13 +48,14 @@ export interface ISession { user: Types.ObjectId quest: string containerId: string | null - status: string + status: 'active' | 'finished' | 'inactive' createdAt: Date updatedAt: Date finishedAt?: Date lastActivityAt: Date tasks: ITask[] hints: string[][] + responses: IResponse[][] graph: INode } @@ -85,6 +88,11 @@ export const Session = model( }, tasks: [taskSchema], hints: [[String]], + responses: { + type: [[responseSchema]], + required: true, + default: [], + }, graph: { type: nodeSchema, default: { diff --git a/server/src/api/controllers/authController.ts b/server/src/api/controllers/authController.ts index a86f0fb..258ba4f 100644 --- a/server/src/api/controllers/authController.ts +++ b/server/src/api/controllers/authController.ts @@ -67,7 +67,7 @@ export function socialLoginHandler(req: Request, res: Response) { res.redirect('/choose-username') return } - res.redirect('/') + res.redirect('/map') } export const registerFromSession = asyncHandler( diff --git a/server/src/api/controllers/commandController.ts b/server/src/api/controllers/commandController.ts index 5e4f5f2..4306601 100644 --- a/server/src/api/controllers/commandController.ts +++ b/server/src/api/controllers/commandController.ts @@ -6,8 +6,6 @@ import { pushToSession } from '../socket.js' import { finishSession } from '../../models/sessionManager.js' import { asyncHandler } from '../../middleware/error.js' -const commandCompleteCallbacks = new Map() - export const newCommand = asyncHandler(async (req: Request, res: Response) => { const { command, pwd, output, error, params } = matchedData(req) @@ -51,42 +49,14 @@ export const newCommand = asyncHandler(async (req: Request, res: Response) => { c.stage = response.stage await c.save() - if (response.stage === 'END') { + session.responses.push(response.responses) + + if ((session.status as string) === 'finished') { await finishSession(session) } - commandCompleteCallbacks.set(session.id, response.callback) - pushToSession(session.id, 'status', 'pending') - res.status(201).json(response) - } else { - res.status(200).end() + pushToSession(session.id, 'response', response) } + res.status(200).end() await session.save() }) - -export const completedCommand = asyncHandler( - async (req: Request, res: Response) => { - const { sessionId } = req.user as any - - const session = await Session.findById(sessionId) - - if (!session) { - res.status(404).json({ message: 'session not found' }) - return - } - - if (session.status === 'inactive') { - res.status(400).json({ message: 'session is not active' }) - return - } - - const callback = commandCompleteCallbacks.get(session.id) - if (!callback) { - res.status(200).json({ message: 'no callback found' }) - return - } - callback() - commandCompleteCallbacks.delete(session.id) - res.json({ message: 'ok' }) - } -) diff --git a/server/src/api/controllers/sessionController.ts b/server/src/api/controllers/sessionController.ts index 2fb6f00..6c4f348 100644 --- a/server/src/api/controllers/sessionController.ts +++ b/server/src/api/controllers/sessionController.ts @@ -25,6 +25,7 @@ function sessionDetail(session: ISession) { ...sessionSummary(session), hints: session.hints, tasks: session.tasks, + responses: session.responses, graph: session.graph, } } @@ -55,6 +56,7 @@ export const createSession = asyncHandler( export const getSessionById = asyncHandler( async (req: Request, res: Response) => { const { sessionId } = matchedData(req) + console.log(sessionId) const session = await Session.findOne({ _id: sessionId, user: (req.user as Express.ExistingUser).id, diff --git a/server/src/api/routes/commandRouter.ts b/server/src/api/routes/commandRouter.ts index ef22842..b255d21 100644 --- a/server/src/api/routes/commandRouter.ts +++ b/server/src/api/routes/commandRouter.ts @@ -5,6 +5,5 @@ import { newCommandValidator } from '../validators/commandValidator.js' const router = Router() router.post('/', newCommandValidator, commandController.newCommand) -router.post('/completed', commandController.completedCommand) export default router diff --git a/server/src/api/routes/sessionRouter.ts b/server/src/api/routes/sessionRouter.ts index ddc7cdb..52e4e0c 100644 --- a/server/src/api/routes/sessionRouter.ts +++ b/server/src/api/routes/sessionRouter.ts @@ -22,6 +22,10 @@ router.post( activeSessionValidator, sessionController.getOrCreateSession ) -router.get('/:id', sessionDetailValidator, sessionController.getSessionById) +router.get( + '/:sessionId', + sessionDetailValidator, + sessionController.getSessionById +) export default router diff --git a/server/src/api/validators/sessionValidator.ts b/server/src/api/validators/sessionValidator.ts index ddb3c84..558b5b6 100644 --- a/server/src/api/validators/sessionValidator.ts +++ b/server/src/api/validators/sessionValidator.ts @@ -9,6 +9,6 @@ export const checkSessionId = () => check('sessionId').notEmpty().isString().isMongoId() export const sessionListValidator = [checkQuestId(), checkSessionStatus()] -export const sessionDetailValidator = [checkSessionId(), noError] +export const sessionDetailValidator = [checkSessionId()] export const createSessionValidator = [checkQuestId(), noError] export const activeSessionValidator = [checkQuestId(), noError] diff --git a/server/src/game/sessionHandler.ts b/server/src/game/sessionHandler.ts index 6808f5c..c061d57 100644 --- a/server/src/game/sessionHandler.ts +++ b/server/src/game/sessionHandler.ts @@ -8,12 +8,12 @@ import type { IStage, ITask, } from '@linux-odyssey/models' -import { pushToSession } from '../api/socket.js' export interface ExecuteResult { responses: IResponse[] hints: string[] - callback: () => void + tasks?: ITask[] + status: string stage?: string end?: boolean } @@ -79,15 +79,17 @@ export default class SessionHandler { this.addNewTasks() + this.session.responses.push(stage.responses) + + if (stage.id === 'END') { + this.session.status = 'finished' + } + return { responses: stage.responses, hints: stage.hints, - callback: () => { - pushToSession(this.session.id, 'tasks', this.session.tasks) - if (stage.hints && stage.hints.length > 0) - pushToSession(this.session.id, 'hints', stage.hints) - pushToSession(this.session.id, 'status', this.session.status) - }, + tasks: this.session.tasks, + status: this.session.status, } } @@ -98,11 +100,7 @@ export default class SessionHandler { return { responses, hints, - callback: () => { - if (hints && hints.length > 0) - pushToSession(this.session.id, 'hints', hints) - pushToSession(this.session.id, 'status', this.session.status) - }, + status: this.session.status, } } } diff --git a/tests/cypress/e2e/UItests.cy.ts b/tests/cypress/e2e/UItests.cy.ts index 1ea8646..031e104 100644 --- a/tests/cypress/e2e/UItests.cy.ts +++ b/tests/cypress/e2e/UItests.cy.ts @@ -75,7 +75,7 @@ describe('example helloworld app', () => { describe('Game Start Page UI', () => { const title = '終端機的基本知識' beforeEach(() => { - cy.PrepareForGame() + cy.PrepareForGame('get-started') }) it('Check Header', () => { cy.get('#GameTitle').should('contain', 'Linux Odyssey').and('be.visible') @@ -124,7 +124,7 @@ describe('example helloworld app', () => { }) describe('Game Play', () => { beforeEach(() => { - cy.PrepareForGame() + cy.PrepareForGame('get-started') }) it('Typing in Terminal', () => { cy.typeInCommand('12345{enter}') @@ -136,7 +136,7 @@ describe('example helloworld app', () => { cy.checkTaskInit() cy.InitTerminal() cy.typeInCommand('echo start{enter}') - cy.checkPending() + // cy.checkPending() cy.waitUntilActive() cy.get('#Lbutton').should('be.visible').and('be.disabled') cy.get('#Rbutton').should('be.visible').and('be.disabled') @@ -144,7 +144,7 @@ describe('example helloworld app', () => { cy.getQuestInfo('✓ 輸入 echo start 來開始教程').should('be.visible') cy.getQuestInfo('➤ 在終端機輸入 echo hello').should('be.visible') cy.typeInCommand('echo hello{enter}') - cy.checkPending() + // cy.checkPending() cy.waitUntilActive() cy.get('#Lbutton').should('be.visible').and('be.enabled') cy.get('#Rbutton').should('be.visible').and('be.disabled') diff --git a/tests/cypress/e2e/completeQuests.cy.ts b/tests/cypress/e2e/completeQuests.cy.ts index 57ba8e2..dbd9de8 100644 --- a/tests/cypress/e2e/completeQuests.cy.ts +++ b/tests/cypress/e2e/completeQuests.cy.ts @@ -1,7 +1,4 @@ describe('Complete quests with only commands', () => { - beforeEach(() => { - cy.PrepareForGame() - }) it('Complete quest : get-started', () => { cy.CompleteStageWithCommands('get-started') }) diff --git a/tests/cypress/support/commands.ts b/tests/cypress/support/commands.ts index b77cd88..23d6484 100644 --- a/tests/cypress/support/commands.ts +++ b/tests/cypress/support/commands.ts @@ -38,7 +38,7 @@ Cypress.Commands.add('LoginWithPassword', (username, password) => { cy.get('#password').type(password) cy.get('#LogInOrSignUp').click() }) -Cypress.Commands.add('PrepareForGame', () => { +Cypress.Commands.add('PrepareForGame', (questId) => { cy.visit('/') // make sure the login page is loaded cy.url().should('satisfy', (elements: string) => { @@ -51,8 +51,9 @@ Cypress.Commands.add('PrepareForGame', () => { ) // make sure the map page is loaded cy.url().should('include', '/map') - cy.visit('/game/get-started') - cy.url().should('include', '/game/get-started') + const url = `/game/${questId}` + cy.visit(url) + cy.url().should('include', url) // check task first so the terminal has better possibility loaded cy.checkTaskInit() cy.get('.xterm-screen', { timeout: 10000 }) @@ -114,8 +115,7 @@ Cypress.Commands.add('checkPending', () => { ) }) Cypress.Commands.add('CompleteStageWithCommands', (stagename) => { - cy.visit(`/game/${stagename}`) - cy.url().should('include', `/game/${stagename}`, { timeout: 50000 }) + cy.PrepareForGame(stagename) cy.log(`Completing stage :${stagename}`) cy.checkTaskInit() cy.CheckTextElement('#reset', '重來', 'Reset').click() @@ -123,19 +123,22 @@ Cypress.Commands.add('CompleteStageWithCommands', (stagename) => { cy.InitTerminal() cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').then((answers) => { const answerarr = answers.split('\n') - for (const element of answerarr) { + answerarr.forEach((element, index) => { cy.typeInCommand(`${element}{enter}`) - if (answerarr.indexOf(element) + 1 !== answerarr.length) { - cy.checkPending() + if (index < answerarr.length - 1) { cy.waitUntilActive() + } else { + cy.waitUntilActive(true).then(() => { + cy.CheckTextElement( + '#QuestCompleted', + '關卡完成!', + 'Quest Completed!' + ) + cy.get('div[class="modal"]').find('p').should('be.visible') + cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖') + }) } - if (answerarr.indexOf(element) + 1 === answerarr.length) { - cy.waitUntilActive(true) - cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!') - cy.get('div[class="modal"]').find('p').should('be.visible') - cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖') - } - } + }) }) }) Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {