Added Bootstrap and Vue I18N; added header and footer components; added language switcher

This commit is contained in:
2026-01-14 17:06:21 -05:00
parent 69912cce08
commit 099d48621b
17 changed files with 446 additions and 32 deletions

View File

@@ -2,5 +2,7 @@
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
"printWidth": 100,
"useTabs": true,
"tabWidth": 4
}

View File

@@ -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.

130
package-lock.json generated
View File

@@ -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",

View File

@@ -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": {

View File

@@ -1,11 +1,11 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import { BApp } from 'bootstrap-vue-next'
</script>
<template>
<h1>You did it!</h1>
<p>
Visit <a href="https://vuejs.org/" target="_blank" rel="noopener">vuejs.org</a> to read the
documentation
</p>
<BApp>
<RouterView />
</BApp>
</template>
<style scoped></style>

View File

@@ -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!')
})
})

View File

@@ -0,0 +1,11 @@
<script setup lang="ts">
import { BContainer } from 'bootstrap-vue-next';
</script>
<template>
<!-- <div class="bg-dark text-white text-center py-3 fixed-bottom">Footer</div> -->
<BContainer fluid class="bg-dark text-white fixed-bottom py-3">
Footer
</BContainer>
</template>

View File

@@ -0,0 +1,64 @@
<script setup lang="ts">
import {
BButton,
BCollapse,
BDropdownItem,
BNavbar,
BNavbarBrand,
BNavbarNav,
BNavbarToggle,
BNavItem,
BNavItemDropdown,
vBColorMode
} from 'bootstrap-vue-next'
import { useI18n } from 'vue-i18n'
// use global scope
const { t } = useI18n({
useScope: 'global',
inheritLocale: true,
})
</script>
<template>
<b-navbar v-b-color-mode="'dark'" toggleable="lg" variant="primary">
<b-navbar-brand>{{ t('navbar.natoGlossary') }}</b-navbar-brand>
<b-navbar-toggle target="nav-collapse" />
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item href="#">
<RouterLink
:to="{ name: 'Home' }"
class="nav-link active"
aria-current="page"
href="#"
>{{ t('navbar.home') }}</RouterLink
>
</b-nav-item>
<b-nav-item href="#">
<RouterLink
:to="{ name: 'List' }"
class="nav-link active"
aria-current="page"
href="#"
>{{ t('navbar.list') }}</RouterLink
>
</b-nav-item>
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ms-auto mb-2 mb-lg-0">
<b-nav-item-dropdown text="Lang" right>
<b-dropdown-item
v-for="locale in $i18n.availableLocales"
:key="`locale-${locale}`"
v-on:click="$i18n.locale = locale"
>{{ locale }}</b-dropdown-item
>
</b-nav-item-dropdown>
</b-navbar-nav>
<b-button type="button">{{ t('navbar.login') }}</b-button>
</b-collapse>
</b-navbar>
</template>
<style lang="css" scoped></style>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import AppHeader from '../components/AppHeader.vue'
import AppFooter from '../components/AppFooter.vue'
</script>
<template>
<div>
<AppHeader />
<RouterView />
<AppFooter />
</div>
</template>
<style lang="css" scoped></style>

12
src/locales/en.json Normal file
View File

@@ -0,0 +1,12 @@
{
"hello": "hello world!",
"errors": ["Error!"],
"navbar": {
"natoGlossary": "NATO GLossary",
"home": "Home",
"list": "List",
"language": "Language",
"logout": "logout",
"login": "login"
}
}

12
src/locales/fr.json Normal file
View File

@@ -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"
}
}

View File

@@ -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')

View File

@@ -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

View File

@@ -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 }
})

7
src/views/Home.vue Normal file
View File

@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<div>This is Home</div>
</template>
<style lang="css" scoped></style>

8
src/views/List.vue Normal file
View File

@@ -0,0 +1,8 @@
<script setup lang="ts">
</script>
<template>
<div>This is List</div>
</template>
<style lang="css" scoped></style>

45
src/vue-i18n.d.ts vendored Normal file
View File

@@ -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
// }
// }
}