From 099d48621bd93742ae625e96aee9878cbdd2136d Mon Sep 17 00:00:00 2001 From: Jesse Desjardins Date: Wed, 14 Jan 2026 17:06:21 -0500 Subject: [PATCH] Added Bootstrap and Vue I18N; added header and footer components; added language switcher --- .prettierrc.json | 4 +- README.md | 66 ++++++++++++++---- package-lock.json | 130 +++++++++++++++++++++++++++++++++++ package.json | 3 + src/App.vue | 12 ++-- src/__tests__/App.spec.ts | 8 +-- src/components/AppFooter.vue | 11 +++ src/components/AppHeader.vue | 64 +++++++++++++++++ src/layout/DefaultLayout.vue | 14 ++++ src/locales/en.json | 12 ++++ src/locales/fr.json | 12 ++++ src/main.ts | 44 ++++++++++++ src/router/index.ts | 26 ++++++- src/stores/counter.ts | 12 ++-- src/views/Home.vue | 7 ++ src/views/List.vue | 8 +++ src/vue-i18n.d.ts | 45 ++++++++++++ 17 files changed, 446 insertions(+), 32 deletions(-) create mode 100644 src/components/AppFooter.vue create mode 100644 src/components/AppHeader.vue create mode 100644 src/layout/DefaultLayout.vue create mode 100644 src/locales/en.json create mode 100644 src/locales/fr.json create mode 100644 src/views/Home.vue create mode 100644 src/views/List.vue create mode 100644 src/vue-i18n.d.ts diff --git a/.prettierrc.json b/.prettierrc.json index 29a2402..60984e4 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -2,5 +2,7 @@ "$schema": "https://json.schemastore.org/prettierrc", "semi": false, "singleQuote": true, - "printWidth": 100 + "printWidth": 100, + "useTabs": true, + "tabWidth": 4 } diff --git a/README.md b/README.md index f61d383..fb6e995 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,22 @@ -# glossary_admin_client +# NATO Glossary, Admin Client -This template should help get you started developing with Vue 3 in Vite. +This is the repo for the admin client of the NATO Glossary project. This README will explain all basic commands for running, developing, testing, and building the app. + +## Stack + +- [Vue 3](https://vuejs.org/) + [Vite](https://vite.dev/) +- [TypeScript](https://www.typescriptlang.org/) +- [Bootstrap](https://getbootstrap.com/) for UI and styling +- [Vue I18N](https://vue-i18n.intlify.dev/) for multi-language options +- [Pinia](https://pinia.vuejs.org/) for state management +- [Vitest](https://vitest.dev/) for unit testing +- [Playwright](https://playwright.dev/) for end-to-end (e2e) testing +- [ESLint](https://eslint.org/) for code linting +- [Prettier](https://prettier.io/) for code formatting + +## File Structure + +Given the small scope of this project, we're beginning with a [Flat Structure](https://alexop.dev/posts/how-to-structure-vue-projects/#1-flat-structure-perfect-for-small-projects) to organize the files. ## Recommended IDE Setup @@ -17,30 +33,36 @@ This template should help get you started developing with Vue 3 in Vite. ## Type Support for `.vue` Imports in TS -TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. ## Customize configuration See [Vite Configuration Reference](https://vite.dev/config/). -## Project Setup +## First-Time Setup -```sh +Install dependencies: + +```bash npm install ``` -### Compile and Hot-Reload for Development +## Development Environment -```sh +To properly run on and test this application, you'll need the following pieces to be running: + +- Development Server (the web app) +- Local Database (a dev database that provides dummy data to testing) +- Development API (the API that serves the dummy data from the database to the web app) + +### Development Server + +Start the development server on `http://localhost:3000`: + +```bash npm run dev ``` -### Type-Check, Compile and Minify for Production - -```sh -npm run build -``` - ### Run Unit Tests with [Vitest](https://vitest.dev/) ```sh @@ -71,3 +93,21 @@ npm run test:e2e -- --debug ```sh npm run lint ``` + +## Production + +Build the application for production: + +```bash +# npm +npm run build +``` + +Locally preview production build: + +```bash +# npm +npm run preview +``` + +Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3fc1ea9..8b6d9c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,8 +8,11 @@ "name": "glossary_admin_client", "version": "0.0.0", "dependencies": { + "bootstrap": "^5.3.8", + "bootstrap-vue-next": "^0.42.0", "pinia": "^3.0.4", "vue": "^3.5.26", + "vue-i18n": "^11.2.8", "vue-router": "^4.6.4" }, "devDependencies": { @@ -1403,6 +1406,50 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@intlify/core-base": { + "version": "11.2.8", + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.8.tgz", + "integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==", + "license": "MIT", + "dependencies": { + "@intlify/message-compiler": "11.2.8", + "@intlify/shared": "11.2.8" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/message-compiler": { + "version": "11.2.8", + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.8.tgz", + "integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==", + "license": "MIT", + "dependencies": { + "@intlify/shared": "11.2.8", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, + "node_modules/@intlify/shared": { + "version": "11.2.8", + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.8.tgz", + "integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1562,6 +1609,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -2965,6 +3023,52 @@ "dev": true, "license": "ISC" }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-vue-next": { + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/bootstrap-vue-next/-/bootstrap-vue-next-0.42.0.tgz", + "integrity": "sha512-CSkk2jL8Y2U9zgdutxjbBx4DRhKFmxTmaTQhhMYXVvczkVksw2mzRu1MU//950z0lvUwSqJo3R7y0opoyEA8zg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap-vue-next" + }, + "peerDependencies": { + "@floating-ui/vue": "*", + "@vueuse/core": "*", + "vue": "^3.5.13", + "vue-router": "*" + }, + "peerDependenciesMeta": { + "@floating-ui/vue": { + "optional": true + }, + "@vueuse/core": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -6293,6 +6397,32 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/vue-i18n": { + "version": "11.2.8", + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz", + "integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==", + "license": "MIT", + "dependencies": { + "@intlify/core-base": "11.2.8", + "@intlify/shared": "11.2.8", + "@vue/devtools-api": "^6.5.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/kazupon" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/vue-i18n/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, "node_modules/vue-router": { "version": "4.6.4", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", diff --git a/package.json b/package.json index dcdde1f..8b4d324 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,11 @@ "format": "prettier --write --experimental-cli src/" }, "dependencies": { + "bootstrap": "^5.3.8", + "bootstrap-vue-next": "^0.42.0", "pinia": "^3.0.4", "vue": "^3.5.26", + "vue-i18n": "^11.2.8", "vue-router": "^4.6.4" }, "devDependencies": { diff --git a/src/App.vue b/src/App.vue index abfd315..3f4f71b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,11 +1,11 @@ - + diff --git a/src/__tests__/App.spec.ts b/src/__tests__/App.spec.ts index 5b17801..b5234e5 100644 --- a/src/__tests__/App.spec.ts +++ b/src/__tests__/App.spec.ts @@ -4,8 +4,8 @@ import { mount } from '@vue/test-utils' import App from '../App.vue' describe('App', () => { - it('mounts renders properly', () => { - const wrapper = mount(App) - expect(wrapper.text()).toContain('You did it!') - }) + it('mounts renders properly', () => { + const wrapper = mount(App) + expect(wrapper.text()).toContain('You did it!') + }) }) diff --git a/src/components/AppFooter.vue b/src/components/AppFooter.vue new file mode 100644 index 0000000..a5cd536 --- /dev/null +++ b/src/components/AppFooter.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue new file mode 100644 index 0000000..372f1f3 --- /dev/null +++ b/src/components/AppHeader.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/src/layout/DefaultLayout.vue b/src/layout/DefaultLayout.vue new file mode 100644 index 0000000..c4bc33a --- /dev/null +++ b/src/layout/DefaultLayout.vue @@ -0,0 +1,14 @@ + + + + + diff --git a/src/locales/en.json b/src/locales/en.json new file mode 100644 index 0000000..2cb94df --- /dev/null +++ b/src/locales/en.json @@ -0,0 +1,12 @@ +{ + "hello": "hello world!", + "errors": ["Error!"], + "navbar": { + "natoGlossary": "NATO GLossary", + "home": "Home", + "list": "List", + "language": "Language", + "logout": "logout", + "login": "login" + } +} diff --git a/src/locales/fr.json b/src/locales/fr.json new file mode 100644 index 0000000..cabe482 --- /dev/null +++ b/src/locales/fr.json @@ -0,0 +1,12 @@ +{ + "hello": "bonjour monde !", + "errors": ["Erreur !"], + "navbar": { + "natoGlossary": "NATO GLossary", + "home": "Home", + "list": "Liste", + "language": "Language", + "logout": "se déconnecter", + "login": "se connecter" + } +} diff --git a/src/main.ts b/src/main.ts index fda1e6e..cc20ba9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,56 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' +import { createI18n } from 'vue-i18n' import App from './App.vue' +import 'bootstrap/dist/css/bootstrap.css' +import 'bootstrap-vue-next/dist/bootstrap-vue-next.css' import router from './router' +/** + * import locale messages resource from json for global scope + */ +import en from './locales/en.json' +import fr from './locales/fr.json' + +/** + * setup vue-i18n with i18n resources with global type definition. + * The defined i18n resource schema in the `*.d.ts` file is checked with typeScript. + * You can check global type definition at `./vue-i18n.d.ts`. + */ +const i18n = createI18n({ + locale: 'en', + fallbackLocale: 'fr', + messages: { + en: en, + fr: fr, + }, + // datetimeFormats: { + // 'en': { + // short: { + // hour: 'numeric', + // minute: 'numeric', + // second: 'numeric', + // timeZoneName: 'short', + // timezone: 'America/Montreal' + // } + // } + // }, + // numberFormats: { + // 'en': { + // currency: { + // style: 'currency', + // currencyDisplay: 'symbol', + // currency: 'CAD' + // } + // } + // } +}) + const app = createApp(App) app.use(createPinia()) app.use(router) +app.use(i18n) app.mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts index e1eab52..51f4817 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,8 +1,30 @@ import { createRouter, createWebHistory } from 'vue-router' +import Home from '@/views/Home.vue' +import List from '@/views/List.vue' + +import DefaultLayout from '@/layout/DefaultLayout.vue' const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [], + history: createWebHistory(import.meta.env.BASE_URL), + routes: [ + { + path: '/', + name: 'Public', + component: DefaultLayout, + children: [ + { + path: '/', + name: 'Home', + component: Home, + }, + { + path: '/list', + name: 'List', + component: List, + }, + ], + }, + ], }) export default router diff --git a/src/stores/counter.ts b/src/stores/counter.ts index b6757ba..417802c 100644 --- a/src/stores/counter.ts +++ b/src/stores/counter.ts @@ -2,11 +2,11 @@ import { ref, computed } from 'vue' import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', () => { - const count = ref(0) - const doubleCount = computed(() => count.value * 2) - function increment() { - count.value++ - } + const count = ref(0) + const doubleCount = computed(() => count.value * 2) + function increment() { + count.value++ + } - return { count, doubleCount, increment } + return { count, doubleCount, increment } }) diff --git a/src/views/Home.vue b/src/views/Home.vue new file mode 100644 index 0000000..5e16543 --- /dev/null +++ b/src/views/Home.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/views/List.vue b/src/views/List.vue new file mode 100644 index 0000000..b243465 --- /dev/null +++ b/src/views/List.vue @@ -0,0 +1,8 @@ + + + + + diff --git a/src/vue-i18n.d.ts b/src/vue-i18n.d.ts new file mode 100644 index 0000000..f62be21 --- /dev/null +++ b/src/vue-i18n.d.ts @@ -0,0 +1,45 @@ +/** + * global type definitions + * using the typescript interface, you can define the i18n resources that is type-safed! + */ + +/** + * you need to import the some interfaces + */ +import { DefineLocaleMessage, DefineDateTimeFormat, DefineNumberFormat } from 'vue-i18n' + +declare module 'vue-i18n' { + // define the locale messages schema + export interface DefineLocaleMessage { + hello: string + errors: string[] + navbar: { + natoGlossary: string + home: string + list: string + language: string + logout: string + login: string + } + } + + // // define the datetime format schema + // export interface DefineDateTimeFormat { + // short: { + // hour: 'numeric' + // minute: 'numeric' + // second: 'numeric' + // timeZoneName: 'short' + // timezone: string + // } + // } + + // // define the number format schema + // export interface DefineNumberFormat { + // currency: { + // style: 'currency' + // currencyDisplay: 'symbol' + // currency: string + // } + // } +}