chore: moved exampled to frontend

This commit is contained in:
Felix Dubrownik
2023-03-03 14:11:13 +01:00
parent 541f94131a
commit b9fa81f76b
134 changed files with 22 additions and 40368 deletions

View File

@ -0,0 +1 @@
node_modules

View File

@ -0,0 +1,2 @@
VITE_HANKO_API=http://localhost:8000
VITE_TODO_API=http://localhost:8002

View File

@ -0,0 +1,15 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/eslint-config-typescript",
"@vue/eslint-config-prettier",
],
parserOptions: {
ecmaVersion: "latest",
},
};

28
frontend/examples/vue/.gitignore vendored Normal file
View File

@ -0,0 +1,28 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View File

@ -0,0 +1,19 @@
# pull official base image
FROM node:16-alpine
# set working directory
WORKDIR /app
# add `/app/node_modules/.bin` to $PATH
ENV PATH /app/node_modules/.bin:$PATH
# install app dependencies
COPY package.json ./
COPY package-lock.json ./
RUN npm install
# add app
COPY . ./
# start app
CMD ["npm", "start"]

View File

@ -0,0 +1,21 @@
# Hanko React example
This is a [Vue](https://vuejs.org/) project bootstrapped with Vue version 3.2.39.
## Starting the app
### Prerequisites
- a running Hanko API (see the instructions on how to run the API [in Docker](../../backend/README.md#Docker) or [from Source](../../backend/README.md#from-source))
- a running express backend (see the [README](../express) for the express backend)
### Set up environment variables
In the `.env` file set up the correct environment variables:
- `VITE_HANKO_API`: this is the URL of the Hanko API (default: `http://localhost:8000`, can be customized using the `server.public.address` option in the [configuration file](../../backend/docs/Config.md))
- `VITE_TODO_API`: this is the URL of the [express](../express) backend (default: `http://localhost:8002`)
### Run development server
Run `npm install` to install dependencies, then run `npm run start` for a development server. Navigate to `http://localhost:8888/`. The application will automatically reload if you change any of the source files.

1
frontend/examples/vue/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hanko Vue Example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,27 @@
{
"name": "example-vue",
"scripts": {
"start": "vite --port 8888 --host",
"build": "vite build"
},
"dependencies": {
"@teamhanko/hanko-elements": "^0.2.1-alpha",
"vue": "^3.2.47",
"vue-router": "^4.1.6"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.1.4",
"@types/node": "^16.11.56",
"@vitejs/plugin-vue": "^3.0.3",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.0",
"@vue/tsconfig": "^0.1.3",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0",
"npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"typescript": "~4.7.4",
"vite": "^3.0.9",
"vue-tsc": "^0.40.7"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 B

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import { RouterView } from "vue-router";
</script>
<template>
<RouterView />
</template>
<style>
.content {
padding: 24px;
border-radius: 17px;
color: black;
background-color: white;
width: 100%;
max-width: 500px;
min-width: 330px;
margin: 10vh auto;
}
.error {
color: red;
padding: 0 0 10px;
}
.button {
font-size: 1rem;
border: none;
background: none;
cursor: pointer;
}
.button:disabled {
color: grey !important;
cursor: default;
text-decoration: none !important;
}
.nav {
width: 100%;
padding: 10px;
opacity: 0.9;
}
.nav .button:hover {
text-decoration: underline;
}
.nav .button {
color: white;
float: right;
}
</style>

View File

@ -0,0 +1,19 @@
body {
color: white;
margin: 0;
background: url("bg.jpg") no-repeat center center fixed;
background-size: cover;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
* {
box-sizing: border-box;
}
hanko-auth::part(form-item) {
min-width: 100%; /* input fields and buttons are on top of each other */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,21 @@
<script setup lang="ts">
import { useRouter } from "vue-router";
import { register } from "@teamhanko/hanko-elements";
import { onMounted } from "vue";
const router = useRouter();
const api = import.meta.env.VITE_HANKO_API;
const emit = defineEmits(["on-error"]);
const redirectToTodo = () => {
router.push({ path: "/todo" });
};
onMounted(() => {
register({ shadow: true }).catch((e) => emit("on-error", e));
});
</script>
<template>
<hanko-auth @hankoAuthSuccess="redirectToTodo" :api="api" />
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { register } from "@teamhanko/hanko-elements";
import { onMounted } from "vue";
const api = import.meta.env.VITE_HANKO_API;
const emit = defineEmits(["on-error"]);
onMounted(() => {
register({ shadow: true }).catch((e) => emit("on-error", e));
});
</script>
<template>
<hanko-profile :api="api" />
</template>

View File

@ -0,0 +1,11 @@
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import "./assets/base.css";
const app = createApp(App);
app.use(router);
app.mount("#app");

View File

@ -0,0 +1,27 @@
import { createRouter, createWebHistory } from "vue-router";
import LoginView from "../views/LoginView.vue";
import TodoView from "../views/TodoView.vue";
import ProfileView from "../views/ProfileView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "login",
component: LoginView,
},
{
path: "/todo",
name: "todo",
component: TodoView,
},
{
path: "/profile",
name: "profile",
component: ProfileView,
},
],
});
export default router;

View File

@ -0,0 +1,56 @@
export interface Todo {
todoID?: string;
description: string;
checked: boolean;
}
export type Todos = Todo[];
export class TodoClient {
api: string;
constructor(api: string) {
this.api = api;
}
addTodo(todo: Todo) {
return fetch(`${this.api}/todo`, {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(todo),
});
}
listTodos() {
return fetch(`${this.api}/todo`, {
credentials: "include",
});
}
patchTodo(id: string, checked: boolean) {
return fetch(`${this.api}/todo/${id}`, {
method: "PATCH",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ checked }),
});
}
deleteTodo(id: string) {
return fetch(`${this.api}/todo/${id}`, {
method: "DELETE",
credentials: "include",
});
}
logout() {
return fetch(`${this.api}/logout`, {
credentials: "include",
});
}
}

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import HankoAuth from "@/components/HankoAuth.vue";
import type { Ref } from "vue";
import { ref } from "vue";
const error: Ref<Error | null> = ref(null);
function setError(e: Error) {
error.value = e;
}
</script>
<template>
<main class="content">
<div class="error">{{ error?.message }}</div>
<HankoAuth @on-error="setError"/>
</main>
</template>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import HankoProfile from "@/components/HankoProfile.vue";
import { useRouter } from "vue-router";
import { TodoClient } from "@/utils/TodoClient";
import type { Ref } from "vue";
import { ref } from "vue";
const router = useRouter();
const api = import.meta.env.VITE_TODO_API;
const client = new TodoClient(api);
const error: Ref<Error | null> = ref(null);
function setError(e: Error) {
error.value = e;
}
function todos() {
router.push("/todo");
}
function logout() {
client
.logout()
.then(() => {
router.push("/");
return;
})
.catch((e) => {
error.value = e;
});
}
</script>
<template>
<nav class="nav">
<button @click.prevent="logout" class="button">Logout</button>
<button disabled class="button">Profile</button>
<button @click.prevent="todos" class="button">Todos</button>
</nav>
<main class="content">
<div class="error">{{ error?.message }}</div>
<HankoProfile @on-error="setError" />
</main>
</template>

View File

@ -0,0 +1,280 @@
<script setup lang="ts">
import type { Todos } from "@/utils/TodoClient";
import { TodoClient } from "@/utils/TodoClient";
import { useRouter } from "vue-router";
import type { Ref } from "vue";
import { onMounted, ref } from "vue";
const router = useRouter();
const api = import.meta.env.VITE_TODO_API;
const client = new TodoClient(api);
const error: Ref<Error | null> = ref(null);
const todos: Ref<Todos> = ref([]);
const description = ref("");
onMounted(() => {
listTodos();
});
function changeDescription(event: any) {
description.value = event.currentTarget.value;
}
const changeCheckbox = (event: any) => {
const { currentTarget } = event;
patchTodo(currentTarget.value, currentTarget.checked);
};
function addTodo() {
const todo = { description: description.value, checked: false };
client
.addTodo(todo)
.then((res) => {
if (res.status === 401) {
router.push("/");
return;
}
description.value = "";
listTodos();
return;
})
.catch((e) => {
error.value = e;
});
}
function listTodos() {
client
.listTodos()
.then((res) => {
if (res.status === 401) {
router.push("/");
return;
}
return res.json();
})
.then((todo) => {
if (todo) {
todos.value = todo;
}
})
.catch((e) => {
error.value = e;
});
}
const patchTodo = (id: string, checked: boolean) => {
client
.patchTodo(id, checked)
.then((res) => {
if (res.status === 401) {
router.push("/");
return;
}
listTodos();
return;
})
.catch((e) => {
error.value = e;
});
};
const deleteTodo = (id: string) => {
client
.deleteTodo(id)
.then((res) => {
if (res.status === 401) {
router.push("/");
return;
}
listTodos();
return;
})
.catch((e) => {
error.value = e;
});
};
function profile() {
router.push("/profile");
}
function logout() {
client
.logout()
.then(() => {
router.push("/");
return;
})
.catch((e) => {
error.value = e;
});
}
</script>
<template>
<nav class="nav">
<button @click.prevent="logout" class="button">Logout</button>
<button @click.prevent="profile" class="button">Profile</button>
<button disabled class="button">Todos</button>
</nav>
<div class="content">
<h1 class="headline">Todos</h1>
<div class="error">{{ error?.message }}</div>
<form @submit.prevent="addTodo" class="form">
<input
required
class="input"
type="text"
:value="description"
@change="changeDescription"
/>
<button type="submit" class="button">+</button>
</form>
<div class="list">
<div v-for="(todo, index) in todos" class="item" :key="index">
<input
class="checkbox"
:id="todo.todoID"
type="checkbox"
:value="todo.todoID"
:checked="todo.checked"
@change="changeCheckbox"
/>
<label class="description" :for="todo.todoID">{{
todo.description
}}</label>
<button class="button" @click="() => deleteTodo(todo.todoID)">×</button>
</div>
</div>
</div>
</template>
<style scoped>
.nav {
width: 100%;
padding: 10px;
opacity: 0.9;
}
.button {
font-size: 1rem;
border: none;
background: none;
cursor: pointer;
}
.button:disabled {
color: grey !important;
cursor: default;
text-decoration: none !important;
}
.nav .button:hover {
text-decoration: underline;
}
.nav .button {
color: white;
float: right;
}
.content {
padding: 24px;
border-radius: 17px;
color: black;
background-color: white;
width: 100%;
max-width: 500px;
min-width: 330px;
margin: 10vh auto;
}
.headline {
text-align: center;
margin-top: 0;
}
.form {
display: flex;
margin-bottom: 17px;
}
.form .input {
flex-grow: 1;
margin-right: 10px;
}
.form .button {
color: black;
}
.list {
display: flex;
flex-direction: column;
row-gap: 7px;
}
.item {
display: flex;
justify-content: space-between;
align-items: flex-start;
column-gap: 7px;
}
.description {
flex-grow: 1;
cursor: pointer;
}
.error {
color: red;
padding: 0 0 10px;
}
.input {
border: 1px solid black;
border-radius: 2.4px;
}
.checkbox {
margin-left: 0;
-webkit-appearance: none;
appearance: none;
background-color: #fff;
font: inherit;
color: currentColor;
width: 1em;
height: 1em;
border: 1px solid currentColor;
border-radius: 0.15em;
transform: translateY(-0.075em);
display: grid;
place-content: center;
}
.checkbox::before {
content: "";
width: 0.65em;
height: 0.65em;
transform: scale(0);
box-shadow: inset 1em 1em black;
transform-origin: bottom left;
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
.checkbox:checked::before {
transform: scale(1);
}
</style>

View File

@ -0,0 +1,8 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@ -0,0 +1,16 @@
{
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"references": [
{
"path": "./tsconfig.config.json"
}
]
}

View File

@ -0,0 +1,20 @@
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: { isCustomElement: (tag) => tag.startsWith("hanko-") },
},
}),
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});