Compare commits

..
438 Commits
Author SHA1 Message Date
Justin Lin 9b4f50ebbd Formbricks (#157)
add quest feedback and bug report form using Formbricks
2024-08-02 22:07:36 +08:00
wancat 5fd24190d2 feat: change form to production 2024-08-02 21:17:18 +08:00
wancat d2bb079f50 feat: add bug report 2024-08-02 20:54:38 +08:00
wancat d959ab22fe refactor: move open survey to formbricks 2024-08-02 16:49:26 +08:00
wancat 9c8260a411 feat: integrate form after finishing quest 2024-08-02 16:15:06 +08:00
wancat 3c92c9d143 feat: add formbricks 2024-08-02 15:32:47 +08:00
raygur 5208d87bd6 feat: formbricks editing 2024-08-02 14:44:28 +08:00
RayGur 634464065d feat: formbricks edit 2024-08-02 14:44:28 +08:00
RayGur 4c90793fc6 feat: formbricks init 2024-08-02 14:44:28 +08:00
RayGur 169587c54d feat: formbricks setup 2024-08-02 14:41:14 +08:00
Justin Lin 4dd5f94c60 fix: map background (#156)
Put catelog background to public folder
2024-08-02 14:36:38 +08:00
Justin Lin 14bcb3b0d5 Basic checkpoint quests (#155) 2024-08-02 14:35:32 +08:00
Annie Huang 8281a9d882 fix: map background 2024-08-02 13:19:53 +08:00
Chen, Tai-Lin dcf72d046c quest: fix checkpoint-03 2024-08-02 11:12:30 +08:00
Chen, Tai-Lin b73607fb8d quest: fix checkpoint-02 bug 2024-08-02 10:45:31 +08:00
Chen, Tai-Lin b64d707d16 quest: fix checkpoints bug 2024-08-02 10:22:42 +08:00
Chen, Tai-Lin 7dcac306f9 quest: checkpoint03 2024-08-02 02:27:41 +08:00
Chen, Tai-Lin eddbda23d7 quest: fix content in remove 2024-08-02 01:06:05 +08:00
Chen, Tai-Lin 124c5cc401 quest: checkpoint-02 2024-08-02 01:05:38 +08:00
Chen, Tai-Lin 21d08b8156 quest: checkpoint01 2024-08-02 00:28:59 +08:00
Chen, Tai-Lin 3745011504 fix quest content 2024-08-01 21:57:43 +08:00
Justin Lin 3f08452ea4 feat: 119-Quest-Intro (#154)
- Add an introduction for every quest
- Not yet written documents for the introduction, so the contents are
from the information in the game
2024-08-01 20:52:10 +08:00
Annie Huang acfb09c6ca delete redundant code 2024-08-01 19:58:13 +08:00
Annie Huang c2c100e6e6 simplify data usage 2024-08-01 19:55:24 +08:00
Annie Huang 4033eefa0d remove redundant variable 2024-08-01 19:41:55 +08:00
Annie Huang 334b34ef65 Green button for unlocked and completed quest 2024-08-01 19:40:17 +08:00
Annie Huang ac89d521fc remove console log 2024-08-01 19:32:28 +08:00
Annie Huang 60641ba0ad change background loading method 2024-08-01 19:29:01 +08:00
Annie Huang dd67bcb1bd Add: Quest Introduction 2024-08-01 19:11:27 +08:00
Justin Lin d002b21aa8 Feat map (#152) 2024-07-31 20:37:49 +08:00
wancat 033c808c02 chore: remove echarts dependency 2024-07-31 18:25:01 +08:00
wancat 1be4f73e7e refactor: styles of nodes and edges 2024-07-31 18:17:44 +08:00
wancat 9ddfcd2a86 feat: add edges (straight line for now) 2024-07-31 18:11:18 +08:00
wancat ca36f6391a feat: add dragging 2024-07-31 18:00:19 +08:00
wancat 1572b2ea06 feat: rewrite map state handling 2024-07-31 17:54:59 +08:00
wancat 8f6914d46c feat: new map by svg 2024-07-31 17:12:43 +08:00
Justin Lin 517ab43e8c Execute Quest (#150) 2024-07-31 16:37:32 +08:00
Justin Lin 74c49e1df7 Quest tab (#151) 2024-07-31 16:37:17 +08:00
wancat cb0f3cc3b0 fix: hint instruction 2024-07-31 08:46:50 +08:00
Chen, Tai-Lin 5605b27127 modify quests' text 2024-07-30 23:43:23 +08:00
wancat a7368110dd feat: update quest requirements to include tab 2024-07-29 22:19:32 +08:00
wancat bdb7def73a feat: finish tab quest 2024-07-29 22:16:58 +08:00
wancat b74866e50f wip: tab 2024-07-28 15:14:56 +08:00
wancat d577656eb1 feat: allow testing on creating new session 2024-07-28 15:14:45 +08:00
Lin, Hung-Shin 6400aa4a56 Style: Add file/folder icon in file tree (#149) 2024-07-28 12:22:59 +08:00
wancat 887d205ec1 quest: execute 2024-07-26 16:25:38 +08:00
wancat 8597c22b9f wip: execute tutorial content 2024-07-23 18:37:52 +08:00
Chen, Tai-Lin d8c0ee4b20 style: add file/folder icon and modify some style in visualization part 2024-07-23 18:30:57 +08:00
Chen, Tai-Lin 8180e9a0cf delete unused text 2024-07-23 12:15:03 +08:00
Chen, Tai-Lin 6daa11bff7 fix: start buttom languege issue 2024-07-23 11:20:56 +08:00
Chen, Tai-Lin 976dc6363c fix: wrong answer in answer.sh 2024-07-23 10:29:50 +08:00
Lin, Hung-Shin 8d0a210f0e Fix current quests (#144) 2024-07-03 08:15:18 +08:00
wancat c38104d043 fix: typo 2024-07-03 08:09:57 +08:00
wancat 83e563ab56 fix: requirement in get-started 2024-07-03 08:05:58 +08:00
wancat fa2059814c fix: increase line height in description part 2024-07-03 08:05:40 +08:00
Chen, Tai-Lin f9fa5a2102 fix: update quest [remove] 2024-07-03 01:57:39 +08:00
Chen, Tai-Lin 8ed6acbcc1 fix: update quest [move] 2024-07-03 01:47:59 +08:00
Chen, Tai-Lin d2452c91a6 fix: update quest [copy] 2024-07-03 01:42:37 +08:00
Chen, Tai-Lin 5bac9ce099 style: revise description part 2024-07-03 01:25:34 +08:00
Chen, Tai-Lin 2aaff0f7bc fix: quests exceptions 2024-07-03 01:10:59 +08:00
Chen, Tai-Lin 96ab39437b fix: quest error [remove] 2024-07-03 01:02:15 +08:00
Chen, Tai-Lin d6b606e807 fix: update quest [get-started] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin b792992391 fix: update quest [explore] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin 922100d8da fix: update quest [cast] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin 52597e8c5e fix: update quest [explore] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin a6c160ac0e fix: update quest [cast] 2024-07-03 01:00:33 +08:00
Chen, Tai-Lin 3859403b56 fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 01:00:17 +08:00
Chen, Tai-Lin 2a29b0b8ac fix: update quest [read] 2024-07-03 00:57:33 +08:00
Chen, Tai-Lin aaa97e0dca fix: update quest [path] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 3a28af0a67 fix: update quest [move] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 551d502845 fix: update quest [helloworld] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 3f0191112d fix: update quest [get-started] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin d565709a59 fix: update quest [explore] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 435a426f5b fix: update quest [remove] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 377afb3078 fix: update quest [create] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 9d26951978 fix: update quest [cast] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin db3b00f8ee fix: update quest [copy] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin c09c4ff9e0 fix: quests exceptions error 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin b3b374b3cd fix: update quest [explore] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 5ca187651d fix: update quest answer file [get-started] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin f2ca4a26ac fix: update quest's exceptions 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 462f17a779 fix: update quest [cast] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3deabb25a9 fix: update quest [get-started] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3dad7ddccb fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 25888a1465 fix: modify component name and title (HintPart=>DescriptionPart) 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3559814f55 fix: quest error [remove] 2024-07-03 00:44:30 +08:00
Chen, Tai-Lin 8142ff816c Merge branch 'fix-current-quests' of https://github.com/lancatlin/linux-odyssey into fix-current-quests 2024-07-03 00:34:39 +08:00
Chen, Tai-Lin 9c160253c9 fix: update quest [read] 2024-07-03 00:34:33 +08:00
Chen, Tai-Lin 80e1a40b6e fix: update quest [path] 2024-07-03 00:34:33 +08:00
Chen, Tai-Lin 2b70f3c7cd fix: update quest [move] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 2e29919ad9 fix: update quest [helloworld] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin ca79042a3b fix: update quest [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin c8d2320bf5 fix: update quest [explore] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 441909efd5 fix: update quest [remove] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 90e3fc2bd0 fix: update quest [create] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 598141d735 fix: update quest [cast] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin e11432be45 fix: update quest [copy] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 2c1fe7bb96 fix: quests exceptions error 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 8c54bfe192 fix: update quest [explore] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin d292195302 fix: update quest answer file [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 6bbf70a258 fix: update quest's exceptions 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 526b856de7 fix: update quest [cast] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 1a20ffb295 fix: update quest [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 4efa9b8831 fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 3944e91d0c fix: modify component name and title (HintPart=>DescriptionPart) 2024-07-03 00:34:32 +08:00
wancat c30b4a7c6e feat: add isGuest migration script 2024-07-03 00:34:32 +08:00
wancat 14cd16b611 feat: add isGuest field in user model 2024-07-03 00:34:32 +08:00
wancat b07b60f6bd feat: add register guest button 2024-07-03 00:34:32 +08:00
wancat 9d1bed7543 feat: add register-guest api 2024-07-03 00:34:32 +08:00
wancat 715352f77c feat: install date-fns 2024-07-03 00:34:32 +08:00
Annie Huang c4550a395a fix map view 2024-07-03 00:34:32 +08:00
dependabot[bot] 5795cb16b8 build(deps): bump tar from 6.2.0 to 6.2.1
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 691a6dcb25 fix: update quest [read] 2024-07-03 00:32:04 +08:00
Chen, Tai-Lin 979ea72f3a fix: update quest [path] 2024-07-03 00:31:57 +08:00
Chen, Tai-Lin f9e40edb8a fix: update quest [move] 2024-07-03 00:31:46 +08:00
Chen, Tai-Lin 276bfee5ed fix: update quest [helloworld] 2024-07-03 00:31:40 +08:00
Chen, Tai-Lin f03c359f89 fix: update quest [get-started] 2024-07-03 00:31:30 +08:00
Chen, Tai-Lin 7db08a5cda fix: update quest [explore] 2024-07-03 00:31:20 +08:00
Chen, Tai-Lin b19f0da678 fix: update quest [remove] 2024-07-03 00:31:10 +08:00
Chen, Tai-Lin c87eef233c fix: update quest [create] 2024-07-03 00:30:59 +08:00
Chen, Tai-Lin dabc40eb1e fix: update quest [cast] 2024-07-03 00:30:44 +08:00
Chen, Tai-Lin 0efe2235f8 fix: update quest [copy] 2024-07-03 00:30:37 +08:00
Chen, Tai-Lin 9d45993d0d fix: quests exceptions error 2024-07-02 22:32:00 +08:00
Lin, Hung-Shin 99871fa299 Fix exception not showing bug (#143) 2024-07-02 11:58:27 +08:00
wancat 248271355f fix: restore quests changes to prevent conflicts 2024-07-02 11:57:55 +08:00
Chen, Tai-Lin 79f9cb3e95 fix: update quest [explore] 2024-07-02 02:09:09 +08:00
Chen, Tai-Lin 47c7ff04a9 fix: update quest answer file [get-started] 2024-07-02 00:49:42 +08:00
Chen, Tai-Lin f831f4d4e5 fix: update quest's exceptions 2024-07-01 23:39:58 +08:00
Chen, Tai-Lin 29799faa79 fix: update quest [cast] 2024-07-01 23:10:46 +08:00
Chen, Tai-Lin 2727c9f4bb fix: update quest [get-started] 2024-07-01 22:38:42 +08:00
wancat 988cb65b51 fix: wrong exception condition 2024-07-01 10:41:48 +08:00
wancat 4324932fc9 fix: exception not showing 2024-07-01 10:41:26 +08:00
wancat e9b49502ab fix: progress not update when completing quest 2024-06-28 16:42:15 +08:00
Lin, Hung-Shin d5519e7157 Fix duplicate responses (#140) 2024-06-28 16:19:48 +08:00
wancat ed596c245d fix: duplicate response 2024-06-28 16:17:28 +08:00
Lin, Hung-Shin 57c080d88b Add start session button (#134)
- Add new file StartButton
- Covering Hint part and Terminal part
- Load instruction before pressing button
- Load session after pressing the button
2024-06-28 16:15:26 +08:00
wancat d795dbd788 refactor: Remove commented out code and unused imports 2024-06-28 16:14:26 +08:00
wancat 218d433c14 fix: remove unused logging 2024-06-28 16:05:45 +08:00
wancat 48835f8830 feat: change e2e to not required 2024-06-28 11:41:12 +08:00
Chen, Tai-Lin 9ed726d597 fix: revise quest content [get-started] (1 to be fixed) 2024-06-28 02:25:34 +08:00
Chen, Tai-Lin fcb52b9d9e fix: modify component name and title (HintPart=>DescriptionPart) 2024-06-28 02:14:22 +08:00
wancat dcd8f441f8 feat: echo start after creating session 2024-06-27 12:14:15 +08:00
wancat 94ef3749f5 fix: socket & terminal states across quests 2024-06-27 12:04:35 +08:00
wancat 3398d18055 feat: load session and display start button if not exists 2024-06-27 11:55:34 +08:00
wancat c64043316e feat: replace getOrCreateActive session to getActiveSession 2024-06-27 11:54:28 +08:00
wancat 7cdc542c19 refactor: rewrite session store using pinia and fix start button 2024-06-27 11:23:21 +08:00
wancat 78b9529699 refactor: rewrite map page using user profile store 2024-06-27 10:52:02 +08:00
wancat 8a31a58c56 refactor: rewrite user profile store using pinia 2024-06-27 10:48:08 +08:00
wancat 103ce4774c refactor: rename dev docker compose file and remove unused services 2024-06-27 10:31:26 +08:00
Annie Huang 4e6602d82f change post to get (not yet success) 2024-06-27 10:30:00 +08:00
Annie Huang 2eaf3c8f24 auto type echo start 2024-06-27 10:30:00 +08:00
Annie Huang 15465feda9 auto connect session, create session after button 2024-06-27 10:30:00 +08:00
Annie Huang ea2ba2c7a0 load quest info before start 2024-06-27 10:30:00 +08:00
Annie Huang 0b136a830a add start button
- new stage will load previous stored data at first
2024-06-27 10:30:00 +08:00
Lin, Hung-Shin 3848ca6941 116 guest account (#137)
* add isGuest flag in User model
* add register guest api
* add continue as guest button in frontend
2024-06-27 10:03:04 +08:00
wancat 443e810471 feat: add isGuest migration script 2024-06-17 20:12:32 +08:00
wancat 198dcae3f8 feat: add isGuest field in user model 2024-06-17 19:50:58 +08:00
wancat 7347c37a9e feat: add register guest button 2024-06-17 19:49:00 +08:00
wancat 1d725d82ae feat: add register-guest api 2024-06-17 19:23:30 +08:00
wancat b5c601f4a9 feat: install date-fns 2024-06-17 19:23:23 +08:00
Annie Huang 5069ac12da fix map view 2024-04-11 23:39:57 +08:00
Lin, Hung-Shin cec4d86058 build(deps): bump tar from 6.2.0 to 6.2.1 (#131)
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/isaacs/node-tar/commit/bef7b1e4ffab822681fea2a9b22187192ed14717"><code>bef7b1e</code></a>
6.2.1</li>
<li><a
href="https://github.com/isaacs/node-tar/commit/fe8cd57da5686f8695415414bda49206a545f7f7"><code>fe8cd57</code></a>
prevent extraction in excessively deep subfolders</li>
<li><a
href="https://github.com/isaacs/node-tar/commit/fe7ebfdcede1f8a2e65db12e19ecc4b3a9934648"><code>fe7ebfd</code></a>
remove security.md</li>
<li>See full diff in <a
href="https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tar&package-manager=npm_and_yarn&previous-version=6.2.0&new-version=6.2.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-04-11 22:39:51 +08:00
dependabot[bot] 93b0bae204 build(deps): bump tar from 6.2.0 to 6.2.1
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-11 11:56:58 +00:00
Lin, Hung-Shin 4713b11c95 Fix e2e Testing (#130)
**Fix e2e broken caused by website modification**

- Add back "GameTitle" for **Linux Odyssey** in header
- Delete echo hint in quest **move** (It stucks the testing)
- Modify paths for several testing describes
- Fix type problems in command.ts
2024-04-11 15:19:38 +08:00
Annie Huang 36e6bc10d3 fix: e2e broken caused by website modification 2024-04-09 11:32:58 +08:00
Annie Huang 580d5fc3f8 Feat landing page - Mobile Optimisation (#112)
This optimisation is for mobile usage and also make header a better
component.
- Header Restruction
- Menu pop up when using smaller screen
- Seperate header components and other components
2024-04-07 23:49:53 +08:00
Annie Huang d8392ec3f6 delete redundant code 2024-04-07 23:44:41 +08:00
Annie Huang 5f99f42f5f fix type problems 2024-04-07 20:41:17 +08:00
Annie Huang bdf757c2d4 fix: header components in the middle 2024-04-07 20:40:44 +08:00
Annie Huang 453bfb7f1e delete redundant code 2024-04-06 22:06:48 +08:00
wancat 3b470ed952 fix: session store type 2024-04-06 22:06:48 +08:00
Annie Huang b4af2d46d3 fix hint display problems 2024-04-06 22:06:48 +08:00
Annie Huang 0e96e30b96 categorise components 2024-04-06 22:06:48 +08:00
Annie Huang 3e3ac58c6b Restruct heading 2024-04-06 22:06:48 +08:00
wancat 9f690548cf fix: session store type 2024-04-06 22:06:48 +08:00
Annie Huang cd2ec6df75 fix hint display problems 2024-04-06 22:06:48 +08:00
Annie Huang 8e7b5f2552 modify menu position while open 2024-04-06 22:06:48 +08:00
Annie Huang 385c6bc110 delete testing id 2024-04-06 22:06:48 +08:00
wancat 092cf284dd fix: session store type 2024-04-06 22:06:48 +08:00
Annie Huang 7806852cab fix hint display problems 2024-04-06 22:06:48 +08:00
Annie Huang 1e31cf3be5 fix file locations and transfer to ts 2024-04-06 22:06:29 +08:00
Annie Huang aa903fbacf fix file position 2024-04-06 22:06:29 +08:00
Annie Huang df980a7d56 delete redundant code 2024-04-06 22:06:29 +08:00
Annie Huang 980d0497f4 fix header justify end property 2024-04-06 22:06:29 +08:00
Annie Huang e37a23c34f mobile menu (but not justify end while bigger screen, still solving) 2024-04-06 22:06:29 +08:00
Annie Huang f4fc6de4cc canceling [..vh] 2024-04-06 22:06:29 +08:00
Annie Huang 9bfc6614ca hide some text when screen is small 2024-04-06 22:06:29 +08:00
Annie Huang 8c4a9ea871 add header to login and register page, modified header class but not fit for all circumstances 2024-04-06 22:06:29 +08:00
Annie Huang ad29137b86 categorise components - change path 2024-04-06 22:06:29 +08:00
Annie Huang b5e49547ea categorise components 2024-04-06 22:06:29 +08:00
Annie Huang 9a4f9b958a Restruct heading 2024-04-06 22:06:29 +08:00
Annie Huang 29602785cd rwd player opinions 2024-04-06 22:05:59 +08:00
Lin, Hung-Shin bf44cce580 Ts project refs (#108)
# TypeScript Project References
Change shared packages to use [project
reference](https://www.typescriptlang.org/docs/handbook/project-references.html).
Project references allow TypeScript to automatically build dependency
packages. By this change, we can speed up and simplify the building
process of `server` and `app`. Vite doesn't support project references
yet, so we need to run `yarn workspace app build:refs` before starting
Vite, or you can simply run `yarn build` to build everything.

# ts-node and SWC

Inspired by @chreamy #106 , I changed to use ts-node as dev-server
runner, and use [swc](https://swc.rs/) for loader to speed up, which
remove the needs of having a separate tsc daemon.
2024-04-06 00:10:28 +08:00
wancat 7c88a60d94 fix: change test to typescript 2024-04-06 00:03:27 +08:00
wancat 1f55ae09d6 fix: build app docker image 2024-04-05 23:59:26 +08:00
wancat e2b5bb3b70 fix: terminal type 2024-04-05 23:04:51 +08:00
wancat ba69685ea2 fix: analytics build image 2024-04-05 22:59:22 +08:00
wancat 84e65c9ef6 feat: enable swc and ts-node in server dev 2024-04-05 22:42:02 +08:00
wancat f466a7a793 feat: use babel to replace ts-jest (improve performance) 2024-04-05 22:42:02 +08:00
wancat d74a549b89 fix: docker 2024-04-05 22:42:02 +08:00
wancat 33a1a110e8 feat: use project references to build dependencies efficiently 2024-04-05 22:42:02 +08:00
Lin, Hung-Shin d4c9d4577c 117 fix oauth (#127)
* change session cookie from 'strict' to 'lax' to fix "forbidden" issue
* add scope settings to Google

some additional improvements:
* update ESLint settings to allow unused parameters before used ones

some related discussions:
* https://github.com/auth0/passport-auth0/issues/70
*
https://stackoverflow.com/questions/59221899/add-samesite-attribute-to-passport-js-session-cookie
2024-04-05 22:24:16 +08:00
wancat 27b6096633 fix: eslint unused error 2024-04-05 22:22:44 +08:00
Annie Huang a8a367f477 delete testing id 2024-04-05 22:19:38 +08:00
wancat 78b92ee8ce fix: session store type 2024-04-05 22:19:38 +08:00
Annie Huang 14fb870838 fix hint display problems 2024-04-05 22:19:38 +08:00
wancat 455fde41ab fix: OAuth flow by setting scope and cookie to 'lax' 2024-04-05 17:56:09 +08:00
dependabot[bot] ff1cdc9a95 build(deps-dev): bump vite from 4.5.2 to 4.5.3
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.3/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 15:48:55 +08:00
wancat df140c64f8 fix: Update eslint rule for unused variables 2024-04-01 11:53:02 +08:00
Lin, Hung-Shin 4efd670ed6 build(deps): bump follow-redirects from 1.15.5 to 1.15.6 (#114)
Bumps
[follow-redirects](https://github.com/follow-redirects/follow-redirects)
from 1.15.5 to 1.15.6.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/35a517c5861d79dc8bff7db8626013d20b711b06"><code>35a517c</code></a>
Release version 1.15.6 of the npm package.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/c4f847f85176991f95ab9c88af63b1294de8649b"><code>c4f847f</code></a>
Drop Proxy-Authorization across hosts.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/8526b4a1b2ab3a2e4044299377df623a661caa76"><code>8526b4a</code></a>
Use GitHub for disclosure.</li>
<li>See full diff in <a
href="https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.15.5&new-version=1.15.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-03-28 21:45:49 +08:00
Lin, Hung-Shin 4814626a71 build(deps): bump express from 4.18.2 to 4.19.2 (#115)
[//]: # (dependabot-start)
⚠️  **Dependabot is rebasing this PR** ⚠️ 

Rebasing might not happen immediately, so don't worry if this takes some
time.

Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.

---

[//]: # (dependabot-end)

Bumps [express](https://github.com/expressjs/express) from 4.18.2 to
4.19.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/releases">express's
releases</a>.</em></p>
<blockquote>
<h2>4.19.2</h2>
<h2>What's Changed</h2>
<ul>
<li><a
href="https://github.com/expressjs/express/commit/0b746953c4bd8e377123527db11f9cd866e39f94">Improved
fix for open redirect allow list bypass</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.19.1...4.19.2">https://github.com/expressjs/express/compare/4.19.1...4.19.2</a></p>
<h2>4.19.1</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix ci after location patch by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5552">expressjs/express#5552</a></li>
<li>fixed un-edited version in history.md for 4.19.0 by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5556">expressjs/express#5556</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.19.0...4.19.1">https://github.com/expressjs/express/compare/4.19.0...4.19.1</a></p>
<h2>4.19.0</h2>
<h2>What's Changed</h2>
<ul>
<li>fix typo in release date by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5527">expressjs/express#5527</a></li>
<li>docs: nominating <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> to be
project captian by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5511">expressjs/express#5511</a></li>
<li>docs: loosen TC activity rules by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5510">expressjs/express#5510</a></li>
<li>Add note on how to update docs for new release by <a
href="https://github.com/crandmck"><code>@​crandmck</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5541">expressjs/express#5541</a></li>
<li><a
href="https://redirect.github.com/expressjs/express/pull/5551/commits/660ccf5fa33dd0baab069e5c8ddd9ffe7d8bbff1">Prevent
open redirect allow list bypass due to encodeurl</a></li>
<li>Release 4.19.0 by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5551">expressjs/express#5551</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/crandmck"><code>@​crandmck</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5541">expressjs/express#5541</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.18.3...4.19.0">https://github.com/expressjs/express/compare/4.18.3...4.19.0</a></p>
<h2>4.18.3</h2>
<h2>Main Changes</h2>
<ul>
<li>Fix routing requests without method</li>
<li>deps: body-parser@1.20.2
<ul>
<li>Fix strict json error message on Node.js 19+</li>
<li>deps: content-type@~1.0.5</li>
<li>deps: raw-body@2.5.2</li>
</ul>
</li>
</ul>
<h2>Other Changes</h2>
<ul>
<li>Use https: protocol instead of deprecated git: protocol by <a
href="https://github.com/vcsjones"><code>@​vcsjones</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5032">expressjs/express#5032</a></li>
<li>build: Node.js@16.18 and Node.js@18.12 by <a
href="https://github.com/abenhamdine"><code>@​abenhamdine</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5034">expressjs/express#5034</a></li>
<li>ci: update actions/checkout to v3 by <a
href="https://github.com/armujahid"><code>@​armujahid</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5027">expressjs/express#5027</a></li>
<li>test: remove unused function arguments in params by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5124">expressjs/express#5124</a></li>
<li>Remove unused originalIndex from acceptParams by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5119">expressjs/express#5119</a></li>
<li>Fixed typos by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5117">expressjs/express#5117</a></li>
<li>examples: remove unused params by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5113">expressjs/express#5113</a></li>
<li>fix: parameter str is not described in JSDoc by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5130">expressjs/express#5130</a></li>
<li>fix: typos in History.md by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5131">expressjs/express#5131</a></li>
<li>build : add Node.js@19.7 by <a
href="https://github.com/abenhamdine"><code>@​abenhamdine</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5028">expressjs/express#5028</a></li>
<li>test: remove unused function arguments in params by <a
href="https://github.com/raksbisht"><code>@​raksbisht</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5137">expressjs/express#5137</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/blob/master/History.md">express's
changelog</a>.</em></p>
<blockquote>
<h1>4.19.2 / 2024-03-25</h1>
<ul>
<li>Improved fix for open redirect allow list bypass</li>
</ul>
<h1>4.19.1 / 2024-03-20</h1>
<ul>
<li>Allow passing non-strings to res.location with new encoding handling
checks</li>
</ul>
<h1>4.19.0 / 2024-03-20</h1>
<ul>
<li>Prevent open redirect allow list bypass due to encodeurl</li>
<li>deps: cookie@0.6.0</li>
</ul>
<h1>4.18.3 / 2024-02-29</h1>
<ul>
<li>Fix routing requests without method</li>
<li>deps: body-parser@1.20.2
<ul>
<li>Fix strict json error message on Node.js 19+</li>
<li>deps: content-type@~1.0.5</li>
<li>deps: raw-body@2.5.2</li>
</ul>
</li>
<li>deps: cookie@0.6.0
<ul>
<li>Add <code>partitioned</code> option</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/expressjs/express/commit/04bc62787be974874bc1467b23606c36bc9779ba"><code>04bc627</code></a>
4.19.2</li>
<li><a
href="https://github.com/expressjs/express/commit/da4d763ff6ba9df6dbd8f1f0b1d05412dda934d5"><code>da4d763</code></a>
Improved fix for open redirect allow list bypass</li>
<li><a
href="https://github.com/expressjs/express/commit/4f0f6cc67d531431c096ea006c2191b92931bbc3"><code>4f0f6cc</code></a>
4.19.1</li>
<li><a
href="https://github.com/expressjs/express/commit/a003cfab034fbadb1c78ae337ee8ab389adda217"><code>a003cfa</code></a>
Allow passing non-strings to res.location with new encoding handling
checks f...</li>
<li><a
href="https://github.com/expressjs/express/commit/a1fa90fcea7d8e844e1c9938ad095d62669c3abd"><code>a1fa90f</code></a>
fixed un-edited version in history.md for 4.19.0</li>
<li><a
href="https://github.com/expressjs/express/commit/11f2b1db227fd42c2508c427032c1ec671b306be"><code>11f2b1d</code></a>
build: fix build due to inconsistent supertest behavior in older
versions</li>
<li><a
href="https://github.com/expressjs/express/commit/084e36506a18774f85206a65d8da04dc1107fc1b"><code>084e365</code></a>
4.19.0</li>
<li><a
href="https://github.com/expressjs/express/commit/0867302ddbde0e9463d0564fea5861feb708c2dd"><code>0867302</code></a>
Prevent open redirect allow list bypass due to encodeurl</li>
<li><a
href="https://github.com/expressjs/express/commit/567c9c665d0de4c344b8e160146050770233783c"><code>567c9c6</code></a>
Add note on how to update docs for new release (<a
href="https://redirect.github.com/expressjs/express/issues/5541">#5541</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/69a4cf2819c4449ec6ea45649691fb43a528d5d1"><code>69a4cf2</code></a>
deps: cookie@0.6.0</li>
<li>Additional commits viewable in <a
href="https://github.com/expressjs/express/compare/4.18.2...4.19.2">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~wesleytodd">wesleytodd</a>, a new releaser
for express since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=express&package-manager=npm_and_yarn&previous-version=4.18.2&new-version=4.19.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-03-28 21:45:13 +08:00
dependabot[bot] 1179faf176 build(deps): bump express from 4.18.2 to 4.19.2
Bumps [express](https://github.com/expressjs/express) from 4.18.2 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.2...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-28 13:44:47 +00:00
Lin, Hung-Shin a9acf54139 Chris - convert analytics to TS (#106) 2024-03-28 21:43:17 +08:00
wancat b64f49915f fix: restore server changes 2024-03-28 21:39:26 +08:00
wancat ef279ba00e fix: building and production starting scripts 2024-03-28 21:35:07 +08:00
wancat eb106c020a fix: missing express type annotation 2024-03-28 21:28:18 +08:00
Lin, Hung-Shin 432084cc66 Delete package-lock.json 2024-03-28 21:23:30 +08:00
Lin, Hung-Shin 2fb1315c5a Feat typescript frontend (#107)
There is still tons of errors in e2e. Fix it after merging.
2024-03-28 21:22:42 +08:00
wancat 75f5407dde fix: fix overwritten new response model 2024-03-28 21:18:00 +08:00
wancat ebd713200c refactor: update socket types 2024-03-28 21:06:09 +08:00
wancat bb2a1a145f refactor: move interfaces to types.ts 2024-03-28 20:55:40 +08:00
Chen, Tai-Lin 7f33100132 update 2024-03-25 15:48:37 +08:00
Chen, Tai-Lin 59a0865157 reverse change 2024-03-25 14:53:44 +08:00
chreamy c843a03fc8 feat: analytics UI 2024-03-25 00:08:14 -05:00
dependabot[bot] 182cc7d1bb build(deps): bump follow-redirects from 1.15.5 to 1.15.6
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-16 23:56:54 +00:00
chreamy daa3f11b2e test build 2024-03-11 22:16:17 -05:00
chreamy a4c121dc94 test build 2024-03-11 22:14:30 -05:00
chreamy 5adaa47c5c test build 2024-03-11 22:10:32 -05:00
chreamy 3345fb3b3f test build 2024-03-11 22:06:43 -05:00
chreamy 0313671ff5 test build 2024-03-11 21:37:38 -05:00
chreamy e31511fdc1 fix: file imports 2024-03-11 21:33:01 -05:00
chreamy 685be8c1d2 fix: file imports 2024-03-11 21:27:15 -05:00
chreamy 0d2e62be24 fix: file imports 2024-03-11 21:18:44 -05:00
chreamy 962db4e8a4 fix: file imports 2024-03-11 21:14:45 -05:00
chreamy 52d8e86043 fix: file imports 2024-03-11 21:05:02 -05:00
Christopher LiuandLin, Hung-Shin 51fb25aa50 Update packages/analytics/package.json
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:51:49 -05:00
Christopher LiuandLin, Hung-Shin d2d8374926 Update packages/analytics/package.json
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:51:40 -05:00
Christopher LiuandLin, Hung-Shin 4570162e7b Update packages/analytics/nodemon.json
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:51:27 -05:00
Christopher LiuandLin, Hung-Shin 3194c03f48 Update packages/analytics/src/interface.ts
Co-authored-by: Lin, Hung-Shin <wancat@wancat.cc>
2024-03-11 20:50:49 -05:00
Chen, Tai-Lin c4fd92e289 fix types issue 2024-03-11 21:04:32 +08:00
chreamy ce538a049c modified: packages/analytics/src/models/sessions.ts
modified:   packages/analytics/src/models/users.ts
2024-03-05 01:51:30 -06:00
chreamy 36932e1eb1 This part is somehow influencing the checks, please review 2024-03-05 01:43:40 -06:00
chreamy ca3d9b4236 Merge branch 'ts-analytics' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-03-05 01:38:58 -06:00
chreamy 76e6cda0cb revert changes 2024-03-05 01:38:38 -06:00
Christopher Liu 3dee3d0373 Merge branch 'main' into ts-analytics 2024-03-05 02:32:48 -05:00
chreamy 218a25daee feat: user sessions history & fix ts <any> 2024-03-05 01:27:53 -06:00
chreamy 94ae2d7df4 Merge branch 'ts-analytics' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-03-05 01:27:10 -06:00
chreamy a71411d5bf Feat: User Session History 2024-03-05 00:26:22 -06:00
Chen, Tai-Lin 807974d727 fix types issue 2024-03-05 00:27:08 +08:00
Chen, Tai-Lin ec63bba0fe fix using any type 2024-03-04 23:38:18 +08:00
Chen, Tai-Lin ddaefca5fb fix using Ref<any> type 2024-03-04 23:38:18 +08:00
Chen, Tai-Lin ba43b4c5e8 fix wrong yarn package location 2024-03-04 23:38:18 +08:00
Chen, Tai-Lin f52017ef9f update .vue 2024-03-04 23:37:46 +08:00
Chen, Tai-Lin af9b443865 update socket.ts 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 4739849549 update views 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 63a57aa2cf update components 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin ead321cd62 update config 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 516e59f855 update utils 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin 4ddbb5aede update app 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin d924ccaaa9 update store 2024-03-04 23:35:05 +08:00
Chen, Tai-Lin c324bfcfee update utils 2024-03-04 23:34:06 +08:00
wancat 817cf8c40e hotfix: redirect to map after login 2024-03-01 23:49:58 +08:00
wancat 4e8a56f127 hotfix: analytics build 2024-03-01 23:21:09 +08:00
wancat 6aed5789a5 Merge remote-tracking branch 'origin/main' into feat-landing-page 2024-03-01 23:13:15 +08:00
Lin, Hung-Shin d737347d0b UI dialogue (#110) 2024-03-01 23:09:39 +08:00
wancat 5041ec9dc6 Contact 2024-03-01 22:48:21 +08:00
wancat c2c792f10c video and about 2024-03-01 22:27:00 +08:00
wancat 85b34abc7e responsive content block 2024-03-01 21:59:12 +08:00
wancat 2bfd2cc06d setup main block 2024-03-01 21:49:31 +08:00
Annie Huang 5dbdadbb27 change text block color 2024-03-01 21:28:51 +08:00
wancat 1e2a43db50 items-center the header 2024-03-01 21:24:21 +08:00
Annie Huang ba7409b7a8 - add icons
- change first page to landing-page
- add text background
- modify text arrangement
2024-03-01 20:36:36 +08:00
Annie Huang f9decee80b fix: scroll bug
modify contact layout
2024-03-01 16:17:13 +08:00
Annie Huang bef134871d - edit position
- add group info
- add opinions
- add start button
2024-02-29 23:06:47 +08:00
Annie Huang 46e27c7456 - add contacts
- change contents
- fix broken page
2024-02-29 15:23:07 +08:00
Annie Huang 9146c433b3 add youtube embedded 2024-02-29 15:23:07 +08:00
Annie Huang 00e421861f Landing page
- header
- jump to section
2024-02-29 15:23:07 +08:00
Annie Huang cdd76f7d0c add new page 2024-02-29 15:23:07 +08:00
wancat a389d2752d disable wait pending in e2e tests 2024-02-29 10:54:53 +08:00
wancat 039da53bbe feat: send status to the frontend 2024-02-29 10:54:53 +08:00
wancat 622d64dd45 fix: overflow in hint 2024-02-29 10:54:53 +08:00
wancat 90e661f77b Display dialogue in hints. Adjust Game page layout 2024-02-29 10:54:53 +08:00
wancat 88ee8f2cc3 feat: send response to frontend 2024-02-29 10:54:53 +08:00
wancat bb4a11b20c fix: use swc to optimize ts-node speed 2024-02-29 00:27:06 +08:00
wancat 031d7a8ceb fix: analytics dockerfile 2024-02-29 00:10:11 +08:00
wancat 48dbac7907 fix: import path and dev server using tsc and nodemon 2024-02-29 00:04:09 +08:00
Christopher Liu ea33434bea Update config.ts 2024-02-28 23:38:33 +08:00
chreamy 923de901c4 convert analytics to TS 2024-02-28 23:38:33 +08:00
Lin, Hung-Shin fb69cb4e10 Feat automation test - Transfer to Typescript (#104)
- Update Readme for new build-up commands (still not the latest version)
- Add Cpress optional OAuth activation
- Transfer testing files into Typescript
2024-02-28 23:29:24 +08:00
wancat 818e4e95e4 fix: compile cypress to commonjs to fix docker issue 2024-02-28 23:20:29 +08:00
Annie Huang 2aa1c84e73 delete port setting of db in testing yml 2024-02-27 22:23:10 +08:00
Annie Huang 1b52349f03 delete redundant code 2024-02-27 14:26:37 +08:00
wancat a9bc2cc127 fix: disable sourceMap in cypress tsconfig 2024-02-27 12:54:30 +08:00
wancat 7c08d21142 fix: downgrade yarn to v1 in docker 2024-02-27 12:02:22 +08:00
wancat b059cca7b2 fix jest types by import @jest/globals 2024-02-27 11:45:10 +08:00
Annie Huang 9e108e397b extend from global settings and modify cypress config 2024-02-27 11:41:37 +08:00
chreamy dc972173a4 Merge branch 'ts-analytics' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-02-26 20:43:25 -06:00
chreamy 56d07f14b4 Merge branch 'main' of https://github.com/linux-odyssey/linux-odyssey into ts-analytics 2024-02-26 20:43:16 -06:00
Christopher Liu b47bb62831 Update config.ts 2024-02-26 20:33:54 -06:00
chreamy ece0ec7c41 convert analytics to TS 2024-02-26 20:23:03 -06:00
Annie Huang 004d232a84 try transfering config to ts(but import is failing) 2024-02-27 00:12:07 +08:00
Annie Huang 0a730e4eef exclude cypress config and e2e.js 2024-02-26 23:50:51 +08:00
Annie Huang 5091f8f24f revise jest tsconfig 2024-02-26 23:44:53 +08:00
Annie Huang 87060aa3ef try: fix collison excluding and including 2024-02-23 18:19:33 +08:00
Annie Huang 670a5b2e87 try: fix collison by settings 2024-02-23 17:51:31 +08:00
Annie Huang f540a0166d add include jest 2024-02-23 17:21:09 +08:00
Annie Huang b98f4e4c07 exclude jest 2024-02-23 17:17:23 +08:00
Annie Huang 31eb638a6e fix typo 2024-02-23 17:06:59 +08:00
Annie Huang 583605ef3a try: fix collison of Jest 2024-02-23 16:59:55 +08:00
wancat 8c6b86ad17 restore changes 2024-02-23 15:09:46 +08:00
wancat 8670d2b3bb fix: ignore eslint warning 2024-02-23 15:06:12 +08:00
wancat 07ec58e41c fix: enable restart of db 2024-02-23 15:06:01 +08:00
wancat 025553f42a Merge branch 'main' into feat-automation-test 2024-02-23 14:59:00 +08:00
Lin, Hung-Shin bfa839f2f9 Refactor server (#102) 2024-02-23 14:55:55 +08:00
Annie Huang b57e7adb37 update readme 2024-02-23 13:02:23 +08:00
Annie Huang b1a9a6102c Optional oAuth activation in Cypress 2024-02-23 11:34:47 +08:00
wancat c5a7e77f04 fix: testing base url. change cypress base image 2024-02-22 23:15:29 +08:00
Annie Huang ed142ab27b update declare types 2024-02-22 23:03:43 +08:00
wancat f29dcf810f fix: Dockerfile missing workspace name
This commit updates the Dockerfile to build the @linux-odyssey/models and @linux-odyssey/constants workspaces, in addition to the existing workspaces.
2024-02-22 22:26:45 +08:00
wancat 48cfcb2c36 fix dockerfiles by using classic yarn 2024-02-22 22:24:29 +08:00
wancat 14eea84a36 fix: restore yarn version to classic 2024-02-22 22:03:55 +08:00
wancat c0efce69f4 fix: jest. enable coverage 2024-02-22 21:31:29 +08:00
wancat 37b1bead28 change dependency type 2024-02-22 20:51:49 +08:00
wancat 9f1008336d refactor: routes and index.js 2024-02-22 20:44:47 +08:00
wancat 59590639ee refactor: authController, add session types 2024-02-22 20:35:19 +08:00
wancat 885f1c5b27 refactor: leaderboard controller 2024-02-22 20:18:04 +08:00
wancat ac8e9d1cfa refactor quest controller 2024-02-22 20:16:08 +08:00
wancat fd543ed594 finish session controller 2024-02-22 20:10:03 +08:00
wancat c0ec382510 enable source map in dev mode, add delay for nodemon starting 2024-02-22 20:09:47 +08:00
wancat e84ca12647 fix missing types in utils 2024-02-19 23:57:08 +08:00
wancat c912319967 refactor: userController and add req user type 2024-02-19 23:51:21 +08:00
wancat 4c421b4cb3 refactor: game 2024-02-19 23:13:37 +08:00
wancat 2d05149cea refactor socket 2024-02-19 19:24:18 +08:00
wancat ad66191d88 refactor: validators 2024-02-19 19:09:32 +08:00
wancat 5e8df7ae01 refactor: middlewares 2024-02-19 19:08:34 +08:00
Annie Huang 9bac360498 fixed type error 2024-02-19 18:46:50 +08:00
wancat 7c1d83ccd3 refactor: cli 2024-02-19 18:36:50 +08:00
wancat 4ab3096fa5 refactor: fix file condition checks 2024-02-19 18:35:39 +08:00
wancat 64215d3ef1 refactor: container 2024-02-19 18:23:52 +08:00
wancat 075e21fb89 change scripts from "watch" to "dev" 2024-02-19 18:05:01 +08:00
wancat 9dc0b43737 add types to models 2024-02-19 18:05:01 +08:00
wancat 5489de2432 refactor: auth 2024-02-19 18:05:01 +08:00
wancat d6c95cea4a refactor utils 2024-02-19 18:05:00 +08:00
Annie Huang 2cb82fdcdc fix type error 2024-02-15 21:20:32 +08:00
Annie Huang 2b495f7cda update testing library settings and global functions 2024-02-14 16:16:01 +08:00
Lin, Hung-Shin 9509bbc355 Refactor packages (#101) 2024-02-09 23:19:36 +08:00
wancat c3d9130731 Remove logging config into console 2024-02-09 14:33:27 +08:00
Lin, Hung-Shin 4f9b275211 Fix issue 51 (#99)
Please test if this passes security check, modified
server/src/api/routes/index.js
2024-02-09 14:26:28 +08:00
wancat db16413754 feat: set cookie to secure and strict 2024-02-09 14:24:32 +08:00
Annie Huang 5c17f5ea3c typescript initial edit 2024-02-08 17:49:16 +08:00
wancat ca2a40341c Merge branch 'main' into fix-issue-51 2024-02-08 17:48:11 +08:00
wancat c06d6d6deb change e2e to use node module 2024-02-08 17:40:01 +08:00
Annie Huang f797f4d897 update readme -- build up method 2024-02-07 14:24:54 +08:00
wancat 640d6b58c5 fix ts error 2024-02-07 10:41:46 +08:00
wancat 79a5dac7f1 fix mongo no matching document when deleting container 2024-02-07 10:37:38 +08:00
wancat cfda174dc5 refactor constants 2024-02-07 10:36:56 +08:00
wancat ee03b9999e refactor models to typescript 2024-02-07 10:20:19 +08:00
wancat 253bd04588 finish file-graph and solve jest ts 2024-02-07 10:03:53 +08:00
wancat 2a542302bc update eslint to omit blank line between members 2024-02-06 09:31:20 +08:00
wancat e91774fe3b refactor utils 2024-02-06 09:20:01 +08:00
Lin, Hung-Shin 23f9023f8b Feat typescript (#93)
tired. Merge first
2024-02-05 13:15:53 +08:00
wancat 5b0d1f92c4 setup docker prod 2024-02-05 13:13:34 +08:00
wancat ec4e97a7d6 able to run server without in docker 2024-02-05 12:41:07 +08:00
wancat 19cd66475c add building scripts 2024-02-05 12:33:05 +08:00
wancat c353f81b9f fix vue eslint error 2024-02-05 11:57:53 +08:00
wancat 6e4e1cfed1 fix eslint 2024-02-05 11:49:27 +08:00
wancat 78966d351f change to use esm 2024-01-30 09:06:41 +08:00
wancat eb231e4056 Remove project references and add build:watch all 2024-01-29 13:26:41 +08:00
wancat ba3b40792b add dotenv to server 2024-01-29 13:21:33 +08:00
wancat 7216158053 add app 2024-01-29 13:21:07 +08:00
wancat 5f36f732b1 feat: docker dev mode 2024-01-29 12:42:50 +08:00
wancat 30aefb47f9 feat: setup project reference 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 1531846482 fix: eslint 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin a18e0d9b68 fix: setup Jest with TS 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 557af91d4b feat: change to use commonjs output 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 2bdb804f24 WIP: fixing tests 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 8eaec839d3 feat: add post-build-script to gradually migrate 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin 3977db87a3 feat: add typescript to file-graph and server 2024-01-29 12:42:50 +08:00
Lin, Hung-Shin e6abd08963 Fix: Issue 96 (#98) 2024-01-28 20:40:56 -06:00
Lin, Hung-Shin 68e922878b Update tests/package.json 2024-01-29 10:32:38 +08:00
Lin, Hung-Shin e9e3bc25af build(deps): bump follow-redirects from 1.15.2 to 1.15.4 (#95)
Bumps
[follow-redirects](https://github.com/follow-redirects/follow-redirects)
from 1.15.2 to 1.15.4.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/65858205e59f1e23c9bf173348a7a7cbb8ac47f5"><code>6585820</code></a>
Release version 1.15.4 of the npm package.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/7a6567e16dfa9ad18a70bfe91784c28653fbf19d"><code>7a6567e</code></a>
Disallow bracketed hostnames.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/05629af696588b90d64e738bc2e809a97a5f92fc"><code>05629af</code></a>
Prefer native URL instead of deprecated url.parse.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/1cba8e85fa73f563a439fe460cf028688e4358df"><code>1cba8e8</code></a>
Prefer native URL instead of legacy url.resolve.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/72bc2a4229bc18dc9fbd57c60579713e6264cb92"><code>72bc2a4</code></a>
Simplify _processResponse error handling.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/3d42aecdca39b144a0a2f27ea134b4cf67dd796a"><code>3d42aec</code></a>
Add bracket tests.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/bcbb096b32686ecad6cd34235358ed6f2217d4f0"><code>bcbb096</code></a>
Do not directly set Error properties.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/192dbe7ce671ecad813c074bffe3b3f5d3680fee"><code>192dbe7</code></a>
Release version 1.15.3 of the npm package.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/bd8c81e4f32d12f28a35d265f88b1716703687c6"><code>bd8c81e</code></a>
Fix resource leak on destroy.</li>
<li><a
href="https://github.com/follow-redirects/follow-redirects/commit/9c728c314b06f9595dcd7f245d40731e8a27d79f"><code>9c728c3</code></a>
Split linting and testing.</li>
<li>Additional commits viewable in <a
href="https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=follow-redirects&package-manager=npm_and_yarn&previous-version=1.15.2&new-version=1.15.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-01-28 20:31:31 -06:00
Lin, Hung-Shin d4bb5d8f73 build(deps-dev): bump vite from 4.4.12 to 4.5.2 (#100)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 4.4.12 to 4.5.2.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted -->4.5.2 (2024-01-19)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)
(<a href="https://github.com/vitejs/vite/commit/eeec23b">eeec23b</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15653">#15653</a></li>
</ul>
<h2><!-- raw HTML omitted -->4.5.1 (2023-12-04)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15223">#15223</a>,
proxy html path should be encoded (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15226">#15226</a>)
(<a href="https://github.com/vitejs/vite/commit/41bb354">41bb354</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/15223">#15223</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/15226">#15226</a></li>
</ul>
<h2>4.5.0 (2023-10-18)</h2>
<ul>
<li>feat: backport mdx as known js source (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14560">#14560</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14670">#14670</a>)
(<a href="https://github.com/vitejs/vite/commit/45595ef">45595ef</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14560">#14560</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14670">#14670</a></li>
<li>feat: scan .marko files (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14669">#14669</a>)
(<a href="https://github.com/vitejs/vite/commit/ed7bdc5">ed7bdc5</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14669">#14669</a></li>
<li>feat(ssr): backport ssr.resolve.conditions and
ssr.resolve.externalConditions (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14498">#14498</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14668">#14668</a>)
(<a href="https://github.com/vitejs/vite/commit/520139c">520139c</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14498">#14498</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14668">#14668</a></li>
</ul>
<h2><!-- raw HTML omitted -->4.4.11 (2023-10-05)<!-- raw HTML omitted
--></h2>
<ul>
<li>revert: &quot;fix: use string manipulation instead of regex to
inject esbuild helpers (<a
href="https://github.com/vitejs/vite/commit/54e1275">54e1275</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14094">#14094</a></li>
</ul>
<h2><!-- raw HTML omitted -->4.4.10 (2023-10-03)<!-- raw HTML omitted
--></h2>
<ul>
<li>fix: add source map to Web Workers (fix <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14216">#14216</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14217">#14217</a>)
(<a href="https://github.com/vitejs/vite/commit/df6f32f">df6f32f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14216">#14216</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14217">#14217</a></li>
<li>fix: handle errors during <code>hasWorkspacePackageJSON</code>
function (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14394">#14394</a>)
(<a href="https://github.com/vitejs/vite/commit/6f6e5de">6f6e5de</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14394">#14394</a></li>
<li>fix: handle sourcemap correctly when multiple line import exists (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14232">#14232</a>)
(<a href="https://github.com/vitejs/vite/commit/218861f">218861f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14232">#14232</a></li>
<li>fix: if host is specified check whether it is valid (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14013">#14013</a>)
(<a href="https://github.com/vitejs/vite/commit/b1b816a">b1b816a</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14013">#14013</a></li>
<li>fix: include <code>vite/types/*</code> in exports field (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14296">#14296</a>)
(<a href="https://github.com/vitejs/vite/commit/40e99a1">40e99a1</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14296">#14296</a></li>
<li>fix: initWasm options should be optional (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14152">#14152</a>)
(<a href="https://github.com/vitejs/vite/commit/119c074">119c074</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14152">#14152</a></li>
<li>fix: restore builtins list (<a
href="https://github.com/vitejs/vite/commit/f8b9adb">f8b9adb</a>)</li>
<li>fix: use string manipulation instead of regex to inject esbuild
helpers (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14094">#14094</a>)
(<a href="https://github.com/vitejs/vite/commit/128ad8f">128ad8f</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14094">#14094</a></li>
<li>fix: ws never connects after restarting server if server.hmr.server
is set (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14127">#14127</a>)
(<a href="https://github.com/vitejs/vite/commit/441642e">441642e</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14127">#14127</a></li>
<li>fix(analysis): warnings for dynamic imports that use static template
literals (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14458">#14458</a>)
(<a href="https://github.com/vitejs/vite/commit/0c6d289">0c6d289</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14458">#14458</a></li>
<li>fix(cli): convert special base (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14283">#14283</a>)
(<a href="https://github.com/vitejs/vite/commit/d4bc0fb">d4bc0fb</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14283">#14283</a></li>
<li>fix(css): remove pure css chunk sourcemap (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14290">#14290</a>)
(<a href="https://github.com/vitejs/vite/commit/cd7e033">cd7e033</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14290">#14290</a></li>
<li>fix(css): reset render cache on renderStart (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14326">#14326</a>)
(<a href="https://github.com/vitejs/vite/commit/d334b3d">d334b3d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14326">#14326</a></li>
<li>fix(glob): trigger HMR for glob in a package (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14117">#14117</a>)
(<a href="https://github.com/vitejs/vite/commit/0f582bf">0f582bf</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14117">#14117</a></li>
<li>fix(import-analysis): preserve importedUrls import order (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14465">#14465</a>)
(<a href="https://github.com/vitejs/vite/commit/269aa43">269aa43</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14465">#14465</a></li>
<li>fix(manifest): preserve pure css chunk assets (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14297">#14297</a>)
(<a href="https://github.com/vitejs/vite/commit/3d63ae6">3d63ae6</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14297">#14297</a></li>
<li>fix(optimizer): define crawlDeps after scanProcessing and
optimizationResult are complete (fix <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/1428">#1428</a>
(<a href="https://github.com/vitejs/vite/commit/fcaf749">fcaf749</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14284">#14284</a>
<a
href="https://redirect.github.com/vitejs/vite/issues/14285">#14285</a></li>
<li>fix(resolve): support submodules of optional peer deps (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14489">#14489</a>)
(<a href="https://github.com/vitejs/vite/commit/104971d">104971d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14489">#14489</a></li>
<li>fix(vite): precisely check if files are in dirs (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14241">#14241</a>)
(<a href="https://github.com/vitejs/vite/commit/c4758d1">c4758d1</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14241">#14241</a></li>
<li>feat: improve deno and bun support (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14379">#14379</a>)
(<a href="https://github.com/vitejs/vite/commit/8bc1f9d">8bc1f9d</a>),
closes <a
href="https://redirect.github.com/vitejs/vite/issues/14379">#14379</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/d0360c12476ccc61e9e78c500ed1bd74ed65a2cf"><code>d0360c1</code></a>
release: v4.5.2</li>
<li><a
href="https://github.com/vitejs/vite/commit/eeec23bbc9d476c54a3a6d36e78455867185a7cb"><code>eeec23b</code></a>
fix: fs deny for case insensitive systems (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15653">#15653</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/c0751156f0c015f6dbd5c7a58afd8cff2fde1a2f"><code>c075115</code></a>
release: v4.5.1</li>
<li><a
href="https://github.com/vitejs/vite/commit/41bb3546a839ed2c822367b3933770c0693a0fb0"><code>41bb354</code></a>
fix: backport <a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15223">#15223</a>,
proxy html path should be encoded (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/15226">#15226</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/055d2b86b0543a7c1a2a4d5bc7298af62bc51fa7"><code>055d2b8</code></a>
release: v4.5.0</li>
<li><a
href="https://github.com/vitejs/vite/commit/ed7bdc520679577509466ce808a1794ba8377204"><code>ed7bdc5</code></a>
feat: scan .marko files (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14669">#14669</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/45595ef82f786d6b321ce002f2cd4951659114ac"><code>45595ef</code></a>
feat: backport mdx as known js source (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14560">#14560</a>)
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/14670">#14670</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/520139cdff88ae3a0bf89692133cce3e453cb29a"><code>520139c</code></a>
feat(ssr): backport ssr.resolve.conditions and
ssr.resolve.externalConditions...</li>
<li>See full diff in <a
href="https://github.com/vitejs/vite/commits/v4.5.2/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=4.4.12&new-version=4.5.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2024-01-28 20:31:13 -06:00
chreamy 7bb7ac3f00 csrf token 2024-01-20 00:14:59 -06:00
chreamy 42278ad51b csrf token 2024-01-20 00:11:48 -06:00
dependabot[bot] 107c153e98 build(deps-dev): bump vite from 4.4.12 to 4.5.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.12 to 4.5.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-20 02:24:08 +00:00
chreamy d45fbee647 fix: CSRF issue #51 2024-01-18 03:23:39 -06:00
chreamy 2ac73a06c9 fix: CSRF issue #51 2024-01-18 03:15:38 -06:00
chreamy 91a9397c58 fix: CSRF issue #51 2024-01-18 03:12:25 -06:00
chreamy af088730e8 fix: issue #96 2024-01-17 22:02:39 -06:00
chreamy 4b9c7de031 fix: issue #96 2024-01-17 21:55:52 -06:00
chreamy 712a1ff319 test 2024-01-17 21:36:44 -06:00
Lin, Hung-Shin 21c1a4d11c Fix Build Quests Error (#94)
#92
2024-01-13 20:18:22 +08:00
dependabot[bot] d069233407 build(deps): bump follow-redirects from 1.15.2 to 1.15.4
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.2 to 1.15.4.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.2...v1.15.4)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-10 21:59:52 +00:00
Lin, Hung-Shin 4a4ce11bcc Analytics revise (#91)
Analytics revise
2023-12-21 18:13:53 +08:00
Chen, Tai-Lin 9c650a4c26 fix: restore db name 2023-12-21 18:09:48 +08:00
Chen, Tai-Lin b96bcb27ad refactor: remove unused functions 2023-12-21 18:01:25 +08:00
Chen, Tai-Lin b0e12b2027 refactor: use questList to calculate total quests and completed 2023-12-21 17:58:52 +08:00
Chen, Tai-Lin 3b68693177 feat: add pagination buttons 2023-12-21 17:48:36 +08:00
Chen, Tai-Lin fa36b63fbe feat: add order to pagination 2023-12-21 17:28:48 +08:00
Chen, Tai-Lin 68db16419d refactor: add Pagination class 2023-12-21 17:09:20 +08:00
Chen, Tai-Lin 19cd10ab7b fix: session pagination 2023-12-21 16:37:37 +08:00
Chen, Tai-Lin bc0055781e feat: finish errorCommands pagination 2023-12-21 16:31:34 +08:00
Chen, Tai-Lin 8c634c8115 wip: models/commands analytics 2023-12-20 15:05:10 +08:00
Lin, Hung-Shin 27c5794700 fix: no response when buildImage 2023-12-20 13:31:08 +08:00
Chen, Tai-Lin ceb9416fab update: mongoose version 2023-12-13 15:05:46 +08:00
Chen, Tai-Lin c6733814ba feat: add sorting order 2023-12-13 15:05:25 +08:00
Chen, Tai-Lin 1b94ae75ca feat: change to nextKey in users 2023-12-13 14:40:47 +08:00
Chen, Tai-Lin 3e7092dbd6 feat: modify analytics 2023-12-11 21:13:06 +08:00
Chen, Tai-Lin 61f627b125 feat: add quest page in analytics 2023-12-11 17:55:15 +08:00
Chen, Tai-Lin a578d4f910 fix: analytics completed rate 2023-12-11 01:09:06 +08:00
Lin, Hung-Shin 1bca61da40 feat: add mongo dump script, require ci checks before deployment (#89)
feat: add mongo dump script, require ci checks before deployment
2023-12-10 08:58:02 +08:00
Lin, Hung-Shin 660a7ce4e8 feat-automation-test (stabilise) (#90)
feat-automation-test (stabilise)
2023-12-10 08:57:18 +08:00
Chen, Tai-Lin ac31ca8659 feat: show analytics data page by page 2023-12-10 03:21:30 +08:00
Chen, Tai-Lin 9200336b49 fix: analytics modify 2023-12-10 02:21:02 +08:00
Chen, Tai-Lin c243e29002 fix: re-calculate completed rate in analytics 2023-12-10 01:25:27 +08:00
Annie Huang 175db7b8bc reduce timeout 2023-12-10 00:08:45 +08:00
Annie Huang 581e75ee57 Add configs retrying and longer timeout to load 2023-12-09 21:18:03 +08:00
Chen, Tai-Lin b625abae68 change docker compose file 2023-12-08 20:49:01 +08:00
dependabot[bot] b0bfbb789c build(deps-dev): bump vite from 4.4.4 to 4.4.12 (#88)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.4.4 to 4.4.12.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.4.12/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.4.12/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-06 15:27:17 +08:00
wancat 2ce277c7e5 fix: gpt-4-turbo not supported yet, rollback 2023-12-06 15:25:15 +08:00
wancat 7ef77267d3 fix: update to use main branch action 2023-12-06 15:21:32 +08:00
wancat d34ae3ef7a feat: change code review model to save budget 2023-12-06 15:17:13 +08:00
wancat bfa7ed1cf9 fix: require CI to be passed before deployment 2023-12-06 15:10:04 +08:00
wancat aa5088cc97 feat: add mongo dump and restore script 2023-12-06 15:07:46 +08:00
Annie Huang 0d0465f722 feat Automation Tests - Updates (#87)
- Change spec name
- Add spec and complete all quests
- Add Icon (totem version)
- Add some id for tests
- Add answer. sh for all quests

Not yet added verifying the buttons function and other pages
2023-12-05 00:46:18 +08:00
Annie Huang 360279f765 Add logo to nav bar 2023-12-05 00:21:15 +08:00
Annie Huang 6c73a8abc0 remove typing delay, add last (command) parameter
prevent pressing enter too quick
2023-12-04 17:21:13 +08:00
Annie Huang 55a0dcb443 edit typing delay 2023-12-04 16:51:01 +08:00
Annie Huang 5b3c52bd9b reduce typing delay and timeout 2023-12-04 16:45:25 +08:00
Annie Huang a911d953cd Add searching element timeout 2023-12-04 15:54:26 +08:00
Annie Huang c52b5a53cd extend typing delay 2023-12-04 15:41:53 +08:00
Annie Huang c9a43eeeb1 remove redundant class 2023-12-04 15:24:53 +08:00
Annie Huang 69624d7a5d Add logo to login/register page 2023-12-04 15:22:05 +08:00
Annie Huang 4681a743b4 update icon (previous one was boroken) 2023-12-04 14:43:59 +08:00
Annie Huang e0b3ec74de extend typing delay to prevent test fail 2023-12-04 14:05:21 +08:00
Annie Huang d0b179108a fix failing on finishing 2023-12-03 21:30:34 +08:00
Annie Huang 10ac3c720e Add check finished, typing delay 2023-12-03 19:16:02 +08:00
Annie Huang 7a3b6b1cfb - Add auto-complete quest command
- Change spec name
- Add spec complete all quests
- Add Icon (totem version)
- Add some id for tests
- Add answer.sh for all quests
2023-12-03 01:40:51 +08:00
307 changed files with 12216 additions and 6006 deletions

No files matched your search

+3 -2
View File
@@ -1,3 +1,4 @@
*/node_modules
**/node_modules
.git
*/dist
**/dist
**/tsconfig.tsbuildinfo
+4
View File
@@ -0,0 +1,4 @@
TESTING=true
TESTING_USERNAME=alex
TESTING_PASSWORD=Alex1234
SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6
+1 -1
View File
@@ -2,4 +2,4 @@ vite.config.js
.gitignore
data
mongo-init.js
app/dist
dist
+57 -5
View File
@@ -4,22 +4,33 @@ module.exports = {
node: true,
jest: true,
},
plugins: ['cypress'],
plugins: ['@typescript-eslint', 'cypress', 'vue'],
extends: [
'plugin:vue/vue3-essential',
'airbnb-base',
'plugin:prettier/recommended', // We added this
'plugin:cypress/recommended',
],
parser: 'vue-eslint-parser',
parserOptions: {
// parser: 'babel-eslint',
ecmaVersion: 13,
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
typescript: {},
},
vue: {
version: '3.0', // Explicitly specify Vue 3
},
},
rules: {
'no-console': 'warn',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'import/extensions': 'off',
camelcase: ['error', { properties: 'never', ignoreDestructuring: true }],
'no-param-reassign': ['error', { props: false }],
'no-underscore-dangle': ['error', { allow: ['_id'] }],
@@ -29,5 +40,46 @@ module.exports = {
'prettier/prettier': ['error', { endOfLine: 'auto', semi: false }],
'import/prefer-default-export': 'off',
'no-restricted-syntax': ['off', 'ForOfStatement'],
'lines-between-class-members': [
'error',
'always',
{ exceptAfterSingleLine: true },
],
'no-use-before-define': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
// Allow missing file extensions
'import/extensions': [
'warn',
'ignorePackages',
{
js: 'off',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
},
overrides: [
{
files: ['**/tests/**/*.js', '**/tests/**/*.ts', '**/*.test.ts'], // Adjust the pattern to match your test files
rules: {
'import/extensions': 'off', // Turn off the rule for test files
'import/no-extraneous-dependencies': 'off',
},
},
],
}
+3 -20
View File
@@ -27,28 +27,11 @@ jobs:
- name: Install dependencies
run: yarn install
- name: Build
run: yarn build
- name: Lint
run: yarn lint
- name: Test
run: yarn test
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Build Test Environment, Stage 1
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
- name: Build Test Quests
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
- name: Run Cypress Tests
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
run: docker-compose -f docker-compose.testing.yml run cypress
+24
View File
@@ -0,0 +1,24 @@
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v3
- name: Build Test Environment, Stage 1
run: docker-compose -f docker-compose.testing.yml build base backend app-test cypress
- name: Build Test Quests
run: docker-compose -f docker-compose.testing.yml run --rm backend yarn build:quests
- name: Run Cypress Tests
env:
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
run: docker-compose -f docker-compose.testing.yml run cypress
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
secrets: inherit
build:
# needs: ci-checks
needs: ci-checks
uses: ./.github/workflows/build.yml
secrets: inherit
-17
View File
@@ -12,23 +12,6 @@ jobs:
uses: ./.github/workflows/ci-checks.yml
secrets: inherit
code_review:
permissions:
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Code Review GPT
uses: mattzcarey/code-review-gpt@v0.1.4-alpha
with:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
MODEL: 'gpt-4'
GITHUB_TOKEN: ${{ github.token }}
build:
needs: ci-checks
uses: ./.github/workflows/build.yml
secrets: inherit
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
secrets: inherit
build:
# needs: ci-checks
needs: ci-checks
uses: ./.github/workflows/build.yml
secrets: inherit
+6
View File
@@ -52,6 +52,9 @@ web_modules/
# TypeScript cache
*.tsbuildinfo
# TypeScript build
built
# Optional npm cache directory
.npm
@@ -132,3 +135,6 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
#screenshots
tests/cypress/screenshots
+1 -1
View File
@@ -1,2 +1,2 @@
data/*
app/dist
dist
+2 -1
View File
@@ -1,4 +1,5 @@
{
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
+11 -4
View File
@@ -17,14 +17,21 @@
## Development
Run everything (frontend, backend, db, swagger, quests):
Run mongoDB first:
docker compose build
docker compose up -d
If you want to enable social login, you should have OAuth client token in a `.env` file:
The db will automatically restarts by Docker.
docker compose --env-file .env up -d
Build up dependencies:
yarn build:deps
Run dev server (frontend, backend):
yarn dev
If you want to enable social login, you should have OAuth client token in a `.env` file:
### Build Quest Images
+5 -2
View File
@@ -1,10 +1,12 @@
# Base stage
FROM node:20-alpine as base
WORKDIR /app
COPY package.json yarn.lock ./
COPY package.json yarn.lock tsconfig.json ./
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
COPY packages/constants/package.json ./packages/constants/package.json
COPY packages/models/package.json ./packages/models/package.json
COPY app/package.json ./app/package.json
RUN yarn install --frozen-lockfile
@@ -18,8 +20,9 @@ FROM base as build
COPY packages/file-graph ./packages/file-graph
COPY packages/utils ./packages/utils
COPY packages/constants ./packages/constants
COPY packages/models ./packages/models
COPY app ./app
RUN yarn build
RUN yarn workspace app build
# Production stage
FROM nginx:stable-alpine as prod
+2 -2
View File
@@ -2,12 +2,12 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/icon_totem.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Linux Odyssey</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+13 -7
View File
@@ -2,15 +2,15 @@
"name": "app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "API_TARGET=http://homesrv.dala-banana.ts.net:3000 vite",
"dev:local": "vite",
"dev": "vite",
"dev:docker": "API_TARGET=http://backend:3000 vite",
"build": "vite build",
"build": "vue-tsc -b && vite build",
"type-check": "vue-tsc -b",
"preview": "vite preview"
},
"dependencies": {
"@formbricks/js": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
@@ -18,11 +18,13 @@
"@fortawesome/vue-fontawesome": "^3.0.3",
"@linux-odyssey/constants": "*",
"@linux-odyssey/file-graph": "*",
"@linux-odyssey/models": "*",
"@linux-odyssey/utils": "*",
"@types/dompurify": "^3.0.5",
"axios": "^1.6.0",
"dompurify": "^3.0.6",
"echarts": "^5.4.3",
"marked": "^10.0.0",
"pinia": "^2.1.7",
"socket.io-client": "^4.7.1",
"validator": "^13.11.0",
"vue": "^3.3.4",
@@ -34,10 +36,14 @@
"xterm-addon-fit": "^0.7.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"@types/validator": "^13.11.9",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/compiler-sfc": "^3.3.13",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"vite": "^4.4.0"
"typescript": "^5.3.3",
"vite": "^4.5.3",
"vue-tsc": "^2.0.10"
}
}
@@ -1,4 +1,4 @@
export default {
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
</pattern>
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 630 KiB

+33 -62
View File
@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
@@ -27,7 +27,7 @@ const password = ref('')
const errorMessage = ref('')
const toast = useToast()
const availableMethods = ref({})
const availableMethods = ref<{ [key: string]: boolean }>({})
onMounted(async () => {
const res = await api.get('/auth/available-methods')
availableMethods.value = res.data
@@ -44,7 +44,7 @@ const success = () => {
router.push({ name: 'map' })
}
const error = (err) => {
const error = (err: string) => {
errorMessage.value = err
}
@@ -83,13 +83,28 @@ const handleChange = () => {
errorMessage.value = 'Something went wrong'
}
}
const registerGuest = async () => {
try {
await api.post('/auth/register-guest')
router.push({ name: 'map' })
} catch (err) {
console.error(err)
toast.error('Something went wrong.')
}
}
</script>
<template>
<!-- login page -->
<div
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl font-black mb-2">{{ title }}</h1>
<div class="w-full flex items-center justify-center mb-2 gap-3">
<img src="../img/icon_totem.svg" class="h-10" />
<h1 class="text-text-primary text-3xl font-black" id="title">
{{ title }}
</h1>
</div>
<div class="text-error inline-flex justify-center items-center md:hidden">
<font-awesome-icon
:icon="['fas', 'circle-exclamation']"
@@ -109,10 +124,7 @@ const handleChange = () => {
:icon="['fab', 'google']"
class="text-text-secondary px-3 text-lg"
/>
<span class="font-medium">
<!-- Continue with Google -->
以 Google 繼續
</span>
<span class="font-medium"> 以 Google 繼續 </span>
</a>
<a
id="GitHubLogin"
@@ -124,32 +136,17 @@ const handleChange = () => {
:icon="['fab', 'github']"
class="text-text-secondary px-3 text-lg"
/>
<span class="font-medium">
<!-- Continue with GitHub -->
以 GitHub 繼續
</span>
<span class="font-medium"> 以 GitHub 繼續 </span>
</a>
<p
id="or"
class="text-text-secondary flex justify-center mt-3"
v-if="type === 'login' || type === 'register'"
>
<!-- or -->
或
</p>
</div>
<div class="mb-6">
<!-- <input
type="text"
id="username"
ref="usernameInput"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
:placeholder="type === 'login' ? 'Email / Username' : 'Username'"
v-model="username"
required
@input="handleChange()"
autocomplete="username"
/> -->
<input
type="text"
id="username"
@@ -163,6 +160,7 @@ const handleChange = () => {
/>
<label
class="text-sm font-normal text-text-secondary"
id="NameRules"
v-if="type === 'username' || type === 'register'"
>
<p>可用小寫字母、數字、"_"和"-"</p>
@@ -170,17 +168,6 @@ const handleChange = () => {
</label>
</div>
<div class="mb-6" v-if="type === 'register'">
<!-- <input
type="email"
id="email"
ref="emailInput"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
placeholder="Email"
v-model="email"
required
@input="handleChange()"
autocomplete="email"
/> -->
<input
type="email"
id="email"
@@ -194,15 +181,6 @@ const handleChange = () => {
/>
</div>
<div class="mb-6" v-if="type === 'login' || type === 'register'">
<!-- <input
type="password"
id="password"
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
placeholder="Password"
v-model="password"
required
@input="handleChange()"
/> -->
<input
type="password"
id="password"
@@ -225,46 +203,39 @@ const handleChange = () => {
class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full"
type="submit"
>
<span v-if="type === 'login'">
<!-- Log In -->
登入
</span>
<span v-else>
<!-- Sign Up -->
註冊
</span>
<span v-if="type === 'login'"> 登入 </span>
<span v-else> 註冊 </span>
</button>
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
<span id="CheckRegistered" v-if="type === 'register'">
<!-- Already have an account? -->
已經有帳號了嗎?
<RouterLink
id="SwitchtoLogin"
class="text-text-primary font-bold"
to="/login"
>
<u>
<!-- Log in -->
登入
</u>
<u> 登入 </u>
</RouterLink></span
>
<span id="CheckRegistered" v-else-if="type === 'login'">
<!-- Don't have an account? -->
還沒有帳號嗎?
<RouterLink
id="SwitchtoRegister"
class="text-text-primary font-bold"
to="/register"
>
<u>
<!-- Sign up -->
註冊
</u>
<u> 註冊 </u>
</RouterLink></span
>
</p>
<button
id="register-guest"
class="rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
@click="registerGuest()"
>
以訪客身分繼續
</button>
</form>
</div>
</template>
+28
View File
@@ -0,0 +1,28 @@
<script setup lang="ts">
defineProps({
url: {
type: String,
required: true,
},
icons: {
type: Array,
required: true,
},
title: {
type: String,
required: true,
},
desciption: String,
})
</script>
<template>
<div class="flex flex-col">
<a :href="url" target="_blank" class="h-10 w-10" :title="desciption">
<font-awesome-icon
:icon="icons"
:alt="title"
class="text-text-primary h-10 delay-130 hover:-translate-y-1 hover:scale-110"
/>
</a>
</div>
</template>
-72
View File
@@ -1,72 +0,0 @@
<template>
<section class="h-[10%] p-3 grid grid-cols-4 gap-4 place-content-between">
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">
<!-- Survey -->
問卷
</p>
</a>
</button>
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled"
/>
<p class="text-text-disabled inline ml-1">
<!-- Solution -->
解答
</p>
</button>
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
<font-awesome-icon
:icon="['fas', 'arrow-rotate-left']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">
<!-- Reset -->
重來
</p>
</button>
<button
id="continue"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
<font-awesome-icon
:icon="['far', 'circle-right']"
class="text-text-primary"
/>
<p class="text-text-primary inline ml-1">
<!-- Continue -->
繼續
</p>
</button>
</section>
</template>
<script setup>
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { createSession } from '../store/session'
import { surveyUrl } from '../config'
const router = useRouter()
const reset = async () => {
console.log('resetting...')
await createSession()
}
const continuePlay = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// useToast().error('Failed to load map')
useToast().error('無法讀取地圖')
console.error(err)
}
}
</script>
+18 -7
View File
@@ -1,26 +1,34 @@
<script setup>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const cw = ref(0)
const ch = ref(0)
const fallingCharArr = ref([])
const fallingCharArr = ref<
{ value: string; x: number; y: number; speed: number }[]
>([])
const fontSize = ref(16)
const maxColums = ref(0)
const charArr = ['0', '1', '', '', '']
const ctx = ref(null)
const ctx = ref<CanvasRenderingContext2D | null>(null)
const randomInt = (min, max) => {
const randomInt = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min) + min)
}
const randomFloat = (min, max) => {
const randomFloat = (min: number, max: number) => {
return Math.random() * (max - min) + min
}
const draw = (point) => {
const draw = (point: {
value: string
x: number
y: number
speed: number
}) => {
point.value = charArr[randomInt(0, charArr.length)].toUpperCase()
if (point.y < 0) point.speed = randomFloat(10, 20)
if (ctx.value === null) return
ctx.value.fillStyle = '#0F0'
ctx.value.font = `${fontSize.value}px san-serif`
ctx.value.fillText(point.value, point.x, point.y)
@@ -33,6 +41,7 @@ const draw = (point) => {
}
const update = () => {
if (ctx.value === null) return
ctx.value.fillStyle = 'rgba(0,0,0,0.05)'
ctx.value.fillRect(0, 0, cw.value, ch.value)
@@ -48,7 +57,7 @@ onMounted(() => {
ch.value = document.documentElement.clientHeight
maxColums.value = cw.value / fontSize.value
const canvas = document.querySelector('canvas')
const canvas = document.querySelector('canvas') as HTMLCanvasElement
ctx.value = canvas.getContext('2d')
canvas.width = cw.value
@@ -58,6 +67,8 @@ onMounted(() => {
fallingCharArr.value.push({
x: i * fontSize.value,
y: randomFloat(-500, 0),
value: '',
speed: 0,
})
}
+16 -2
View File
@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
const props = defineProps({
node: {
type: Object,
@@ -20,7 +20,21 @@ function nodeClass() {
</script>
<template>
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
<a href="#" :class="nodeClass()">
<font-awesome-icon
v-if="node.type === 'folder'"
:icon="['far', 'folder']"
class="text-text-primary size-8"
/>
<font-awesome-icon
v-else
:icon="['far', 'file']"
class="text-text-primary size-8"
/>
<br />
{{ node.discovered ? node.name : '???' }}
{{ node.name === '' && node.discovered ? '/' : '' }}
</a>
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
<FileNode :node="child" :pwd="pwd" />
-160
View File
@@ -1,160 +0,0 @@
<script setup>
import { defineProps, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { logout } from '../utils/auth'
import { bugReportUrl, surveyUrl } from '../config'
import { reset } from '../store/session'
import userProfileStore, {
loadUserProfile,
resetUserProfile,
} from '../store/userProfile'
const router = useRouter()
const toast = useToast()
const handleLogout = async () => {
try {
await logout()
resetUserProfile()
reset()
router.push({ name: 'login' })
} catch (err) {
// toast.error('Failed to logout')
toast.error('登出失敗')
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: {
type: String,
required: true,
},
})
onMounted(async () => {
try {
await loadUserProfile()
} catch (err) {
// toast.error('Failed to load user profile')
toast.error('無法讀取使用者資料')
}
})
</script>
<template>
<div id="header" class="bg-bg-secondary content-center h-full w-full p-1">
<div class="h-full flex items-center justify-center mx-3">
<div class="flex items-center gap-3">
<!-- <button id="stream" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'bars']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="backward" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'arrow-left']"
class="text-border h-full w-full"
/>
</button> -->
<button
id="HeaderText"
class="text-text-primary inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
Linux Odyssey
</button>
<p
id="HeaderText"
class="text-text inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
{{ title }}
</p>
</div>
<div class="h-full w-full flex gap-3 items-center justify-end">
<!-- <button id="warning" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'triangle-exclamation']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="setting" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'gear']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="fullscreen" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'expand']"
class="text-text-primary h-full w-full"
/>
</button> -->
<p
id="UsernameText"
class="text-text inline-block whitespace-nowrap"
style="font-size: 2vh"
>
{{ userProfileStore.username }}
</p>
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary h-full w-full"
/>
</a>
<button title="LeaderBoard" @click="handleLeaderBoard" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'trophy']"
class="text-text-primary h-full w-full"
/>
</button>
<button title="Map" @click="handleMap" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'map']"
class="text-text-primary h-full w-full"
/>
</button>
<a
title="Bug Report"
:href="bugReportUrl"
target="_blank"
class="h-5 w-5"
>
<font-awesome-icon
:icon="['fas', 'bug']"
class="text-text-primary h-full w-full"
/>
</a>
<button title="Sign Out" @click="handleLogout" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'arrow-right-from-bracket']"
class="text-text-primary h-full w-full"
/>
</button>
</div>
</div>
</div>
</template>
-78
View File
@@ -1,78 +0,0 @@
<script setup>
import { ref, watch } from 'vue'
import sessionStore from '../store/session'
import MarkdownText from './MarkdownText.vue'
const current = ref(-1)
watch(
() => sessionStore.session.hints,
(hints) => {
current.value = hints.length - 1
},
{ deep: true }
)
const left = () => {
if (current.value > 0) {
current.value -= 1
}
}
const right = () => {
if (current.value < sessionStore.session.hints.length - 1) {
current.value += 1
}
}
</script>
<template>
<div class="w-full flex items-center p-2">
<font-awesome-icon
: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">
<!-- Hint -->
提示
</h1>
<div v-if="current !== -1" class="flex w-full justify-end items-end">
<button
class="p-2 m-1 w-1/8"
id="Lbutton"
@click="left"
:disabled="current <= 0"
>
<font-awesome-icon
:icon="['fas', 'arrow-right']"
class="text-text"
rotation="180"
/>
</button>
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
<p class="inline text-text font-xl w-1/14 p-2 m-1">
{{ sessionStore.session.hints.length }}
</p>
<button
class="p-2 m-1 w-1/8"
id="Rbutton"
@click="right"
:disabled="current === sessionStore.session.hints.length - 1"
>
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
</button>
</div>
</div>
<div id="hint" class="bg-bg flex flex-wrap p-8">
<ul>
<li
v-for="hint in sessionStore.session.hints[current]"
:key="hint"
class="text-text font-xl whitespace-pre-wrap"
>
<MarkdownText :content="hint" />
</li>
</ul>
<br />
</div>
</template>
+4 -1
View File
@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { computed, defineProps } from 'vue'
import markdown2HTML from '../utils/markdown'
@@ -25,6 +25,9 @@ const html = computed(() => {
line-height: 1;
}
.markdown-content :deep(p) {
line-height: 1.5;
}
.markdown-content :deep(ol) {
list-style: decimal;
padding-left: 1em;
+121
View File
@@ -0,0 +1,121 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref, defineProps, onMounted } from 'vue'
import { getQuest } from '../utils/api'
import MarkdownText from './MarkdownText.vue'
const props = defineProps({
questTitle: {
type: String,
required: true,
},
questId: {
type: String,
required: true,
},
questUnlocked: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['closeIntro'])
const showIntro = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const { questTitle, questId, questUnlocked } = props
const questInstruction = ref('')
const questColor = ref('')
const questTextColor = ref('')
const closeModal = () => {
showIntro.value = false
emit('closeIntro', true)
router.push({ name: 'map' })
}
const handleQuests = (id: string) => {
if (questUnlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
toast.warning('你還沒完成前一個關卡!')
}
}
const handleColour = () => {
if (questUnlocked) {
questColor.value = '#00ff00'
questTextColor.value = '#000000'
} else {
questColor.value = '#505050'
questTextColor.value = '#a0a0a0'
}
}
onMounted(async () => {
const quest = await getQuest(questId)
questInstruction.value = quest.instruction
handleColour()
})
</script>
<template>
<div v-if="showIntro" class="modal">
<div class="modal-content bg-bg-secondary rounded-lg">
<span class="close text-text content-between" @click="closeModal"
>&times;</span
>
<h2
id="QuestTitle"
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
>
{{ questTitle }}
</h2>
<div class="w-full h-3/5 p-8">
<div class="px-5 whitespace-pre-wrap">
<MarkdownText
class="text-text markdown-content"
:content="questInstruction"
/>
</div>
</div>
<a
id="StartAdventure"
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
>
</div>
</div>
</template>
<style scoped>
.modal {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.modal-content {
position: relative;
margin: 10% auto;
padding: 20px;
width: 60%;
height: 50%;
}
.close {
position: absolute;
top: 0;
right: 10px;
font-size: 30px;
cursor: pointer;
}
.markdown-content {
text-align: left;
}
</style>
+236 -173
View File
@@ -1,191 +1,254 @@
<!-- eslint-disable no-console -->
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue'
import { useToast } from 'vue-toastification'
import { use, init } from 'echarts/core'
import { GraphChart } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
import {
TitleComponent,
TooltipComponent,
DataZoomComponent,
} from 'echarts/components'
import { DAG } from '@linux-odyssey/utils'
import api from '../utils/api'
import { NodeImage } from '../img/svg.js'
import { getQuests } from '../utils/api'
import useUserProfile from '../store/userProfile'
import QuestIntro from './QuestIntro.vue'
const marginX = 500
const marginY = 300
const chartContainer = ref(null)
let chartInstance = null
const fullwidth = window.screen.width
use([
GraphChart,
SVGRenderer,
TitleComponent,
TooltipComponent,
DataZoomComponent,
])
async function getQuests() {
try {
const res = await api.get('/quests')
return res.data
} catch (err) {
console.error(err)
throw err
}
}
async function getProgress() {
try {
const res = await api.get('/users/me')
return res.data.progress
} catch (err) {
console.error(err)
throw err
}
}
const genOption = (nodes, edges) => ({
renderer: 'svg',
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
// dataZoom: [
// {
// type: 'slider', // This type means "slider data zoom" which provides a slider bar for zooming
// orient: 'vertical', // This makes the data zoom component vertical
// start: 0, // The starting percentage of the window out of the data extent
// end: 100, // The ending percentage of the window out of the data extent
// show: true,
// },
// ],
series: [
{
type: 'graph',
layout: 'none',
symbol: () => {
return NodeImage
},
symbolSize: [fullwidth / 13, fullwidth / 30],
roam: 'move',
zoom: 1.5,
center: ['120%', '30%'],
label: {
show: true,
fontSize: fullwidth / 90,
},
edgeSymbol: ['pin', 'arrow'],
edgeSymbolSize: [2, 15],
edgeLabel: {
fontSize: 25,
},
data: nodes,
// links: [],
links: edges,
lineStyle: {
width: 3,
color: 'gray',
opacity: 4,
curveness: 0.08,
},
itemStyle: {
color: (params) => {
const {
data: { completed, unlocked },
} = params
if (completed) {
return '#00ff00'
}
if (unlocked) {
return '#ADADB5'
}
return '#505050'
},
},
},
],
})
function getOption(quests, progress) {
const dag = new DAG(quests)
const nodes = dag.getNodes().map((node) => ({
id: node._id,
name: node.title,
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
x: marginY * node.layer * 2,
completed: progress[node._id]?.completed || false,
unlocked: node.requirements.every((req) => progress[req]?.completed),
}))
const lineAppearence = (node) => {
try {
const { unlocked } = node
return {
type: unlocked ? 'line' : 'dashed',
color: unlocked ? '#ADADB5' : '#454552',
}
} catch (err) {
console.error(err)
throw err
}
}
const edges = dag.getEdgesArray().map((edge) => ({
source: edge[0],
target: edge[1],
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
}))
return genOption(nodes, edges)
}
const router = useRouter()
const store = useUserProfile()
const toast = useToast()
function initChart(option) {
chartInstance = init(chartContainer.value)
chartInstance.setOption(option)
chartInstance.on('click', (params) => {
const {
data: { id, unlocked },
} = params
const svgWidth = ref(window.innerWidth)
const svgHeight = ref(window.innerHeight)
if (unlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
// toast.warning('You have not completed the previous quest yet!')
toast.warning('你還沒完成前一個關卡!')
}
})
const marginX = 120
const marginY = 120
const nodeWidth = 160
const nodeHeight = 50
const opened = ref<Node | null>(null)
type Node = {
id: string
title: string
x: number
y: number
index: number
completed: boolean
unlocked: boolean
}
type Edge = {
source: Node
target: Node
}
const graphData = ref<{ nodes: Node[]; edges: Edge[] }>({
nodes: [],
edges: [],
})
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const offsetX = ref(0)
const offsetY = ref(0)
function startDrag(event: MouseEvent) {
isDragging.value = true
dragStartX.value = event.clientX - offsetX.value
dragStartY.value = event.clientY - offsetY.value
}
function drag(event: MouseEvent) {
if (!isDragging.value) return
offsetX.value = event.clientX - dragStartX.value
offsetY.value = event.clientY - dragStartY.value
}
function endDrag() {
isDragging.value = false
}
async function computeGraphData() {
if (!store.progress) return
try {
const quests = await getQuests()
const dag = new DAG(quests)
const nodesValues = dag.getNodes()
const nodes = nodesValues.map((node) => ({
id: node._id,
title: node.title,
x: marginX * node.layer * 2,
y:
marginY * node.index -
(marginX * dag.getLayer(node._id)) / 2 +
svgHeight.value / 2,
completed: store.progress[node._id]?.completed || false,
unlocked: node.requirements.every(
(req: string) => store.progress[req]?.completed
),
index: node.index,
}))
const edges = dag
.getEdgesArray()
.map((edge) => ({
source: nodes.find((n) => n.id === edge[0]),
target: nodes.find((n) => n.id === edge[1]),
}))
.filter(
(edge): edge is Edge =>
edge.source !== undefined && edge.target !== undefined
)
graphData.value = { nodes, edges }
} catch (error) {
console.error('Error computing graph data:', error)
toast.error('Failed to load quest data')
}
}
function handleNodeClick(node: Node) {
opened.value = node
}
function closeIntro(close: boolean) {
if (close) {
opened.value = null
}
}
onMounted(async () => {
const quests = await getQuests()
const progress = await getProgress()
const option = getOption(quests, progress)
initChart(option)
await store.loadUserProfile()
await computeGraphData()
})
watch(() => store.progress, computeGraphData, { deep: true })
const curvedPath = 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 nodeStyle = computed(() => {
return (node: Node) => {
if (node.completed) return 'completed'
if (node.unlocked) return 'unlocked'
return 'locked'
}
})
const edgeStyle = computed(() => {
return (edge: Edge) => {
return edge.target.unlocked ? 'unlocked' : 'locked'
}
})
</script>
<template>
<div class="relative bg-black w-full h-full flex">
<img
src="../img/catelogbg.svg"
alt="bg"
class="p-2 stroke-2 scale-y-120 justify-center items-center"
/>
<h1
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
<div
class="flex place-content-center w-full bg-catelogbg bg-cover bg-scroll"
>
<!-- Get through your linux journey! -->
踏上你的Linux冒險之旅吧!
</h1>
<div class="flex flex-wrap absolute w-full h-full z-1">
<div ref="chartContainer" class="w-full h-full flex flex-wrap"></div>
<h1
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冒險之旅吧!
</h1>
<svg
:width="svgWidth"
:height="svgHeight"
class="absolute"
@mousedown="startDrag"
@mousemove="drag"
@mouseup="endDrag"
@mouseleave="endDrag"
>
<g :transform="`translate(${offsetX}, ${offsetY})`">
<g
v-for="edge in graphData.edges"
:key="`${edge.source.id}-${edge.target.id}`"
>
<path
:d="curvedPath(edge.source, edge.target)"
:class="['edge', edgeStyle(edge)]"
/>
</g>
<g
v-for="node in graphData.nodes"
:key="node.id"
@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>
<QuestIntro
:questTitle="opened.title"
:questId="opened.id"
:questUnlocked="opened.unlocked"
v-if="opened"
@close-intro="closeIntro"
/>
</div>
</div>
</template>
<style scoped>
svg {
overflow: visible;
}
.node {
&.completed {
fill: #6cf76c;
}
&.unlocked {
fill: #8c8c92;
}
&.locked {
fill: #505050;
}
}
.node-text {
&.completed {
fill: #1d1d1d;
}
&.unlocked {
fill: #ffffff;
}
&.locked {
fill: #a0a0a0;
}
}
.edge {
stroke-width: 3;
fill: none;
&.unlocked {
stroke: #adadb5;
stroke-dasharray: none;
}
&.locked {
stroke: #454552;
stroke-dasharray: 5, 5;
}
}
</style>
@@ -1,13 +1,7 @@
<template>
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
<h1 id="CommandBook" class="inline text-text-primary font-xl">
<!-- Command Cheatsheets -->
指令之書
</h1>
<h1 id="CommandBook" class="inline text-text-primary font-xl">指令之書</h1>
<div id="cheatsheets">
<p class="text-text-secondary">
<!-- --Command List-- -->
-- --指令列表-- --
</p>
<p class="text-text-secondary">-- --指令列表-- --</p>
</div>
</template>
@@ -1,17 +1,19 @@
<script setup>
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref } from 'vue'
import { Vue3Lottie } from 'vue3-lottie'
// import { surveyUrl } from '../config'
import CompleteAnimation from '../img/complete-animation.json'
import CompleteAnimation from '../../img/complete-animation.json'
const showModal = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const closeModal = () => {
showModal.value = false
}
const defineComponent = () => ({
components: {
Vue3Lottie,
@@ -22,11 +24,11 @@ const defineComponent = () => ({
}
},
})
const backtoMap = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
// useToast().error('Failed to load map')
toast.error('無法讀取地圖')
console.error(err)
}
@@ -47,7 +49,6 @@ setTimeout(() => {
id="QuestCompleted"
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
>
<!-- Quest Completed! -->
關卡完成!
</h2>
<div class="z-10 right-1/2 top-1/2" @animationend="defineComponent">
@@ -61,17 +62,6 @@ setTimeout(() => {
<p class="text-base text-text text-center">
接下來回到地圖,迎接新的挑戰吧!
</p>
<!-- <p class="text-base text-text">
您已成功完成挑戰,真的非常感謝您的參與!<br />
如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要!
</p> -->
<!-- <a
id="SurveyButton"
:href="surveyUrl"
target="_blank"
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
>填寫問卷</a
> -->
<a
id="BacktoMap"
target="_blank"
@@ -80,6 +70,21 @@ setTimeout(() => {
>回到地圖</a
>
</div>
<div style="position: relative; height: 100vh; overflow: auto">
<iframe
src="https://app.formbricks.com/s/clyffdm0c0be2wh6335clle0l"
frameborder="0"
style="
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: 0;
"
>
</iframe>
</div>
</div>
</template>
@@ -0,0 +1,70 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import useSession from '../../store/session'
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
const router = useRouter()
const sessionStore = useSession()
const reset = async () => {
console.log('resetting...')
await sessionStore.createSession()
}
const continuePlay = async () => {
try {
router.push({ name: 'map' })
} catch (err) {
useToast().error('無法讀取地圖')
console.error(err)
}
}
</script>
<template>
<section class="mt-2 flex flex-row gap-3 justify-center">
<button
id="survey"
class="bg-bg-secondary rounded-lg p-2"
@click="openQuestSurvey"
>
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline md:flex">問卷</p>
</button>
<button
title="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>
</button>
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled px-1"
/>
<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">
<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>
</button>
<button
id="continue"
class="bg-bg-secondary rounded-lg p-2"
@click="continuePlay"
>
<font-awesome-icon
:icon="['far', 'circle-right']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
</button>
</section>
</template>
@@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import useSession from '../../store/session'
import MarkdownText from '../MarkdownText.vue'
const sessionStore = useSession()
const current = ref(-1)
const length = computed(() => {
return sessionStore.session?.responses.length ?? 0
})
const disabled = computed(() => {
return current.value === length.value - 1
})
watch(length, () => {
current.value = length.value - 1
})
const left = () => {
if (current.value > 0) {
current.value -= 1
}
}
const right = () => {
if (current.value < length.value - 1) {
current.value += 1
}
}
</script>
<template>
<div class="h-full flex flex-col">
<div class="flex items-center p-2 h-10">
<font-awesome-icon
: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>
<div v-if="current !== -1" class="flex w-full justify-end items-end">
<button
class="p-2 m-1 w-1/8"
id="Lbutton"
@click="left"
:disabled="current <= 0"
>
<font-awesome-icon
:icon="['fas', 'arrow-right']"
class="text-text"
rotation="180"
/>
</button>
<p class="inline text-text font-xl w-1/14 p-2 m-1">{{ current + 1 }}</p>
<p class="inline text-text font-xl w-1/14 p-2 m-1">/</p>
<p class="inline text-text font-xl w-1/14 p-2 m-1">
{{ length }}
</p>
<button
class="p-2 m-1 w-1/8"
id="Rbutton"
@click="right"
:disabled="disabled"
>
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
</button>
</div>
</div>
<div id="hint" class="bg-bg p-8 overflow-y-auto">
<ul>
<li
v-for="response in sessionStore.session?.responses[current]"
:key="response.type"
class="text-text font-xl whitespace-pre-wrap"
>
<div v-for="content in response.content" :key="content">
<MarkdownText :content="content" />
</div>
</li>
<li
v-for="hint in sessionStore.session?.hints[current]"
:key="hint"
class="text-text-primary font-xl whitespace-pre-wrap"
>
<MarkdownText :content="hint" />
</li>
</ul>
<br />
</div>
</div>
</template>
@@ -1,10 +1,12 @@
<script setup>
<script setup lang="ts">
import { computed } from 'vue'
import MarkdownText from './MarkdownText.vue'
import sessionStore from '../store/session'
import MarkdownText from '../MarkdownText.vue'
import useSession from '../../store/session'
import { Task } from '../../types'
const sessionStore = useSession()
const tasks = computed(() => {
return sessionStore.session.tasks.map((task) => {
return sessionStore.session?.tasks.map((task: Task) => {
const prefix = task.completed ? '✓' : '➤'
const color = task.completed ? 'text-text-primary' : 'text-text'
return {
@@ -29,10 +31,7 @@ const tasks = computed(() => {
:content="sessionStore.quest.instruction"
/>
<br />
<p id="tasks" class="text-text">
<!-- Tasks: -->
任務:
</p>
<p id="tasks" class="text-text">任務:</p>
<ul v-if="sessionStore.session">
<li v-for="task in tasks" :key="task.id">
<MarkdownText :class="task.class" :content="task.content" />
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { computed } from 'vue'
import useSession from '../../store/session'
const toast = useToast()
const sessionStore = useSession()
const showCover = computed(() => sessionStore.session === null)
const startSession = async () => {
try {
await sessionStore.createSession()
} catch (err: any) {
console.error(err)
toast.error(err.message)
}
}
</script>
<template>
<div
v-if="showCover"
class="bg-bg-secondary h-full w-1/2 fixed bg-opacity-50 flex justify-center items-center"
>
<button
id="start"
class="bg-bg-secondary rounded-lg p-3"
@click="startSession"
>
<font-awesome-icon
:icon="['fas', 'play']"
class="text-text-primary h-full px-3"
/>
<p class="text-text-primary inline ml-1 md:flex">開始</p>
</button>
</div>
</template>
@@ -1,25 +1,23 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useTerminal } from '../../store/session'
const terminal = ref<HTMLElement | null>(null)
onMounted(() => {
const term = useTerminal()
if (!terminal.value) return
term.mount(terminal.value)
window.addEventListener('resize', () => term.resizeScreen())
})
</script>
<template>
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
<button id="Terminal" class="text-text h-[5%]">
<!-- Terminal -->
終端機
</button>
<button id="Terminal" class="text-text h-[5%]">終端機</button>
<div
id="terminal"
ref="terminal"
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
></div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
import { useTerminal } from '../store/session'
const terminal = ref(null)
onMounted(() => {
const term = useTerminal()
term.mount(terminal.value)
window.addEventListener('resize', () => term.resizeScreen())
})
</script>
@@ -1,6 +1,8 @@
<script setup>
import FileNode from './FileNode.vue'
import sessionStore from '../store/session.js'
<script setup lang="ts">
import FileNode from '../FileNode.vue'
import useSession from '../../store/session'
const sessionStore = useSession()
</script>
<template>
@@ -8,6 +10,7 @@ import sessionStore from '../store/session.js'
<ul>
<li>
<FileNode
v-if="sessionStore.session"
:node="sessionStore.session.graph"
:pwd="sessionStore.session.pwd"
/>
@@ -45,14 +48,14 @@ import sessionStore from '../store/session.js'
position: absolute;
top: 0;
right: 50%;
border-top: 1px solid #00ff00;
border-top: 2px solid #00ff00;
width: 50%;
height: 20px;
}
.tree li::after {
right: auto;
left: 50%;
border-left: 1px solid #00ff00;
border-left: 2px solid #00ff00;
}
.tree li:only-child::after,
@@ -70,7 +73,7 @@ import sessionStore from '../store/session.js'
}
.tree li:last-child::before {
border-right: 1px solid #00ff00;
border-right: 2px solid #00ff00;
border-radius: 0 5px 0 0;
-webkit-border-radius: 0 5px 0 0;
-moz-border-radius: 0 5px 0 0;
@@ -86,7 +89,7 @@ import sessionStore from '../store/session.js'
position: absolute;
top: 0;
left: 50%;
border-left: 1px solid #00ff00;
border-left: 2px solid #00ff00;
width: 0;
height: 20px;
}
@@ -97,7 +100,7 @@ import sessionStore from '../store/session.js'
text-decoration: none;
color: #ffffff;
font-family: arial, verdana, tahoma;
font-size: 11px;
font-size: 12px;
display: inline-block;
border-radius: 5px;
@@ -109,18 +112,10 @@ import sessionStore from '../store/session.js'
-moz-transition: all 0.5s;
}
a.discovered {
background: #526b4e;
color: #000;
border: 1px solid #94a0b4;
}
ul.discovered,
li.discovered {
border-color: #94a0b4;
}
a.pwd {
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
.tree li a.pwd {
border: 3px solid #00ff00;
background: #008080;
font-weight: bold;
font-size: 13px;
}
</style>
@@ -1,19 +1,21 @@
<script setup>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import TreeChart from './TreeChart.vue'
const treeRef = ref(null)
const treeRef = ref<HTMLElement | null>(null)
let pos = { top: 0, left: 0, x: 0, y: 0 }
function mouseMoveHandler(e) {
function mouseMoveHandler(e: MouseEvent) {
const dx = e.clientX - pos.x
const dy = e.clientY - pos.y
if (treeRef.value === null) return
treeRef.value.scrollTop = pos.top - dy
treeRef.value.scrollLeft = pos.left - dx
}
function mouseUpHandler() {
if (treeRef.value === null) return
treeRef.value.style.cursor = 'grab'
treeRef.value.style.removeProperty('user-select')
@@ -21,7 +23,8 @@ function mouseUpHandler() {
document.removeEventListener('mouseup', mouseUpHandler)
}
function mouseDownHandler(e) {
function mouseDownHandler(e: MouseEvent) {
if (treeRef.value === null) return
treeRef.value.style.cursor = 'grabbing'
treeRef.value.style.userSelect = 'none'
@@ -37,6 +40,7 @@ function mouseDownHandler(e) {
}
onMounted(() => {
if (treeRef.value === null) return
treeRef.value.style.cursor = 'grab'
treeRef.value.addEventListener('mousedown', mouseDownHandler)
})
@@ -0,0 +1,107 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { logout } from '../../utils/auth'
import { surveyUrl } from '../../config'
import { openBugReport } from '../../utils/formbricks'
import useSession from '../../store/session'
import useUserProfile from '../../store/userProfile'
const userStore = useUserProfile()
const sessionStore = useSession()
const router = useRouter()
const toast = useToast()
const handleLogout = async () => {
try {
await logout()
userStore.resetUserProfile()
sessionStore.reset()
router.push({ name: 'login' })
} catch (err) {
toast.error('登出失敗')
console.error(err)
}
}
onMounted(async () => {
try {
await userStore.loadUserProfile()
} catch (err) {
toast.error('無法讀取使用者資料')
console.error(err)
}
})
</script>
<template>
<!-- <button id="warning" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'triangle-exclamation']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="setting" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'gear']"
class="text-text-primary h-full w-full"
/>
</button>
<button id="fullscreen" class="flex items-center h-5 w-5">
<font-awesome-icon
:icon="['fas', 'expand']"
class="text-text-primary h-full w-full"
/>
</button> -->
<p
id="UsernameText"
class="text-text inline-block whitespace-nowrap px-1.5"
style="font-size: 2vh"
>
{{ userStore.username }}
</p>
<a
title="Survey"
:href="surveyUrl"
target="_blank"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary inline"
/>
<span class="text-text sm:hidden px-2">Survey</span>
</a>
<RouterLink
title="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">
<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"
@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"
@click="handleLogout"
class="md:h-5 md:w-5 px-1.5 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>
+66
View File
@@ -0,0 +1,66 @@
<script setup lang="ts">
import { defineProps, ref } from 'vue'
defineProps({
title: {
type: String,
required: false,
},
headerComponent: {
type: Object,
required: false,
},
})
const menuOpen = ref(false)
const windowWidth = window.innerWidth < 768
const toggleMenu = () => {
menuOpen.value = !menuOpen.value
}
</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"
>
<div class="flex flex-row items-center gap-3" :class="{ hidden: menuOpen }">
<img src="../../img/icon_totem.svg" class="h-8 justify-self-start" />
<RouterLink
to="/"
id="GameTitle"
class="text-text-primary inline-block font-bold whitespace-nowrap pt-1 transition ease-in-out hover:scale-110 duration-300"
style="font-size: 2vh"
>
Linux Odyssey
</RouterLink>
<p
id="QuestTitle"
class="text-text inline-block font-bold whitespace-nowrap"
style="font-size: 2vh"
>
{{ title }}
</p>
</div>
<div
class="w-full flex flex-col items-center justify-end sm:w-auto sm:block sm:ml-6"
:class="{ hidden: !menuOpen }"
>
<component :is="headerComponent" />
</div>
<div
v-if="windowWidth"
:class="{ 'w-full flex justify-end pb-3': menuOpen }"
>
<button
type="button"
class="flex items-center h-5 w-5 rounded-md sm:hidden"
aria-controls="mobile-menu"
aria-expanded="false"
@click="toggleMenu"
>
<font-awesome-icon
:icon="['fas', 'bars']"
class="text-text-primary h-full w-full"
/>
</button>
</div>
</div>
</template>
@@ -0,0 +1,62 @@
<script setup lang="ts">
import { bugReportUrl } from '../../config'
const scrollToSection = (sectionId: string) => {
const section = document.getElementById(sectionId)
if (section) {
const container = document.querySelector('.overflow-y-auto') as HTMLElement // Replace '.overflow-y-auto' with the appropriate class or selector for your container
if (container) {
container.scrollTo({
top: section.offsetTop - container.offsetTop,
behavior: 'smooth', // Add smooth scrolling behavior if desired
})
}
}
}
</script>
<template>
<button
id="Intro"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('intro')"
style="font-size: 2vh"
>
前言
</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
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"
>
關於
</button>
<button
id="Contact"
class="text-text inline-block font-bold whitespace-nowrap pt-1 px-2 hover:text-text-primary"
@click="scrollToSection('contact')"
style="font-size: 2vh"
>
聯絡我們
</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"
>
登入
</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>
</template>
-7
View File
@@ -1,7 +0,0 @@
export const surveyUrl = '/api/v1/survey'
export const bugReportUrl = '/api/v1/bug-report'
export default {
surveyUrl,
bugReportUrl,
}
+4
View File
@@ -0,0 +1,4 @@
export const surveyUrl = '/api/v1/survey'
export const bugReportUrl = '/api/v1/bug-report'
export const facebookUrl = 'https://www.facebook.com/LinuxOdyssey'
export const githubUrl = 'https://github.com/linux-odyssey/linux-odyssey'
+9
View File
@@ -0,0 +1,9 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H512V512H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_426_5" transform="scale(0.00195312)"/>
</pattern>
<image id="image0_426_5" width="512" height="512" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 32 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H512V512H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_424_19" transform="scale(0.00195312)"/>
</pattern>
<image id="image0_424_19" width="512" height="512" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 35 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="474" height="475" viewBox="0 0 474 475" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H474V475H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_426_3" transform="matrix(0.00215972 0 0 0.00215517 -0.00105485 0)"/>
</pattern>
<image id="image0_426_3" width="464" height="464" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 34 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
</pattern>
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 630 KiB

File renamed without changes.
+38 -25
View File
@@ -3,49 +3,62 @@ import { library } from '@fortawesome/fontawesome-svg-core'
/* import specific icons */
import {
faBars,
faArrowLeft,
faList,
faGear,
faTriangleExclamation,
faExpand,
faTerminal,
faArrowRotateLeft,
faArrowRightFromBracket,
faArrowRight,
faArrowRightFromBracket,
faArrowRotateLeft,
faBars,
faBug,
faCircleExclamation,
faMap,
faTrophy,
faEnvelope,
faExpand,
faFileInvoice,
faGear,
faList,
faMap,
faPlay,
faTerminal,
faTriangleExclamation,
faTrophy,
} from '@fortawesome/free-solid-svg-icons'
import {
faLightbulb,
faCircleQuestion,
faCircleRight,
faFile,
faFolder,
faLightbulb,
} 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(
faBars,
faArrowLeft,
faList,
faGear,
faTriangleExclamation,
faExpand,
faTerminal,
faArrowRotateLeft,
faArrowRightFromBracket,
faArrowRight,
faArrowRightFromBracket,
faArrowRotateLeft,
faBars,
faBug,
faMap,
faTrophy,
faCircleExclamation,
faLightbulb,
faEnvelope,
faExpand,
faFileInvoice,
faGear,
faList,
faMap,
faPlay,
faTerminal,
faTriangleExclamation,
faTrophy,
faCircleQuestion,
faCircleRight,
faFile,
faFolder,
faLightbulb,
faFacebook,
faGithub,
faGoogle,
faFileInvoice
faGoogle
)
+11 -1
View File
@@ -1,4 +1,6 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './style.css'
/* import font awesome icon component */
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
@@ -8,16 +10,24 @@ import Toast, { POSITION } from 'vue-toastification'
/* import Vue Lottie */
import Vue3Lottie from 'vue3-lottie'
import 'vue-toastification/dist/index.css'
import App from './App.vue'
import router from './router'
import formbricks from './utils/formbricks'
const app = createApp(App)
const pinia = createPinia()
app.component('font-awesome-icon', FontAwesomeIcon)
app.use(pinia)
app.use(router)
router.afterEach(() => {
if (typeof formbricks !== 'undefined') {
formbricks.registerRouteChange()
}
})
const options = {
position: POSITION.TOP_RIGHT,
}
+4 -5
View File
@@ -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',
@@ -58,7 +57,7 @@ const router = createRouter({
routes,
})
router.beforeEach(async (to, from, next) => {
router.beforeEach(async (to, _from, next) => {
const loggedIn = await isLoggedIn()
// Check if the route requires authentication
if (to.meta.requiresAuth && !loggedIn) {
-151
View File
@@ -1,151 +0,0 @@
import { reactive } from 'vue'
import { useToast } from 'vue-toastification'
import { FileGraph } from '@linux-odyssey/file-graph'
import api from '../utils/api'
import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal'
import { LoadQuestError, LoadSessionError } from '../utils/errors'
const toast = useToast()
function newSession() {
const graph = new FileGraph({
path: '/',
type: 'folder',
discovered: false,
})
return {
status: 'inactive',
graph,
pwd: '',
hints: [],
tasks: [],
}
}
const store = reactive({
session: newSession(),
questId: '',
quest: null,
})
const socket = new Socket()
const term = new SocketTerminal(40, 80)
function setQuest(questId) {
return api.get(`/quests/${questId}`).then((res) => {
store.questId = res.data._id
store.quest = res.data
})
}
async function setSession(session) {
try {
store.session = session
store.session.graph = new FileGraph(session.graph)
term.reset()
await socket.connect(session)
term.focus()
} catch (err) {
console.error(err)
toast.error(err.message)
throw err
}
}
export async function createSession() {
try {
console.log('Creating a new session...')
const res = await api.post('/sessions', { questId: store.questId })
const { data } = res
await setSession(data)
} catch (err) {
console.error(err)
toast.error('Failed to create a new session. Please try again later.')
}
}
async function getActiveSession(questId) {
try {
const res = await api.post('/sessions/active', { questId })
await setSession(res.data)
} catch (err) {
console.error(err)
toast.warning('Failed to connect previous session. Creating a new one...')
await createSession()
}
}
function updateGraph(event) {
if (event.discover) {
store.session.graph.discover(event.discover)
}
if (event.pwd) {
store.session.pwd = event.pwd
}
}
function updateHints(hints) {
store.session.hints.push(hints)
}
function updateStatus(status) {
store.session.status = status
}
function setTasks(tasks) {
store.session.tasks = tasks
}
export function useTerminal() {
return term
}
export function reset() {
socket.reset()
term.reset()
store.session = newSession()
store.quest = null
}
export async function init(questId) {
if (!questId) throw new Error('No quest ID provided')
reset()
try {
await setQuest(questId)
} catch (err) {
console.error(err)
throw new LoadQuestError('Failed to load quest', questId)
}
try {
await getActiveSession(questId)
} catch (err) {
console.error(err)
throw new LoadSessionError('Failed to load session', questId)
}
}
function setup() {
socket.on('terminal', (data) => {
term.write(data)
})
term.onData((data) => {
socket.emit('terminal', data)
})
socket.on('graph', (event) => {
updateGraph(event)
})
socket.on('hints', (event) => {
updateHints(event)
})
socket.on('tasks', (tasks) => {
setTasks(tasks)
})
socket.on('status', (event) => {
updateStatus(event)
})
}
setup()
export default store
+105
View File
@@ -0,0 +1,105 @@
import { defineStore } from 'pinia'
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
import { IQuest } from '@linux-odyssey/models'
import { createSession, getActiveSession, getQuest } from '../utils/api'
import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal'
import { Session, StageResponse } from '../types'
const socket = new Socket()
const term = new SocketTerminal(40, 80)
let hasSetup = false
interface Store {
session: Session | null
questId: string
quest: IQuest | null
}
const useSession = defineStore('session', {
state: (): Store => ({
session: null,
questId: '',
quest: null,
}),
actions: {
async setQuest(questId: string) {
this.quest = await getQuest(questId)
this.questId = questId
},
async setSession(session: Session) {
this.session = session
this.session.graph = new FileGraph(session.graph)
term.reset()
await socket.connect(session)
term.focus()
},
async createSession() {
const session = await createSession(this.questId)
await this.setSession(session)
socket.emit('terminal', 'echo start\n')
},
async getActiveSession() {
const session = await getActiveSession(this.questId)
if (session) {
this.setSession(session)
}
},
updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
if (!this.session) return
if (event.discover) {
this.session.graph.discover(event.discover)
}
if (event.pwd) {
this.session.pwd = event.pwd
}
},
newResponse(response: StageResponse) {
if (!this.session) return
this.session.responses.push(response.responses)
this.session.hints.push(response.hints)
if (response.tasks) {
this.session.tasks = response.tasks
}
if (
response.status === 'finished' &&
this.session.status !== 'finished'
) {
this.finish()
}
this.session.status = response.status
},
finish() {},
reset() {
socket.reset()
term.reset()
this.$reset()
},
setup() {
if (hasSetup) return
socket.on('terminal', (data: string) => {
term.write(data)
})
term.onData((data: string) => {
socket.emit('terminal', data)
})
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
this.updateGraph(event)
})
socket.on('response', (response: StageResponse) => {
this.newResponse(response)
})
hasSetup = true
},
setStatus(status: string) {
if (!this.session) return
this.session.status = status
},
},
})
export function useTerminal() {
return term
}
export default useSession
-30
View File
@@ -1,30 +0,0 @@
import { reactive } from 'vue'
import api from '../utils/api'
const store = reactive({
username: '',
email: '',
progress: {},
})
export async function loadUserProfile() {
try {
const res = await api.get('/users/me')
const { username, email, progress } = res.data
store.username = username
store.email = email
store.progress = progress
return store
} catch (err) {
console.error(err)
throw new Error(`Failed to load user profile: ${err.message}`)
}
}
export function resetUserProfile() {
store.username = ''
store.email = ''
store.progress = {}
}
export default store
+35
View File
@@ -0,0 +1,35 @@
import { defineStore } from 'pinia'
import { getUserProfile } from '../utils/api'
interface IProgress {
quest: string
sessions: string[]
completed: boolean
startedAt: Date
finishedAt?: Date
}
export interface UserProfile {
username: string
email: string
progress: Record<string, IProgress>
}
const useUserProfile = defineStore('userProfile', {
state: (): UserProfile => ({
username: '',
email: '',
progress: {},
}),
actions: {
async loadUserProfile() {
const res = await getUserProfile()
this.$patch(res)
},
async resetUserProfile() {
this.$reset()
},
},
})
export default useUserProfile
+26
View File
@@ -0,0 +1,26 @@
import { FileGraph } from '@linux-odyssey/file-graph'
// eslint-disable-next-line import/no-relative-packages
import { IResponse } from '@linux-odyssey/models'
export interface Task {
id: number
name: string
completed: boolean
}
export interface StageResponse {
status: string
hints: string[]
tasks: Task[]
responses: IResponse[]
}
export interface Session {
_id: string
status: string
graph: FileGraph
pwd: string
hints: string[][]
tasks: Task[]
responses: IResponse[][]
}
-11
View File
@@ -1,11 +0,0 @@
import axios from 'axios'
const api = axios.create({
baseURL: '/api/v1',
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
})
export default api
+50
View File
@@ -0,0 +1,50 @@
import axios from 'axios'
import { IQuest } from '@linux-odyssey/models'
import { GetSessionsRequest } from '@linux-odyssey/constants'
import { Session } from '../types'
import { UserProfile } from '../store/userProfile'
const api = axios.create({
baseURL: '/api/v1',
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
})
export default api
export async function getUserProfile(): Promise<UserProfile> {
const res = await api.get<UserProfile>('/users/me')
return res.data
}
export async function getQuest(id: string): Promise<IQuest> {
const res = await api.get<IQuest>(`/quests/${id}`)
return res.data
}
export async function createSession(questId: string): Promise<Session> {
const res = await api.post<Session>('/sessions', { questId })
return res.data
}
export async function getSessions(
request: GetSessionsRequest
): Promise<Session[]> {
const res = await api.get<Session[]>('/sessions', { params: request })
return res.data
}
export async function getActiveSession(
questId: string
): Promise<Session | null> {
const res = await api.get<Session | null>('/sessions/active', {
params: { questId },
})
return res.data
}
export async function getQuests(): Promise<IQuest[]> {
const res = await api.get<IQuest[]>('/quests')
return res.data
}
@@ -12,7 +12,12 @@ export async function isLoggedIn() {
return loggedIn
}
function handleRequestError(err) {
function handleRequestError(err: {
response: {
data?: any
status?: number
}
}) {
if (!err.response) throw err // Not an axios error
const { status } = err.response
if (status === 429) {
@@ -41,14 +46,14 @@ function handleRequestError(err) {
throw err
}
export const login = (username, password) => {
export const login = (username: string, password: string) => {
return api
.post('/auth/login', { username, password })
.then(() => true)
.catch(handleRequestError)
}
export const register = (username, email, password) => {
export const register = (username: string, email: string, password: string) => {
return api
.post('/auth/register', {
username,
@@ -59,7 +64,7 @@ export const register = (username, email, password) => {
.catch(handleRequestError)
}
export const checkUsername = async (username) => {
export const checkUsername = async (username: string) => {
return api
.get('/auth/check-username', {
params: { username },
@@ -68,7 +73,7 @@ export const checkUsername = async (username) => {
.catch(handleRequestError)
}
export const chooseUsername = async (username) => {
export const chooseUsername = async (username: string) => {
return api
.post('/auth/register-from-session', { username })
.then(() => true)
@@ -1,27 +1,28 @@
/* eslint-disable max-classes-per-file */
export class TooManyRequestsError extends Error {
constructor(message) {
constructor(message: string) {
super(message)
this.name = 'TooManyRequestsError'
}
}
export class ValidationError extends Error {
constructor(message) {
constructor(message: string) {
super(message)
this.name = 'ValidationError'
}
}
export class UnauthorizedError extends Error {
constructor(message) {
constructor(message: string) {
super(message)
this.name = 'UnauthorizedError'
}
}
export class LoadQuestError extends Error {
constructor(message, questId) {
questId: string
constructor(message: string, questId: string) {
super(message)
this.name = 'LoadQuestError'
this.questId = questId
@@ -29,7 +30,8 @@ export class LoadQuestError extends Error {
}
export class LoadSessionError extends Error {
constructor(message, questId) {
questId: string
constructor(message: string, questId: string) {
super(message)
this.name = 'LoadSessionError'
this.questId = questId
+40
View File
@@ -0,0 +1,40 @@
import formbricks from '@formbricks/js/website'
import useSession from '../store/session'
import useUserProfile from '../store/userProfile'
if (typeof window !== 'undefined') {
formbricks.init({
environmentId: 'clzcdqqn0000kadvtqykdo0pc',
apiHost: 'https://app.formbricks.com',
})
}
export function openQuestSurvey() {
const sessionStore = useSession()
const userStore = useUserProfile()
formbricks.track('quest_completed', {
hiddenFields: {
quest: sessionStore.questId,
session: sessionStore.session?._id,
username: userStore.username,
email: userStore.email,
},
})
}
export function openBugReport() {
const sessionStore = useSession()
const userStore = useUserProfile()
formbricks.track('bug_report', {
hiddenFields: {
quest: sessionStore.questId,
session: sessionStore.session?._id,
username: userStore.username,
email: userStore.email,
},
})
}
export default formbricks
@@ -1,7 +1,7 @@
import { marked } from 'marked'
import DOMPurify from 'dompurify'
export default function markdown2HTML(markdown) {
export default function markdown2HTML(markdown: string) {
const html = marked.parse(markdown)
return DOMPurify.sanitize(html)
}
@@ -1,13 +1,18 @@
import io from 'socket.io-client'
import { io, Socket } from 'socket.io-client'
import { Session } from '../types'
type SocketCallback = (data: any) => void
class SocketWrapper {
socket: Socket | null
listeners: { event: string; callback: SocketCallback }[]
constructor() {
this.socket = null
this.listeners = []
}
connect(session) {
return new Promise((resolve, reject) => {
connect(session: Session) {
return new Promise<void>((resolve, reject) => {
this.disconnect()
this.socket = io('', {
@@ -28,7 +33,7 @@ class SocketWrapper {
})
}
on(event, callback) {
on(event: string, callback: SocketCallback) {
this.listeners.push({ event, callback })
if (this.socket) {
this.socket.on(event, callback)
@@ -36,12 +41,14 @@ class SocketWrapper {
}
bindListeners() {
this.listeners.forEach(({ event, callback }) => {
this.socket.on(event, callback)
})
this.listeners.forEach(
({ event, callback }: { event: string; callback: SocketCallback }) => {
if (this.socket !== null) this.socket.on(event, callback)
}
)
}
emit(event, data) {
emit(event: string, data: any) {
if (!this.socket) {
console.warn('Socket is not connected!')
return
@@ -3,9 +3,11 @@ import { FitAddon } from 'xterm-addon-fit'
import 'xterm/css/xterm.css'
class SocketTerminal {
constructor(rows, cols) {
private term: Terminal
private fitAddon: FitAddon
constructor(rows: number, cols: number) {
this.term = new Terminal({
rendererType: 'canvas', // 渲染类型
rows, // 行数
cols, // 不指定行数,自动回车后光标从下一行开始
convertEol: true, // 启用时,光标将设置为下一行的开头
@@ -13,9 +15,11 @@ class SocketTerminal {
disableStdin: false, // 是否应禁用输入
// cursorStyle: "underline", //光标样式
cursorBlink: true, // 光标闪烁
foreground: '#ECECEC', // 字体
background: '#000000', // 背景色
cursor: 'help', // 设置光标
theme: {
foreground: '#ECECEC', // 字体
background: '#000000', // 背景色
cursor: 'help', // 设置光标
},
fontSize: 18,
fontWeight: 'normal',
})
@@ -24,29 +28,28 @@ class SocketTerminal {
this.fitAddon.fit()
}
write(message) {
write(message: string): void {
this.term.write(message)
}
onData(callback) {
onData(callback: any): void {
this.term.onData(callback)
}
focus() {
focus(): void {
this.term.focus()
}
resizeScreen() {
resizeScreen(): void {
this.fitAddon.fit()
}
mount(reference) {
mount(reference: HTMLElement): void {
this.term.open(reference)
this.term.focus()
this.resizeScreen()
}
reset() {
reset(): void {
this.term.reset()
}
}
+20 -8
View File
@@ -1,14 +1,20 @@
<script setup>
<script setup lang="ts">
import { isValidUsername } from '@linux-odyssey/utils'
import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue'
import { TooManyRequestsError, ValidationError } from '../utils/errors'
import { checkUsername, chooseUsername } from '../utils/auth'
async function check({ username, error }) {
async function check({
username,
error,
}: {
username: string
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
if (username) {
if (!isValidUsername(username)) {
// error('Invalid username.')
error('無效的帳號名稱')
return
}
@@ -16,7 +22,6 @@ async function check({ username, error }) {
await checkUsername(username)
} catch (err) {
if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
@@ -24,19 +29,26 @@ async function check({ username, error }) {
error(err.message)
return
}
// error('Something went wrong. Please try again later.')
error('出了點問題,請再試一次。')
}
}
}
async function handleSubmit({ username, success, error }) {
async function handleSubmit({
username,
success,
error,
}: {
username: string
success: () => void
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
try {
await chooseUsername(username)
success()
} catch (err) {
} catch (err: any) {
if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
+44 -45
View File
@@ -1,19 +1,30 @@
<script setup>
import { computed, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import sessionStore, { init } from '../store/session'
import GameHeaderPart from '../components/GameHeaderPart.vue'
// import CommandlistPart from '../components/CommandlistPart.vue'
import HintPart from '../components/HintPart.vue'
import QuestPart from '../components/QuestPart.vue'
import TerminalPart from '../components/TerminalPart.vue'
import VisualizationPart from '../components/VisualizationPart.vue'
import ControlPalette from '../components/ControlPalette.vue'
import CompleteModal from '../components/CompleteModal.vue'
import { LoadQuestError, LoadSessionError } from '../utils/errors'
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue'
import useSession from '../store/session'
import useUserProfile from '../store/userProfile'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
import DescriptionPart from '../components/game/DescriptionPart.vue'
import QuestPart from '../components/game/QuestPart.vue'
import TerminalPart from '../components/game/TerminalPart.vue'
import VisualizationPart from '../components/game/VisualizationPart.vue'
import ControlPalette from '../components/game/ControlPalette.vue'
import CompleteModal from '../components/game/CompleteModal.vue'
import StartButton from '../components/game/StartButton.vue'
import { openQuestSurvey } from '../utils/formbricks'
const sessionStore = useSession()
const userStore = useUserProfile()
const completed = computed(() => {
return sessionStore.session.status === 'finished'
return sessionStore.session?.status === 'finished'
})
watch(completed, (newValue, oldValue) => {
if (newValue && !oldValue) {
console.log('finish', userStore)
openQuestSurvey()
}
})
const props = defineProps({
@@ -23,45 +34,32 @@ const props = defineProps({
},
})
const toast = useToast()
onMounted(async () => {
try {
await init(props.questId)
} catch (err) {
if (err instanceof LoadQuestError) {
// toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
return
}
if (err instanceof LoadSessionError) {
// toast.error(
// `Failed to create session for quest: ${err.questId}. Please re-login and try again.`
// )
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
return
}
console.error(err)
toast.error(err.message)
}
sessionStore.reset()
sessionStore.setup()
await sessionStore.setQuest(props.questId)
await sessionStore.getActiveSession()
})
</script>
<template>
<!-- game header -->
<div class="h-[6vh] w-full">
<GameHeaderPart :title="sessionStore.quest?.title" />
<div class="w-full">
<HeaderPart
:title="sessionStore.quest?.title"
:headerComponent="GameHeaderComponents"
/>
</div>
<!-- current status indicator -->
<input
type="hidden"
id="currentStatus"
:value="sessionStore.session.status"
:value="sessionStore.session?.status"
/>
<!-- main -->
<div id="main" class="h-[94vh] w-full flex p-3 space-x-3">
<div id="main" class="h-full pt-10 w-full flex p-3 space-x-3">
<!-- Topic and Command List -->
<div class="bg-bg-secondary h-full w-1/3 rounded-lg">
<div class="bg-bg-secondary h-full w-1/4 rounded-lg">
<section id="quest" class="h-full p-3 overflow-auto">
<QuestPart />
</section>
@@ -71,17 +69,18 @@ onMounted(async () => {
</section> -->
</div>
<!-- Terminal and Hint -->
<div class="bg-bg h-full w-1/3 rounded-lg">
<section id="terminal" class="h-3/5 overflow-hidden">
<TerminalPart />
<div class="bg-bg h-full rounded-lg w-1/2">
<StartButton :questId="$props.questId" />
<section id="hint" class="h-3/5">
<DescriptionPart />
</section>
<hr class="border-border border" />
<section id="hint" class="h-2/5">
<HintPart />
<section id="terminal" class="h-2/5 overflow-hidden">
<TerminalPart />
</section>
</div>
<!-- Visualization -->
<div class="h-full w-1/3">
<div class="h-full w-1/4">
<div class="bg-border rounded-xl h-[90%] p-2">
<div class="bg-bg rounded-lg h-full p-2">
<section id="visualization" class="bg-bg-primary h-full rounded-md">
+225
View File
@@ -0,0 +1,225 @@
<script setup lang="ts">
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'
</script>
<template>
<div class="w-screen h-screen bg-scroll bg-bg">
<DynamicBackground class="w-full h-full" />
<div class="h-screen w-screen absolute top-0 left-0">
<!-- game header -->
<HeaderPart :headerComponent="LandingHeaderComponents" class="z-50" />
<!-- main content -->
<main class="w-full h-full overflow-y-auto">
<div class="w-full flex flex-col items-center">
<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>
<img src="../img/icon_totem.svg" class="h-[30vh]" />
<h1 class="text-text-primary text-7xl font-extrabold font-mono">
Odyssey
</h1>
</div>
<!-- intro -->
<div class="w-full md:w-2/3 px-5">
<section
id="intro"
class="text-text grid grid-cols-1 gap-3 justify-items-center"
>
<h1
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 互動教學網站。
</h1>
</section>
<section
id="feature"
class="text-text p-15 grid grid-rows-3 gap-10 items-center pt-10"
>
<div
class="bg-bg-primary p-5 rounded-xl opacity-90 grid grid-cols-3 grid-rows-2 justify-items-center items-center"
>
<img
src="../img/icon_shuttle.svg"
class="h-[18vh] row-span-2"
/>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2"
style="font-size: 3.5vh"
>
初學者也能立即上手
</h1>
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
我們明白對於初學者來說,設定和使用開發工具可能是一大障礙。
學習者無需安裝任何軟體,僅需通過瀏覽器即可連上一個真實的 Linux
環境。
</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"
style="font-size: 3.5vh"
>
遊戲般的學習體驗
</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 指令的運用。
</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"
>
<img
src="../img/icon_settings.svg"
class="h-[18vh] row-span-2"
/>
<h1
class="text-text-primary p-5 font-extrabold font-mono col-span-2 pt-8"
style="font-size: 3.5vh"
>
真實的 Linux 操作之旅
</h1>
<h1 class="text-text p-2 col-span-2" style="font-size: 2.5vh">
Linux Odyssey 提供的不僅是理論知識,更是真實的操作體驗。
每一個指令都是在真實的 Linux 環境中執行,
讓學習者能夠真正深入軟體開發的世界。
</h1>
</div>
</section>
<section class="w-full py-10">
<iframe
src="https://www.youtube.com/embed/XzvfmUf1QP8?si=QxnKJMNjsm27fGGF"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
class="w-full aspect-video"
></iframe>
</section>
<section
id="start"
class="text-text bg-bg-secondary border-8 border-text-tertiary rounded-xl transition ease-in-out hover:scale-110 duration-300"
>
<RouterLink title="StartGame" id="StartGame" to="/register">
<div class="text-text text-xl p-2 w-full text-center">
準備好鍵盤,跟我們一起開啟你的 Linux 冒險吧!
</div>
</RouterLink>
</section>
<section
id="about"
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>
<p class="text-lg">國立臺北科技大學 智慧自動化工程科 第二屆</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>
</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>
<h1 class="text-xl text-center">
大專校院資訊應用服務創新競賽<br />
鈦坦敏捷開發特別獎 第二名
</h1>
</div>
<div class="container mx-auto">
<div
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"
>
玩家感想
<!--可以做成翻頁卡牌一次一個-->
</h1>
<h1 class="text-xl text-center">"很好玩很像益智遊戲"</h1>
<h1 class="text-xl text-center">
"我從來不知道自由軟體的存在,謝謝你們。"
</h1>
<h1 class="text-xl text-center">
"還蠻好玩的,讓程式變好玩"
</h1>
<h1 class="text-xl text-center">
"以前不喜歡電腦課現在覺得喜歡一點點"
</h1>
<h1 class="text-xl text-center">
"程式真的對新手很友善,讓我學到了很多東西"
</h1>
<h1 class="text-xl text-center">
"感覺做得很好很簡明易懂關卡設計很用心"
</h1>
<h1 class="text-xl text-center">
"這個體驗我覺得非常有趣,可以體驗到沒辦法體驗的程式,學長也都熱心指導,非常感謝學長的用心的講解,希望以後還可以再體驗這種活動"
</h1>
<h1 class="text-xl text-center">
"以前我覺得寫程式很難不過現在覺得很有趣"
</h1>
<h1 class="text-xl text-center">
"以前覺得寫程式是我生活中碰不到而且很難的項目。但通過這堂課之後,覺得寫程式不再那麼難以親近"
</h1>
</div>
</div>
</section>
</div>
<section
id="contact"
class="text-text mt-10 flex flex-col items-center"
>
<h1 class="text-text-primary font-extrabold font-mono text-3xl">
聯絡我們
</h1>
<div
class="flex flex-row items-center justify-around mt-5 gap-5 mb-5"
>
<ContactButton
:url="facebookUrl"
:icons="['fab', 'facebook']"
title="Facebook"
desciption="Linux Odyssey"
/>
<ContactButton
:url="githubUrl"
:icons="['fab', 'github']"
title="GitHub"
desciption="linux-odyssey/linux-odyssey"
/>
<ContactButton
url="mailto:team@linuxodyssey.xyz"
:icons="['fas', 'envelope']"
title="Email"
desciption="team@linuxodyssey.xyz"
/>
</div>
</section>
</div>
</main>
</div>
</div>
</template>
+13 -6
View File
@@ -1,18 +1,25 @@
<script setup>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useToast } from 'vue-toastification'
import DynamicBackground from '../components/DynamicBackground.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
import api from '../utils/api'
const leaderboard = ref([])
const leaderboard = ref<
{
username: string
score: number
completedQuests: string[]
}[]
>([])
const toast = useToast()
async function getLeaderboard() {
try {
const res = await api.get('/leaderboard')
leaderboard.value = res.data
} catch (err) {
} catch (err: any) {
console.error(err)
toast.error('讀取排行榜失敗')
}
@@ -29,8 +36,8 @@ onMounted(async () => {
<div
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-[6vh] w-full">
<GameHeaderPart title="排行榜" />
<div class="w-full">
<HeaderPart title="排行榜" :headerComponent="GameHeaderComponents" />
</div>
<div class="w-fit h-[94vh] m-3">
<div
+17 -8
View File
@@ -1,6 +1,7 @@
<script setup>
<script setup lang="ts">
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,
@@ -9,28 +10,35 @@ import {
} from '../utils/errors'
import { login } from '../utils/auth'
const handleLogin = async ({ username, password, success, error }) => {
const handleLogin = async ({
username,
password,
success,
error,
}: {
username: string
password: string
success: () => void
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) => {
try {
const isSuccess = await login(username, password)
if (isSuccess) success()
} catch (err) {
if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
if (err instanceof UnauthorizedError) {
// error('Wrong username or password.')
error('錯誤的帳號名稱或密碼。')
return
}
if (err instanceof ValidationError) {
// error('Invalid username or password.')
error('無效的帳號名稱或密碼。')
return
}
console.error(err)
// error('Something went wrong.')
error('出了點問題。')
}
}
@@ -38,10 +46,11 @@ const handleLogin = async ({ username, password, success, error }) => {
<template>
<div class="w-screen h-screen">
<Background class="w-full h-full" />
<DynamicBackground class="w-full h-full" />
<div
class="w-screen h-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<HeaderPart />
<div class="w-fit">
<AuthForm @onSubmit="handleLogin" type="login" />
</div>
+10 -12
View File
@@ -1,20 +1,18 @@
<script setup>
<script setup lang="ts">
import QuestMap from '../components/QuestMap.vue'
import GameHeaderPart from '../components/GameHeaderPart.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
</script>
<template>
<div class="h-[6vh] w-full">
<GameHeaderPart title="關卡地圖" />
<!-- <p
class="text-text-primary absolute z-1 font-bold inline-block tracking-widest"
style="left: 1.2%; top: 1.5%; height: 100%; font-size: 2vh"
>
Quest Map
Unix編年史
</p> -->
<div class="w-fit">
<HeaderPart
title="關卡地圖"
:headerComponent="GameHeaderComponents"
class="z-50"
/>
</div>
<div class="flex flex-col justify-center items-center w-full h-[94vh]">
<div class="flex flex-col justify-center items-center w-full h-full pt-5">
<QuestMap />
</div>
</template>
+32 -17
View File
@@ -1,10 +1,10 @@
<script setup>
<script setup lang="ts">
import validator from 'validator'
import { isValidUsername } from '@linux-odyssey/utils'
import { passwordPolicy } from '@linux-odyssey/constants'
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 { checkUsername, register } from '../utils/auth'
import {
UnauthorizedError,
@@ -12,17 +12,28 @@ import {
ValidationError,
} from '../utils/errors'
function handleRegister({ username, email, password, success, error }) {
function handleRegister({
username,
email,
password,
success,
error,
}: {
username: string
email: string
password: string
success: () => void
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
register(username, email, password)
.then(success)
.catch((err) => {
if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
if (err instanceof UnauthorizedError) {
// error('Invalid username or password.')
error('無效的帳號名稱或密碼。')
return
}
@@ -31,26 +42,31 @@ function handleRegister({ username, email, password, success, error }) {
return
}
console.error(err)
// error('Something went wrong.')
error('出了點問題。')
})
}
async function check({ username, email, password, error }) {
async function check({
username,
email,
password,
error,
}: {
username: string
email: string
password: string
// eslint-disable-next-line no-unused-vars
error: (msg: string) => void
}) {
if (username && !isValidUsername(username)) {
// error('Invalid username.')
error('無效的帳號名稱')
return
}
if (email && !validator.isEmail(email)) {
// error('Invalid email.')
error('無效的電子郵件')
return
}
if (password && !validator.isStrongPassword(password, passwordPolicy)) {
// error(
// 'Your password must be 8+ characters with at least one number, one upper and one lower case letter.'
// )
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
return
}
@@ -62,12 +78,10 @@ async function check({ username, email, password, error }) {
return
}
if (err instanceof TooManyRequestsError) {
// error('Too many requests. Try again in 2 minutes.')
error('太多請求,兩分鐘後再試一次。')
return
}
console.error(err)
// error('Something went wrong.')
error('出了點問題。')
}
}
@@ -75,11 +89,12 @@ async function check({ username, email, password, error }) {
<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"
>
<div class="w-fit">
<HeaderPart />
<div class="w-fit mt-8">
<AuthForm
@onSubmit="handleRegister"
@onChange="check"
@@ -24,6 +24,9 @@ export default {
DEFAULT: '#FF0000',
},
},
backgroundImage: {
catelogbg: "url('/catelogbg.svg')",
},
},
},
plugins: [],
+30
View File
@@ -0,0 +1,30 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.vue"],
"references": [
{ "path": "../packages/utils" },
{ "path": "../packages/constants" },
{ "path": "../packages/file-graph" },
{ "path": "../packages/models" }
]
}
File renamed without changes.
+6
View File
@@ -0,0 +1,6 @@
module.exports = {
presets: [
['@babel/preset-env', { targets: { node: 'current' } }],
'@babel/preset-typescript',
],
}
+2
View File
@@ -36,6 +36,8 @@ services:
- DOCKER_NETWORK=linux-odyssey-players
- SECRET_KEY=${SECRET_KEY}
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
- BACKEND_URL=http://backend:3000
- BASE_URL=${BASE_URL}
analytics:
image: linuxodyssey/analytics
+8 -5
View File
@@ -12,6 +12,7 @@ services:
- BASE_URL=http://app-test
- TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234
# - DEBUG=cypress:*
depends_on:
- app-test
@@ -23,8 +24,9 @@ services:
target: prod
networks:
internal:
ports:
- 5173:80
# Only open the port for debugging
# ports:
# - 5173:80
depends_on:
- backend
@@ -43,7 +45,7 @@ services:
- /var/run/docker.sock:/var/run/docker.sock
- ./logs:/app/logs
environment:
- BASE_URL=http://localhost:5173
- BASE_URL=http://app-test
- MONGO_URL=mongodb://user:user-password@db-test:27017/odyssey-test
- DOCKER_NETWORK=linux-odyssey-players
- SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 # Only for testing
@@ -57,6 +59,7 @@ services:
- TESTING=true
- TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234
- BACKEND_URL=http://backend:3000
db-test:
image: mongo
@@ -70,8 +73,8 @@ services:
- MONGO_USER=user
- MONGO_PASS=user-password
- MONGO_DB=odyssey-test
ports:
- 27017:27017
# ports:
# - 27017:27017
base:
image: linuxodyssey/quest-base
+1 -71
View File
@@ -1,78 +1,8 @@
version: '3'
services:
app:
depends_on:
- backend
build:
context: .
dockerfile: app/Dockerfile
target: dev
ports:
- 5173:5173
networks:
- internal
volumes:
- ./app:/app/app
- ./packages:/app/packages
backend:
depends_on:
- db
build:
context: .
dockerfile: server/Dockerfile
target: dev
networks:
- internal
- players
ports:
- '3000:3000'
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./server/src:/app/server/src
- ./server/swagger.yaml:/app/server/swagger.yaml
- ./packages:/app/packages
- ./quests:/app/quests
- ./logs:/app/logs
environment:
- BASE_URL=http://localhost:5173
- MONGO_URL=mongodb://db:27017/odyssey-test
- DOCKER_NETWORK=linux-odyssey-players
- SECRET_KEY=b1d2e917-17b9-4fce-9055-082dee6ec5d6 # Only for testing
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
- SURVEY_URL=${SURVEY_URL}
- BUG_REPORT_URL=${BUG_REPORT_URL}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- TESTING=true
- TESTING_USERNAME=alex
- TESTING_PASSWORD=Alex1234
- HOST_PWD=${PWD}
- MOUNT_QUEST=${MOUNT_QUEST}
analytics:
build:
context: .
dockerfile: packages/analytics/Dockerfile
target: dev
depends_on:
- db
networks:
- internal
ports:
- '3001:3001'
environment:
- MONGO_URL=mongodb://db:27017/odyssey-test
- ADMIN_USERNAME=alex
- ADMIN_PASSWORD=Alex1234
volumes:
- ./packages/analytics:/app/packages/analytics
- ./packages/models:/app/packages/models
db:
image: mongo
restart: always
networks:
- internal
ports:
+34
View File
@@ -29,4 +29,38 @@ down() {
docker compose -f docker-compose.testing.yml down
}
dump() {
# Define service name and dump directory name
SERVICE_NAME="db"
DUMP_NAME="mongodump-$(date --iso).archive"
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
# Fetch credentials from Docker environment variables
MONGO_URL=$(docker compose exec -T backend printenv MONGO_URL)
echo mongo uri: $MONGO_URL
# Dump MongoDB data using credentials
docker compose exec -T $SERVICE_NAME sh -c "mongodump --gzip --archive=/tmp/$DUMP_NAME -d odyssey-test" &&
# Copy the dump from the container to the host
# Note: You will need to know the exact container name for this step
docker cp ${CONTAINER_ID}:/tmp/$DUMP_NAME . &&
echo "MongoDB data dumped successfully."
}
restore() {
# Define service name and dump directory name
SERVICE_NAME="db"
DUMP_NAME=$1
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
# Copy the dump from the host to the container
docker cp $DUMP_NAME ${CONTAINER_ID}:/tmp &&
# Dump MongoDB data using credentials
docker compose exec -T $SERVICE_NAME sh -c "mongorestore --gzip --archive=/tmp/$DUMP_NAME" &&
echo "MongoDB data dumped successfully."
}
$@
-5
View File
@@ -1,5 +0,0 @@
BASE_URL=https://example.com
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
GITHUB_CLIENT_ID=
GITNUB_CLIENT_SECRET=
+201
View File
@@ -0,0 +1,201 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type { Config } from 'jest'
const config: Config = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls, instances, contexts and results before every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8',
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
moduleNameMapper: {
'^(\\.{1,2}/.*)\\.js$': '$1', // Redirect .js imports to .ts files
},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// "<rootDir>"
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
// testEnvironment: "jest-environment-node",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
}
export default config
+25 -6
View File
@@ -13,13 +13,13 @@
],
"scripts": {
"app": "yarn workspace app dev",
"app:local": "yarn workspace app dev:local",
"build": "yarn workspace app build",
"build": "yarn workspace server build && yarn workspace app build && yarn workspace analytics build",
"dev": "concurrently -n \"App,Server\" -c \"bgBlue.bold,bgGreen.bold\" \"yarn workspace app dev\" \"yarn workspace server dev\"",
"server": "yarn workspace server dev",
"cli": "yarn workspace client start",
"lint": "eslint --ext .js,.vue .",
"lint": "eslint .",
"format": "prettier --ignore-path .prettierignore --write app server",
"test": "yarn node --experimental-vm-modules $(yarn bin jest)",
"test": "yarn node --experimental-vm-modules --no-warnings $(yarn bin jest)",
"test:e2e": "yarn workspace tests test",
"test:open": "yarn workspace tests test:open",
"analytics": "yarn workspace analytics dev",
@@ -28,14 +28,33 @@
"build:quests": "yarn workspace server build:quests"
},
"devDependencies": {
"@babel/core": "^7.24.0",
"@babel/preset-env": "^7.24.0",
"@babel/preset-typescript": "^7.23.3",
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vue/eslint-config-prettier": "^7.1.0",
"babel-jest": "^29.7.0",
"concurrently": "^8.2.2",
"eslint": "^8.44.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.15.1",
"prettier": "^3.0.0"
"jest": "^29.7.0",
"prettier": "^3.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
},
"dependencies": {
"latest": "^0.2.0",
"typescript-eslint": "^7.4.0"
}
}
+2
View File
@@ -0,0 +1,2 @@
ADMIN_USERNAME=alex
ADMIN_PASSWORD=Alex1234
+4 -10
View File
@@ -1,10 +1,10 @@
FROM node:20-alpine as build
WORKDIR /app
COPY ./package.json yarn.lock ./
COPY ./package.json yarn.lock tsconfig.json ./
COPY packages/analytics/package.json ./packages/analytics/package.json
COPY packages/models/package.json ./packages/models/package.json
RUN yarn install --frozen-lockfile --production
RUN yarn install --frozen-lockfile
ENV HOST=0.0.0.0
ENV PORT=3001
EXPOSE 3001
@@ -15,11 +15,5 @@ COPY packages/analytics ./packages/analytics
COPY packages/models ./packages/models
ENV NODE_ENV=production
WORKDIR /app/packages/analytics
CMD [ "yarn", "start" ]
# Development stage
FROM build as dev
RUN yarn install --frozen-lockfile
WORKDIR /app/packages/analytics
ENV NODE_ENV=development
CMD [ "yarn", "dev" ]
RUN yarn build
CMD [ "yarn", "start" ]
+4 -1
View File
@@ -1,3 +1,6 @@
{
"ext": "js,handlebars"
"watch": ["src", "views"],
"ext": "ts,handlebars,js",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --loader ts-node/esm src/index.ts"
}
+16 -5
View File
@@ -6,16 +6,27 @@
"type": "module",
"license": "GPL",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
"start": "node dist/index.js",
"build": "tsc -b -f",
"watch": "tsc -b --watch",
"dev": "nodemon"
},
"dependencies": {
"@linux-odyssey/models": "*",
"express": "^4.18.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-basic-auth": "^1.2.1",
"express-handlebars": "^7.1.2"
"express-handlebars": "^7.1.2",
"mongoose": "^8.0.3"
},
"devDependencies": {
"nodemon": "^3.0.1"
"@swc/core": "^1.4.2",
"@swc/helpers": "^0.5.6",
"@types/express": "^4.17.21",
"@types/node": "^20.11.20",
"nodemon": "^3.1.0",
"regenerator-runtime": "^0.14.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
}
}
+115 -1
View File
@@ -9,7 +9,7 @@ th {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
border: 2px solid black;
border: 2px solid #dcdcdd;
}
th,
@@ -17,3 +17,117 @@ td {
width: 1px;
white-space: nowrap;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #000000;
color: #333;
margin: 0;
padding: 20px;
box-sizing: border-box;
}
.title h1 {
color: #00ff00;
text-align: center;
font-size: 50px;
margin-top: 50px;
font-family:
ui-monospace,
SFMono-Regular,
Menlo,
Monaco,
Consolas,
Liberation Mono,
Courier New,
monospace;
}
.title img {
width: 135px;
}
.title {
display: flex;
flex-direction: row;
justify-content: center;
gap: 30px;
margin-bottom: 30px;
}
.heading {
color: #ffffff;
}
nav {
display: flex;
flex-direction: row;
justify-content: center;
gap: 30px;
}
.nav a {
color: white;
text-decoration: none;
font-weight: 600;
font-size: 20px;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
color: #dcdcdd;
background-color: #24262e;
}
.data-table td {
padding: 10px;
text-align: left;
font-weight: 400;
}
.data-table a {
color: #00dd00;
font-weight: 500;
}
.data-table th {
color: #dcdcdd;
padding: 10px;
border: 1px solid #dcdcdd;
text-align: left;
}
.container {
background-color: #24262e;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.sort {
display: flex;
margin-bottom: 25px;
gap: 15px;
}
.sort a {
background-color: #24262e;
height: 50px;
padding: 0 5 0 5;
border-radius: 10px;
text-decoration: none !important;
}
.sort .btn h3 {
margin: 15 3 0 3;
font-weight: 600;
color: #dcdcdd;
font-size: 15px;
}
.pagination {
width: 100%;
justify-content: center;
margin-top: 20px;
}
+9
View File
@@ -0,0 +1,9 @@
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
<defs>
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
</pattern>
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
</defs>
</svg>

After

Width:  |  Height:  |  Size: 630 KiB

-18
View File
@@ -1,18 +0,0 @@
function getOrFailed(key) {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`)
}
return process.env[key]
}
const config = {
host: process.env.HOST || 'localhost',
port: process.env.PORT || 3001,
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
username: getOrFailed('ADMIN_USERNAME'),
password: getOrFailed('ADMIN_PASSWORD'),
}
config.baseUrl = process.env.BASE_URL || `http://${config.host}:${config.port}`
export default config
+34
View File
@@ -0,0 +1,34 @@
import dotenv from 'dotenv'
if (process.env.NODE_ENV !== 'production') {
dotenv.config({ path: '.env.dev' })
}
function getOrFailed(key: string): string {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`)
}
return process.env[key] as string
}
interface Config {
host: string
port: number
db: string
username: string
password: string
baseUrl: string
}
const config: Config = {
host: process.env.HOST || 'localhost',
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3001,
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
username: getOrFailed('ADMIN_USERNAME'),
password: getOrFailed('ADMIN_PASSWORD'),
baseUrl:
process.env.BASE_URL ||
`http://${process.env.HOST || 'localhost'}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 3001}`,
}
export default config
View File
Whitespace-only changes.
@@ -1,9 +0,0 @@
import { errorCommands } from '../models/commands.js'
// eslint-disable-next-line import/prefer-default-export
export async function commandListController(req, res) {
const payload = {
errorCommands: await errorCommands(),
}
res.render('commands', payload)
}
@@ -0,0 +1,26 @@
import type { Request, Response } from 'express'
import Pagination from '../models/pagination.js'
import { errorCommands } from '../models/commands.js'
export async function commandListController(
req: Request,
res: Response
): Promise<void> {
const { nextKey, order } = req.query as { nextKey?: string; order: string }
const itemsPerPage = 100
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const payload = await errorCommands(pagination)
const newNextKey = payload[payload.length - 1]?._id
res.render('commands', {
errorCommands: payload,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching data')
}
}
@@ -1,15 +0,0 @@
import { userCount } from '../models/users.js'
import { sessionCount, sessionCompleted } from '../models/sessions.js'
// eslint-disable-next-line import/prefer-default-export
export async function homeController(req, res) {
const payload = {
title: 'Linux Odyssey Admin',
userCount: await userCount(),
sessionCount: await sessionCount(),
sessionCompleted: await sessionCompleted(),
}
payload.completedRate =
Math.round((payload.sessionCompleted / payload.sessionCount) * 10000) / 100
res.render('home', payload)
}
@@ -0,0 +1,23 @@
import { Request, Response } from 'express'
import { userCount } from '../models/users.js'
import { questList } from '../models/quests.js'
export async function homeController(req: Request, res: Response) {
const quests = await questList()
const [totalQuests, totalCompleted] = quests.reduce(
([total, completed], quest) => [
total + quest.started,
completed + quest.completed,
],
[0, 0]
)
const payload = {
title: 'Linux Odyssey Admin',
userCount: await userCount(),
totalQuests,
totalCompleted,
completedRate: Math.round((totalCompleted / totalQuests) * 10000) / 100,
}
res.render('home', payload)
}
@@ -0,0 +1,10 @@
import { Request, Response } from 'express'
import { questList } from '../models/quests.js'
export async function questListController(req: Request, res: Response) {
const quests = await questList()
quests.forEach((quest) => {
quest.failed = quest.started - quest.completed
})
res.render('quests', { quests })
}
@@ -1,12 +0,0 @@
import { sessionList, sessionDetail } from '../models/sessions.js'
export async function sessionListController(req, res) {
const sessions = await sessionList()
res.render('sessions', { sessions })
}
export async function sessionDetailController(req, res) {
const { id } = req.params
const session = await sessionDetail(id)
res.render('sessionDetail', { session })
}
@@ -0,0 +1,29 @@
import { Request, Response } from 'express'
import Pagination from '../models/pagination.js'
import { sessionList, sessionDetail } from '../models/sessions.js'
export async function sessionListController(req: Request, res: Response) {
const { nextKey, order } = req.query as { nextKey: string; order: string }
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const sessions = await sessionList(pagination)
const newNextKey = sessions[sessions.length - 1]?._id
res.render('sessions', {
sessions,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.log(error)
res.status(500).send('Error fetching session data')
}
}
export async function sessionDetailController(req: Request, res: Response) {
const { id } = req.params
const session = await sessionDetail(id)
res.render('sessionDetail', { session })
}
@@ -1,6 +0,0 @@
import { userList } from '../models/users.js'
// eslint-disable-next-line import/prefer-default-export
export async function userListController(req, res) {
res.render('users', { users: await userList() })
}
@@ -0,0 +1,36 @@
import { Request, Response } from 'express'
import mongoose from 'mongoose'
import Pagination from '../models/pagination.js'
import { userList, userDetail, idToUser } from '../models/users.js'
export async function userListController(req: Request, res: Response) {
const { nextKey, order } = req.query as { nextKey: string; order: string }
const itemsPerPage = 50
try {
const pagination = new Pagination(order, itemsPerPage, nextKey)
const users = await userList(pagination)
const newNextKey = users[users.length - 1]?._id
res.render('users', {
users,
nextKey: newNextKey,
order: pagination.getOrder(),
})
} catch (error) {
console.error(error)
res.status(500).send('Error fetching user data')
}
}
export async function userDetailController(req: Request, res: Response) {
const id = new mongoose.Types.ObjectId(req.params.id)
const sessions = await userDetail(id)
try {
const user = await idToUser(id)
res.render('usersDetail', { sessions, user })
} catch (error) {
console.error(error)
res.status(500).send('Error fetching user data')
}
}
Loaded 100 of 307 files, more files were not shown because too many files have changed in this diff. Show more