Merged in feature/storybook (pull request #49)

Integrate Storybook

* Merged in feature/caprover-deploy (pull request #47)

Feature/caprover deploy

Approved-by: Christian Cueni
* Merge branch 'feature/storybook' of bitbucket.org:iterativ/vbv_lernwelt into feature/storybook

* Add stories

* Add BuenosAires font to storybook

* Mute typecheck error

* Update bitbucket deployment

* Add ItNavigationProgress component

* Update pipelines file

* Update pipelines file

* Merged develop into feature/storybook

Approved-by: Daniel Egger
This commit is contained in:
Elia Bieri 2023-03-29 13:45:13 +00:00
parent bd028d4b18
commit 10f34f53d8
66 changed files with 19799 additions and 1256 deletions

View File

@ -12,4 +12,4 @@ node_modules
venv venv
.git .git
.envrc .envrc
/server/vbv_lernwelt/media/documents

1
.npmrc Normal file
View File

@ -0,0 +1 @@
legacy-peer-deps=true

View File

@ -133,6 +133,23 @@ npx vue-i18n-extract --add
./caprover_deploy.sh myvbv ./caprover_deploy.sh myvbv
``` ```
### CapRover feature branch deployment
You can deploy every feature branch to CapRover directly from Bitbucket Pipelines
with a manual step.
When you run caprover_deploy.sh without arguments, it will deploy the current branch
```
./caprover_deploy.sh
```
### Cleanup caprover feature branch deployments
```
python caprover_cleanup.py
```
## IntelliJ Configuration ## IntelliJ Configuration
* In the .idea/vbv_lernwelt.iml file change the module type to "PYTHON_MODULE". * In the .idea/vbv_lernwelt.iml file change the module type to "PYTHON_MODULE".
@ -149,7 +166,8 @@ npx vue-i18n-extract --add
## currents.dev ## currents.dev
This project uses [currents.dev](https://currents.dev) to run the Cypress tests concurrently. This project uses [currents.dev](https://currents.dev) to run the Cypress tests
concurrently.
The following steps were taken to set it up: The following steps were taken to set it up:
- Create a new project on currents.dev - Create a new project on currents.dev
@ -159,8 +177,10 @@ The following steps were taken to set it up:
```json ```json
"cypress:ci": "currents run --parallel --record --key $CURRENTS_KEY", "cypress:ci": "currents run --parallel --record --key $CURRENTS_KEY",
``` ```
- Create a new Bitbucket pipelines job that exports the `CURRENTS_KEY` and runs the `cypress:ci` script - Create a new Bitbucket pipelines job that exports the `CURRENTS_KEY` and runs
- Refactor the pipeline steps so that the dependencies are installed first and then the other steps are run in the `cypress:ci` script
- Refactor the pipeline steps so that the dependencies are installed first and then the
other steps are run in
parallel. parallel.
You can then run multiple instances of the previously created job in parallel. You can then run multiple instances of the previously created job in parallel.
See `.bitbucket-pipelines.yml` for an example. See `.bitbucket-pipelines.yml` for an example.

View File

@ -3,7 +3,7 @@ image: iterativ/vbv-lernwelt-bitbucket
## job definition for running Cypress tests in parallel ## job definition for running Cypress tests in parallel
e2e: &e2e e2e: &e2e
name: Cypress test worker name: cypress test worker
max-time: 45 max-time: 45
services: services:
- postgres - postgres
@ -17,35 +17,34 @@ e2e: &e2e
- export IT_SERVE_VUE=false - export IT_SERVE_VUE=false
- export IT_ALLOW_LOCAL_LOGIN=true - export IT_ALLOW_LOCAL_LOGIN=true
- source ./env/bitbucket/prepare_for_test.sh - source ./env/bitbucket/prepare_for_test.sh
- npm install
- npm run build - npm run build
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt
- ./prepare_server_cypress.sh --start-background - ./prepare_server_cypress.sh --start-background
- npm run cypress:ci - npm run cypress:ci
artifacts: artifacts:
- cypress/**/*.png - cypress/**/*.png
- cypress/**/*.mp4 - cypress/**/*.mp4
deploy: &deploy
name: deploy
max-time: 30
caches:
- node
- clientnode
- vbvpip
- docker
services:
- docker
script:
- source ./env/bitbucket/prepare_for_deployment.sh
# it will by default take the branch name, when there is no provided parameter
- ./caprover_deploy.sh ${DEPLOY_ENVIRONMENT:-}
- cat /tmp/caprover_app_url.txt
pipelines: pipelines:
default: default:
- step:
name: install dependencies
services:
- postgres
caches:
- node
- clientnode
- vbvpip
- cypress
script:
- export IT_SERVE_VUE=false
- export IT_ALLOW_LOCAL_LOGIN=true
- source ./env/bitbucket/prepare_for_test.sh
- npm install
- npm run build
- python -m venv vbvvenv
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt
- parallel: - parallel:
- step: - step:
<<: *e2e <<: *e2e
@ -60,8 +59,6 @@ pipelines:
- vbvpip - vbvpip
script: script:
- source ./env/bitbucket/prepare_for_test.sh - source ./env/bitbucket/prepare_for_test.sh
- python -m venv vbvvenv
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt - pip install -r server/requirements/requirements-dev.txt
- ./server/run_tests_coverage.sh - ./server/run_tests_coverage.sh
- step: - step:
@ -73,8 +70,6 @@ pipelines:
- vbvpip - vbvpip
script: script:
- source ./env/bitbucket/prepare_for_test.sh - source ./env/bitbucket/prepare_for_test.sh
- python -m venv vbvvenv
- source vbvvenv/bin/activate
- pip install -r server/requirements/requirements-dev.txt - pip install -r server/requirements/requirements-dev.txt
- git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff -w git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt - git-crypt status -e | sort > git-crypt-encrypted-files-check.txt && diff -w git-crypt-encrypted-files.txt git-crypt-encrypted-files-check.txt
- trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --entropy=True --max_depth=100 . - trufflehog --exclude_paths trufflehog-exclude-patterns.txt --allow trufflehog-allow.json --entropy=True --max_depth=100 .
@ -103,32 +98,34 @@ pipelines:
- npm run prettier:check - npm run prettier:check
- npm run lint - npm run lint
- npm run typecheck - npm run typecheck
- step:
<<: *deploy
name: deploy feature
trigger: manual
tags: tags:
v202*: v202*:
- step: - step:
name: Hello world name: prepare prod deployment
script: script:
- echo "Release ready!" - echo "Release ready!"
- step: - step:
name: Deploy to PROD name: deploy prod
deployment: prod deployment: prod
trigger: manual trigger: manual
script: script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://myservicecrm.swisscom.ch/deploy-iesc-xxx - source ./env/bitbucket/prepare_for_deployment.sh
- ./caprover_deploy.sh myvbv
custom: custom:
deploy-preprod: deploy-stage:
- step: - step:
name: Deploy to PREPROD <<: *deploy
deployment: preprod deployment: stage
script: deploy-dev:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://preprod.myservicecrm.ch/deploy-iesc-xxx
deploy-api:
- step: - step:
name: Deploy to API <<: *deploy
deployment: api deployment: dev
script:
- ./deploy.sh --commit "$BITBUCKET_COMMIT" --token "$DEPLOY_TOKEN" --url https://api.myservicecrm.ch/deploy-iesc-xxx
definitions: definitions:
caches: caches:

41
caprover_cleanup.py Normal file
View File

@ -0,0 +1,41 @@
import logging
import click
from caprover_api import caprover_api
from environs import Env
logging.basicConfig(level=logging.DEBUG)
env = Env()
env.read_env("./env_secrets/caprover_feature.env", recurse=False, override=True)
app_name = "vbv-feature-caprover-auto-deploy4"
cap = caprover_api.CaproverAPI(
dashboard_url=env.str("CAPROVER_DOMAIN"),
password=env.str("CAPROVER_PASSWORD"),
)
@click.command()
@click.option(
"-a",
"--app-name",
default="vbv-feature-*",
help="app name pattern to delete, can contain * as wildcard",
)
@click.option(
"--automated/--no-automated",
default=False,
help="`automated` deletes without confirmation",
)
def main(app_name, automated):
print(f"delete all apps with name {app_name}")
cap.delete_app_matching_pattern(
app_name_pattern=app_name,
delete_volumes=True,
automated=automated,
)
if __name__ == "__main__":
main()

View File

@ -1,80 +1,111 @@
import logging import logging
import sys import random
import string
from environs import Env import click
# TODO: I use a locally patched caprover api for now
sys.path.append(r'/Users/daniel/workspace/iterativ-caprover/Caprover-API')
from caprover_api import caprover_api from caprover_api import caprover_api
from environs import Env
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
env = Env() def generate_random_string(length):
env.read_env("./env_secrets/caprover_stage.env", recurse=False, override=True) base_characters = string.ascii_letters + string.digits
app_name = "myvbv-stage" random_string = "".join(random.choice(base_characters) for _ in range(length))
return random_string
cap = caprover_api.CaproverAPI(
dashboard_url=env.str('CAPROVER_DOMAIN'), @click.command()
password=env.str('CAPROVER_PASSWORD'), @click.option(
"-a",
"--app-name",
required=True,
help="app name to deploy should start with vbv-feature-, use slugified branch name",
) )
@click.option(
db_pass = env.str('POSTGRES_PASSWORD') "-i",
db_user = env.str('POSTGRES_USER') "--image-name",
db_name = env.str('POSTGRES_DB') default=None,
help="docker image name to deploy (like docker.io/iterativ/vbv-lernwelt-django)",
cap.deploy_one_click_app(
one_click_app_name='postgres',
namespace=app_name,
# check https://github.com/caprover/one-click-apps/blob/master/public/v4/apps/postgres.yml
app_variables={
'$$cap_postgres_version': '14.2',
'$$cap_pg_user': db_user,
'$$cap_pg_pass': db_pass,
'$$cap_pg_db': db_name,
},
automated=True
) )
@click.option(
cap.create_and_update_app( "-e",
app_name=app_name, "--environment-file",
enable_ssl=True, default=None,
force_ssl=True, help="path to environment file to use for deployment (like ./env_secrets/caprover_feature.env)",
expose_as_web_app=True,
image_name='docker.io/iterativ/vbv-lernwelt-django',
environment_variables={
# 'DJANGO_SETTINGS_MODULE': 'config.settings.base',
'IT_APP_ENVIRONMENT': env.str('IT_APP_ENVIRONMENT'),
'POSTGRES_HOST': f'srv-captain--{app_name}-postgres-db',
'POSTGRES_PORT': 5432,
'POSTGRES_DB': db_name,
'POSTGRES_USER': db_user,
'POSTGRES_PASSWORD': db_pass,
'IT_ALLOW_LOCAL_LOGIN': env.str('IT_ALLOW_LOCAL_LOGIN'),
'IT_DJANGO_SECRET_KEY': env.str('IT_DJANGO_SECRET_KEY'),
'IT_DJANGO_ADMIN_URL': env.str('IT_DJANGO_ADMIN_URL'),
'IT_DJANGO_ALLOWED_HOSTS': env.str('IT_DJANGO_ALLOWED_HOSTS'),
'IT_DJANGO_DEBUG': 'false',
'IT_SERVE_VUE': 'false',
'IT_SENTRY_DSN': env.str('IT_SENTRY_DSN'),
'IT_OAUTH_CLIENT_NAME': env.str('IT_OAUTH_CLIENT_NAME'),
'IT_OAUTH_CLIENT_ID': env.str('IT_OAUTH_CLIENT_ID'),
'IT_OAUTH_CLIENT_SECRET': env.str('IT_OAUTH_CLIENT_SECRET'),
'IT_OAUTH_ACCESS_TOKEN_URL': env.str('IT_OAUTH_ACCESS_TOKEN_URL'),
'IT_OAUTH_AUTHORIZE_URL': env.str('IT_OAUTH_AUTHORIZE_URL'),
'IT_OAUTH_API_BASE_URL': env.str('IT_OAUTH_API_BASE_URL'),
'IT_OAUTH_LOCAL_DIRECT_URI': env.str('IT_OAUTH_LOCAL_DIRECT_URI'),
'IT_OAUTH_TENANT_ID': env.str('IT_OAUTH_TENANT_ID'),
'IT_OAUTH_SCOPE': env.str('IT_OAUTH_SCOPE'),
'IT_OAUTH_SERVER_METADATA_URL': env.str('IT_OAUTH_SERVER_METADATA_URL'),
'IT_OAUTH_TOKEN_NAME': env.str('IT_OAUTH_TOKEN_NAME'),
'IT_OAUTH_LOGOUT_URL': env.str('IT_OAUTH_LOGOUT_URL'),
},
) )
def main(app_name, image_name, environment_file):
env = Env()
if environment_file is not None:
env.read_env(environment_file, recurse=False, override=True)
cap = caprover_api.CaproverAPI(
dashboard_url=env.str("CAPROVER_DOMAIN"),
password=env.str("CAPROVER_PASSWORD"),
)
db_pass = env.str("POSTGRES_PASSWORD", generate_random_string(63))
db_user = env.str("POSTGRES_USER", "postgres")
db_name = env.str("POSTGRES_DB", "vbv_lernwelt")
cap.deploy_one_click_app(
one_click_app_name="postgres",
namespace=app_name,
# check https://github.com/caprover/one-click-apps/blob/master/public/v4/apps/postgres.yml
app_variables={
"$$cap_postgres_version": "14.2",
"$$cap_pg_user": db_user,
"$$cap_pg_pass": db_pass,
"$$cap_pg_db": db_name,
},
automated=True,
)
default_allowed_hosts = f"{app_name}.iterativ.ch,{app_name}.control.iterativ.ch"
cap.create_and_update_app(
app_name=app_name,
enable_ssl=True,
force_ssl=True,
expose_as_web_app=True,
image_name=image_name,
environment_variables={
# 'DJANGO_SETTINGS_MODULE': 'config.settings.base',
"IT_APP_ENVIRONMENT": env.str("IT_APP_ENVIRONMENT", "caprover_feature"),
"POSTGRES_HOST": f"srv-captain--{app_name}-postgres",
"POSTGRES_PORT": 5432,
"POSTGRES_DB": db_name,
"POSTGRES_USER": db_user,
"POSTGRES_PASSWORD": db_pass,
"IT_DJANGO_ALLOWED_HOSTS": env.str(
"IT_DJANGO_ALLOWED_HOSTS", default_allowed_hosts
),
"IT_DJANGO_SECRET_KEY": env.str(
"IT_DJANGO_SECRET_KEY", generate_random_string(63)
),
"IT_DJANGO_ADMIN_URL": env.str("IT_DJANGO_ADMIN_URL", "admin/"),
"IT_DJANGO_DEBUG": "false",
"IT_SERVE_VUE": "false",
"IT_ALLOW_LOCAL_LOGIN": "true",
# used for deployment of "real" environments, not used now for feature branches
# 'IT_ALLOW_LOCAL_LOGIN': env.str('IT_ALLOW_LOCAL_LOGIN'),
# 'IT_SENTRY_DSN': env.str('IT_SENTRY_DSN'),
# 'IT_OAUTH_CLIENT_NAME': env.str('IT_OAUTH_CLIENT_NAME'),
# 'IT_OAUTH_CLIENT_ID': env.str('IT_OAUTH_CLIENT_ID'),
# 'IT_OAUTH_CLIENT_SECRET': env.str('IT_OAUTH_CLIENT_SECRET'),
# 'IT_OAUTH_ACCESS_TOKEN_URL': env.str('IT_OAUTH_ACCESS_TOKEN_URL'),
# 'IT_OAUTH_AUTHORIZE_URL': env.str('IT_OAUTH_AUTHORIZE_URL'),
# 'IT_OAUTH_API_BASE_URL': env.str('IT_OAUTH_API_BASE_URL'),
# 'IT_OAUTH_LOCAL_DIRECT_URI': env.str('IT_OAUTH_LOCAL_DIRECT_URI'),
# 'IT_OAUTH_TENANT_ID': env.str('IT_OAUTH_TENANT_ID'),
# 'IT_OAUTH_SCOPE': env.str('IT_OAUTH_SCOPE'),
# 'IT_OAUTH_SERVER_METADATA_URL': env.str('IT_OAUTH_SERVER_METADATA_URL'),
# 'IT_OAUTH_TOKEN_NAME': env.str('IT_OAUTH_TOKEN_NAME'),
# 'IT_OAUTH_LOGOUT_URL': env.str('IT_OAUTH_LOGOUT_URL'),
},
)
if __name__ == "__main__":
main()

View File

@ -1,19 +1,36 @@
#!/bin/bash #!/bin/bash
# script should fail when any process returns non zero code
set -ev
VERSION=$(git log -1 --pretty=%h) VERSION=$(git log -1 --pretty=%h)
REPO="iterativ/vbv-lernwelt-django" REPO="iterativ/vbv-lernwelt-django"
LATEST="${REPO}:latest" LATEST="${REPO}:latest"
BUILD_TIMESTAMP=$( date '+%F_%H:%M:%S' ) BUILD_TIMESTAMP=$( date '+%F_%H:%M:%S' )
VERSION_TAG="${REPO}:$VERSION" VERSION_TAG="${REPO}:$VERSION"
LOCAL_LOGOUT=$VITE_LOGOUT_REDIRECT BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
LOCAL_SENTRY_ENV=$VITE_SENTRY_ENV BRANCH_NAME_SLUG=$(echo "$BRANCH_NAME" | sed -e 's/[^[:alnum:]]/-/g' | tr -s '-' | tr A-Z a-z)
echo "$BRANCH_NAME_SLUG"
APP_NAME=${1:-vbv-lernwelt} DEFAULT_APP_NAME="vbv-$BRANCH_NAME_SLUG"
APP_NAME=${1:-$DEFAULT_APP_NAME}
# shorten APP_NAME to 32 characters
# app names with more character seem to fail in some steps in CapRover
# CapRover generates longer names out from the app name and there
# seems to be a limit in Docker of 64 characters
if (( ${#APP_NAME} > 32 )); then
hash="$(echo -n "$APP_NAME" | md5sum | cut -c1-4)"
APP_NAME="$(echo -n "${APP_NAME:0:28}-${hash}")"
fi
echo "Deploy to $APP_NAME" echo "Deploy to $APP_NAME"
VITE_GRAPHQL_URL="/server/graphql/" VITE_GRAPHQL_URL="/server/graphql/"
VITE_LOGOUT_REDIRECT="/"
VITE_SENTRY_ENV="development"
if [[ "$APP_NAME" == "myvbv" ]] if [[ "$APP_NAME" == "myvbv" ]]
then then
VITE_LOGOUT_REDIRECT="https://edumgr.b2clogin.com/edumgr.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/logout/?post_logout_redirect_uri=https://myvbv.iterativ.ch/" VITE_LOGOUT_REDIRECT="https://edumgr.b2clogin.com/edumgr.onmicrosoft.com/b2c_1_signupandsignin/oauth2/v2.0/logout/?post_logout_redirect_uri=https://myvbv.iterativ.ch/"
@ -28,20 +45,28 @@ then
VITE_SENTRY_ENV="development" VITE_SENTRY_ENV="development"
fi fi
# script should fail when any process returns non zero code # create client for django
set -ev
# create client
npm run build npm run build
VITE_LOGOUT_REDIRECT=$LOCAL_LOGOUT
VITE_SENTRY_ENV=$LOCAL_SENTRY_ENV
python server/manage.py collectstatic --no-input python server/manage.py collectstatic --no-input
# create and push new docker container # create and push new docker container
## Note that images build with buildx do not appear in your docker images list, therefore the push true must be set docker build --platform=linux/amd64 -f compose/django/Dockerfile -t "$REPO" -t "$LATEST" -t "$VERSION_TAG" --build-arg VERSION="$VERSION" --build-arg BUILD_TIMESTAMP="$BUILD_TIMESTAMP" --build-arg GIT_COMMIT="$(git log -1 --format=%h)" .
docker buildx build --push=true --platform=linux/amd64 -f compose/django/Dockerfile -t "$REPO" -t "$LATEST" -t "$VERSION_TAG" --build-arg VERSION="$VERSION" --build-arg BUILD_TIMESTAMP="$BUILD_TIMESTAMP" --build-arg GIT_COMMIT="$(git log -1 --format=%h)" . docker push "$VERSION_TAG"
#deploy to caprover, explicitly use the version tag... so if there is a mismatch you get an error message APP_URL="$APP_NAME.control.iterativ.ch"
echo "Checking if $APP_URL is available..."
if ! curl --output /dev/null --silent --head --fail "$APP_URL"; then
echo "HTTP request to $APP_URL did not return a 200 status code, so we need to create the caprover app"
python caprover_cleanup.py -a "$APP_NAME*" --automated
python caprover_create_app.py -a "$APP_NAME"
fi
# deploy to caprover, explicitly use the version tag... so if there is a mismatch you get an error message
caprover deploy -h https://captain.control.iterativ.ch -a "$APP_NAME" -i docker.io/"$VERSION_TAG" caprover deploy -h https://captain.control.iterativ.ch -a "$APP_NAME" -i docker.io/"$VERSION_TAG"
if [ -n "$CI" ]; then
echo "Running within Bitbucket Pipelines"
export CAPROVER_APP_URL="$APP_URL"
echo "https://$CAPROVER_APP_URL" > /tmp/caprover_app_url.txt
fi

View File

@ -1,6 +1,5 @@
/* eslint-env node */ /* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution"); require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = { module.exports = {
root: true, root: true,
extends: [ extends: [
@ -8,6 +7,7 @@ module.exports = {
"eslint:recommended", "eslint:recommended",
"@vue/eslint-config-typescript/recommended", "@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier", "@vue/eslint-config-prettier",
"plugin:storybook/recommended",
], ],
env: { env: {
"vue/setup-compiler-macros": true, "vue/setup-compiler-macros": true,

1
client/.npmrc Normal file
View File

@ -0,0 +1 @@
legacy-peer-deps=true

30
client/.storybook/main.ts Normal file
View File

@ -0,0 +1,30 @@
import type { StorybookConfig } from "@storybook/vue3-vite";
import * as path from "path";
import * as initialize from "storybook-tailwind-foundations/initialize.js";
const tailwindConfigPath = path.join(__dirname, "../tailwind.config.js"); // or your own config file
initialize.default.default(tailwindConfigPath);
const config: StorybookConfig = {
stories: [
"../src/**/*.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)",
"../node_modules/storybook-tailwind-foundations/**/*.stories.js",
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-a11y",
"storybook-addon-designs",
],
framework: {
name: "@storybook/vue3-vite",
options: {},
},
staticDirs: [{ from: "../../server/vbv_lernwelt/static/icons", to: "/static/icons" }],
docs: {
autodocs: "tag",
},
};
export default config;

View File

@ -0,0 +1,5 @@
<style>
a[title="myVBV"] {
background-color: #00224d;
}
</style>

View File

@ -0,0 +1,6 @@
import { addons } from "@storybook/manager-api";
import myvbvTheme from "./myvbvTheme";
addons.setConfig({
theme: myvbvTheme,
});

View File

@ -0,0 +1,108 @@
import { vueRouter } from "storybook-vue3-router";
import { makeDecorator } from "@storybook/addons";
import type { StoryContext, StoryFn } from "@storybook/types";
import {
createRouter,
createWebHashHistory,
type NavigationGuard,
type RouteLocationNormalizedLoaded,
/* types */
type Router,
type RouterOptions,
} from "vue-router";
import { action } from "@storybook/addon-actions";
import type { RouteRecordRaw } from "vue-router";
type MockRouter = Router & { isMocked?: boolean };
type MockRoute = RouteLocationNormalizedLoaded & { isMocked?: boolean };
const Home = {
template: `
<div>
<h2>Home</h2>
<div style="display: flex; gap: 1em">
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</div>
</div>
`,
};
const About = {
template: `
<div>
<h2>About</h2>
<div style="display: flex; gap: 1em">
<router-link to="/">Home</router-link>
<router-link to="/about">About</router-link>
</div>
</div>
`,
};
const defaultRoutes: RouteRecordRaw[] = [
{
path: "/",
name: "home",
component: Home,
beforeEnter: (to, from) =>
action("beforeEnter")({ to: to.fullPath, from: from.fullPath }),
},
{
path: "/about",
name: "about",
component: About,
beforeEnter: (to, from) =>
action("beforeEnter")({ to: to.fullPath, from: from.fullPath }),
},
];
export const withVueRouter = (
app,
/* optional: routes param - uses `defaultRoutes` if not provided */
routes = defaultRoutes,
/* optional: router options - used to pass `initialRoute` value, `beforeEach()` navigation guard methods and vue-router `createRouter` options */
options?: {
initialRoute?: string;
beforeEach?: NavigationGuard;
vueRouterOptions?: RouterOptions;
}
) => {
/* setup router var */
let router;
/* check if there is an existing router */
const existingRouter = app.config.globalProperties.$router as MockRouter;
const existingRoute = app.config.globalProperties.$route as MockRoute;
if (
(!existingRouter || existingRouter.isMocked === true) &&
(!existingRoute || existingRoute.isMocked === true)
) {
/* create vue router */
router = createRouter({
history: createWebHashHistory(),
routes,
...options?.vueRouterOptions,
});
/* setup optional global router guards */
// globalRouterGuardFn(router, options?.beforeEach)
/* tell storybook to use vue router */
app.use(router);
} else {
/* set router to value of existing router */
// router = existingRouter
// /* reset routes (remove old / add new) */
// resetRoutes(router, routes)
// /* setup optional global router guards (if provided and there is an existing router this will force a page reload) */
// globalRouterGuardFn(existingRouter, options?.beforeEach, true)
// }
// /* go to initial route */
// initialRoute(router, options?.initialRoute)
}
};

View File

@ -0,0 +1,10 @@
import { create } from "@storybook/theming/create";
import logo from "./vbv-logo.svg";
export default create({
base: "light",
brandTitle: "myVBV",
brandUrl: "https://myvbv.iterativ.ch",
brandImage: logo,
brandTarget: "_self",
});

View File

@ -0,0 +1,10 @@
<script>
window.global = window;
</script>
<script defer src="https://myvbv.iterativ.ch/server/core/icons/"></script>
<link
href="https://myvbv.iterativ.ch/static/fonts/BuenosAires/stylesheet.css"
rel="stylesheet"
/>

View File

@ -0,0 +1,28 @@
import { Preview, setup } from "@storybook/vue3";
import { createI18n } from "vue-i18n";
import de from "../src/locales/de.json";
import "../tailwind.css";
import { withVueRouter } from "./mockRouter";
setup((app) => {
const i18n = createI18n({
locale: "de", // set locale
messages: { de },
});
withVueRouter(app);
app.use(i18n);
});
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" x="0px"
y="0px"
viewBox="0 0 450.709 418.11" enable-background="new 0 0 450.709 418.11" xml:space="preserve">
<g>
<g>
<path fill="#FFFFFF" d="M262.573,231.338c-8.74-7.379-17.496-10.559-28.81-10.559c-23.314,0-38.378,16.943-38.378,43.168v67.706
c0,1.566,1.269,2.835,2.835,2.835h11.603c1.566,0,2.835-1.269,2.835-2.835v-40.89h33.462c1.566,0,2.835-1.269,2.835-2.835v-10.34
c0-1.566-1.269-2.835-2.835-2.835h-33.462v-11.31c0-17.186,7.677-26.653,21.612-26.653c7.148,0,12.845,2.163,18.155,6.957
c0.069,0.063,0.586,0.523,1.2,1.07c1.228,1.093,3.126,0.917,4.13-0.385l6.31-8.181c0.888-1.151,0.76-2.788-0.295-3.789
C263.175,231.899,262.648,231.401,262.573,231.338z"/>
</g>
<g>
<path fill="#FFFFFF" d="M225.62,125.182c0.003-1.573,1.289-2.843,2.863-2.829c0.675,0.006,1.231,0.014,1.333,0.024
c6.856,0.657,13.394,3.223,18.955,7.448c9.876,7.504,15.388,19.34,14.743,31.662c-1.041,19.793-17.869,35.296-38.312,35.296
h-26.977c-1.566,0-2.835-1.269-2.835-2.835V86.457c0-1.566,1.269-2.835,2.835-2.835h11.585c1.566,0,2.835,1.269,2.835,2.835
v91.169c0,1.566,1.269,2.835,2.835,2.835h10.081c11.139,0,20.622-8.355,21.591-19.019c1.042-11.523-7.505-21.809-19.019-22.824
c-1.445-0.127-2.537-1.369-2.534-2.82L225.62,125.182z"/>
</g>
</g>
<g>
<path fill="#0069B0" d="M351.43,291.772"/>
<path fill="#0079C3" d="M366.594,287.348c1.275,1.882-0.074,4.424-2.347,4.424h0.012H351.79c-0.941,0-1.82-0.467-2.347-1.245
l-29.151-43.055l-29.151,43.055c-0.527,0.779-1.406,1.245-2.347,1.245h-12.47c-2.273,0-3.622-2.542-2.347-4.424l43.967-64.92
c1.124-1.66,3.57-1.66,4.694,0L366.594,287.348z"/>
<path fill="#FFFFFF" d="M176.731,287.348c1.275,1.882-0.074,4.424-2.347,4.424h0.012h-12.469c-0.941,0-1.82-0.467-2.347-1.245
l-29.151-43.055l-29.151,43.055c-0.527,0.779-1.406,1.245-2.347,1.245h-12.47c-2.273,0-3.622-2.542-2.347-4.424l43.967-64.92
c1.124-1.66,3.57-1.66,4.694,0L176.731,287.348z"/>
<path fill="#0079C3" d="M84.114,132.238c-1.275-1.882,0.074-4.424,2.347-4.424h-0.012h12.469c0.941,0,1.82,0.467,2.347,1.245
l29.151,43.055l29.151-43.055c0.527-0.779,1.406-1.245,2.347-1.245h12.47c2.273,0,3.622,2.542,2.347,4.424l-43.967,64.92
c-1.124,1.66-3.57,1.66-4.694,0L84.114,132.238z"/>
<path fill="#FFFFFF" d="M273.978,132.238c-1.275-1.882,0.074-4.424,2.347-4.424h-0.012h12.469c0.941,0,1.82,0.467,2.347,1.245
l29.151,43.055l29.151-43.055c0.527-0.779,1.406-1.245,2.347-1.245h12.47c2.273,0,3.622,2.542,2.347,4.424l-43.967,64.92
c-1.124,1.66-3.57,1.66-4.694,0L273.978,132.238z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -7,7 +7,7 @@
<!-- workaround for vitejs bundling -> reference https:// --> <!-- workaround for vitejs bundling -> reference https:// -->
<link <link
href="https://vbv-lernwelt.control.iterativ.ch/static/fonts/BuenosAires/stylesheet.css" href="https://myvbv.iterativ.ch/static/fonts/BuenosAires/stylesheet.css"
rel="stylesheet" rel="stylesheet"
/> />
<script defer src="/server/core/icons/"></script> <script defer src="/server/core/icons/"></script>

14863
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
"version": "0.0.0", "version": "0.0.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build && node versionize && cp ./dist/index.html ../server/vbv_lernwelt/templates/vue/index.html && rm -rf ../server/vbv_lernwelt/static/vue/* && cp -r ./dist/static/vue ../server/vbv_lernwelt/static/", "build": "vue-tsc --noEmit && vite build && node versionize && cp ./dist/index.html ../server/vbv_lernwelt/templates/vue/index.html && rm -rf ../server/vbv_lernwelt/static/vue/* && cp -r ./dist/static/vue ../server/vbv_lernwelt/static/ && npm run build-storybook",
"build:tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --minify", "build:tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --minify",
"codegen": "graphql-codegen", "codegen": "graphql-codegen",
"test": "vitest run", "test": "vitest run",
@ -13,7 +13,9 @@
"vue-i18n-extract": "vue-i18n-extract report", "vue-i18n-extract": "vue-i18n-extract report",
"prettier": "prettier . --write", "prettier": "prettier . --write",
"prettier:check": "prettier . --check", "prettier:check": "prettier . --check",
"tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch" "tailwind": "tailwindcss -i tailwind.css -o ../server/vbv_lernwelt/static/css/tailwind.css --watch",
"storybook": "storybook dev -p 6006",
"build-storybook": "rm -rf ../server/vbv_lernwelt/static/storybook/* && storybook build -o ../server/vbv_lernwelt/static/storybook"
}, },
"dependencies": { "dependencies": {
"@headlessui/tailwindcss": "^0.1.2", "@headlessui/tailwindcss": "^0.1.2",
@ -39,6 +41,17 @@
"@rollup/plugin-alias": "^3.1.9", "@rollup/plugin-alias": "^3.1.9",
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.1.4",
"@savvywombat/tailwindcss-grid-areas": "^3.0.0", "@savvywombat/tailwindcss-grid-areas": "^3.0.0",
"@storybook/addon-a11y": "^7.0.0-rc.5",
"@storybook/addon-essentials": "^7.0.0-rc.5",
"@storybook/addon-interactions": "^7.0.0-rc.5",
"@storybook/addon-links": "^7.0.0-rc.5",
"@storybook/addons": "^7.0.0-rc.5",
"@storybook/blocks": "^7.0.0-rc.5",
"@storybook/manager-api": "^7.0.0-rc.5",
"@storybook/testing-library": "^0.0.14-next.1",
"@storybook/theming": "^7.0.0-rc.5",
"@storybook/vue3": "^7.0.0-rc.5",
"@storybook/vue3-vite": "^7.0.0-rc.5",
"@tailwindcss/forms": "^0.5.2", "@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@testing-library/vue": "^6.6.1", "@testing-library/vue": "^6.6.1",
@ -55,6 +68,7 @@
"autoprefixer": "^10.4.8", "autoprefixer": "^10.4.8",
"eslint": "8.22.0", "eslint": "8.22.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.5.0",
"eslint-plugin-storybook": "^0.6.11",
"eslint-plugin-vue": "^9.4.0", "eslint-plugin-vue": "^9.4.0",
"jsdom": "^20.0.0", "jsdom": "^20.0.0",
"postcss": "^8.4.14", "postcss": "^8.4.14",
@ -63,10 +77,16 @@
"prettier-plugin-organize-imports": "^3.1.1", "prettier-plugin-organize-imports": "^3.1.1",
"prettier-plugin-sort-json": "^1.0.0", "prettier-plugin-sort-json": "^1.0.0",
"prettier-plugin-tailwindcss": "^0.2.1", "prettier-plugin-tailwindcss": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"replace-in-file": "^6.3.5", "replace-in-file": "^6.3.5",
"sass": "^1.54.6", "sass": "^1.54.6",
"sass-loader": "^12.6.0", "sass-loader": "^12.6.0",
"start-server-and-test": "^1.14.0", "start-server-and-test": "^1.14.0",
"storybook": "^7.0.0-rc.5",
"storybook-addon-designs": "^v7.0.0-beta.2",
"storybook-tailwind-foundations": "^1.1.2",
"storybook-vue3-router": "^3.0.0-next.1",
"tailwindcss": "^3.1.8", "tailwindcss": "^3.1.8",
"typescript": "^4.8.2", "typescript": "^4.8.2",
"vite": "^3.0.9", "vite": "^3.0.9",

View File

@ -122,7 +122,10 @@ const profileDropdownData: DropdownListItem[] = [
v-if="userStore.loggedIn" v-if="userStore.loggedIn"
:show="state.showMenu" :show="state.showMenu"
:course-session="courseSessionsStore.courseSessionForRoute" :course-session="courseSessionsStore.courseSessionForRoute"
:media-url="courseSessionsStore.courseSessionForRoute?.media_library_url"
:user="userStore"
@closemodal="state.showMenu = false" @closemodal="state.showMenu = false"
@logout="userStore.handleLogout()"
/> />
</Teleport> </Teleport>
<Transition name="nav"> <Transition name="nav">

View File

@ -0,0 +1,64 @@
// @ts-nocheck
import avatar from "../../.storybook/uk1.patrizia.huggel.jpg";
import type { Meta, StoryObj } from "@storybook/vue3";
import MobileMenu from "./MobileMenu.vue";
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
const meta: Meta<typeof MobileMenu> = {
title: "VBV/MobileMenu",
component: MobileMenu,
tags: ["autodocs"],
argTypes: { onClosemodal: { action: "closeModal" } },
parameters: {
viewport: {
defaultViewport: "mobile1",
},
},
decorators: [() => ({ template: '<div class="-ml-4 lg:-ml-16"><story /></div>' })],
};
export default meta;
type Story = StoryObj<typeof MobileMenu>;
export const MobileMenuLoggedOut: Story = {
args: {
show: true,
courseSession: undefined,
mediaUrl: undefined,
},
};
export const MobileMenuLoggedInInCourse: Story = {
args: {
show: true,
courseSession: {
course: {
title: "Test Course",
},
course_url: "https://www.vbv.ch",
learning_path_url: "https://www.vbv.ch",
},
user: {
first_name: "Vreni",
last_name: "Schmid",
avatar_url: avatar,
loggedIn: true,
},
mediaUrl: "https://www.vbv.ch",
},
};
export const MobileMenuLoggedInDashboard: Story = {
args: {
show: true,
courseSession: undefined,
user: {
first_name: "Vreni",
last_name: "Schmid",
avatar_url: avatar,
loggedIn: true,
},
mediaUrl: "https://www.vbv.ch",
},
};

View File

@ -2,21 +2,20 @@
import IconLogout from "@/components/icons/IconLogout.vue"; import IconLogout from "@/components/icons/IconLogout.vue";
import IconSettings from "@/components/icons/IconSettings.vue"; import IconSettings from "@/components/icons/IconSettings.vue";
import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue"; import ItFullScreenModal from "@/components/ui/ItFullScreenModal.vue";
import { useCourseSessionsStore } from "@/stores/courseSessions"; import type { UserState } from "@/stores/user";
import { useUserStore } from "@/stores/user";
import type { CourseSession } from "@/types"; import type { CourseSession } from "@/types";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const userStore = useUserStore();
const courseSessionsStore = useCourseSessionsStore();
const props = defineProps<{ const props = defineProps<{
show: boolean; show: boolean;
courseSession: CourseSession | undefined; courseSession: CourseSession | undefined;
mediaUrl: string | undefined;
user: UserState | undefined;
}>(); }>();
const emits = defineEmits(["closemodal"]); const emits = defineEmits(["closemodal", "logout"]);
const clickLink = (to: string | undefined) => { const clickLink = (to: string | undefined) => {
if (to) { if (to) {
@ -30,17 +29,17 @@ const clickLink = (to: string | undefined) => {
<ItFullScreenModal :show="show" @closemodal="$emit('closemodal')"> <ItFullScreenModal :show="show" @closemodal="$emit('closemodal')">
<div> <div>
<div> <div>
<div v-if="userStore.loggedIn" class="-mx-4 border-b px-8 pb-4"> <div v-if="user?.loggedIn" class="-mx-4 border-b px-8 pb-4">
<div class="-ml-4 flex"> <div class="-ml-4 flex">
<div v-if="userStore.avatar_url"> <div v-if="user?.avatar_url">
<img <img
class="inline-block h-16 w-16 rounded-full" class="inline-block h-16 w-16 rounded-full"
:src="userStore.avatar_url" :src="user?.avatar_url"
alt="" alt=""
/> />
</div> </div>
<div class="ml-6"> <div class="ml-6">
<h3>{{ userStore.first_name }} {{ userStore.last_name }}</h3> <h3>{{ user?.first_name }} {{ user?.last_name }}</h3>
<button <button
class="mt-2 inline-block flex items-center" class="mt-2 inline-block flex items-center"
@click="clickLink('/settings')" @click="clickLink('/settings')"
@ -79,24 +78,17 @@ const clickLink = (to: string | undefined) => {
</a> </a>
</li> </li>
<li class="mt-6"> <li class="mt-6">
<button <button data-cy="medialibrary-link" @click="clickLink(mediaUrl)">
data-cy="medialibrary-link"
@click="
clickLink(
courseSessionsStore.courseSessionForRoute?.media_library_url
)
"
>
{{ $t("mediaLibrary.title") }} {{ $t("mediaLibrary.title") }}
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
<button <button
v-if="userStore.loggedIn" v-if="user?.loggedIn"
type="button" type="button"
class="mt-6 flex items-center" class="mt-6 flex items-center"
@click="userStore.handleLogout()" @click="$emit('logout')"
> >
<IconLogout class="inline-block" /> <IconLogout class="inline-block" />
<span class="ml-1">{{ $t("mainNavigation.logout") }}</span> <span class="ml-1">{{ $t("mainNavigation.logout") }}</span>

View File

@ -0,0 +1,42 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import LearningPathCircle from "@/components/learningPath/page/LearningPathCircle.vue";
//👇 This default export determines where your story goes in the story list
const meta: Meta<typeof LearningPathCircle> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: "VBV/LearningPath/LearningPathCircle",
component: LearningPathCircle,
tags: ["autodocs"],
decorators: [
() => ({
template:
'<div style="margin: 3em;"><story class="h-24 w-24 snap-center"/></div>',
}),
],
};
export default meta;
type Story = StoryObj<typeof LearningPathCircle>;
export const FirstStory: Story = {
args: {
sectors: [
{
progress: "finished",
},
{
progress: "finished",
},
{
progress: "in_progress",
},
{
progress: "none",
},
],
},
};

View File

@ -0,0 +1,60 @@
// @ts-nocheck
import ItButton from "@/components/ui/ItButton.vue";
import type { Meta, StoryObj } from "@storybook/vue3";
const meta: Meta<typeof ItButton> = {
title: "VBV/Button",
component: ItButton,
tags: ["autodocs"],
render: (args) => ({
components: { ItButton },
setup() {
return { args };
},
template: '<ItButton v-bind="args" v-html="args.label"></ItButton>',
}),
parameters: {
design: {
type: "figma",
url: "https://www.figma.com/file/Svqt2dNY0cF4FCyyiWmitN/vbv-storybook-library?node-id=1-2&t=3AfFOE3SqBxniETT-4",
},
},
argTypes: {
variant: {
options: ["primary", "secondary", "blue", "text"],
control: { type: "select" },
},
size: {
options: ["normal", "large"],
control: { type: "radio" },
},
},
};
export default meta;
type Story = StoryObj<typeof ItButton>;
export const TextButton: Story = {
args: {
label: "Hello world",
},
};
export const ImageLeftButton: Story = {
args: {
label: '<it-icon-message class="-ml-1 mr-3 h-5 w-5"></it-icon-message> Hello world',
},
};
export const ImageRightButton: Story = {
args: {
label: 'Hello world <it-icon-message class="ml-3 -mr-1 h-5 w-5"></it-icon-message>',
},
};
export const ImageLargeRightButton: Story = {
args: {
size: "large",
label: 'Hello world <it-icon-message class="ml-3 -mr-1 h-6 w-6"></it-icon-message>',
},
};

View File

@ -0,0 +1,71 @@
<script setup lang="ts">
import { computed } from "vue";
type Props = {
variant: "primary" | "secondary" | "blue" | "text";
size: "normal" | "large";
disabled: boolean;
href?: string;
};
const props = withDefaults(defineProps<Props>(), {
variant: "primary",
size: "normal",
disabled: false,
href: undefined,
});
const emits = defineEmits(["click"]);
const tag = computed(() => (props.href ? "a" : "button"));
const classes = computed(() => {
const result = ["flex items-center"];
if (props.disabled) {
result.push("opacity-50 cursor-not-allowed");
}
switch (props.variant) {
case "secondary":
result.push("btn-secondary");
break;
case "blue":
result.push("btn-blue");
break;
case "text":
result.push("btn-text");
break;
default:
result.push("btn-primary");
}
if (props.size === "large") {
result.push("px-6 py-3 text-xl font-bold");
}
return result;
});
function handleClick(event: MouseEvent) {
if (props.disabled) {
event.preventDefault();
} else {
emits("click");
}
}
</script>
<template>
<component
:is="tag"
:class="classes"
:disabled="disabled"
:href="href"
@click="handleClick"
>
<slot></slot>
</component>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import ItCheckboxGroup from "./ItCheckboxGroup.vue";
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
const meta: Meta<typeof ItCheckboxGroup> = {
title: "VBV/CheckboxGroup",
component: ItCheckboxGroup,
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
tags: ["autodocs"],
args: { label: "A Label" }, // default value
argTypes: { "onUpdate:items": { action: "itemsUpdate" } },
};
export default meta;
type Story = StoryObj<typeof ItCheckboxGroup>;
export const CheckboxGroup: Story = {
args: {
label: "Label",
items: [
{
value: "checkbox1",
label: "Label 1",
subtitle: "Subtitle 1",
checked: false,
},
{
value: "checkbox2",
label: "Label 2",
subtitle: "Subtitle 2",
checked: false,
},
{
value: "checkbox3",
label: "Label 3",
checked: false,
},
],
},
};

View File

@ -0,0 +1,22 @@
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
import type { Meta, StoryObj } from "@storybook/vue3";
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
const meta: Meta<typeof ItNavigationProgress> = {
title: "VBV/NavigationProgress",
component: ItNavigationProgress,
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof ItNavigationProgress>;
export const NavigationProgressPartial: Story = {
args: {
steps: 5,
currentStep: 3,
startBadgeText: "Einleitung",
endBadgeText: "Abgabe",
},
};

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { computed } from "vue";
export interface Props {
steps: number;
currentStep: number;
startBadgeText: string;
endBadgeText: string;
}
const props = defineProps<Props>();
function getPillClasses(step: number) {
if (step == props.currentStep) {
return "bg-sky-500 text-bold";
} else if (step < props.currentStep) {
return "bg-green-500";
}
return "border";
}
const startBadgeClasses = computed(() => {
if (0 == props.currentStep) {
return "bg-sky-500 text-bold";
}
return "bg-green-500";
});
const endBadgeClasses = computed(() => {
if (props.steps + 1 == props.currentStep) {
return "bg-sky-500 text-bold";
} else if (props.steps + 2 == props.currentStep) {
return "bg-green-500 text-bold";
}
return "border";
});
</script>
<template>
<div class="flex flex-row text-sm">
<div
class="inline-flex h-7 items-center justify-center rounded-3xl px-3"
:class="startBadgeClasses"
>
{{ props.startBadgeText }}
</div>
<div v-for="step in props.steps" :key="step" class="flex flex-row">
<hr class="w-16 self-center border border-[1px] border-gray-400" />
<div
class="inline-flex h-7 w-7 items-center justify-center rounded-full py-1 px-3"
:class="getPillClasses(step)"
>
{{ step }}
</div>
</div>
<hr class="w-16 self-center border border-gray-400" />
<div
class="inline-flex h-7 items-center justify-center rounded-3xl px-3"
:class="endBadgeClasses"
>
{{ props.endBadgeText }}
</div>
</div>
</template>

View File

@ -0,0 +1,45 @@
// ButtonGroup.stories.ts
import type { Meta, StoryObj } from "@storybook/vue3";
import ItProgress from "./ItProgress.vue";
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
const meta: Meta<typeof ItProgress> = {
title: "VBV/ProgressBar",
component: ItProgress,
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof ItProgress>;
export const NoProgress: Story = {
args: {
statusCount: {
fail: 0,
success: 0,
unknown: 10,
},
},
};
export const FiftyPrecentSuccessProgress: Story = {
args: {
statusCount: {
fail: 0,
success: 5,
unknown: 5,
},
},
};
export const FiftyPrecentFailProgress: Story = {
args: {
statusCount: {
fail: 5,
success: 0,
unknown: 5,
},
},
};

View File

@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import RatingScale from "./RatingScale.vue";
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
const meta: Meta<typeof RatingScale> = {
title: "VBV/RatingScale",
component: RatingScale,
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof RatingScale>;
export const RatingScaleGreen: Story = {
args: {
ratings: [3, 4, 3, 4],
title: "Frage 3",
text: "Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?",
},
};
export const RatingScaleOrange: Story = {
args: {
ratings: [2, 1, 2, 3],
title: "Frage 5",
text: "Wie zufrieden bist du mit dem Kurs “Überbetriebliche Kurse” im Allgemeinen?",
},
};

View File

@ -0,0 +1,29 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import VerticalBarChart from "./VerticalBarChart.vue";
// More on how to set up stories at: https://storybook.js.org/docs/7.0/vue/writing-stories/introduction
const meta: Meta<typeof VerticalBarChart> = {
title: "VBV/VerticalBarChart",
component: VerticalBarChart,
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof VerticalBarChart>;
export const VerticalBarChartOneThirdYes: Story = {
args: {
title: "Frage X",
text: "Fragentext",
ratings: [true, true, false, true, true, false, true, false],
},
};
export const VerticalBarChartAllFalse: Story = {
args: {
title: "Frage Y",
text: "Das ist ein anderer Fragetext",
ratings: [false, false, false, false, false, false, false, false],
},
};

View File

@ -9,7 +9,7 @@ const documents = {
export function graphql( export function graphql(
source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n" source: "\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"
): typeof documents["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"]; ): (typeof documents)["\n mutation SendFeedbackMutation($input: SendFeedbackInput!) {\n sendFeedback(input: $input) {\n feedbackResponse {\n id\n }\n errors {\n field\n messages\n }\n }\n }\n"];
export function graphql(source: string): unknown; export function graphql(source: string): unknown;
export function graphql(source: string) { export function graphql(source: string) {

View File

@ -32,8 +32,7 @@ const app = createApp(App);
Sentry.init({ Sentry.init({
app, app,
environment: environment: import.meta.env.VITE_SENTRY_ENV || "development",
import.meta.env.VITE_SENTRY_ENV || "http://localhost:8000/server/graphql/",
dsn: "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352", dsn: "https://2df6096a4fd94bd6b4802124d10e4b8d@o8544.ingest.sentry.io/4504157846372352",
tracesSampleRate: 0.0, tracesSampleRate: 0.0,
enabled: enabled:
@ -52,7 +51,7 @@ loadLocaleMessages("de").then(() => {
app.use(pinia); app.use(pinia);
app.use(i18n); app.use(i18n);
app.use(urql, { app.use(urql, {
url: import.meta.env.VITE_GRAPHQL_URL || "/server/graphql", url: import.meta.env.VITE_GRAPHQL_URL || "/server/graphql/",
}); });
app.mount("#app"); app.mount("#app");

View File

@ -7,6 +7,7 @@ import ItCheckbox from "@/components/ui/ItCheckbox.vue";
import ItCheckboxGroup from "@/components/ui/ItCheckboxGroup.vue"; import ItCheckboxGroup from "@/components/ui/ItCheckboxGroup.vue";
import ItDropdown from "@/components/ui/ItDropdown.vue"; import ItDropdown from "@/components/ui/ItDropdown.vue";
import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue"; import ItDropdownSelect from "@/components/ui/ItDropdownSelect.vue";
import ItNavigationProgress from "@/components/ui/ItNavigationProgress.vue";
import ItRadioGroup from "@/components/ui/ItRadioGroup.vue"; import ItRadioGroup from "@/components/ui/ItRadioGroup.vue";
import ItTextarea from "@/components/ui/ItTextarea.vue"; import ItTextarea from "@/components/ui/ItTextarea.vue";
import ItToggleSwitch from "@/components/ui/ItToggleSwitch.vue"; import ItToggleSwitch from "@/components/ui/ItToggleSwitch.vue";
@ -522,6 +523,15 @@ function log(data: any) {
<h2 class="mt-8 mb-8">ItToggleSwitch</h2> <h2 class="mt-8 mb-8">ItToggleSwitch</h2>
<ItToggleSwitch></ItToggleSwitch> <ItToggleSwitch></ItToggleSwitch>
<h2 class="mt-8 mb-8">ItNavigationProgress</h2>
<ItNavigationProgress
:steps="4"
:current-step="1"
start-badge-text="Einleitung"
end-badge-text="Abgabe"
/>
</main> </main>
</template> </template>

View File

@ -1,10 +1,6 @@
import DashboardPage from "@/pages/DashboardPage.vue"; import DashboardPage from "@/pages/DashboardPage.vue";
import LoginPage from "@/pages/LoginPage.vue"; import LoginPage from "@/pages/LoginPage.vue";
import { import { redirectToLoginIfRequired, updateLoggedIn } from "@/router/guards";
expertRequired,
redirectToLoginIfRequired,
updateLoggedIn,
} from "@/router/guards";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";

View File

@ -37,7 +37,10 @@ export const useNotificationsStore = defineStore("notifications", () => {
if (userStore.loggedIn && timerHandle === null) { if (userStore.loggedIn && timerHandle === null) {
log.debug("Notification polling started"); log.debug("Notification polling started");
updateUnreadCount(); updateUnreadCount();
timerHandle = setInterval(async () => await updateUnreadCount(), 30000); timerHandle = setInterval(
async () => await updateUnreadCount(),
30000
) as unknown as number;
} else if (!userStore.loggedIn) { } else if (!userStore.loggedIn) {
log.debug("Notification polling stopped"); log.debug("Notification polling stopped");
timerHandle = null; timerHandle = null;

View File

@ -5,7 +5,7 @@ import { loadLocaleMessages, setI18nLanguage } from "@/i18n";
import { useAppStore } from "@/stores/app"; import { useAppStore } from "@/stores/app";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
const logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT; const logoutRedirectUrl = import.meta.env.VITE_LOGOUT_REDIRECT || "/";
// typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state // typed state https://stackoverflow.com/questions/71012513/when-using-pinia-and-typescript-how-do-you-use-an-action-to-set-the-state
export type AvailableLanguages = "de" | "fr" | "it"; export type AvailableLanguages = "de" | "fr" | "it";

View File

@ -0,0 +1,51 @@
<template>
<button type="button" :class="classes" :style="style" @click="onClick">
{{ label }}
</button>
</template>
<script lang="ts" setup>
import { computed } from "vue";
import "./button.css";
const props = withDefaults(
defineProps<{
/**
* The label of the button
*/
label: string;
/**
* primary or secondary button
*/
primary?: boolean;
/**
* size of the button
*/
size?: "small" | "medium" | "large";
/**
* background color of the button
*/
backgroundColor?: string;
}>(),
{ primary: false }
);
const emit = defineEmits<{
(e: "click", id: number): void;
}>();
const classes = computed(() => ({
"storybook-button": true,
"storybook-button--primary": props.primary,
"storybook-button--secondary": !props.primary,
[`storybook-button--${props.size || "medium"}`]: true,
}));
const style = computed(() => ({
backgroundColor: props.backgroundColor,
}));
const onClick = () => {
emit("click", 1);
};
</script>

View File

@ -0,0 +1,42 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import MyHeader from "./ExampleHeader.vue";
const meta: Meta<typeof MyHeader> = {
/* 👇 The title prop is optional.
* See https://storybook.js.org/docs/7.0/vue/configure/overview#configure-story-loading
* to learn how to generate automatic titles
*/
title: "Example/Header",
component: MyHeader,
render: (args: any) => ({
components: { MyHeader },
setup() {
return { args };
},
template: '<my-header :user="args.user" />',
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/7.0/react/configure/story-layout
layout: "fullscreen",
},
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof MyHeader>;
export const LoggedIn: Story = {
args: {
user: {
name: "Jane Doe",
},
},
};
export const LoggedOut: Story = {
args: {
user: null,
},
};

View File

@ -0,0 +1,59 @@
<template>
<header>
<div class="wrapper">
<div>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fill-rule="evenodd">
<path
d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
fill="#FFF"
/>
<path
d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
fill="#555AB9"
/>
<path
d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
fill="#91BAF8"
/>
</g>
</svg>
<h1>Acme</h1>
</div>
<div>
<span v-if="user" class="welcome">
Welcome,
<b>{{ user.name }}</b>
!
</span>
<my-button v-if="user" size="small" label="Log out" @click="$emit('logout')" />
<my-button v-if="!user" size="small" label="Log in" @click="$emit('login')" />
<my-button
v-if="!user"
primary
size="small"
label="Sign up"
@click="$emit('createAccount')"
/>
</div>
</div>
</header>
</template>
<script lang="ts" setup>
import MyButton from "./ExampleButton.vue";
import "./header.css";
defineProps<{ user: { name: string } | null }>();
defineEmits<{
(event: "createAccount"): void;
(event: "login"): void;
(event: "logout"): void;
}>();
</script>

View File

@ -0,0 +1,34 @@
import { userEvent, within } from "@storybook/testing-library";
import type { Meta, StoryObj } from "@storybook/vue3";
import MyPage from "./ExamplePage.vue";
const meta: Meta<typeof MyPage> = {
title: "Example/Page",
component: MyPage,
render: () => ({
components: { MyPage },
template: "<my-page />",
}),
parameters: {
// More on how to position stories at: https://storybook.js.org/docs/7.0/vue/configure/story-layout
layout: "fullscreen",
},
// This component will have an automatically generated docsPage entry: https://storybook.js.org/docs/7.0/vue/writing-docs/docs-page
tags: ["autodocs"],
};
export default meta;
type Story = StoryObj<typeof MyPage>;
// More on interaction testing: https://storybook.js.org/docs/7.0/vue/writing-tests/interaction-testing
export const LoggedIn: Story = {
play: async ({ canvasElement }: any) => {
const canvas = within(canvasElement);
const loginButton = await canvas.getByRole("button", {
name: /Log in/i,
});
await userEvent.click(loginButton);
},
};
export const LoggedOut: Story = {};

View File

@ -0,0 +1,93 @@
<template>
<article>
<my-header
:user="user"
@login="onLogin"
@logout="onLogout"
@create-account="onCreateAccount"
/>
<section>
<h2>Pages in Storybook</h2>
<p>
We recommend building UIs with a
<a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
<strong>component-driven</strong>
</a>
process starting with atomic components and ending with pages.
</p>
<p>
Render pages with mock data. This makes it easy to build and review page states
without needing to navigate to them in your app. Here are some handy patterns
for managing page data in Storybook:
</p>
<ul>
<li>
Use a higher-level connected component. Storybook helps you compose such data
from the "args" of child component stories
</li>
<li>
Assemble data in the page component from your services. You can mock these
services out using Storybook.
</li>
</ul>
<p>
Get a guided tutorial on component-driven development at
<a
href="https://storybook.js.org/tutorials/"
target="_blank"
rel="noopener noreferrer"
>
Storybook tutorials
</a>
. Read more in the
<a
href="https://storybook.js.org/docs"
target="_blank"
rel="noopener noreferrer"
>
docs
</a>
.
</p>
<div class="tip-wrapper">
<span class="tip">Tip</span>
Adjust the width of the canvas with the
<svg
width="10"
height="10"
viewBox="0 0 12 12"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fill-rule="evenodd">
<path
id="a"
d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
fill="#999"
/>
</g>
</svg>
Viewports addon in the toolbar
</div>
</section>
</article>
</template>
<script lang="ts" setup>
import MyHeader from "./ExampleHeader.vue";
import "./page.css";
import { ref } from "vue";
const user = ref<{ name: string } | null>(null);
const onLogin = () => {
user.value = { name: "Jane Doe" };
};
const onLogout = () => {
user.value = null;
};
const onCreateAccount = () => {
user.value = { name: "Jane Doe" };
};
</script>

View File

@ -0,0 +1,217 @@
import { Meta } from "@storybook/blocks";
import Code from "./assets/code-brackets.svg";
import Colors from "./assets/colors.svg";
import Comments from "./assets/comments.svg";
import Direction from "./assets/direction.svg";
import Flow from "./assets/flow.svg";
import Plugin from "./assets/plugin.svg";
import Repo from "./assets/repo.svg";
import StackAlt from "./assets/stackalt.svg";
<Meta title="Example/Introduction" />
<style>
{`
.subheading {
--mediumdark: '#999999';
font-weight: 700;
font-size: 13px;
color: #999;
letter-spacing: 6px;
line-height: 24px;
text-transform: uppercase;
margin-bottom: 12px;
margin-top: 40px;
}
.link-list {
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 1fr;
row-gap: 10px;
}
@media (min-width: 620px) {
.link-list {
row-gap: 20px;
column-gap: 20px;
grid-template-columns: 1fr 1fr;
}
}
@media all and (-ms-high-contrast:none) {
.link-list {
display: -ms-grid;
-ms-grid-columns: 1fr 1fr;
-ms-grid-rows: 1fr 1fr;
}
}
.link-item {
display: block;
padding: 20px;
border: 1px solid #00000010;
border-radius: 5px;
transition: background 150ms ease-out, border 150ms ease-out, transform 150ms ease-out;
color: #333333;
display: flex;
align-items: flex-start;
}
.link-item:hover {
border-color: #1EA7FD50;
transform: translate3d(0, -3px, 0);
box-shadow: rgba(0, 0, 0, 0.08) 0 3px 10px 0;
}
.link-item:active {
border-color: #1EA7FD;
transform: translate3d(0, 0, 0);
}
.link-item strong {
font-weight: 700;
display: block;
margin-bottom: 2px;
}
.link-item img {
height: 40px;
width: 40px;
margin-right: 15px;
flex: none;
}
.link-item span,
.link-item p {
margin: 0;
font-size: 14px;
line-height: 20px;
}
.tip {
display: inline-block;
border-radius: 1em;
font-size: 11px;
line-height: 12px;
font-weight: 700;
background: #E7FDD8;
color: #66BF3C;
padding: 4px 12px;
margin-right: 10px;
vertical-align: top;
}
.tip-wrapper {
font-size: 13px;
line-height: 20px;
margin-top: 40px;
margin-bottom: 40px;
}
.tip-wrapper code {
font-size: 12px;
display: inline-block;
}
`}
</style>
# Welcome to Storybook
Storybook helps you build UI components in isolation from your app's business logic, data, and context.
That makes it easy to develop hard-to-reach states. Save these UI states as **stories** to revisit during development, testing, or QA.
Browse example stories now by navigating to them in the sidebar.
View their code in the `stories` directory to learn how they work.
We recommend building UIs with a [**component-driven**](https://componentdriven.org) process starting with atomic components and ending with pages.
<div className="subheading">Configure</div>
<div className="link-list">
<a
className="link-item"
href="https://storybook.js.org/docs/react/addons/addon-types"
target="_blank"
>
<img src={Plugin} alt="plugin" />
<span>
<strong>Presets for popular tools</strong>
Easy setup for TypeScript, SCSS and more.
</span>
</a>
<a
className="link-item"
href="https://storybook.js.org/docs/react/configure/webpack"
target="_blank"
>
<img src={StackAlt} alt="Build" />
<span>
<strong>Build configuration</strong>
How to customize webpack and Babel
</span>
</a>
<a
className="link-item"
href="https://storybook.js.org/docs/react/configure/styling-and-css"
target="_blank"
>
<img src={Colors} alt="colors" />
<span>
<strong>Styling</strong>
How to load and configure CSS libraries
</span>
</a>
<a
className="link-item"
href="https://storybook.js.org/docs/react/get-started/setup#configure-storybook-for-your-stack"
target="_blank"
>
<img src={Flow} alt="flow" />
<span>
<strong>Data</strong>
Providers and mocking for data libraries
</span>
</a>
</div>
<div className="subheading">Learn</div>
<div className="link-list">
<a className="link-item" href="https://storybook.js.org/docs" target="_blank">
<img src={Repo} alt="repo" />
<span>
<strong>Storybook documentation</strong>
Configure, customize, and extend
</span>
</a>
<a className="link-item" href="https://storybook.js.org/tutorials/" target="_blank">
<img src={Direction} alt="direction" />
<span>
<strong>In-depth guides</strong>
Best practices from leading teams
</span>
</a>
<a
className="link-item"
href="https://github.com/storybookjs/storybook"
target="_blank"
>
<img src={Code} alt="code" />
<span>
<strong>GitHub project</strong>
View the source and add issues
</span>
</a>
<a className="link-item" href="https://discord.gg/storybook" target="_blank">
<img src={Comments} alt="comments" />
<span>
<strong>Discord chat</strong>
Chat with maintainers and the community
</span>
</a>
</div>
<div className="tip-wrapper">
<span className="tip">Tip</span>Edit the Markdown in{" "}
<code>stories/Introduction.stories.mdx</code>
</div>

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/code-brackets</title><g id="illustration/code-brackets" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#87E6E5" d="M11.4139325,12 C11.7605938,12 12,12.5059743 12,13.3779712 L12,17.4951758 L6.43502246,23.3839989 C5.85499251,23.9978337 5.85499251,25.0021663 6.43502246,25.6160011 L12,31.5048242 L12,35.6220288 C12,36.4939606 11.7605228,37 11.4139325,37 C11.2725831,37 11.1134406,36.9158987 10.9453839,36.7379973 L0.435022463,25.6160011 C-0.145007488,25.0021663 -0.145007488,23.9978337 0.435022463,23.3839989 L10.9453839,12.2620027 C11.1134051,12.0841663 11.2725831,12 11.4139325,12 Z M36.5860675,12 C36.7274169,12 36.8865594,12.0841013 37.0546161,12.2620027 L47.5649775,23.3839989 C48.1450075,23.9978337 48.1450075,25.0021663 47.5649775,25.6160011 L37.0546161,36.7379973 C36.8865949,36.9158337 36.7274169,37 36.5860675,37 C36.2394062,37 36,36.4940257 36,35.6220288 L36,31.5048242 L41.5649775,25.6160011 C42.1450075,25.0021663 42.1450075,23.9978337 41.5649775,23.3839989 L36,17.4951758 L36,13.3779712 C36,12.5060394 36.2394772,12 36.5860675,12 Z"/><rect id="Rectangle-7-Copy-5" width="35.57" height="4" x="5.009" y="22.662" fill="#A0DB77" rx="2" transform="translate(22.793959, 24.662305) rotate(-75.000000) translate(-22.793959, -24.662305)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/comments</title><g id="illustration/comments" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Path" fill="#96D07C" d="M2.52730803,17.9196415 C2.44329744,17.9745167 2.36370847,18.000488 2.29303375,18.000488 C2.1197031,18.000488 2,17.8443588 2,17.5752855 L2,4 C2,1.790861 3.790861,3.23296945e-13 6,3.23296945e-13 L33.9995117,3.23296945e-13 C36.2086507,3.23296945e-13 37.9995117,1.790861 37.9995117,4 L37.9995117,9.999512 C37.9995117,12.208651 36.2086507,13.999512 33.9995117,13.999512 L8,13.999512 C7.83499225,13.999512 7.6723181,13.9895206 7.51254954,13.9701099 L2.52730803,17.9196415 Z"/><path id="Path" fill="#73E1E0" d="M7.51066,44.9703679 L2.52730803,47.9186655 C2.44329744,47.9735407 2.36370847,47.999512 2.29303375,47.999512 C2.1197031,47.999512 2,47.8433828 2,47.5743095 L2,35 C2,32.790861 3.790861,31 6,31 L26,31 C28.209139,31 30,32.790861 30,35 L30,41 C30,43.209139 28.209139,45 26,45 L8,45 C7.8343417,45 7.67103544,44.9899297 7.51066,44.9703679 Z"/><path id="Path" fill="#FFD476" d="M46,19.5 L46,33.0747975 C46,33.3438708 45.8802969,33.5 45.7069663,33.5 C45.6362915,33.5 45.5567026,33.4740287 45.472692,33.4191535 L40.4887103,29.4704446 C40.3285371,29.489956 40.1654415,29.5 40,29.5 L18,29.5 C15.790861,29.5 14,27.709139 14,25.5 L14,19.5 C14,17.290861 15.790861,15.5 18,15.5 L42,15.5 C44.209139,15.5 46,17.290861 46,19.5 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/direction</title><g id="illustration/direction" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFD476" d="M23.4917015,33.6030641 L2.93840258,31.4321033 C2.38917316,31.3740904 1.99096346,30.8818233 2.04897631,30.3325939 C2.0747515,30.0885705 2.18934861,29.8625419 2.37095722,29.6975265 L34.2609105,0.721285325 C34.6696614,0.349881049 35.3021022,0.38015648 35.6735064,0.788907393 C35.9232621,1.06377731 36.0001133,1.45442096 35.8730901,1.80341447 L24.5364357,32.9506164 C24.3793473,33.3822133 23.9484565,33.6513092 23.4917015,33.6030641 L23.4917015,33.6030641 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" d="M24.3163597,33.2881029 C24.0306575,33.0138462 23.9337246,32.5968232 24.069176,32.2246735 L35.091923,1.9399251 C35.2266075,1.56988243 35.5659249,1.31333613 35.9586669,1.28460955 C36.5094802,1.24432106 36.9886628,1.65818318 37.0289513,2.20899647 L40.2437557,46.1609256 C40.2644355,46.4436546 40.1641446,46.7218752 39.9678293,46.9263833 C39.5853672,47.3248067 38.9523344,47.3377458 38.5539111,46.9552837 L24.3163597,33.2881029 L24.3163597,33.2881029 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/flow</title><g id="illustration/flow" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" fill-rule="nonzero" d="M30,29 C32.7614237,29 35,26.7614237 35,24 C35,14.6111593 27.3888407,7 18,7 C8.61115925,7 1,14.6111593 1,24 C1,33.3888407 8.61115925,41 18,41 C19.3333404,41 20.6447683,40.8466238 21.9154603,40.5471706 C19.5096374,39.3319645 17.5510566,37.8612875 16.0456579,36.1314815 C14.1063138,33.9030427 12.769443,31.0725999 12.0293806,27.6556449 C11.360469,26.565281 11,25.3082308 11,24 C11,20.1340068 14.1340068,17 18,17 C21.8659932,17 25,20.1340068 25,24 C25,26.125 27.7040312,29 30,29 Z"/><path id="Combined-Shape-Copy" fill="#FFC445" fill-rule="nonzero" d="M42,29 C44.7614237,29 47,26.7614237 47,24 C47,14.6111593 39.3888407,7 30,7 C20.6111593,7 13,14.6111593 13,24 C13,33.3888407 20.6111593,41 30,41 C31.3333404,41 32.6447683,40.8466238 33.9154603,40.5471706 C31.5096374,39.3319645 29.4051056,37.9781963 28.0456579,36.1314815 C26.0625,33.4375 23,27.1875 23,24 C23,20.1340068 26.1340068,17 30,17 C33.8659932,17 37,20.1340068 37,24 C37.02301,26.3435241 39.7040312,29 42,29 Z" transform="translate(30.000000, 24.000000) scale(-1, -1) translate(-30.000000, -24.000000)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/plugin</title><g id="illustration/plugin" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#79C9FC" d="M26,15.3994248 C26,15.4091303 26,15.4188459 26,15.4285714 L26,21.4694881 C25.8463595,21.4969567 25.6941676,21.51275 25.5873784,21.51275 C25.4974117,21.51275 25.4230979,21.4768034 25.377756,21.4206259 L25.2660784,21.2822603 L25.1317423,21.1657666 C24.2436317,20.3956144 23.100098,19.9633214 21.895551,19.9633214 C19.2039137,19.9633214 17,22.1075558 17,24.7804643 C17,27.4533728 19.2039137,29.5976071 21.895551,29.5976071 C23.1972122,29.5976071 24.3149423,29.2878193 25.1231445,28.3613697 C25.4542273,27.9818463 25.568273,27.9073214 25.5873784,27.9073214 C25.681532,27.9073214 25.8352452,27.9239643 26,27.9524591 L26,32.5714286 C26,32.5811541 26,32.5908697 26,32.6005752 L26,33 C26,35.209139 24.209139,37 22,37 L4,37 C1.790861,37 0,35.209139 0,33 L0,15 C0,12.790861 1.790861,11 4,11 L22,11 C24.209139,11 26,12.790861 26,15 L26,15.3994248 Z"/><path id="Path" fill="#87E6E5" d="M27.9998779,32.5714286 C27.9998779,33.3604068 28.6572726,34 29.4682101,34 L46.5315458,34 C47.3424832,34 47.9998779,33.3604068 47.9998779,32.5714286 L47.9998779,15.4285714 C47.9998779,14.6395932 47.3424832,14 46.5315458,14 L29.4682101,14 C28.6572726,14 27.9998779,14.6395932 27.9998779,15.4285714 L27.9998779,21.8355216 C27.9334367,22.2650514 27.8567585,22.6454496 27.746391,22.8084643 C27.4245309,23.2838571 26.2402709,23.51275 25.5873784,23.51275 C24.8705773,23.51275 24.2322714,23.1857725 23.8214379,22.6767605 C23.3096996,22.2329909 22.6349941,21.9633214 21.895551,21.9633214 C20.2963823,21.9633214 19,23.2245992 19,24.7804643 C19,26.3363293 20.2963823,27.5976071 21.895551,27.5976071 C22.5398535,27.5976071 23.2399343,27.477727 23.6160247,27.0466112 C24.1396029,26.4464286 24.7367044,25.9073214 25.5873784,25.9073214 C26.2402709,25.9073214 27.5912951,26.1766031 27.8226692,26.6116071 C27.8819199,26.7230038 27.9403239,26.921677 27.9998779,27.1556219 L27.9998779,32.5714286 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/repo</title><g id="illustration/repo" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Rectangle-62-Copy" fill="#B7F0EF" d="M27.2217723,9.04506931 L41.2217723,6.2682098 C43.3886973,5.83840648 45.4937616,7.2466219 45.9235649,9.41354696 C45.9743993,9.66983721 46,9.93049166 46,10.1917747 L46,32.581381 C46,34.4904961 44.650862,36.1335143 42.7782277,36.5049459 L28.7782277,39.2818054 C26.6113027,39.7116087 24.5062384,38.3033933 24.0764351,36.1364682 C24.0256007,35.880178 24,35.6195235 24,35.3582405 L24,12.9686342 C24,11.0595191 25.349138,9.4165009 27.2217723,9.04506931 Z" opacity=".7"/><path id="Combined-Shape" fill="#87E6E5" d="M6.77822775,6.2682098 L20.7782277,9.04506931 C22.650862,9.4165009 24,11.0595191 24,12.9686342 L24,35.3582405 C24,37.5673795 22.209139,39.3582405 20,39.3582405 C19.738717,39.3582405 19.4780625,39.3326398 19.2217723,39.2818054 L5.22177225,36.5049459 C3.34913798,36.1335143 2,34.4904961 2,32.581381 L2,10.1917747 C2,7.98263571 3.790861,6.19177471 6,6.19177471 C6.26128305,6.19177471 6.5219375,6.21737537 6.77822775,6.2682098 Z"/><path id="Rectangle-63-Copy-2" fill="#61C1FD" d="M22,10 C23.1666667,10.2291667 24.0179036,10.625 24.5537109,11.1875 C25.0895182,11.75 25.5716146,12.875 26,14.5625 C26,29.3020833 26,37.5208333 26,39.21875 C26,40.9166667 26.4241536,42.9583333 27.2724609,45.34375 L24.5537109,41.875 L22.9824219,45.34375 C22.327474,43.1979167 22,41.2291667 22,39.4375 C22,37.6458333 22,27.8333333 22,10 Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48" version="1.1" viewBox="0 0 48 48"><title>illustration/stackalt</title><g id="illustration/stackalt" fill="none" fill-rule="evenodd" stroke="none" stroke-width="1"><path id="Combined-Shape" fill="#FFAE00" d="M23.8628277,0 L23.8628277,48 L3.32291648,36.2491883 L3.32155653,11.9499781 L23.8628277,0 Z M23.8670509,0 L44.408322,11.9499781 L44.4069621,36.2491883 L23.8670509,48 L23.8670509,0 Z" opacity=".196"/><path id="Rectangle-46-Copy-3" fill="#66BF3C" d="M15.8232279,19.1155258 L24.7368455,21.4714881 C29.6053842,22.7582937 33.4077423,26.5606518 34.694548,31.4291905 L37.0505103,40.3428082 C37.6150232,42.4786032 36.3412474,44.6676353 34.2054524,45.2321482 C33.5569474,45.4035549 32.87575,45.4091235 32.2245294,45.2483418 L23.3459013,43.0562718 C18.2976962,41.809906 14.3561301,37.8683399 13.1097642,32.8201348 L10.9176943,23.9415066 C10.3881737,21.7967682 11.6975664,19.6288529 13.8423049,19.0993322 C14.4935255,18.9385505 15.1747229,18.9441191 15.8232279,19.1155258 Z" opacity=".5" transform="translate(23.999997, 32.166058) rotate(-45.000000) translate(-23.999997, -32.166058)"/><path id="Rectangle-46-Copy-2" fill="#FFAE00" d="M15.8232279,11.2216893 L24.7368455,13.5776516 C29.6053842,14.8644572 33.4077423,18.6668153 34.694548,23.5353541 L37.0505103,32.4489717 C37.6150232,34.5847667 36.3412474,36.7737988 34.2054524,37.3383117 C33.5569474,37.5097184 32.87575,37.515287 32.2245294,37.3545053 L23.3459013,35.1624353 C18.2976962,33.9160695 14.3561301,29.9745034 13.1097642,24.9262983 L10.9176943,16.0476701 C10.3881737,13.9029317 11.6975664,11.7350164 13.8423049,11.2054957 C14.4935255,11.044714 15.1747229,11.0502826 15.8232279,11.2216893 Z" opacity=".5" transform="translate(23.999997, 24.272222) rotate(-45.000000) translate(-23.999997, -24.272222)"/><path id="Rectangle-46-Copy" fill="#FC521F" d="M15.8232279,3.32785281 L24.7368455,5.68381509 C29.6053842,6.97062075 33.4077423,10.7729788 34.694548,15.6415176 L37.0505103,24.5551352 C37.6150232,26.6909302 36.3412474,28.8799623 34.2054524,29.4444752 C33.5569474,29.6158819 32.87575,29.6214505 32.2245294,29.4606688 L23.3459013,27.2685988 C18.2976962,26.022233 14.3561301,22.0806669 13.1097642,17.0324618 L10.9176943,8.15383364 C10.3881737,6.00909519 11.6975664,3.84117987 13.8423049,3.31165925 C14.4935255,3.15087753 15.1747229,3.15644615 15.8232279,3.32785281 Z" opacity=".5" transform="translate(23.999997, 16.378385) rotate(-45.000000) translate(-23.999997, -16.378385)"/></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,30 @@
.storybook-button {
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-weight: 700;
border: 0;
border-radius: 3em;
cursor: pointer;
display: inline-block;
line-height: 1;
}
.storybook-button--primary {
color: white;
background-color: #1ea7fd;
}
.storybook-button--secondary {
color: #333;
background-color: transparent;
box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
}
.storybook-button--small {
font-size: 12px;
padding: 10px 16px;
}
.storybook-button--medium {
font-size: 14px;
padding: 11px 20px;
}
.storybook-button--large {
font-size: 16px;
padding: 12px 24px;
}

View File

@ -0,0 +1,32 @@
.wrapper {
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 15px 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
svg {
display: inline-block;
vertical-align: top;
}
h1 {
font-weight: 700;
font-size: 20px;
line-height: 1;
margin: 6px 0 6px 10px;
display: inline-block;
vertical-align: top;
}
button + button {
margin-left: 10px;
}
.welcome {
color: #333;
font-size: 14px;
margin-right: 10px;
}

View File

@ -0,0 +1,69 @@
section {
font-family: "Nunito Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 24px;
padding: 48px 20px;
margin: 0 auto;
max-width: 600px;
color: #333;
}
section h2 {
font-weight: 700;
font-size: 32px;
line-height: 1;
margin: 0 0 4px;
display: inline-block;
vertical-align: top;
}
section p {
margin: 1em 0;
}
section a {
text-decoration: none;
color: #1ea7fd;
}
section ul {
padding-left: 30px;
margin: 1em 0;
}
section li {
margin-bottom: 8px;
}
section .tip {
display: inline-block;
border-radius: 1em;
font-size: 11px;
line-height: 12px;
font-weight: 700;
background: #e7fdd8;
color: #66bf3c;
padding: 4px 12px;
margin-right: 10px;
vertical-align: top;
}
section .tip-wrapper {
font-size: 13px;
line-height: 20px;
margin-top: 40px;
margin-bottom: 40px;
}
section .tip-wrapper svg {
display: inline-block;
height: 12px;
width: 12px;
margin-right: 4px;
vertical-align: top;
margin-top: 3px;
}
section .tip-wrapper svg path {
fill: #1ea7fd;
}

View File

@ -124,7 +124,7 @@ svg {
} }
.btn-large-icon { .btn-large-icon {
@apply flex items-center px-6 py-3 text-xl font-bold; @apply flex items-center px-6 py-3 text-xl font-bold;
} }
} }

View File

@ -28,7 +28,8 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
# dependencies for building Python packages # dependencies for building Python packages
build-essential \ build-essential \
# psycopg2 dependencies # psycopg2 dependencies
libpq-dev libpq-dev \
git
# Requirements are installed here to ensure they will be cached. # Requirements are installed here to ensure they will be cached.
COPY ./server/requirements . COPY ./server/requirements .
@ -90,7 +91,6 @@ RUN chmod +x /start
#RUN sed -i 's/\r$//g' /start-celerybeat #RUN sed -i 's/\r$//g' /start-celerybeat
#RUN chmod +x /start-celerybeat #RUN chmod +x /start-celerybeat
# #
#
#COPY ./compose/production/django/celery/flower/start /start-flower #COPY ./compose/production/django/celery/flower/start /start-flower
#RUN sed -i 's/\r$//g' /start-flower #RUN sed -i 's/\r$//g' /start-flower
#RUN chmod +x /start-flower #RUN chmod +x /start-flower
@ -99,7 +99,6 @@ RUN chmod +x /start
# copy application code to WORKDIR # copy application code to WORKDIR
COPY --from=client-builder --chown=django:django ${APP_HOME} ${APP_HOME} COPY --from=client-builder --chown=django:django ${APP_HOME} ${APP_HOME}
# make django owner of the WORKDIR directory as well. # make django owner of the WORKDIR directory as well.
RUN chown django:django ${APP_HOME} RUN chown django:django ${APP_HOME}

20
env/bitbucket/prepare_for_deployment.sh vendored Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
# needed for some tests
export LC_ALL=C.UTF-8
export LANG=C.UTF-8
export OPENSSL_CONF=/etc/ssl/
python -m venv vbvvenv
source vbvvenv/bin/activate
export PATH="$PWD/vbvvenv/bin:$PATH"
docker login --username=iterativ --password=$DOCKER_PASSWORD_TOKEN
# only used for `python manage.py collectstatic` so value doesn't matter
export IT_APP_ENVIRONMENT=development
pip install -r $BITBUCKET_CLONE_DIR/server/requirements/requirements-dev.txt
npm install -g caprover

4321
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -698,25 +698,26 @@ if APP_ENVIRONMENT in ["production", "caprover"] or APP_ENVIRONMENT.startswith(
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
ANYMAIL = {} ANYMAIL = {}
# Sentry SENTRY_DSN = env("IT_SENTRY_DSN", "")
# ------------------------------------------------------------------------------
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import LoggingIntegration
from sentry_sdk.integrations.redis import RedisIntegration
SENTRY_DSN = env("IT_SENTRY_DSN")
SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO) SENTRY_LOG_LEVEL = env.int("DJANGO_SENTRY_LOG_LEVEL", logging.INFO)
sentry_logging = LoggingIntegration( if SENTRY_DSN:
level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs # Sentry
event_level=logging.ERROR, # Send errors as events # ------------------------------------------------------------------------------
) import sentry_sdk
integrations = [sentry_logging, DjangoIntegration(), RedisIntegration()] from sentry_sdk.integrations.django import DjangoIntegration
# pylint: disable=abstract-class-instantiated from sentry_sdk.integrations.logging import LoggingIntegration
sentry_sdk.init( from sentry_sdk.integrations.redis import RedisIntegration
dsn=SENTRY_DSN,
integrations=integrations, sentry_logging = LoggingIntegration(
environment=env("SENTRY_ENVIRONMENT", default="production"), level=SENTRY_LOG_LEVEL, # Capture info and above as breadcrumbs
traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0), event_level=logging.ERROR, # Send errors as events
) )
integrations = [sentry_logging, DjangoIntegration(), RedisIntegration()]
# pylint: disable=abstract-class-instantiated
sentry_sdk.init(
dsn=SENTRY_DSN,
integrations=integrations,
environment=env("SENTRY_ENVIRONMENT", default="production"),
traces_sample_rate=env.float("SENTRY_TRACES_SAMPLE_RATE", default=0.0),
)

View File

@ -36,3 +36,6 @@ https://github.com/q0w/django-watchfiles/archive/issue-1.zip
# code checking # code checking
truffleHog truffleHog
# deployement and CI
git+https://github.com/iterativ/Caprover-API.git@5013f8fc929e8e3281b9d609e968a782e8e99530

View File

@ -49,6 +49,8 @@ brotli==1.0.9
# via whitenoise # via whitenoise
build==0.8.0 build==0.8.0
# via pip-tools # via pip-tools
caprover-api @ git+https://github.com/iterativ/Caprover-API.git@5013f8fc929e8e3281b9d609e968a782e8e99530
# via -r requirements-dev.in
certifi==2021.10.8 certifi==2021.10.8
# via # via
# requests # requests
@ -63,6 +65,7 @@ charset-normalizer==2.0.12
# via requests # via requests
click==8.1.1 click==8.1.1
# via # via
# -r requirements.in
# black # black
# django-click # django-click
# moreorless # moreorless
@ -396,6 +399,7 @@ pytz==2022.1
# l18n # l18n
pyyaml==6.0 pyyaml==6.0
# via # via
# caprover-api
# drf-spectacular # drf-spectacular
# libcst # libcst
# pre-commit # pre-commit
@ -406,6 +410,7 @@ redis==4.2.1
# django-redis # django-redis
requests==2.27.1 requests==2.27.1
# via # via
# caprover-api
# coreapi # coreapi
# djangorestframework-stubs # djangorestframework-stubs
# wagtail # wagtail

View File

@ -7,6 +7,7 @@ whitenoise[brotli] # https://github.com/evansd/whitenoise
redis # https://github.com/redis/redis-py redis # https://github.com/redis/redis-py
uvicorn[standard] # https://github.com/encode/uvicorn uvicorn[standard] # https://github.com/encode/uvicorn
environs environs
click
# Django # Django
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View File

@ -44,6 +44,7 @@ charset-normalizer==2.0.12
# via requests # via requests
click==8.1.1 click==8.1.1
# via # via
# -r requirements.in
# django-click # django-click
# uvicorn # uvicorn
concurrent-log-handler==0.9.20 concurrent-log-handler==0.9.20

View File

@ -9,3 +9,4 @@ scripts/supabase/init.sql
ramon.wenger@iterativ.ch.gpg ramon.wenger@iterativ.ch.gpg
.envs/ .envs/
client/package-lock.json client/package-lock.json
package-lock.json