1.I have implemented the webapp with the knowledge I gained in the “Mastering Nuxt 2025” course: https://masteringnuxt.com/2025/lessons. This course specifically covers the nuxt v4 future version and vue 3.
2.In my development machine I have Node.js v22.17.0 and npm v10.9.2, so if you would like to run the app locally, those are the proven versions.
3.The initial empty project was generated with command: "npm create nuxt@latest -- -t ui".
4.Dependency installation was done with Nuxi dependency integrator, which installs and integrates the dependencies into the project. For example I added ESLint with the command: "pnpm dlx nuxi module add eslint". If Nuxi doesn't know how to integrate, then a manual installation, for example in Husky case with "pnpm add --save-dev husky" and then integrating manually into the project.
5.For the project I am using these main technologies:
5.1.Nuxt v4 (early access version); note: in the package.json you will see v3.17.7, however it is actually running in v4 mode, because in the /nuxt.config.ts I have explicitly enabled v4 mode.
5.2.Vue v3.5.17
5.3.TypeScript - for type-safe programming
5.4.@nuxt/ui - for reusable components
5.5.Tailwind (comes with @nuxt/ui)
5.6.@nuxt/eslint - for consistent code quality enforcement
5.7.@nuxt/test-utils - for testing framework
5.8.Husky - this npm tool enforces lint checks with Git hooks (calls lint and E2E tests) when committing - when trying to commit either through terminal or through any IDE, like WebStorm. If I would have any lint errors in my code or failing tests, and would try to commit, then the commit would abort showing lint errors or details about failed tests. This way no one can ever commit with lint errors or failing tests. I installed it like this:
5.8.1."pnpm add --save-dev husky"
5.8.2."pnpm exec husky init"
5.8.3.in /.husky/pre-commit I added “pnpm lint” and "pnpm test:e2e"
6.After generating the project, I was adding it to a remote Git repository by:
6.1."git init"
6.2."git add ."
6.3."git commit -m 'initial commit after generating empty project'"
6.4.Then in GitHub I create a new empty repository. I made it as private, so no one can steal my homework implementation for plagiarism.
6.5.Copied the SSH public key to GitHub
6.6."git remote add origin git@github.com:jurgis-upenieks/jurgis-mintos-homework.git"
6.7."git branch -M main"
6.8."git push -u origin main"
7.To get the source code, first ask JurgisU to enable access, because the repository is private, then run: "git clone git@github.com:jurgis-upenieks/jurgis-mintos-homework.git"
8.To run the app locally, in the root dir first run “pnpm i” and then “pnpm dev”.
9.Adhering to Nuxt v4 convention, server-side code goes under the /server/ and client-side code goes under /app/.
10.The /app/app.vue is the root component. <UApp> wrapper enables Nuxt UI, <NuxtLayout> is for pages common wrapper layout, and <NuxtPage/> is route slot where the page component is plugged in.
11.<NuxtLayout> allows common wrapper layout code that is used in all page components to be specified in a single file. The file is located according to Nuxt v4 convention at /app/layouts/default.vue.
12.Types go in /app/types.ts. In such a small project, there is no need for more detailed organization.
13.Composables are located in /app/composables/ adhering to Nuxt v4 convention. Basically like React hooks with some small differences.
14.Components are located in /app/components/ adhering to Nuxt v4 convention. They are single isolated scope logic/layout/styling building blocks.
15.The app is using Universal Rendering, which is a hybrid between spa and ssr (best of both worlds). Initially the server sends a rendered bare-bones html, then in the background silently continues sending the spa version. Once it’s received, it does hydration - seamlessly switches from rendered version to spa, while seamlessly keeping/transferring the all states.
16.For custom styling I am using Tailwind - it is used not just internally by NuxtUI, and also provides a theme for NuxtUI, but I am using it for custom styling as well (see for example /app/layouts/default.vue). Instead of writing component custom styles in a <style> section, I am writing tiny Tailwind provided CSS class names straight in the component's <template> section. This normally is a quicker approach.
17.Page Routing config is inferred by Nuxt from how the page components are structured in folders in /app/pages/ and how they are named.
18.I have implemented the core requirements in the landing page component /app/pages/index.vue. Instead of writing custom layout/styling/logic, I am using NuxtUI reusable components as much as possible, and using custom solutions only where it's absolutely needed.
18.1.For the token list I am using NuxtUI UInputTags component.
18.2.For the multiselect items I am using internal parts of the NuxtUI USelect. The USelect normally is a multiselect dropdown, however I only need the items menu. I have determined that internally the USelect for the options menu is using reka-ui components. I have extracted those parts out to a new component /app/components/MultiselectMenu.vue
18.3.Both the NuxtUI UInputTags and reka-ui multiselect menu are connected by the same state selectedCurrencies, and thanks to Vue/Nuxt all the synchronization between both is handled automatically, making the core functionality code in the page component extremely short.
18.4.The allCurrencies data is determined during the server-side-rendering phase, so that the user receives the bare-bones rendered HTML and states with already rendered data. Because of this, the client side doesn't need to request the currencies list from the server.
18.5.In the SSR part of this page component, the currencies list is taken from a JSON file that is available only for the server side. Also I made it so that on the server side the file is being read every time a page is rendered, mimicking reading it from a database or from an external service.
18.6.There is one more important aspect - the server-side-rendering code to determine the currencies is only working when the user lands straight to that page. However, when the user first lands on another page (like /technical-documentation), and then goes to that page, this code will not be working, because the site will be already hydrated at that point (converted to SPA mode). So in the else statement I have implemented a fallback code where, in case of SPA mode, the client-side is requesting the currencies from the server side.
19.The layout is responsive - elements shift and resize as needed by the screen/window width. Also mobile friendly.
20.E2E testing. To run the E2E tests, run these commands:
20.1."pnpm exec playwright install" to install the web driver on your development machine
20.2.To run the E2E tests with a virtual browser, run "pnpm test:e2e"
20.3.If you want to visually see how the user interaction happens in a visible browser, run "pnpm test:e2e:headed"
21.I have implemented one E2E test /tests/e2e/currencies.spec.ts, which performs the following user interactions and checks:
21.1.First it opens the landing page and checks if the page opens with the right site title.
21.2.Then it checks if both elements are available for interaction - the multiselect menu and the token list.
21.3.Then in the multiselect menu it selects the first item and checks if that becomes reflected in the token list.
21.4.Then in the multiselect menu it selects the third item and checks if in the token list 2 items are now present and the second one has the text of the third from the multiselect.
21.5.Then it checks if in the multiselect 2 items are selected.
21.6.Then in the token list it removes the first item and checks if in the token list only one remains and in the multiselect also only one remains selected.
21.7.Then in the multiselect list it unselects the third one and checks if in the multiselect none remain selected and in the token list becomes empty.
22.The app is fully production ready, because I have deployed it to my Raspberry Pi Linux web server.
22.1.In the Nginx configuration I have added redirecting from external subdomain address jurgis-mintos-homework.afla.lv to internal port 3001.
22.2.Then with Certbot I enabled HTTPS for the address.
22.3.Then I registered and enabled a Linux service, which runs the app with the command "PORT=3001 node jurgis-mintos-homework/server/index.mjs"
22.4.To build the app I am using the command "pnpm build"
22.5.Then with SCP I am uploading the .output directory to my Raspberry Pi.
22.6.Then on the Raspberry Pi I started the Linux background service that runs the Node.js server with the app.