Imported boilerplate_frontend as a subtree under frontend/.

This commit is contained in:
Ulf Gebhardt 2024-01-28 12:49:49 +01:00
commit bfaf53de5b
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
120 changed files with 39962 additions and 0 deletions

5
frontend/.dockerignore Normal file
View File

@ -0,0 +1,5 @@
node_modules/
build/
coverage/
.vuepress/.temp/
.vuepress/.cache/

5
frontend/.env.dist Normal file
View File

@ -0,0 +1,5 @@
# META
PUBLIC_ENV__META__BASE_URL="http://localhost:3000"
PUBLIC_ENV__META__DEFAULT_AUTHOR="IT Team 4 Change"
PUBLIC_ENV__META__DEFAULT_DESCRIPTION="IT4C Frontend Boilerplate"
PUBLIC_ENV__META__DEFAULT_TITLE="IT4C"

5
frontend/.eslintignore Normal file
View File

@ -0,0 +1,5 @@
node_modules/
build/
coverage/
.storybook/
.vuepress/

182
frontend/.eslintrc.cjs Normal file
View File

@ -0,0 +1,182 @@
// eslint-disable-next-line import/no-commonjs
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: [
'standard',
'eslint:recommended',
'plugin:@eslint-community/eslint-comments/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'plugin:promise/recommended',
'plugin:security/recommended-legacy',
'plugin:vue/vue3-recommended',
'plugin:@intlify/vue-i18n/recommended',
'plugin:storybook/recommended',
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'import', 'promise', 'security', 'vue', 'storybook'],
settings: {
'import/resolver': {
typescript: true,
node: true,
},
'vue-i18n': {
localeDir: './src/locales/*.json',
},
},
rules: {
'no-console': 'error',
'no-debugger': 'error',
camelcase: 'error',
indent: ['error', 2],
'linebreak-style': ['error', 'unix'],
semi: ['error', 'never'],
// This makes sure our vike router does not throw errors
'vue/multi-word-component-names': [
'error',
{
ignores: ['+Page'],
},
],
// Optional eslint-comments rule
'@eslint-community/eslint-comments/no-unused-disable': 'error',
'@eslint-community/eslint-comments/disable-enable-pair': ['error', { allowWholeFile: true }],
// import
'import/export': 'error',
'import/no-deprecated': 'error',
'import/no-empty-named-blocks': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
'import/no-unused-modules': 'error',
'import/no-named-as-default': 'error',
'import/no-named-as-default-member': 'error',
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-import-module-exports': 'error',
'import/no-nodejs-modules': 'off',
'import/unambiguous': 'off', // not compatible with scriptless vue files
'import/default': 'error',
'import/named': 'error',
'import/namespace': 'error',
'import/no-absolute-path': 'error',
'import/no-cycle': 'error',
'import/no-dynamic-require': 'error',
'import/no-internal-modules': 'off',
'import/no-relative-packages': 'error',
'import/no-relative-parent-imports': [
'error',
{ ignore: ['#[src,root,components,pages,assets,layouts,stores,plugins,context,types]/*'] },
],
'import/no-self-import': 'error',
'import/no-unresolved': 'error',
'import/no-useless-path-segments': 'error',
'import/no-webpack-loader-syntax': 'error',
'import/consistent-type-specifier-style': 'error',
'import/exports-last': 'off',
'import/extensions': [
'error',
'never',
{
json: 'always',
},
],
'import/first': 'error',
'import/group-exports': 'off',
'import/newline-after-import': 'error',
'import/no-anonymous-default-export': 'off', // todo - consider to enable again
'import/no-default-export': 'off', // incompatible with vite & vike
'import/no-duplicates': 'error',
'import/no-named-default': 'error',
'import/no-namespace': 'error',
'import/no-unassigned-import': 'error',
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'newlines-between': 'always',
alphabetize: {
order: 'asc', // sort in ascending order. Options: ["ignore", "asc", "desc"]
caseInsensitive: true, // ignore case. Options: [true, false]
},
distinctGroup: true,
},
],
'import/prefer-default-export': 'off',
// promise
'promise/catch-or-return': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/always-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/avoid-new': 'warn',
'promise/no-new-statics': 'error',
'promise/no-return-in-finally': 'warn',
'promise/valid-params': 'warn',
'promise/prefer-await-to-callbacks': 'error',
'promise/no-multiple-resolved': 'error',
},
overrides: [
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json', '**/tsconfig.json'],
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
],
rules: {
// allow explicitly defined dangling promises
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
'no-void': ['error', { allowAsStatement: true }],
},
},
{
files: ['!*.json'],
plugins: ['prettier'],
extends: ['plugin:prettier/recommended'],
rules: {
'prettier/prettier': 'error',
},
},
{
files: ['*.json'],
plugins: ['json'],
extends: ['plugin:json/recommended-with-comments'],
},
{
files: ['*.vue'],
plugins: ['vuetify'],
extends: ['plugin:vuetify/recommended'],
},
{
files: ['*.[test,spec].[tj]s'],
plugins: ['vitest'],
extends: ['plugin:vitest/all'],
},
{
files: ['*.yaml', '*.yml'],
parser: 'yaml-eslint-parser',
plugins: ['yml'],
extends: ['plugin:yml/prettier'],
},
],
}

1
frontend/.github/.remarkignore vendored Normal file
View File

@ -0,0 +1 @@
*

13
frontend/.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,13 @@
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!--
Please take a look at the issue templates at https://github.com/[ORGA/USER]/[REPO]/issues/new/choose
before submitting a new issue. Following one of the issue templates will ensure maintainers can route your request efficiently.
Thanks!
-->
## 💬 Issue
<!-- Describe your Issue in detail. -->
<!-- Attach screenshots and drawings if needed. -->

10
frontend/.github/ISSUE_TEMPLATE/bug.md vendored Normal file
View File

@ -0,0 +1,10 @@
---
name: 🐛 Bug
about: Create a report to help us improve
labels: bug
title: 🐛 [Bug]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🐛 Bug
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the bug is.-->

View File

@ -0,0 +1,10 @@
---
name: 💥 DevOp
about: Help us manage our deployed Software.
labels: devops
title: 💥 [DevOps]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 💥 DevOps
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->

13
frontend/.github/ISSUE_TEMPLATE/epic.md vendored Normal file
View File

@ -0,0 +1,13 @@
---
name: 🌟 Epic
about: Define a big development Step
labels: epic
title: 🌟 [EPIC]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
## 🌟 EPIC
<!-- Describe your Epic in detail. Include screenshots and drawings -->

View File

@ -0,0 +1,10 @@
---
name: 🚀 Feature
about: Suggest an idea for this project
labels: feature
title: 🚀 [Feature]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🚀 Feature
<!-- Give a short summary of the Feature. Use Screenshots if you want. -->

View File

@ -0,0 +1,13 @@
---
name: 💬 Question
about: If you need help understanding our Software.
labels: question
title: 💬 [Question]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- Question the project's team -->
<!-- If you need an answer right away, consider to take other means of communication with the project's team -->
## 💬 Question
<!-- Describe your Question in detail. Include screenshots and drawings if needed. -->

View File

@ -0,0 +1,10 @@
---
name: 🔧 Refactor
about: Help us improve our code by refactoring it.
labels: refactor
title: 🔧 [Refactor]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🔧 Refactor
<!-- Describe your issue in detail. Include screenshots if needed. Give us as much information as possible. Use a clear and concise description of what the problem is.-->

View File

@ -0,0 +1,13 @@
---
name: 🎂 Release
about: Define a Release
labels: release
title: 🎂 [RELEASE]
---
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
<!-- THIS ISSUE-TYPE IS NOT FOR YOU! -->
<!-- Proceed only if you know what you are doing - have a chat with Project's Team first -->
## 🎂 RELEASE
<!-- Describe your Release in detail. Include screenshots and drawings -->

View File

@ -0,0 +1,15 @@
<!-- You can find the latest issue templates here https://github.com/ulfgebhardt/issue-templates -->
## 🍰 Pullrequest
<!-- Describe the Pullrequest. Use Screenshots if possible. -->
### Issues
<!-- Which Issues does this fix, which are related?
- fixes #XXX
- relates #XXX
-->
- None
### Todo
<!-- In case some parts are still missing, list them here. -->
- [X] None

26
frontend/.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,26 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
- package-ecosystem: npm
directory: "/"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
- package-ecosystem: docker
directory: "/"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"

17
frontend/.github/file-filters.yml vendored Normal file
View File

@ -0,0 +1,17 @@
# These file filter patterns are used by the action https://github.com/dorny/paths-filter
frontend-test-lint-code: &frontend-test-lint-code
- '**/*'
frontend-test-unit-code: &frontend-test-unit-code
- '**/*'
frontend-test-build-code: &frontend-test-build-code
- '**/*'
frontend-test-build-docs: &frontend-test-build-docs
- '**/*.md'
- '.vuepress/*'
frontend-test-build-storybook: &frontend-test-build-storybook
- '**/*'

View File

@ -0,0 +1,19 @@
name: "frontend:deploy:chromatic"
on:
push:
branches:
- master
jobs:
build-and-deploy:
name: Chromatic - Frontend
runs-on: ubuntu-latest
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Frontend | Build
run: npm install && npm run chromatic -- --exit-zero-on-changes

View File

@ -0,0 +1,21 @@
name: "frontend:deploy:docs to github"
on:
push:
branches:
- master
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@master
- name: vuepress-deploy
uses: jenkey2011/vuepress-deploy@master
env:
ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }}
#TARGET_REPO: username/repo
#TARGET_BRANCH: master
BUILD_SCRIPT: npm install && npm run docs:build
BUILD_DIR: build/docs/
VUEPRESS_BASE: "boilerplate-frontend"

View File

@ -0,0 +1,34 @@
name: "frontend:test:build test code"
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - frontend-test-build-code
runs-on: ubuntu-latest
outputs:
changes: ${{ steps.changes.outputs.frontend-test-build-code }}
steps:
- uses: actions/checkout@v4
- name: Check for frontend file changes
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build:
if: needs.files-changed.outputs.changes == 'true'
name: Build - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Frontend | Build
run: npm install && npm run build

View File

@ -0,0 +1,34 @@
name: "frontend:test:build test docs"
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - frontend-test-build-docs
runs-on: ubuntu-latest
outputs:
changes: ${{ steps.changes.outputs.frontend-test-build-docs }}
steps:
- uses: actions/checkout@v4
- name: Check for frontend file changes
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build:
if: needs.files-changed.outputs.changes == 'true'
name: Build Docs - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Frontend | Build Docs
run: npm install && npm run docs:build

View File

@ -0,0 +1,34 @@
name: "frontend:test:build test storybook"
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - frontend-test-build-storybook
runs-on: ubuntu-latest
outputs:
changes: ${{ steps.changes.outputs.frontend-test-build-storybook }}
steps:
- uses: actions/checkout@v4
- name: Check for frontend file changes
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
storybook:
if: needs.files-changed.outputs.changes == 'true'
name: Build Storybook - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Frontend | Build Storybook
run: npm install && npm run storybook:build

View File

@ -0,0 +1,34 @@
name: "frontend:test:lint code with defined linters"
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - frontend-test-lint-code
runs-on: ubuntu-latest
outputs:
changes: ${{ steps.changes.outputs.frontend-test-lint-code }}
steps:
- uses: actions/checkout@v4
- name: Check for frontend file changes
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
lint:
if: needs.files-changed.outputs.changes == 'true'
name: Lint - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Frontend | Lint
run: npm install && npm run test:lint

View File

@ -0,0 +1,34 @@
name: "frontend:test:unit test code with defined suites"
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - frontend-test-unit-code
runs-on: ubuntu-latest
outputs:
changes: ${{ steps.changes.outputs.frontend-test-unit-code }}
steps:
- uses: actions/checkout@v4
- name: Check for frontend file changes
uses: dorny/paths-filter@v3.0.0
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
unit:
if: needs.files-changed.outputs.changes == 'true'
name: Unit - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Frontend | Unit
run: npm install && npm run test:unit

View File

@ -0,0 +1,77 @@
name: "test:lint pull request CI"
on:
pull_request_target:
types:
- opened
- edited
- synchronize
permissions:
pull-requests: write
statuses: write
jobs:
main:
name: Validate PR title
runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps:
- uses: amannn/action-semantic-pull-request@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
# Configure which types are allowed (newline delimited).
# Default: https://github.com/commitizen/conventional-commit-types
#types: |
# fix
# feat
# Configure which scopes are allowed (newline delimited).
# Append a scope for each service here
scopes: |
frontend
docu
docker
release
workflow
other
# Configure that a scope must always be provided.
requireScope: true
# Configure which scopes (newline delimited) are disallowed in PR
# titles. For instance by setting # the value below, `chore(release):
# ...` and `ci(e2e,release): ...` will be rejected.
#disallowScopes: |
# release
# Configure additional validation for the subject based on a regex.
# This example ensures the subject doesn't start with an uppercase character.
subjectPattern: ^(?![A-Z]).+$
# If `subjectPattern` is configured, you can use this property to override
# the default error message that is shown when the pattern doesn't match.
# The variables `subject` and `title` can be used within the message.
subjectPatternError: |
The subject "{subject}" found in the pull request title "{title}"
didn't match the configured pattern. Please ensure that the subject
doesn't start with an uppercase character.
# If you use GitHub Enterprise, you can set this to the URL of your server
#githubBaseUrl: https://github.myorg.com/api/v3
# If the PR contains one of these labels (newline delimited), the
# validation is skipped.
# If you want to rerun the validation when labels change, you might want
# to use the `labeled` and `unlabeled` event triggers in your workflow.
#ignoreLabels: |
# bot
# ignore-semantic-pull-request
# If you're using a format for the PR title that differs from the traditional Conventional
# Commits spec, you can use these options to customize the parsing of the type, scope and
# subject. The `headerPattern` should contain a regex where the capturing groups in parentheses
# correspond to the parts listed in `headerPatternCorrespondence`.
# See: https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-commits-parser#headerpattern
headerPattern: '^(\w*)(?:\(([\w$.\-*/ ]*)\))?: (.*)$'
headerPatternCorrespondence: type, scope, subject
# For work-in-progress PRs you can typically use draft pull requests
# from GitHub. However, private repositories on the free plan don't have
# this option and therefore this action allows you to opt-in to using the
# special "[WIP]" prefix to indicate this state. This will avoid the
# validation of the PR title and the pull request checks remain pending.
# Note that a second check will be reported if this is enabled.
wip: true

9
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
node_modules/
build/
coverage/
!.storybook/
!.vuepress/
.vuepress/.temp/
.vuepress/.cache/
build-storybook.log
.env

14
frontend/.prettierrc.json Normal file
View File

@ -0,0 +1,14 @@
{
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"quoteProps": "as-needed",
"jsxSingleQuote": true,
"trailingComma": "all",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "auto"
}

16
frontend/.remarkrc.json Normal file
View File

@ -0,0 +1,16 @@
{
"plugins": [
"remark-gfm",
"remark-preset-lint-consistent",
"remark-preset-lint-markdown-style-guide",
"remark-preset-lint-recommended",
[
"remark-lint-maximum-line-length",
false
],
[
"remark-lint-list-item-indent",
"space"
]
]
}

View File

@ -0,0 +1,26 @@
<!-- .storybook/StoryWrapper.vue -->
<template>
<v-app :theme="themeName">
<v-main>
<slot name="story"></slot>
</v-main>
</v-app>
</template>
<script>
export const DEFAULT_THEME = 'light'
export default {
props: {
themeName: {
default: DEFAULT_THEME,
type: String,
},
},
}
</script>
<style>
.v-application .v-application__wrap {
min-height: unset;
}
</style>

View File

@ -0,0 +1,22 @@
import type { StorybookConfig } from '@storybook/vue3-vite'
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/vue3-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
core: {
disableTelemetry: true, // 👈 Disables telemetry
},
}
export default config

View File

@ -0,0 +1,54 @@
import { setup } from '@storybook/vue3'
import { createPinia } from 'pinia'
import { setPageContext } from '#context/usePageContext'
import i18n from '#plugins/i18n'
import CreateVuetify from '#plugins/vuetify'
import { withVuetifyTheme } from './withVuetifyTheme.decorator'
import type { Preview } from '@storybook/vue3'
setup((app) => {
// Registers your app's plugins into Storybook
const pinia = createPinia()
app.use(pinia)
app.use(i18n)
app.use(CreateVuetify(i18n))
setPageContext(app, { urlPathname: '' })
})
export const decorators = [withVuetifyTheme]
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'paintbrush',
// Array of plain string values or MenuItem shape
items: [
{ value: 'light', title: 'Light', left: '🌞' },
{ value: 'dark', title: 'Dark', left: '🌛' },
],
// Change title based on selected value
dynamicTitle: true,
},
},
}
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
}
export default preview

View File

@ -0,0 +1,20 @@
import { h } from 'vue'
import StoryWrapper, { DEFAULT_THEME } from './StoryWrapper.vue'
export const withVuetifyTheme = (storyFn, context) => {
// Pull our global theme variable, fallback to DEFAULT_THEME
const themeName = context.globals.theme || DEFAULT_THEME
const story = storyFn()
return () => {
return h(
StoryWrapper,
{ themeName }, // Props for StoryWrapper
{
// Puts your story into StoryWrapper's "story" slot with your story args
story: () => h(story, { ...context.args }),
},
)
}
}

View File

@ -0,0 +1,9 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-standard-scss",
"stylelint-config-recommended-vue",
"stylelint-config-recess-order",
"stylelint-config-css-modules"
]
}

View File

@ -0,0 +1,9 @@
import { defineUserConfig } from 'vuepress'
export default defineUserConfig({
title: 'IT4C Frontend Boilerplate Documentation',
description: 'IT4C Frontend Boilerplate Documentation',
dest: 'build/docs',
base: process.env.VUEPRESS_BASE ? `/${process.env.VUEPRESS_BASE}/` : '/',
pagePatterns: ['**/*.md', '**/LICENSE', '!.vuepress', '!node_modules'],
})

121
frontend/Dockerfile Normal file
View File

@ -0,0 +1,121 @@
FROM node:21-alpine3.17 as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
ENV DOCKER_WORKDIR="/app"
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
## We cannot do $(npm run version).${BUILD_NUMBER} here so we default to 0.0.0.0
ENV BUILD_VERSION="0.0.0.0"
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
ENV BUILD_COMMIT="0000000"
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
ENV PORT="3000"
# Labels
LABEL org.label-schema.build-date="${BUILD_DATE}"
LABEL org.label-schema.name="it4c:frontend"
LABEL org.label-schema.description="IT4C Frontend Boilerplate"
LABEL org.label-schema.usage="https://github.com/IT4Change/boilerplate-frontend/blob/master/README.md"
LABEL org.label-schema.url="https://github.com/IT4Change/boilerplate-frontend"
LABEL org.label-schema.vcs-url="https://github.com/IT4Change/boilerplate-frontend/tree/master/"
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
LABEL org.label-schema.vendor="IT4C"
LABEL org.label-schema.version="${BUILD_VERSION}"
LABEL org.label-schema.schema-version="1.0"
LABEL maintainer="info@it4c.dev"
# Install Additional Software
## install: node-gyp dependencies
# RUN apk --no-cache add g++ make python3
# Settings
## Expose Container Port
EXPOSE ${PORT}
## Workdir
RUN mkdir -p ${DOCKER_WORKDIR}
WORKDIR ${DOCKER_WORKDIR}
##################################################################################
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
##################################################################################
FROM base as development
# We don't need to copy or build anything since we gonna bind to the
# local filesystem which will need a rebuild anyway
# Run command
# (for development we need to execute npm install since the
# node_modules are on another volume and need updating)
CMD /bin/sh -c "npm install && npm run dev"
##################################################################################
# STORYBOOK ######################################################################
##################################################################################
FROM base as storybook
# We don't need to copy or build anything since we gonna bind to the
# local filesystem which will need a rebuild anyway
# Run command
# (for development we need to execute npm install since the
# node_modules are on another volume and need updating)
CMD /bin/sh -c "npm install && npm run storybook"
##################################################################################
# DOCUMENTATION ##################################################################
##################################################################################
FROM base as documentation
# We don't need to copy or build anything since we gonna bind to the
# local filesystem which will need a rebuild anyway
# Run command
# (for development we need to execute npm install since the
# node_modules are on another volume and need updating)
CMD /bin/sh -c "npm install && npm run docs:dev"
##################################################################################
# BUILD (Does contain all files and is therefore bloated) ########################
##################################################################################
FROM base as build
# Copy everything
COPY . .
# npm install
RUN npm install --frozen-lockfile --non-interactive
# npm build
RUN npm run build
##################################################################################
# TEST ###########################################################################
##################################################################################
#FROM build as test
# Install Additional Software
# RUN apk add --no-cache bash jq
# Run command
#CMD /bin/sh -c "yarn run dev"
##################################################################################
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
##################################################################################
FROM base as production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/build ./build
# Copy server
COPY --from=build ${DOCKER_WORKDIR}/server ./server
# Copy package.json & tsconfig.json
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
COPY --from=build ${DOCKER_WORKDIR}/package-lock.json ./package-lock.json
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
# Install production packages
RUN npm install --omit=dev --frozen-lockfile --non-interactive
# Run command
CMD /bin/sh -c "npm run server:prod"

201
frontend/LICENSE Normal file
View File

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2023 Ulf Gebhardt
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

197
frontend/README.md Normal file
View File

@ -0,0 +1,197 @@
# boilerplate-frontend
[![nodejs][badge-nodejs-img]][badge-nodejs-href]
[![npm][badge-npm-img]][badge-npm-href]
[![docker][badge-docker-img]][badge-docker-href]
[![jq][badge-jq-img]][badge-jq-href]
[![vue][badge-vue-img]][badge-vue-href]
[![vike][badge-vike-img]][badge-vike-href]
[![vuetify][badge-vuetify-img]][badge-vuetify-href]
[![pinia][badge-pinia-img]][badge-pinia-href]
[![vue-i18n][badge-vue-i18n-img]][badge-vue-i18n-href]
[![eslint][badge-eslint-img]][badge-eslint-href]
[![remark-cli][badge-remark-cli-img]][badge-remark-cli-href]
[![stylelint][badge-stylelint-img]][badge-stylelint-href]
[![vitest][badge-vitest-img]][badge-vitest-href]
[![storybook][badge-storybook-img]][badge-storybook-href]
[![vuepress][badge-vuepress-img]][badge-vuepress-href]
[![chromatic][badge-chromatic-img]][badge-chromatic-href]
The IT4C Boilerplate for frontends
![](src/assets/it4c-logo2-clean-bg_alpha-128x128.png)
## Requirements & Technology
To be able to build this project you need `nodejs`, `npm` and optional `docker` and `jq`.
The project uses `vite` as builder, `vike` to do the SSR. The design framework is `vuetify` which requires the frontend framework `vue3`. For localization `vue-i18n` is used; Session storage is handled with `pinia`.
Testing is done with `vitest` and code style is enforced with `eslint`, `remark-cli` and `stylelint`.
This projects utilizes `storybook` and `chromatic` to develop, document & test frontend components and `vuepress` for static documentation generation.
## Commands
The following commands are available:
| Command | Description |
|-----------------------------|--------------------------------------------------|
| `npm install` | Project setup |
| `npm run build` | Compiles and minifies for production |
| `npm run server:prod` | Runs productions server |
| **Develop** | |
| `npm run dev` | Compiles and hot-reloads for development |
| `npm run server:dev` | Run development server |
| `npm run server:prod:ts` | Run production server without build (ts-node) |
| `npm run server:build` | Build Server into an executable cjs file |
| **Test** | |
| `npm run test:lint` | Run all linters |
| `npm run test:lint:eslint` | Run linter eslint |
| `npm run test:lint:locales` | Run linter locales |
| `npm run test:lint:remark` | Run linter remark |
| `npm run test:lint:style` | Run linter stylelint |
| `npm run test:unit` | Run all unit tests and generate coverage report |
| `npm run test:unit:update` | Run unit tests, coverage and update snapshots |
| `npm run test:unit:dev` | Run all unit tests in watch mode |
| `npm test` | Run all tests & linters |
| **Storybook** | |
| `npm run storybook` | Run Storybook |
| `npm run storybook:build` | Build static storybook |
| `npm run storybook:test` | Run tests against all storybook stories |
| **Documentation** | |
| `npm run docs:dev` | Run Documentation in development mode |
| `npm run docs:build` | Build static documentation |
| **Chromatic** | |
| `npm run chromatic` | Run Chromatic. See Chromatic section for details |
| **Maintenance** | |
| `npm run update` | Check for updates |
### Docker
Docker can be run in development mode utilizing `docker-compose.overwrite.yml`:
```bash
docker compose up
```
Docker can be run in production mode:
```bash
docker compose -f docker-compose.yml up
```
### Chromatic
In order to use the chromatic workflow you need to provide a `CHROMATIC_PROJECT_TOKEN` in the repository secrets.
If you want to run chromatic from the command line you either have to provide this variable as well
```bash
export CHROMATIC_PROJECT_TOKEN=...
npm run chromatic
```
or you have to append it via parameter:
```bash
npm run chromatic -- --project-token=...
```
### Update
You can get a list of packes to update by running `npm run update`.
Appending `-u ` will also update the packages in the `package.json`. You have to run `npm install` again after.
```bash
npm run update -- -u
npm install
```
## Endpoints
The following endpoints are provided given the right command is executed or all three if `docker compose` is used:
| Endpoint | Description |
|------------------------------------------------|---------------|
| [http://localhost:3000](http://localhost:3000) | Web |
| [http://localhost:6006](http://localhost:6006) | Storybook |
| [http://localhost:8080](http://localhost:8080) | Documentation |
## How to use as part of a project
If you want to use this as part of a larger project, e.g. in conjunction with a backend also utilizing a boilerplate you cannot use the template mechanic provided by github for this repository.
You can use the following commands to include the whole git history of the boilerplate and be able to update according to changes to this repo using another remote.
```bash
git remote add xxx_boilerplate_frontend git@github.com:IT4Change/boilerplate-frontend.git
git fetch xxx_boilerplate_frontend
git merge -s ours --no-commit --allow-unrelated-histories xxx_boilerplate_frontend/master
git read-tree --prefix=xxx/ -u xxx_boilerplate_frontend/master
git commit -m "Imported boilerplate_frontend as a subtree under xxx/."
```
To update the subtree you can use
```bash
git subtree pull -P xxx/ xxx_boilerplate_frontend master
git commit -m "Updated boilerplate_frontend in subtree under xxx/."
```
Where `xxx` refers to the folder and product part you want to use the boilerplate in. This assumes that you might need several copies of the frontend boilerplate for you product.
This mechanic was taken from this [source](https://stackoverflow.com/questions/1683531/how-to-import-existing-git-repository-into-another/8396318#8396318)
## Known Problems
- [ ] [Image flicker](https://github.com/vuetifyjs/vuetify/issues/18772)
- [ ] [Black Buttons](https://github.com/vuetifyjs/vuetify/issues/18773)
## License
[Apache 2.0](./LICENSE)
<!-- Badges -->
[badge-nodejs-img]: https://img.shields.io/badge/nodejs-%3E%3D20.5.0-blue
[badge-nodejs-href]: https://nodejs.org/
[badge-npm-img]: https://img.shields.io/badge/npm-latest-blue
[badge-npm-href]: https://www.npmjs.com/package/npm
[badge-docker-img]: https://img.shields.io/badge/docker-latest-blue
[badge-docker-href]: https://www.docker.com/
[badge-jq-img]: https://img.shields.io/badge/jq-latest-blue
[badge-jq-href]: https://jqlang.github.io/jq/
[badge-vue-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=dependencies.vue&label=vue&color=green
[badge-vue-href]: https://vuejs.org/
[badge-vike-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=dependencies.vike&label=vike&color=green
[badge-vike-href]: https://vike.dev/
[badge-vuetify-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=dependencies.vuetify&label=vuetify&color=green
[badge-vuetify-href]: https://vuetifyjs.com/
[badge-pinia-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=dependencies.pinia&label=pinia&color=green
[badge-pinia-href]: https://pinia.vuejs.org/
[badge-vue-i18n-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=dependencies%5B%27vue-i18n%27%5D&label=vue-i18n&color=green
[badge-vue-i18n-href]: https://vue-i18n.intlify.dev/
[badge-eslint-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=devDependencies.eslint&label=eslint&color=yellow
[badge-eslint-href]: https://eslint.org/
[badge-remark-cli-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=devDependencies%5B%27remark-cli%27%5D&label=remark-cli&color=yellow
[badge-remark-cli-href]: https://remark.js.org/
[badge-stylelint-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=devDependencies.stylelint&label=stylelint&color=yellow
[badge-stylelint-href]: https://stylelint.io/
[badge-vitest-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=devDependencies.vitest&label=vitest&color=yellow
[badge-vitest-href]: https://vitest.dev/
[badge-storybook-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=devDependencies.storybook&label=storybook&color=orange
[badge-storybook-href]: https://storybook.js.org/
[badge-vuepress-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=devDependencies.vuepress&label=vuepress&color=orange
[badge-vuepress-href]: https://vuepress.vuejs.org/
[badge-chromatic-img]: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fraw.githubusercontent.com%2FIT4Change%2Fboilerplate-frontend%2Fmaster%2Fpackage.json&query=devDependencies.chromatic&label=chromatic&color=orange
[badge-chromatic-href]: https://www.chromatic.com/

View File

@ -0,0 +1,64 @@
version: '3.4'
services:
# ######################################################
# FRONTEND #############################################
# ######################################################
frontend:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: it4c/frontend:local-development
build:
target: development
environment:
- NODE_ENV="development"
# - DEBUG=true
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- frontend_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./:/app
# ######################################################
# STORYBOOK ############################################
# ######################################################
storybook:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: it4c/frontend:local-storybook
build:
target: storybook
environment:
- NODE_ENV="development"
# - DEBUG=true
ports:
- 6006:6006
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- storybook_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./:/app
# ######################################################
# DOCUMENTATION ########################################
# ######################################################
documentation:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: it4c/frontend:local-documentation
build:
target: documentation
environment:
- NODE_ENV="development"
# - DEBUG=true
ports:
- 8080:8080
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- documentation_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./:/app
volumes:
frontend_node_modules:
storybook_node_modules:
documentation_node_modules:

View File

@ -0,0 +1,33 @@
# This file defines the production settings. It is overwritten by docker-compose.override.yml,
# which defines the development settings. The override.yml is loaded by default. Therefore it
# is required to explicitly define if you want an production build:
# > docker-compose -f docker-compose.yml up
version: '3.4'
services:
frontend:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: it4c/frontend:local-production
build:
context: .
target: production
networks:
- external-net
- internal-net
ports:
- 3000:3000
environment:
# Envs used in Dockerfile
# - DOCKER_WORKDIR="/app"
# - PORT=3000
# - BUILD_DATE="1970-01-01T00:00:00.00Z"
# - BUILD_VERSION="0.0.0.0"
# - BUILD_COMMIT="0000000"
- NODE_ENV="production"
# env_file:
# - ./.env
# - ./frontend/.env
networks:
external-net:
internal-net:
internal: true

35830
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

155
frontend/package.json Normal file
View File

@ -0,0 +1,155 @@
{
"name": "boilerplate-frontend",
"version": "1.0.0",
"description": "The IT4C Boilerplate for frontends",
"main": "build/index.js",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/IT4Change/boilerplate-frontend.git"
},
"keywords": [
"nodejs",
"npm",
"docker",
"jq",
"vue",
"vike",
"vuetify",
"pinia",
"vue-i18n",
"eslint",
"remark-cli",
"stylelint",
"vitest",
"storybook",
"vuepress",
"chromatic"
],
"author": {
"name": "Ulf Gebhardt"
},
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/IT4Change/boilerplate-frontend/issues"
},
"homepage": "https://github.com/IT4Change/boilerplate-frontend#readme",
"scripts": {
"dev": "npm run server:dev",
"prod": "npm run build && npm run server:prod",
"build": "vite build && npm run server:build",
"server": "node --loader ts-node/esm ./server/index.ts",
"server:dev": "npm run server",
"server:prod": "node ./build/index.cjs",
"server:prod:ts": "cross-env NODE_ENV=production npm run server",
"server:build": "tsx scripts/buildServer/buildServer",
"storybook": "storybook dev -p 6006",
"storybook:build": "storybook build -o build/storybook",
"storybook:test": "test-storybook",
"test:lint": "npm run test:lint:eslint && npm run test:lint:remark && npm run test:lint:style && npm run test:lint:locales",
"test:lint:eslint": "eslint --ext .vue,.ts,.tsx,.js,.jsx,.cjs,.mjs,.json,.yml,.yaml --max-warnings 0 .",
"test:lint:locales": "scripts/locales/locales.sh src/locales",
"test:lint:remark": "remark . --quiet --frail",
"test:lint:style": "stylelint --max-warnings 0 --ignore-path .gitignore \"**/*.{css,scss,vue,vuex}\"",
"test:unit": "npm run test:unit:dev -- run --coverage",
"test:unit:update": "npm run test:unit:dev -- run --coverage -u",
"test:unit:dev": "vitest",
"test": "npm run test:lint && npm run test:unit",
"docs:dev": "vuepress dev .",
"docs:build": "vuepress build .",
"chromatic": "npx chromatic --build-script-name storybook:build",
"update": "npx npm-check-updates"
},
"dependencies": {
"@intlify/unplugin-vue-i18n": "^2.0.0",
"@mdi/font": "^7.4.47",
"@types/compression": "^1.7.5",
"@types/express": "^4.17.21",
"@types/node": "^20.11.8",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/compiler-sfc": "^3.4.15",
"@vue/server-renderer": "^3.4.15",
"compression": "^1.7.4",
"cross-env": "^7.0.3",
"express": "^4.18.2",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
"sass": "^1.70.0",
"sass-loader": "^14.0.0",
"sirv": "^2.0.4",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vike": "^0.4.160",
"vite": "^5.0.12",
"vue": "^3.4.15",
"vue-i18n": "^9.9.0",
"vuetify": "^3.5.1"
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.1.0",
"@intlify/eslint-plugin-vue-i18n": "^2.0.0",
"@storybook/addon-essentials": "^7.6.10",
"@storybook/addon-interactions": "^7.6.10",
"@storybook/addon-links": "^7.6.10",
"@storybook/blocks": "^7.6.10",
"@storybook/test-runner": "^0.16.0",
"@storybook/testing-library": "^0.2.2",
"@storybook/vue3": "^7.6.10",
"@storybook/vue3-vite": "^7.6.10",
"@typescript-eslint/eslint-plugin": "^6.19.1",
"@typescript-eslint/parser": "^6.19.1",
"@vitest/coverage-v8": "^1.2.2",
"@vue/test-utils": "^2.4.4",
"@vuepress/bundler-vite": "^2.0.0-rc.2",
"chromatic": "^10.6.1",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^17.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-json": "^3.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-security": "^2.1.0",
"eslint-plugin-storybook": "^0.6.15",
"eslint-plugin-vitest": "^0.3.20",
"eslint-plugin-vue": "^9.20.1",
"eslint-plugin-vuetify": "^2.1.1",
"eslint-plugin-yml": "^1.12.2",
"happy-dom": "^13.3.1",
"prettier": "^3.2.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remark-cli": "^12.0.0",
"remark-gfm": "^4.0.0",
"remark-preset-lint-consistent": "^5.1.2",
"remark-preset-lint-markdown-style-guide": "^5.1.3",
"remark-preset-lint-recommended": "^6.1.3",
"storybook": "^7.6.10",
"stylelint": "^16.2.0",
"stylelint-config-css-modules": "^4.4.0",
"stylelint-config-recess-order": "^4.4.0",
"stylelint-config-recommended-vue": "^1.5.0",
"stylelint-config-standard": "^36.0.0",
"stylelint-config-standard-scss": "^13.0.0",
"tsx": "^4.7.0",
"vite-plugin-checker": "^0.6.2",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-vuetify": "^2.0.1",
"vitest": "^1.2.2",
"vue-tsc": "^1.8.27",
"vuepress": "^2.0.0-rc.0"
},
"imports": {
"#components/*": "./src/components/*",
"#pages/*": "./src/pages/*",
"#assets/*": "./src/assets/*",
"#layouts/*": "./src/layouts/*",
"#stores/*": "./src/stores/*",
"#src/*": "./src/*",
"#plugins/*": "./renderer/plugins/*",
"#context/*": "./renderer/context/*",
"#types/*": "./types/*",
"#root/*": "./*"
}
}

View File

@ -0,0 +1,16 @@
// See https://vike.dev/data-fetching
export default {
clientRouting: true,
prefetchStaticAssets: 'viewport',
passToClient: ['pageProps', /* 'urlPathname', */ 'routeParams'],
meta: {
title: {
// Make the value of `title` available on both the server- and client-side
env: { server: true, client: true },
},
description: {
// Make the value of `description` available only on the server-side
env: { server: true },
},
},
}

View File

@ -0,0 +1,18 @@
import { PageContext } from 'vike/types'
import { createApp } from './app'
import { getTitle } from './utils'
let instance: ReturnType<typeof createApp>
/* async */ function render(pageContext: PageContext) {
if (!instance) {
instance = createApp(pageContext)
instance.app.mount('#app')
} else {
instance.app.changePage(pageContext)
}
document.title = getTitle(pageContext)
}
export default render

View File

@ -0,0 +1,74 @@
import { renderToString as renderToString_ } from '@vue/server-renderer'
import { escapeInject, dangerouslySkipEscape } from 'vike/server'
import { PageContext, PageContextServer } from 'vike/types'
import logoUrl from '#assets/favicon.ico'
import image from '#assets/it4c-logo2-clean-bg_alpha-128x128.png'
import { META } from '#src/env'
import { createApp } from './app'
import { getDescription, getTitle } from './utils'
import type { App } from 'vue'
async function render(pageContext: PageContextServer & PageContext) {
const { app, i18n } = createApp(pageContext, false)
const locale = i18n.global.locale.value
const appHtml = await renderToString(app)
// See https://vike.dev/head
const title = getTitle(pageContext)
const description = getDescription(pageContext)
const documentHtml = escapeInject`<!DOCTYPE html>
<html lang="${locale}">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="${logoUrl}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="${description}" />
<meta name="author" content="${META.DEFAULT_AUTHOR}">
<meta property="og:title" content="${title}" />
<meta property="og:type" content="website" />
<meta property="og:url" content="${META.BASE_URL}" />
<meta property="og:description" content="${description}" />
<meta property="og:image" content="${META.BASE_URL}${image}" />
<meta property="og:image:alt" content="${title}" />
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="601"/>
<meta name="twitter:card" content="summary_large_image" />
<!--<meta name="twitter:site" content="@YourTwitterUsername" />-->
<meta name="twitter:title" content="${title}" />
<meta name="twitter:text:title" content="${title}" />
<meta name="twitter:description" content="${description}" />
<meta name="twitter:image" content="${META.BASE_URL}${image}" />
<meta name="twitter:image:alt" content="${title}" />
<title>${title}</title>
</head>
<body>
<div id="app">${dangerouslySkipEscape(appHtml)}</div>
</body>
</html>`
return {
documentHtml,
pageContext: {
// We can add some `pageContext` here, which is useful if we want to do page redirection https://vike.dev/page-redirection
},
}
}
async function renderToString(app: App) {
let err: unknown
// Workaround: renderToString_() swallows errors in production, see https://github.com/vuejs/core/issues/7876
app.config.errorHandler = (err_) => {
err = err_
}
const appHtml = await renderToString_(app)
if (err) throw err
return appHtml
}
export default render

71
frontend/renderer/app.ts Normal file
View File

@ -0,0 +1,71 @@
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import { PageContext } from 'vike/types'
import { createSSRApp, defineComponent, h, markRaw, reactive, Component } from 'vue'
import PageShell from '#components/PageShell.vue'
import { setPageContext } from '#context/usePageContext'
import i18n from '#plugins/i18n'
import pinia from '#plugins/pinia'
import CreateVuetify from '#plugins/vuetify'
const vuetify = CreateVuetify(i18n)
function createApp(pageContext: PageContext, isClient = true) {
// eslint-disable-next-line no-use-before-define
let rootComponent: InstanceType<typeof PageWithWrapper>
const PageWithWrapper = defineComponent({
data: () => ({
Page: markRaw(pageContext.Page),
pageProps: markRaw(pageContext.pageProps || {}),
isClient,
}),
created() {
// eslint-disable-next-line @typescript-eslint/no-this-alias
rootComponent = this
},
render() {
return h(
PageShell as Component,
{},
{
default: () => {
return h(this.Page, this.pageProps)
},
},
)
},
})
if (isClient) {
pinia.use(piniaPluginPersistedstate)
}
const app = createSSRApp(PageWithWrapper)
app.use(pinia)
app.use(i18n)
app.use(vuetify)
objectAssign(app, {
changePage: (pageContext: PageContext) => {
Object.assign(pageContextReactive, pageContext)
rootComponent.Page = markRaw(pageContext.Page)
rootComponent.pageProps = markRaw(pageContext.pageProps || {})
},
})
const pageContextReactive = reactive(pageContext)
setPageContext(app, pageContextReactive)
return { app, i18n }
}
// Same as `Object.assign()` but with type inference
function objectAssign<Obj extends object, ObjAddendum>(
obj: Obj,
objAddendum: ObjAddendum,
): asserts obj is Obj & ObjAddendum {
Object.assign(obj, objAddendum)
}
export { createApp }

View File

@ -0,0 +1,22 @@
// `usePageContext` allows us to access `pageContext` in any Vue component.
// See https://vike.dev/pageContext-anywhere
import { PageContext } from 'vike/types'
import { inject } from 'vue'
import type { App, InjectionKey } from 'vue'
export const vikePageContext: InjectionKey<PageContext> = Symbol('pageContext')
function usePageContext() {
const pageContext = inject(vikePageContext)
if (!pageContext) throw new Error('setPageContext() not called in parent')
return pageContext
}
function setPageContext(app: App, pageContext: PageContext) {
app.provide(vikePageContext, pageContext)
}
export { usePageContext }
export { setPageContext }

View File

@ -0,0 +1,14 @@
import { createI18n } from 'vue-i18n'
import de from '#src/locales/de.json'
// import { de as $vuetify } from 'vuetify/locale'
import en from '#src/locales/en.json'
// import { en as $vuetify } from 'vuetify/locale'
export default createI18n({
legacy: false, // Vuetify does not support the legacy mode of vue-i18n
globalInjection: true,
locale: 'de',
fallbackLocale: 'en',
messages: { de, en },
})

View File

@ -0,0 +1,4 @@
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia

View File

@ -0,0 +1,16 @@
// eslint-disable-next-line import/no-unassigned-import
import '@mdi/font/css/materialdesignicons.css'
// eslint-disable-next-line import/no-unassigned-import
import 'vuetify/styles'
import { I18n, useI18n } from 'vue-i18n'
import { createVuetify } from 'vuetify'
import { createVueI18nAdapter } from 'vuetify/locale/adapters/vue-i18n'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default (i18n: I18n<any, NonNullable<unknown>, NonNullable<unknown>, string, false>) =>
createVuetify({
locale: {
adapter: createVueI18nAdapter({ i18n, useI18n }),
},
ssr: true,
})

View File

@ -0,0 +1,19 @@
import { PageContext } from 'vike/types'
import { META } from '#src/env'
function getTitle(pageContext: PageContext) {
// The value exported by /pages/**/+title.js is available at pageContext.config.title
const val = pageContext.config.title
if (typeof val === 'string') return val
if (typeof val === 'function') return String(val(pageContext))
return META.DEFAULT_TITLE
}
function getDescription(pageContext: PageContext) {
const val = pageContext.config.description
if (typeof val === 'string') return val
if (typeof val === 'function') return val(pageContext)
return META.DEFAULT_DESCRIPTION
}
export { getTitle, getDescription }

View File

@ -0,0 +1,56 @@
import path from 'node:path'
import { fileURLToPath } from 'url'
// eslint-disable-next-line import/no-extraneous-dependencies
import { build } from 'esbuild'
// eslint-disable-next-line import/no-extraneous-dependencies
import fs, { ensureDir, remove } from 'fs-extra'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
async function buildServer() {
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [path.join(path.resolve(__dirname, '../../server/'), 'index.ts')],
outfile: 'index.cjs',
write: false,
minify: true,
platform: 'node',
bundle: true,
format: 'cjs',
sourcemap: false,
treeShaking: true,
define: { 'import.meta.url': 'importMetaUrl', 'process.env.NODE_ENV': '"production"' },
inject: [path.resolve(__dirname, './import.meta.url-polyfill.ts')],
banner: {
js: `/* eslint-disable prettier/prettier */`,
},
tsconfig: path.resolve(__dirname, './tsconfig.buildServer.json'),
plugins: [
{
name: 'externalize-deps',
setup(build) {
build.onResolve({ filter: /.*/ }, (args) => {
const id = args.path
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true,
}
}
})
},
},
],
})
const { text } = result.outputFiles[0]
const filePath = path.join(path.resolve(__dirname, '../../build/'), 'index.cjs')
if (fs.existsSync(filePath)) {
await remove(filePath)
}
await ensureDir(path.dirname(filePath))
// eslint-disable-next-line import/no-named-as-default-member
await fs.writeFile(filePath, text)
}
void buildServer()

View File

@ -0,0 +1,7 @@
/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any */
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
export const importMetaUrl =
typeof document === 'undefined'
? (new (require('url').URL)('file:' + __filename) as URL).href
: (document.currentScript && (document.currentScript as any).src) ||
new URL('main.js', document.baseURI).href

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"baseUrl": ".",
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"isolatedModules": true,
"lib": ["esnext", "dom", "DOM.Iterable"],
"strict": false,
"sourceMap": false,
"resolveJsonModule": true,
"skipLibCheck": true,
"esModuleInterop": true,
"declaration": false
}
}

View File

@ -0,0 +1 @@
path(..)|[.[]|tostring]|join(".")

View File

@ -0,0 +1,44 @@
#!/bin/bash
if [ $# -eq 0 ]
then
echo "You have to supply at least one argument specifying the folder to lint"
fi
FILES="$1"
tmp=$(mktemp)
exit_code=0
for locale_file in $FILES/*.json
do
jq -f $(dirname "$0")/sort.jq $locale_file > "$tmp"
# check sort order and fix it if required
if [ "$2" == "--fix" ]
then
mv "$tmp" $locale_file
else
if diff -q "$tmp" $locale_file > /dev/null ;
then
: # all good
else
exit_code=$?
echo "$(basename -- $locale_file) is not sorted by keys"
fi
fi
# check keys
if [ -n "$LAST_FILE" ]; then
listPaths="jq -f $(dirname "$0")/keys.jq"
diffString="<( cat $LAST_FILE | $listPaths ) <( cat $locale_file | $listPaths )"
if eval "diff -q $diffString";
then
: # all good
else
eval "diff -y $diffString | grep '[|<>]'";
printf "\n$LAST_FILE\" and $locale_file translation keys do not match, see diff above.\n"
exit_code=1
fi
fi
LAST_FILE=$locale_file
done
exit $exit_code

View File

@ -0,0 +1,13 @@
def walk(f):
. as $in
| if type == "object" then
reduce keys_unsorted[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
def keys_sort_by(f):
to_entries | sort_by(.key|f ) | from_entries;
walk(if type == "object" then keys_sort_by(ascii_upcase) else . end)

View File

@ -0,0 +1,6 @@
import { config } from '@vue/test-utils'
config.global.mocks = {
...config.global.mocks,
$t: (tKey: string) => "$t('" + tKey + "')", // just return translation key
}

View File

@ -0,0 +1,8 @@
import { config } from '@vue/test-utils'
import { vikePageContext } from '#context/usePageContext'
config.global.provide = {
...config.global.provide,
[vikePageContext as symbol]: { urlPathname: '/some-url' },
}

View File

@ -0,0 +1,14 @@
import { config } from '@vue/test-utils'
import i18n from '#plugins/i18n'
import vuetify from '#plugins/vuetify'
config.global.plugins.push(i18n)
config.global.plugins.push(vuetify(i18n))
config.global.mocks = {
...config.global.mocks,
i18n$t: i18n.global.t,
i18n$d: i18n.global.d,
i18n$n: i18n.global.n,
}

View File

@ -0,0 +1,5 @@
import { config } from '@vue/test-utils'
import pinia from '#plugins/pinia'
config.global.plugins.push(pinia)

97
frontend/server/index.ts Normal file
View File

@ -0,0 +1,97 @@
// This file isn't processed by Vite, see https://github.com/vikejs/vike/issues/562
// Consequently:
// - When changing this file, you needed to manually restart your server for your changes to take effect.
// - To use your environment variables defined in your .env files, you need to install dotenv, see https://vike.dev/env
// - To use your path aliases defined in your vite.config.js, you need to tell Node.js about them, see https://vike.dev/path-aliases
// If you want Vite to process your server code then use one of these:
// - vavite (https://github.com/cyco130/vavite)
// - See vavite + Vike examples at https://github.com/cyco130/vavite/tree/main/examples
// - vite-node (https://github.com/antfu/vite-node)
// - HatTip (https://github.com/hattipjs/hattip)
// - You can use Bati (https://batijs.github.io/) to scaffold a Vike + HatTip app. Note that Bati generates apps that use the V1 design (https://vike.dev/migration/v1-design) and Vike packages (https://vike.dev/vike-packages)
import compression from 'compression'
import express from 'express'
import { renderPage } from 'vike/server'
import { root } from './root.js'
const isProduction = process.env.NODE_ENV === 'production'
void startServer()
async function startServer() {
const app = express()
// Vite integration
if (isProduction) {
// In production, we need to serve our static assets ourselves.
// (In dev, Vite's middleware serves our static assets.)
const sirv = (await import('sirv')).default
// assets 1y caching
app.use(
'/assets',
sirv(`${root}/build/client/assets`, {
maxAge: 31536000, // 1Y
immutable: true,
gzip: true,
}),
)
// cache things for 10min
app.use(
sirv(`${root}/build/client`, {
maxAge: 600,
immutable: true,
gzip: true,
}),
)
} else {
// We instantiate Vite's development server and integrate its middleware to our server.
// ⚠️ We instantiate it only in development. (It isn't needed in production and it
// would unnecessarily bloat our production server.)
const vite = await import('vite')
const viteDevMiddleware = (
await vite.createServer({
root,
server: { middlewareMode: true },
})
).middlewares
app.use(viteDevMiddleware)
// on the fly compression
app.use(compression())
}
// ...
// Other middlewares (e.g. some RPC middleware such as Telefunc)
// ...
// Vike middleware. It should always be our last middleware (because it's a
// catch-all middleware superseding any middleware placed after it).
app.get('*', (req, res, next) => {
void (async (req, res, next) => {
const pageContextInit = {
urlOriginal: req.originalUrl,
}
const pageContext = await renderPage(pageContextInit)
const { httpResponse } = pageContext
if (!httpResponse) {
next()
} else {
const { body, statusCode, headers, earlyHints } = httpResponse
if (res.writeEarlyHints)
res.writeEarlyHints({ link: earlyHints.map((e) => e.earlyHintLink) })
headers.forEach(([name, value]) => res.setHeader(name, value))
res.status(statusCode)
// For HTTP streams use httpResponse.pipe() instead, see https://vike.dev/stream
res.send(body)
}
})(req, res, next)
})
const port = process.env.PORT || 3000
app.listen(port)
// eslint-disable-next-line no-console
console.log(`🚀 Server running at http://localhost:${port}`)
}

8
frontend/server/root.ts Normal file
View File

@ -0,0 +1,8 @@
// https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-when-using-the-experimental-modules-flag/50052194#50052194
import { dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const root = `${__dirname}/..`
export { root }

View File

@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
// Make IDEs complain about missing file extension .js in import paths.
// Alternatively, we could always set "module" to "Node16" and add the file extension .js to import paths everywhere.
"compilerOptions": {
"module": "Node16",
"moduleResolution": "Node16"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1 @@
/* stylelint-disable no-empty-source */

View File

@ -0,0 +1,12 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import ClientOnly from './ClientOnly.vue'
describe('ClientOnly', () => {
const wrapper = mount(ClientOnly)
it('renders content if mounted', () => {
expect(wrapper.isVisible()).toBeTruthy()
})
})

View File

@ -0,0 +1,13 @@
<template>
<template v-if="isMounted"><slot /></template>
<template v-else><slot name="placeholder" /></template>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
</script>

View File

@ -0,0 +1,16 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import PageShell from './PageShell.vue'
describe('PageShell', () => {
const wrapper = mount(PageShell, {
slots: {
default: 'Page Content',
},
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
})

View File

@ -0,0 +1,10 @@
<template>
<v-app>
<TopMenu />
<slot />
</v-app>
</template>
<script lang="ts" setup>
import TopMenu from '#components/menu/TopMenu.vue'
</script>

View File

@ -0,0 +1,56 @@
import { mount } from '@vue/test-utils'
import { navigate } from 'vike/client/router'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import VikeBtn from './VikeBtn.vue'
vi.mock('vike/client/router')
vi.mocked(navigate).mockResolvedValue()
describe('VikeBtn', () => {
const Wrapper = () => {
return mount(VikeBtn, {
attrs: { href: '/some-path' },
})
}
let wrapper: ReturnType<typeof Wrapper>
beforeEach(() => {
wrapper = Wrapper()
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
it('icon is hidden', () => {
expect(wrapper.find('.v-icon').exists()).toBe(false)
})
describe('with href attribute app', () => {
beforeEach(async () => {
await wrapper.setProps({ href: '/app' } as Partial<object>)
})
it('has flat variant', () => {
expect(wrapper.classes()).toContain('v-btn--variant-flat')
})
})
describe('with same href attribute', () => {
beforeEach(async () => {
await wrapper.setProps({ href: '/some-url' } as Partial<object>)
})
it('has tonal variant', () => {
expect(wrapper.classes()).toContain('v-btn--variant-tonal')
})
})
describe('click on button', () => {
it('calls navigate method with given href', async () => {
await wrapper.find('.v-btn').trigger('click')
expect(navigate).toHaveBeenCalledWith('/some-path')
})
})
})

View File

@ -0,0 +1,26 @@
<template>
<v-btn
:variant="isRouteSelected($attrs.href as string) ? 'tonal' : 'flat'"
@click.prevent="onClick($attrs.href as string)"
>
<slot />
</v-btn>
</template>
<script lang="ts" setup>
import { navigate } from 'vike/client/router'
import { usePageContext } from '#context/usePageContext'
const pageContext = usePageContext()
function onClick(href: string) {
return navigate(href)
}
const isRouteSelected = (href: string) => {
if (href === '/app') {
return pageContext.urlPathname.indexOf(href) === 0
}
return pageContext.urlPathname === href
}
</script>

View File

@ -0,0 +1,275 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`PageShell > renders 1`] = `
<div
class="v-application v-theme--light v-layout v-layout--full-height v-locale--is-ltr"
>
<div
class="v-application__wrap"
>
<header
class="v-toolbar v-toolbar--flat v-toolbar--density-default v-theme--light v-locale--is-ltr v-app-bar"
style="top: 0px; z-index: 1004; transform: translateY(0%); position: fixed; left: 0px; width: calc(100% - 0px - 0px);"
>
<!---->
<div
class="v-toolbar__content"
style="height: 64px;"
>
<!---->
<!---->
<div
class="v-row"
>
<div
class="v-col"
>
<div
class="v-avatar v-theme--light v-avatar--density-default v-avatar--variant-flat ma-2 pa-1"
style="background-color: #333; color: #fff; caret-color: #fff; width: 48px; height: 48px;"
>
<div
aria-label=""
class="v-responsive v-img v-img--booting"
>
<div
class="v-responsive__sizer"
/>
<transition-stub
appear="true"
css="true"
name="fade-transition"
persisted="false"
>
<img
alt=""
class="v-img__img v-img__img--cover"
src="/src/assets/it4c-logo2-clean-bg_alpha-128x128.png"
style="display: none;"
/>
</transition-stub>
<transition-stub
appear="false"
css="true"
name="fade-transition"
persisted="false"
>
<!---->
</transition-stub>
<!---->
<!---->
<!---->
<!---->
</div>
<!---->
<span
class="v-avatar__underlay"
/>
</div>
</div>
<div
class="v-col d-flex align-center justify-center grow"
>
<a
class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-flat"
href="/"
>
<span
class="v-btn__overlay"
/>
<span
class="v-btn__underlay"
/>
<!---->
<span
class="v-btn__content"
data-no-activator=""
>
$t('menu.home')
</span>
<!---->
<!---->
</a>
<a
class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-flat"
href="/app"
>
<span
class="v-btn__overlay"
/>
<span
class="v-btn__underlay"
/>
<!---->
<span
class="v-btn__content"
data-no-activator=""
>
$t('menu.app')
</span>
<!---->
<!---->
</a>
<a
class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-flat"
href="/about"
>
<span
class="v-btn__overlay"
/>
<span
class="v-btn__underlay"
/>
<!---->
<span
class="v-btn__content"
data-no-activator=""
>
$t('menu.about')
</span>
<!---->
<!---->
</a>
</div>
<div
class="v-col"
>
<div
class="v-input v-input--horizontal v-input--center-affix v-input--density-default v-locale--is-ltr v-input--dirty v-switch d-flex justify-end mr-5"
>
<!---->
<div
class="v-input__control"
>
<div
class="v-selection-control v-selection-control--dirty v-selection-control--density-default"
>
<div
class="v-selection-control__wrapper text-success"
>
<div
class="v-switch__track bg-success"
>
<!---->
<!---->
</div>
<div
class="v-selection-control__input"
>
<input
aria-describedby="switch-4-messages"
aria-disabled="false"
id="switch-4"
type="checkbox"
value="true"
/>
<div
class="v-switch__thumb bg-success"
>
<transition-stub
appear="false"
css="true"
name="scale-transition"
persisted="false"
>
<!---->
</transition-stub>
</div>
</div>
</div>
<label
class="v-label v-label--clickable"
for="switch-4"
>
<!---->
$t('language.german')
</label>
</div>
</div>
<!---->
<div
class="v-input__details"
>
<transition-group-stub
appear="false"
aria-live="polite"
class="v-messages"
css="true"
id="switch-4-messages"
name="slide-y-transition"
persisted="false"
role="alert"
tag="div"
>
<!---->
</transition-group-stub>
<!---->
</div>
</div>
</div>
</div>
<!---->
</div>
<transition-stub
appear="false"
css="true"
name="expand-transition"
persisted="false"
>
<!---->
</transition-stub>
</header>
Page Content
</div>
</div>
`;

View File

@ -0,0 +1,31 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`VikeBtn > renders 1`] = `
<a
class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-flat"
href="/some-path"
>
<span
class="v-btn__overlay"
/>
<span
class="v-btn__underlay"
/>
<!---->
<span
class="v-btn__content"
data-no-activator=""
>
</span>
<!---->
<!---->
</a>
`;

View File

@ -0,0 +1,20 @@
import { SBComp } from '#types/SBComp'
import LogoAvatar from './LogoAvatar.vue'
import type { Meta, StoryObj } from '@storybook/vue3'
const meta = {
title: 'Menu/LogoAvatar',
component: LogoAvatar as SBComp,
tags: ['autodocs'],
argTypes: {},
args: {},
} satisfies Meta<typeof LogoAvatar>
export default meta
type Story = StoryObj<typeof meta>
export const Example: Story = {
args: {},
}

View File

@ -0,0 +1,19 @@
import { mount } from '@vue/test-utils'
import { beforeEach, expect, describe, it } from 'vitest'
import LogoAvatar from './LogoAvatar.vue'
describe('LogoAvatar', () => {
const Wrapper = () => {
return mount(LogoAvatar)
}
let wrapper: ReturnType<typeof Wrapper>
beforeEach(() => {
wrapper = Wrapper()
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
})

View File

@ -0,0 +1,7 @@
<template>
<v-avatar color="#333" class="ma-2 pa-1" :image="Logo" size="48" />
</template>
<script lang="ts" setup>
import Logo from '#assets/it4c-logo2-clean-bg_alpha-128x128.png'
</script>

View File

@ -0,0 +1,20 @@
import { SBComp } from '#types/SBComp'
import TopMenu from './TopMenu.vue'
import type { Meta, StoryObj } from '@storybook/vue3'
const meta = {
title: 'Menu/TopMenu',
component: TopMenu as SBComp,
tags: ['autodocs'],
argTypes: {},
args: {},
} satisfies Meta<typeof TopMenu>
export default meta
type Story = StoryObj<typeof meta>
export const Example: Story = {
args: {},
}

View File

@ -0,0 +1,18 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import { h } from 'vue'
import { VApp } from 'vuetify/components'
import TopMenu from './TopMenu.vue'
describe('TopMenu', () => {
const wrapper = mount(VApp, {
slots: {
default: h(TopMenu),
},
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
})

View File

@ -0,0 +1,38 @@
<template>
<v-app-bar flat>
<v-row>
<v-col>
<LogoAvatar />
</v-col>
<v-col class="d-flex align-center justify-center grow">
<VikeBtn href="/">{{ $t('menu.home') }}</VikeBtn>
<VikeBtn href="/app">{{ $t('menu.app') }}</VikeBtn>
<VikeBtn href="/about">{{ $t('menu.about') }}</VikeBtn>
</v-col>
<v-col>
<v-switch
v-model="isEnabled"
class="d-flex justify-end mr-5"
:label="$t('language.german')"
color="success"
@update:model-value="onChange"
></v-switch>
</v-col>
</v-row>
</v-app-bar>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useLocale } from 'vuetify'
import VikeBtn from '#components/VikeBtn.vue'
import LogoAvatar from './LogoAvatar.vue'
const { current: locale } = useLocale()
const isEnabled = ref(locale.value === 'de')
const onChange = () => {
locale.value = isEnabled.value ? 'de' : 'en'
}
</script>

View File

@ -0,0 +1,52 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LogoAvatar > renders 1`] = `
<div
class="v-avatar v-theme--light v-avatar--density-default v-avatar--variant-flat ma-2 pa-1"
style="background-color: #333; color: #fff; caret-color: #fff; width: 48px; height: 48px;"
>
<div
aria-label=""
class="v-responsive v-img v-img--booting"
>
<div
class="v-responsive__sizer"
/>
<transition-stub
appear="true"
css="true"
name="fade-transition"
persisted="false"
>
<img
alt=""
class="v-img__img v-img__img--cover"
src="/src/assets/it4c-logo2-clean-bg_alpha-128x128.png"
style="display: none;"
/>
</transition-stub>
<transition-stub
appear="false"
css="true"
name="fade-transition"
persisted="false"
>
<!---->
</transition-stub>
<!---->
<!---->
<!---->
<!---->
</div>
<!---->
<span
class="v-avatar__underlay"
/>
</div>
`;

View File

@ -0,0 +1,272 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`TopMenu > renders 1`] = `
<div
class="v-application v-theme--light v-layout v-layout--full-height v-locale--is-ltr"
>
<div
class="v-application__wrap"
>
<header
class="v-toolbar v-toolbar--flat v-toolbar--density-default v-theme--light v-locale--is-ltr v-app-bar"
style="top: 0px; z-index: 1004; transform: translateY(0%); position: fixed; left: 0px; width: calc(100% - 0px - 0px);"
>
<!---->
<div
class="v-toolbar__content"
style="height: 64px;"
>
<!---->
<!---->
<div
class="v-row"
>
<div
class="v-col"
>
<div
class="v-avatar v-theme--light v-avatar--density-default v-avatar--variant-flat ma-2 pa-1"
style="background-color: #333; color: #fff; caret-color: #fff; width: 48px; height: 48px;"
>
<div
aria-label=""
class="v-responsive v-img v-img--booting"
>
<div
class="v-responsive__sizer"
/>
<transition-stub
appear="true"
css="true"
name="fade-transition"
persisted="false"
>
<img
alt=""
class="v-img__img v-img__img--cover"
src="/src/assets/it4c-logo2-clean-bg_alpha-128x128.png"
style="display: none;"
/>
</transition-stub>
<transition-stub
appear="false"
css="true"
name="fade-transition"
persisted="false"
>
<!---->
</transition-stub>
<!---->
<!---->
<!---->
<!---->
</div>
<!---->
<span
class="v-avatar__underlay"
/>
</div>
</div>
<div
class="v-col d-flex align-center justify-center grow"
>
<a
class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-flat"
href="/"
>
<span
class="v-btn__overlay"
/>
<span
class="v-btn__underlay"
/>
<!---->
<span
class="v-btn__content"
data-no-activator=""
>
$t('menu.home')
</span>
<!---->
<!---->
</a>
<a
class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-flat"
href="/app"
>
<span
class="v-btn__overlay"
/>
<span
class="v-btn__underlay"
/>
<!---->
<span
class="v-btn__content"
data-no-activator=""
>
$t('menu.app')
</span>
<!---->
<!---->
</a>
<a
class="v-btn v-theme--light v-btn--density-default v-btn--size-default v-btn--variant-flat"
href="/about"
>
<span
class="v-btn__overlay"
/>
<span
class="v-btn__underlay"
/>
<!---->
<span
class="v-btn__content"
data-no-activator=""
>
$t('menu.about')
</span>
<!---->
<!---->
</a>
</div>
<div
class="v-col"
>
<div
class="v-input v-input--horizontal v-input--center-affix v-input--density-default v-locale--is-ltr v-input--dirty v-switch d-flex justify-end mr-5"
>
<!---->
<div
class="v-input__control"
>
<div
class="v-selection-control v-selection-control--dirty v-selection-control--density-default"
>
<div
class="v-selection-control__wrapper text-success"
>
<div
class="v-switch__track bg-success"
>
<!---->
<!---->
</div>
<div
class="v-selection-control__input"
>
<input
aria-describedby="switch-4-messages"
aria-disabled="false"
id="switch-4"
type="checkbox"
value="true"
/>
<div
class="v-switch__thumb bg-success"
>
<transition-stub
appear="false"
css="true"
name="scale-transition"
persisted="false"
>
<!---->
</transition-stub>
</div>
</div>
</div>
<label
class="v-label v-label--clickable"
for="switch-4"
>
<!---->
$t('language.german')
</label>
</div>
</div>
<!---->
<div
class="v-input__details"
>
<transition-group-stub
appear="false"
aria-live="polite"
class="v-messages"
css="true"
id="switch-4-messages"
name="slide-y-transition"
persisted="false"
role="alert"
tag="div"
>
<!---->
</transition-group-stub>
<!---->
</div>
</div>
</div>
</div>
<!---->
</div>
<transition-stub
appear="false"
css="true"
name="expand-transition"
persisted="false"
>
<!---->
</transition-stub>
</header>
</div>
</div>
`;

14
frontend/src/env.test.ts Normal file
View File

@ -0,0 +1,14 @@
import { describe, it, expect } from 'vitest'
import { META } from './env'
describe('env', () => {
it('has correct default values', () => {
expect(META).toEqual({
BASE_URL: 'http://localhost:3000',
DEFAULT_AUTHOR: 'IT Team 4 Change',
DEFAULT_DESCRIPTION: 'IT4C Frontend Boilerplate',
DEFAULT_TITLE: 'IT4C',
})
})
})

10
frontend/src/env.ts Normal file
View File

@ -0,0 +1,10 @@
const META = {
BASE_URL: (import.meta.env.PUBLIC_ENV__META__BASE_URL ?? 'http://localhost:3000') as string,
DEFAULT_AUTHOR: (import.meta.env.PUBLIC_ENV__META__DEFAULT_AUTHOR ??
'IT Team 4 Change') as string,
DEFAULT_DESCRIPTION: (import.meta.env.PUBLIC_ENV__META__DEFAULT_DESCRIPTION ??
'IT4C Frontend Boilerplate') as string,
DEFAULT_TITLE: (import.meta.env.PUBLIC_ENV__META__DEFAULT_TITLE ?? 'IT4C') as string,
}
export { META }

View File

@ -0,0 +1,25 @@
import { mount } from '@vue/test-utils'
import { beforeEach, expect, describe, it } from 'vitest'
import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'
import DefaultLayout from './DefaultLayout.vue'
describe('LogoAvatar', () => {
const Wrapper = () => {
return mount(VApp, {
slots: {
default: h(DefaultLayout as Component),
},
})
}
let wrapper: ReturnType<typeof Wrapper>
beforeEach(() => {
wrapper = Wrapper()
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
})

View File

@ -0,0 +1,25 @@
<template>
<v-main class="bg-grey-lighten-3">
<v-container>
<v-row>
<v-col v-if="slots.sidemenu" cols="2">
<v-sheet rounded>
<slot name="sidemenu"></slot>
</v-sheet>
</v-col>
<v-col>
<v-sheet rounded class="pa-3">
<slot />
</v-sheet>
</v-col>
</v-row>
</v-container>
</v-main>
</template>
<script lang="ts" setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>

View File

@ -0,0 +1,20 @@
import { SBComp } from '#types/SBComp'
import Layout from './DefaultLayout.vue'
import type { Meta, StoryObj } from '@storybook/vue3'
const meta = {
title: 'Layouts/Default',
component: Layout as SBComp,
tags: ['autodocs'],
argTypes: {},
args: {},
} satisfies Meta<typeof Layout>
export default meta
type Story = StoryObj<typeof meta>
export const Example: Story = {
args: {},
}

View File

@ -0,0 +1,40 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LogoAvatar > renders 1`] = `
<div
class="v-application v-theme--light v-layout v-layout--full-height v-locale--is-ltr"
>
<div
class="v-application__wrap"
>
<main
class="v-main bg-grey-lighten-3"
style="--v-layout-left: 0px; --v-layout-right: 0px; --v-layout-top: 0px; --v-layout-bottom: 0px; transition: none !important;"
>
<div
class="v-container v-locale--is-ltr"
>
<div
class="v-row"
>
<!--v-if-->
<div
class="v-col"
>
<div
class="v-sheet v-theme--light v-sheet--rounded pa-3"
>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
`;

View File

@ -0,0 +1,53 @@
{
"about": {
"h1": "Über",
"link1": "github.com",
"link2": "Webseite",
"text1": "Um mehr über diese Kochplatte zu erfahren kannst du dir den Quellcode auf {link} anschauen.",
"text2": "Du willst mehr erfahren? Besuche uns auf {link}."
},
"app": {
"inc": {
"h1": "Erhöhe den Zähler",
"menu": "Erhöhe",
"text": "Erhöhe: {count}"
},
"reset": {
"h1": "Den Zähler zurücksetzen",
"menu": "Zurücksetzen",
"text": "Zurücksetzen: {count}"
},
"value": {
"h1": "Der Zähler",
"menu": "Wert",
"text": "Der aktuelle Wert des Zählers lautet: {count}"
}
},
"error": {
"404": {
"h1": "404 Seite nicht gefunden",
"text": "Diese Seite konnte nicht gefunden werden."
},
"500": {
"h1": "500 Interner Fehler",
"text": "Irgendetwas ist schief gegangen."
}
},
"home": {
"greet1": "Hochachtungsvoll",
"greet2": "Dein IT Team For Change",
"h1": "IT4C Frontend Kochplatte",
"text1": "Willkommen zu diesem minimalen Frontendstarter.",
"text2": "Es handelt sich um ein einfaches Beispiel um zu zeigen was so geht - nichts besonderes.",
"text3": "In dem Anwendungsbereich wirst du ein Zählerbeispiel finden, welches den Browserspeicher verwendet.",
"text4": "Fröhliches Programmieren"
},
"language": {
"german": "Deutsch"
},
"menu": {
"about": "Über",
"app": "Anwendung",
"home": "Daheim"
}
}

View File

@ -0,0 +1,53 @@
{
"about": {
"h1": "About",
"link1": "github.com",
"link2": "website",
"text1": "To find out more about this boilerplate you can look at the sources on {link}.",
"text2": "Want to get in touch? Find out how on our {link}."
},
"app": {
"inc": {
"h1": "Increase the Counter",
"menu": "Increase",
"text": "Increase: {count}"
},
"reset": {
"h1": "Reset the Counter",
"menu": "Reset",
"text": "Reset: {count}"
},
"value": {
"h1": "The Counter",
"menu": "Value",
"text": "The current value of the counter is: {count}"
}
},
"error": {
"404": {
"h1": "404 Page Not Found",
"text": "This page could not be found."
},
"500": {
"h1": "500 Internal Error",
"text": "Something went wrong."
}
},
"home": {
"greet1": "Sincerly",
"greet2": "Your IT Team For Change",
"h1": "IT4C Frontend Boilerplate",
"text1": "Welcome to this minimal starter for frontends.",
"text2": "This is just a basic example to demonstrate things - nothing fancy.",
"text3": "In the App Section you will find a counter example utilizing the local storage.",
"text4": "Happy Coding"
},
"language": {
"german": "German"
},
"menu": {
"about": "About",
"app": "App",
"home": "Home"
}
}

View File

@ -0,0 +1,14 @@
<template>
<div v-if="is404">
<h1>{{ $t('error.404.h1') }}</h1>
<p>{{ $t('error.404.text') }}</p>
</div>
<div v-else>
<h1>{{ $t('error.500.h1') }}</h1>
<p>{{ $t('error.500.text') }}</p>
</div>
</template>
<script lang="ts" setup>
defineProps({ is404: Boolean })
</script>

View File

@ -0,0 +1 @@
export const title = 'IT4C | Error'

View File

@ -0,0 +1,24 @@
import { SBComp } from '#types/SBComp'
import Page from './+Page.vue'
import type { Meta, StoryObj } from '@storybook/vue3'
const meta = {
title: 'Pages/Error',
component: Page as SBComp,
tags: ['autodocs'],
argTypes: {},
args: {},
} satisfies Meta<typeof Page>
export default meta
type Story = StoryObj<typeof meta>
export const Error500: Story = {
args: {},
}
export const Error404: Story = {
args: { is404: true },
}

View File

@ -0,0 +1,74 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect, beforeEach } from 'vitest'
import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'
import ErrorPage from './+Page.vue'
import { title } from './+title'
describe('ErrorPage', () => {
it('title returns correct title', () => {
expect(title).toBe('IT4C | Error')
})
describe('500 Error', () => {
const WrapperUndefined = () => {
return mount(VApp, {
slots: {
default: h(ErrorPage as Component),
},
})
}
const WrapperFalse = () => {
return mount(VApp, {
slots: {
default: h(ErrorPage as Component, {
is404: false,
}),
},
})
}
let wrapper: ReturnType<typeof WrapperUndefined>
beforeEach(() => {
wrapper = WrapperUndefined()
})
describe('no is404 property set', () => {
it('renders error 500', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.500.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.500.text')")
})
})
describe('is404 property is false', () => {
beforeEach(() => {
wrapper = WrapperFalse()
})
it('renders error 500', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.500.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.500.text')")
})
})
})
describe('404 Error', () => {
const Wrapper = () => {
return mount(VApp, {
slots: {
default: h(ErrorPage as Component, {
is404: true,
}),
},
})
}
let wrapper: ReturnType<typeof Wrapper>
beforeEach(() => {
wrapper = Wrapper()
})
describe('is404 property is true', () => {
it('renders error 400', () => {
expect(wrapper.find('h1').text()).toEqual("$t('error.404.h1')")
expect(wrapper.find('p').text()).toEqual("$t('error.404.text')")
})
})
})
})

View File

@ -0,0 +1,22 @@
<template>
<DefaultLayout>
<h1>{{ $t('about.h1') }}</h1>
<i18n-t scope="global" keypath="about.text1" tag="p">
<template #link>
<a href="https://github.com/IT4Change/boilerplate-frontend/" target="_blank">
{{ $t('about.link1') }}
</a>
</template>
</i18n-t>
<br />
<i18n-t scope="global" keypath="about.text2" tag="p">
<template #link>
<a href="https://it4c.dev" target="_blank">{{ $t('about.link2') }}</a>
</template>
</i18n-t>
</DefaultLayout>
</template>
<script lang="ts" setup>
import DefaultLayout from '#layouts/DefaultLayout.vue'
</script>

View File

@ -0,0 +1 @@
export const title = 'IT4C | About'

View File

@ -0,0 +1,20 @@
import { SBComp } from '#types/SBComp'
import Page from './+Page.vue'
import type { Meta, StoryObj } from '@storybook/vue3'
const meta = {
title: 'Pages/About',
component: Page as SBComp,
tags: ['autodocs'],
argTypes: {},
args: {},
} satisfies Meta<typeof Page>
export default meta
type Story = StoryObj<typeof meta>
export const Example: Story = {
args: {},
}

View File

@ -0,0 +1,23 @@
import { mount } from '@vue/test-utils'
import { describe, it, expect } from 'vitest'
import { Component, h } from 'vue'
import { VApp } from 'vuetify/components'
import AboutPage from './+Page.vue'
import { title } from './+title'
describe('AboutPage', () => {
const wrapper = mount(VApp, {
slots: {
default: h(AboutPage as Component),
},
})
it('title returns correct title', () => {
expect(title).toBe('IT4C | About')
})
it('renders', () => {
expect(wrapper.element).toMatchSnapshot()
})
})

View File

@ -0,0 +1,68 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`AboutPage > renders 1`] = `
<div
class="v-application v-theme--light v-layout v-layout--full-height v-locale--is-ltr"
>
<div
class="v-application__wrap"
>
<main
class="v-main bg-grey-lighten-3"
style="--v-layout-left: 0px; --v-layout-right: 0px; --v-layout-top: 0px; --v-layout-bottom: 0px;"
>
<div
class="v-container v-locale--is-ltr"
>
<div
class="v-row"
>
<!--v-if-->
<div
class="v-col"
>
<div
class="v-sheet v-theme--light v-sheet--rounded pa-3"
>
<h1>
$t('about.h1')
</h1>
<p>
Um mehr über diese Kochplatte zu erfahren kannst du dir den Quellcode auf
<a
href="https://github.com/IT4Change/boilerplate-frontend/"
target="_blank"
>
$t('about.link1')
</a>
anschauen.
</p>
<br />
<p>
Du willst mehr erfahren? Besuche uns auf
<a
href="https://it4c.dev"
target="_blank"
>
$t('about.link2')
</a>
.
</p>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
`;

View File

@ -0,0 +1,66 @@
<template>
<DefaultLayout>
<template #sidemenu>
<v-list rounded>
<v-list-item
link
:title="$t('app.value.menu')"
:active="page === null"
href="/app"
></v-list-item>
<v-list-item
link
:title="$t('app.inc.menu')"
:active="page === 'inc'"
href="/app/inc"
></v-list-item>
<v-divider class="my-2"></v-divider>
<v-list-item
link
:title="$t('app.reset.menu')"
:active="page === 'reset'"
href="/app/reset"
></v-list-item>
</v-list>
</template>
<template #default>
<div v-if="page === null">
<h1>{{ $t('app.value.h1') }}</h1>
<i18n-t scope="global" keypath="app.value.text" tag="p">
<template #count>
<ClientOnly>
<b>{{ counter.count }}</b>
</ClientOnly>
</template>
</i18n-t>
</div>
<div v-else-if="page === 'inc'">
<h1>{{ $t('app.inc.h1') }}</h1>
<ClientOnly>
<v-btn elevation="2" @click="counter.increment()">{{
$t('app.inc.text', { count: counter.count })
}}</v-btn>
</ClientOnly>
</div>
<div v-else-if="page === 'reset'">
<h1>{{ $t('app.reset.h1') }}</h1>
<ClientOnly>
<v-btn elevation="2" @click="counter.reset()">{{
$t('app.reset.text', { count: counter.count })
}}</v-btn>
</ClientOnly>
</div>
</template>
</DefaultLayout>
</template>
<script lang="ts" setup>
import ClientOnly from '#components/ClientOnly.vue'
import DefaultLayout from '#layouts/DefaultLayout.vue'
import { useCounterStore } from '#stores/counter'
const counter = useCounterStore()
defineProps({ page: { type: String, default: null } })
</script>

View File

@ -0,0 +1,11 @@
import type { PageContextBuiltInServer } from 'vike/types'
export default onBeforeRender
/* async */ function onBeforeRender(pageContext: PageContextBuiltInServer) {
return {
pageContext: {
pageProps: pageContext.routeParams,
},
}
}

Some files were not shown because too many files have changed in this diff Show More