initial commit
This commit is contained in:
9
webapp/.editorconfig
Normal file
9
webapp/.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
1
webapp/.gitattributes
vendored
Normal file
1
webapp/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
* text=auto eol=lf
|
30
webapp/.gitignore
vendored
Normal file
30
webapp/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# 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?
|
||||
|
||||
*.tsbuildinfo
|
7
webapp/.prettierrc.json
Normal file
7
webapp/.prettierrc.json
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
8
webapp/.vscode/extensions.json
vendored
Normal file
8
webapp/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
39
webapp/README.md
Normal file
39
webapp/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
# webapp
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
npm run lint
|
||||
```
|
1
webapp/env.d.ts
vendored
Normal file
1
webapp/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
24
webapp/eslint.config.ts
Normal file
24
webapp/eslint.config.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import pluginVue from 'eslint-plugin-vue'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'app/files-to-ignore',
|
||||
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||
},
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
skipFormatting,
|
||||
)
|
13
webapp/index.html
Normal file
13
webapp/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
6254
webapp/package-lock.json
generated
Normal file
6254
webapp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
43
webapp/package.json
Normal file
43
webapp/package.json
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "webapp",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@masonitestudios/dynamic-vue": "^0.2.0",
|
||||
"@tailwindcss/vite": "^4.0.6",
|
||||
"pinia": "^2.3.1",
|
||||
"tailwindcss": "^4.0.6",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/node": "^22.13.1",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-prettier": "^10.1.0",
|
||||
"@vue/eslint-config-typescript": "^14.3.0",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"jiti": "^2.4.2",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"openapi-typescript-codegen": "^0.29.0",
|
||||
"prettier": "^3.4.2",
|
||||
"protoc": "^1.1.3",
|
||||
"ts-proto": "^2.6.1",
|
||||
"typescript": "~5.7.3",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-vue-devtools": "^7.7.1",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
BIN
webapp/public/favicon.ico
Normal file
BIN
webapp/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
11
webapp/src/App.vue
Normal file
11
webapp/src/App.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
1
webapp/src/assets/base.css
Normal file
1
webapp/src/assets/base.css
Normal file
@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
1
webapp/src/assets/logo.svg
Normal file
1
webapp/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
1
webapp/src/assets/main.css
Normal file
1
webapp/src/assets/main.css
Normal file
@ -0,0 +1 @@
|
||||
@import './base.css';
|
97
webapp/src/components/DynamicTable.vue
Normal file
97
webapp/src/components/DynamicTable.vue
Normal file
@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- Optional table title -->
|
||||
<h3 v-if="tableTitle" class="text-xl font-semibold mb-4">{{ tableTitle }}</h3>
|
||||
<table class="min-w-full border-collapse">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
v-for="col in tableColumns"
|
||||
:key="col.id"
|
||||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b"
|
||||
>
|
||||
<!-- Allow custom header slot or fall back to the column label -->
|
||||
<slot :name="`header-${col.id}`">
|
||||
{{ col.label }}
|
||||
</slot>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, rowIndex) in tableData"
|
||||
:key="rowIndex"
|
||||
@click="handleRowClick(row)"
|
||||
class="hover:bg-gray-100 cursor-pointer border-b"
|
||||
>
|
||||
<td
|
||||
v-for="col in tableColumns"
|
||||
:key="col.id"
|
||||
class="px-6 py-4 whitespace-nowrap text-sm text-gray-500"
|
||||
>
|
||||
<!-- Check for a custom cell slot using the pattern "cell-COLUMN_ID" -->
|
||||
<slot :name="`cell-${col.id}`" :row="row">
|
||||
<!-- Default rendering based on data type -->
|
||||
<template v-if="col.type === 'checkbox'">
|
||||
<div class="flex flex-wrap">
|
||||
<span
|
||||
v-for="(val, idx) in row[col.id]"
|
||||
:key="idx"
|
||||
class="mr-1"
|
||||
>
|
||||
{{ val }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="col.type === 'multi-input'">
|
||||
<ul>
|
||||
<li v-for="(item, idx) in row[col.id]" :key="idx">
|
||||
{{ item }}
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row[col.id] }}
|
||||
</template>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
// Define Column interface to mimic our FormInput structure (you can extend it with more properties)
|
||||
export interface TableColumn {
|
||||
id: string;
|
||||
label: string;
|
||||
type: string; // e.g., 'text', 'email', 'number', 'checkbox', 'textarea', 'multi-input', etc.
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
tableColumns: TableColumn[];
|
||||
tableData: Record<string, any>[];
|
||||
tableTitle?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'row-click', row: Record<string, any>): void;
|
||||
}>();
|
||||
|
||||
const handleRowClick = (row: Record<string, any>) => {
|
||||
emit('row-click', row);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Feel free to modify these styles */
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
</style>
|
5
webapp/src/components/Header.vue
Normal file
5
webapp/src/components/Header.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="flex justify-around">Peach</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
41
webapp/src/components/HelloWorld.vue
Normal file
41
webapp/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
msg: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3 class="text-amber-950">
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
29
webapp/src/components/SideNav.vue
Normal file
29
webapp/src/components/SideNav.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<nav v-if="isOpen" class="side-nav">
|
||||
<!-- Navigation items here -->
|
||||
<router-link to="/">Home</router-link>
|
||||
</nav>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
|
||||
const isOpen = ref(true);
|
||||
const toggleNav = () => {
|
||||
isOpen.value = !isOpen.value;
|
||||
};
|
||||
|
||||
// Optionally expose toggleNav if parent needs to control it
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Define your slide transition */
|
||||
.slide-enter-active, .slide-leave-active {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
.slide-enter-from, .slide-leave-to {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
</style>
|
193
webapp/src/components/SideNavBar.vue
Normal file
193
webapp/src/components/SideNavBar.vue
Normal file
@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<!-- Mobile Toggle Button when menu is closed: shows a hamburger button at top-left -->
|
||||
<button
|
||||
v-if="!mobileOpen"
|
||||
@click="toggleMobile"
|
||||
:class="[theme.hamburgerButtonBg, 'sm:hidden fixed top-4 left-4 z-40 p-2 rounded focus:outline-none']">
|
||||
<!-- Hamburger Icon -->
|
||||
<span class="block w-6 h-0.5 bg-current mb-1"></span>
|
||||
<span class="block w-6 h-0.5 bg-current mb-1"></span>
|
||||
<span class="block w-6 h-0.5 bg-current"></span>
|
||||
</button>
|
||||
|
||||
<!-- Sidebar container -->
|
||||
<!-- In mobile view, the sidebar overlays content; in desktop (sm and up), it's static -->
|
||||
<nav :class="sidebarClasses">
|
||||
<!-- Desktop Header: Branding (optional) and Minimize Toggle -->
|
||||
<div class="hidden sm:flex justify-between items-center p-2">
|
||||
<!-- Optional Branding Slot: only shown when sidebar is expanded -->
|
||||
<div v-if="!minimized && $slots.brand" class="flex-shrink-0">
|
||||
<!-- Wrap branding in a container that controls size -->
|
||||
<div class="w-32 h-10 overflow-hidden">
|
||||
<slot name="brand"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Minimize Toggle Button -->
|
||||
<button
|
||||
@click="toggleMinimized"
|
||||
:class="[theme.toggleButtonBg, 'p-2 rounded hover:opacity-80 focus:outline-none']">
|
||||
<!-- Toggle icon: changes based on minimized state -->
|
||||
<svg v-if="minimized" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-6 w-6"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Close Button (X icon) displayed when menu is open -->
|
||||
<button
|
||||
v-if="mobileOpen"
|
||||
@click="toggleMobile"
|
||||
:class="[theme.toggleButtonBg, 'sm:hidden absolute top-4 right-4 z-50 p-2 rounded focus:outline-none']">
|
||||
<!-- X Icon -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none"
|
||||
viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<ul>
|
||||
<li v-for="(link, i) in links" :key="i" class="my-1">
|
||||
<RouterLink
|
||||
:to="link.route || '#'"
|
||||
class="flex items-center p-2 transition-colors"
|
||||
:class="[isActive(link) ? theme.sidebarActive : theme.sidebarHover, theme.sidebarLink]">
|
||||
<span class="flex-shrink-0" :class="minimized ? 'mx-auto' : 'mr-3'">
|
||||
<!-- Render icon provided in the link -->
|
||||
<component :is="link.icon" class="w-6 h-6" />
|
||||
</span>
|
||||
<span v-if="!minimized" class="flex-1">
|
||||
{{ link.title }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
|
||||
<!-- Sub-links; visible only when the sidebar is not minimized -->
|
||||
<ul v-if="link.subLinks && link.subLinks.length" v-show="!minimized" class="ml-8">
|
||||
<li v-for="(sub, j) in link.subLinks" :key="j" class="my-1">
|
||||
<RouterLink
|
||||
:to="sub.route || '#'"
|
||||
class="flex items-center p-2 transition-colors"
|
||||
:class="[isActive(sub) ? theme.sidebarActive : theme.sidebarHover, theme.sidebarLink]">
|
||||
<span v-if="sub.icon" class="flex-shrink-0 mr-3">
|
||||
<component :is="sub.icon" class="w-5 h-5" />
|
||||
</span>
|
||||
<span>{{ sub.title }}</span>
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { RouterLink, useRoute } from 'vue-router';
|
||||
|
||||
// Define the interface for navigation links.
|
||||
export interface NavLink {
|
||||
title: string;
|
||||
icon?: any; // Vue component, SVG, or any renderable element.
|
||||
route?: string;
|
||||
subLinks?: Omit<NavLink, 'subLinks'>[];
|
||||
}
|
||||
|
||||
// Define the interface for theming the component.
|
||||
export interface Theme {
|
||||
// Sidebar container background and text colors.
|
||||
sidebarBg?: string; // e.g. "bg-gray-800"
|
||||
sidebarText?: string; // e.g. "text-white"
|
||||
// Classes for links (default state)
|
||||
sidebarLink?: string; // e.g. "rounded"
|
||||
// Classes on hover state
|
||||
sidebarHover?: string; // e.g. "hover:bg-gray-700"
|
||||
// Classes for the active link
|
||||
sidebarActive?: string; // e.g. "bg-gray-900"
|
||||
// Toggle button (used for mobile hamburger & desktop minimize toggle)
|
||||
toggleButtonBg?: string; // e.g. "bg-gray-700"
|
||||
hamburgerButtonBg?: string;
|
||||
}
|
||||
|
||||
// Accept props: links are required; theme is optional.
|
||||
const props = defineProps<{
|
||||
links: NavLink[];
|
||||
theme?: Partial<Theme>;
|
||||
}>();
|
||||
|
||||
// Provide default theme values.
|
||||
const defaultTheme: Theme = {
|
||||
sidebarBg: "bg-gray-800",
|
||||
sidebarText: "text-white",
|
||||
sidebarLink: "",
|
||||
sidebarHover: "hover:bg-gray-700",
|
||||
sidebarActive: "bg-gray-900",
|
||||
toggleButtonBg: "bg-gray-700",
|
||||
hamburgerButtonBg: "bg-white"
|
||||
};
|
||||
|
||||
// Merge provided theme with defaults.
|
||||
const theme = {
|
||||
...defaultTheme,
|
||||
...props.theme
|
||||
};
|
||||
|
||||
// Get current route to highlight active links.
|
||||
const route = useRoute();
|
||||
|
||||
// Local state: mobile sidebar open/closed and desktop minimized state.
|
||||
const mobileOpen = ref(false);
|
||||
const minimized = ref(false);
|
||||
|
||||
// Toggle mobile sidebar visibility.
|
||||
const toggleMobile = () => {
|
||||
mobileOpen.value = !mobileOpen.value;
|
||||
};
|
||||
|
||||
// Toggle desktop minimized state.
|
||||
const toggleMinimized = () => {
|
||||
minimized.value = !minimized.value;
|
||||
};
|
||||
|
||||
// Check if a given link (or any of its sub-links) is active based on the current route.
|
||||
const isActive = (link: NavLink) => {
|
||||
if (link.route && route.path === link.route) return true;
|
||||
if (link.subLinks) {
|
||||
return link.subLinks.some(sub => sub.route === route.path);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Compute sidebar classes with a 200ms transition for both mobile sliding and desktop width changes.
|
||||
const sidebarClasses = computed(() => {
|
||||
let classes = `${theme.sidebarBg} ${theme.sidebarText} transition-all duration-200 overflow-y-auto shadow-lg z-30`;
|
||||
// On mobile: fixed positioning to overlay content.
|
||||
classes += " fixed sm:static top-0 h-screen";
|
||||
// Mobile: animate slide in/out.
|
||||
classes += mobileOpen.value ? " transform translate-x-0" : " transform -translate-x-full";
|
||||
// On desktop, ensure sidebar is visible.
|
||||
classes += " sm:translate-x-0";
|
||||
// Animate width change on desktop for minimized/expanded states.
|
||||
classes += minimized.value ? " w-20" : " w-64";
|
||||
return classes;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Styling for internal scrolling within the sidebar */
|
||||
nav::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
nav::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
94
webapp/src/components/TheWelcome.vue
Normal file
94
webapp/src/components/TheWelcome.vue
Normal file
@ -0,0 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
|
||||
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
|
||||
+
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
|
||||
and
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
|
||||
/
|
||||
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in
|
||||
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
|
||||
>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
|
||||
(our official Discord server), or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also follow the official
|
||||
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
|
||||
Bluesky account or the
|
||||
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
X account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
87
webapp/src/components/WelcomeItem.vue
Normal file
87
webapp/src/components/WelcomeItem.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
56
webapp/src/components/dynamicTableTest.vue
Normal file
56
webapp/src/components/dynamicTableTest.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<DynamicTable
|
||||
class="bg-white shadow-lg rounded-lg p-6"
|
||||
:tableTitle="'Product List'"
|
||||
:tableColumns="columns"
|
||||
:tableData="products"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<!-- Custom rendering for the "price" column -->
|
||||
<template #cell-price="{ row }">
|
||||
<span>$ {{ (row.price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
</DynamicTable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import DynamicTable from '@/components/DynamicTable.vue';
|
||||
import type { TableColumn } from '@masonitestudios/dynamic-vue';
|
||||
import {
|
||||
type peachListProductsResponse,
|
||||
type peachProduct,
|
||||
PeachService,
|
||||
type rpcStatus
|
||||
} from '@/generated'
|
||||
|
||||
const columns = ref<TableColumn[]>([
|
||||
{ id: 'id', label: 'ID', type: 'text' },
|
||||
{ id: 'name', label: 'Name', type: 'text' },
|
||||
{ id: 'description', label: 'Description', type: 'text' },
|
||||
{ id: 'price', label: 'Price', type: 'number' },
|
||||
]);
|
||||
|
||||
const products = ref<peachProduct[]>([]);
|
||||
|
||||
onMounted(() => {
|
||||
fetchPeachProducts();
|
||||
});
|
||||
|
||||
function fetchPeachProducts() {
|
||||
PeachService.peachListProducts().then((res: peachListProductsResponse | rpcStatus) => {
|
||||
if ('results' in res) {
|
||||
console.log('Products:', res.results);
|
||||
if (res.results) {
|
||||
products.value = res.results;
|
||||
}
|
||||
} else {
|
||||
console.error('Error response:', res);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const handleRowClick = (row: Record<string, any>) => {
|
||||
console.log('Row clicked:', row);
|
||||
};
|
||||
</script>
|
6
webapp/src/components/icons/IconArrowFromShapeRight.vue
Normal file
6
webapp/src/components/icons/IconArrowFromShapeRight.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.75 4.75C8.30228 4.75 8.75 4.30228 8.75 3.75V2.75C8.75 2.19772 8.30228 1.75 7.75 1.75H4.25C2.86929 1.75 1.75 2.86929 1.75 4.25V20.25C1.75 21.6307 2.86929 22.75 4.25 22.75H7.75C8.30228 22.75 8.75 22.3023 8.75 21.75V20.75C8.75 20.1977 8.30228 19.75 7.75 19.75H4.75V4.75H7.75Z" fill="currentColor"/>
|
||||
<path d="M16.25 8.25003C16.25 7.84557 16.4936 7.48093 16.8673 7.32615C17.241 7.17137 17.6711 7.25692 17.9571 7.54292L21.9571 11.5429C22.3476 11.9334 22.3476 12.5666 21.9571 12.9571L17.9571 16.9571C17.6711 17.2431 17.241 17.3287 16.8673 17.1739C16.4936 17.0191 16.25 16.6545 16.25 16.25V13.75H8.25C7.69771 13.75 7.25 13.3023 7.25 12.75V11.75C7.25 11.1977 7.69772 10.75 8.25 10.75H16.25V8.25003Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</template>
|
7
webapp/src/components/icons/IconCommunity.vue
Normal file
7
webapp/src/components/icons/IconCommunity.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
webapp/src/components/icons/IconDocumentation.vue
Normal file
7
webapp/src/components/icons/IconDocumentation.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
webapp/src/components/icons/IconEcosystem.vue
Normal file
7
webapp/src/components/icons/IconEcosystem.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
6
webapp/src/components/icons/IconGear.vue
Normal file
6
webapp/src/components/icons/IconGear.vue
Normal file
@ -0,0 +1,6 @@
|
||||
<template>
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.7848 0.449982C13.8239 0.449982 14.7167 1.16546 14.9122 2.15495L14.9991 2.59495C15.3408 4.32442 17.1859 5.35722 18.9016 4.7794L19.3383 4.63233C20.3199 4.30175 21.4054 4.69358 21.9249 5.56605L22.7097 6.88386C23.2293 7.75636 23.0365 8.86366 22.2504 9.52253L21.9008 9.81555C20.5267 10.9672 20.5267 13.0328 21.9008 14.1844L22.2504 14.4774C23.0365 15.1363 23.2293 16.2436 22.7097 17.1161L21.925 18.4339C21.4054 19.3064 20.3199 19.6982 19.3382 19.3676L18.9017 19.2205C17.1859 18.6426 15.3408 19.6754 14.9991 21.405L14.9122 21.845C14.7167 22.8345 13.8239 23.55 12.7848 23.55H11.2152C10.1761 23.55 9.28331 22.8345 9.08781 21.8451L9.00082 21.4048C8.65909 19.6754 6.81395 18.6426 5.09822 19.2205L4.66179 19.3675C3.68016 19.6982 2.59465 19.3063 2.07505 18.4338L1.2903 17.1161C0.770719 16.2436 0.963446 15.1363 1.74956 14.4774L2.09922 14.1844C3.47324 13.0327 3.47324 10.9672 2.09922 9.8156L1.74956 9.52254C0.963446 8.86366 0.77072 7.75638 1.2903 6.8839L2.07508 5.56608C2.59466 4.69359 3.68014 4.30176 4.66176 4.63236L5.09831 4.77939C6.81401 5.35722 8.65909 4.32449 9.00082 2.59506L9.0878 2.15487C9.28331 1.16542 10.176 0.449982 11.2152 0.449982H12.7848ZM12 15.3C13.8225 15.3 15.3 13.8225 15.3 12C15.3 10.1774 13.8225 8.69998 12 8.69998C10.1774 8.69998 8.69997 10.1774 8.69997 12C8.69997 13.8225 10.1774 15.3 12 15.3Z" fill="currentColor"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
13
webapp/src/components/icons/IconHome.vue
Normal file
13
webapp/src/components/icons/IconHome.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<svg fill="currentColor" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 45.973 45.972" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M44.752,20.914L25.935,2.094c-0.781-0.781-1.842-1.22-2.946-1.22c-1.105,0-2.166,0.439-2.947,1.22L1.221,20.914
|
||||
c-1.191,1.191-1.548,2.968-0.903,4.525c0.646,1.557,2.165,2.557,3.85,2.557h2.404v13.461c0,2.013,1.607,3.642,3.621,3.642h3.203
|
||||
V32.93c0-0.927,0.766-1.651,1.692-1.651h6.223c0.926,0,1.673,0.725,1.673,1.651v12.168h12.799c2.013,0,3.612-1.629,3.612-3.642
|
||||
V27.996h2.411c1.685,0,3.204-1,3.85-2.557C46.3,23.882,45.944,22.106,44.752,20.914z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
7
webapp/src/components/icons/IconSupport.vue
Normal file
7
webapp/src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
19
webapp/src/components/icons/IconTooling.vue
Normal file
19
webapp/src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
42
webapp/src/components/peachUser.vue
Normal file
42
webapp/src/components/peachUser.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="formInputs"
|
||||
form-title="peachUser"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DynamicForm from '@/components/DynamicForm.vue';
|
||||
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'id',
|
||||
label: 'Id',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
id: 'bio',
|
||||
label: 'Bio',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
},
|
||||
];
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
// TODO: handle submit for the peachUser form
|
||||
console.log('Submitted', payload);
|
||||
}
|
||||
</script>
|
@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
:customClasses="{
|
||||
label: 'text-lg text-purple-700',
|
||||
input: {
|
||||
text: 'bg-gray-100 border-purple-500',
|
||||
select: 'bg-white text-purple-600',
|
||||
'multi-input': 'border-dashed'
|
||||
},
|
||||
button: 'bg-purple-600 hover:bg-purple-800',
|
||||
description: 'text-sm italic'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachCreateProductRequest');
|
||||
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'price',
|
||||
label: 'Price',
|
||||
type: 'number',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachCreateProduct(requestData)
|
||||
.then(response => {
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachCreateUserRequest');
|
||||
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'username',
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'bio',
|
||||
label: 'Bio',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachCreateUser(requestData)
|
||||
.then(response => {
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<DynamicTable
|
||||
class="bg-white shadow-lg rounded-lg p-6"
|
||||
:tableTitle="tableTitleComputed"
|
||||
:tableColumns="tableColumns"
|
||||
:tableData="tableData"
|
||||
@row-click="handleRowClick"
|
||||
>
|
||||
<!-- Custom rendering for the "price" column -->
|
||||
<template #cell-price="{ row }">
|
||||
<span>$ {{ (row.price / 100).toFixed(2) }}</span>
|
||||
</template>
|
||||
|
||||
<template #cell-actions="{ row }">
|
||||
<button @click="handleRowClick(row)" class="px-2 bg-gray-200 text-black font-semibold rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300 mt-2 mr-2">
|
||||
edit
|
||||
</button>
|
||||
<button @click.prevent="deleteRowData(row)" class="px-2 bg-gray-200 text-black font-semibold rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300 mt-2">
|
||||
delete
|
||||
</button>
|
||||
</template>
|
||||
</DynamicTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { DynamicTable } from '@masonitestudios/dynamic-vue';
|
||||
import type { TableColumn } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
const props = defineProps<{
|
||||
tableTitle?: string,
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['row-click']);
|
||||
|
||||
const tableColumns = [
|
||||
{ id: 'id', label: 'Id', type: 'text' },
|
||||
{ id: 'name', label: 'Name', type: 'text' },
|
||||
{ id: 'description', label: 'Description', type: 'text' },
|
||||
{ id: 'price', label: 'Price', type: 'number' },
|
||||
{ id: 'actions', label: 'Actions', type: 'text' },
|
||||
];
|
||||
|
||||
const tableData = ref<any[]>([]);
|
||||
const tableTitleComputed = computed(() => props.tableTitle || 'Product List');
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
function fetchData() {
|
||||
PeachService.peachListProducts().then((res: any) => {
|
||||
if (res.results) {
|
||||
tableData.value = res.results;
|
||||
} else {
|
||||
console.error('Error fetching data:', res);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRowData(row: any) {
|
||||
PeachService.peachDeleteProduct(row.id).then((res: any) => {
|
||||
fetchData();
|
||||
}).catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({fetchData});
|
||||
|
||||
function handleRowClick(row: any) {
|
||||
emit('row-click', row);
|
||||
}
|
||||
</script>
|
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<DynamicTable
|
||||
class="bg-white shadow-lg rounded-lg p-6"
|
||||
:tableTitle="tableTitleComputed"
|
||||
:tableColumns="tableColumns"
|
||||
:tableData="tableData"
|
||||
@row-click="handleRowClick"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { DynamicTable } from '@masonitestudios/dynamic-vue';
|
||||
import type { TableColumn } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
const tableColumns = [
|
||||
{ id: 'id', label: 'Id', type: 'text' },
|
||||
{ id: 'username', label: 'Username', type: 'text' },
|
||||
{ id: 'bio', label: 'Bio', type: 'text' }
|
||||
];
|
||||
|
||||
const tableData = ref<any[]>([]);
|
||||
const tableTitleComputed = computed(() => 'Users List');
|
||||
onMounted(() => {
|
||||
fetchData();
|
||||
});
|
||||
|
||||
function fetchData() {
|
||||
PeachService.peachListUsers().then((res: any) => {
|
||||
if (res.results) {
|
||||
tableData.value = res.results;
|
||||
} else {
|
||||
console.error('Error fetching data:', res);
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
function handleRowClick(row: any) {
|
||||
console.log('Row clicked:', row);
|
||||
}
|
||||
</script>
|
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachUpdateProductRequest');
|
||||
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'description',
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
label: 'Price',
|
||||
type: 'number',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
label: 'Id',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
hidden: true,
|
||||
},
|
||||
];
|
||||
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachUpdateProduct(requestData)
|
||||
.then(response => {
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachUpdateUserRequest');
|
||||
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'id',
|
||||
label: 'Id',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'bio',
|
||||
label: 'Bio',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachUpdateUser(requestData)
|
||||
.then(response => {
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
:customClasses="{
|
||||
label: 'text-lg text-purple-700',
|
||||
input: {
|
||||
text: 'bg-gray-100 border-purple-500',
|
||||
select: 'bg-white text-purple-600',
|
||||
'multi-input': 'border-dashed'
|
||||
},
|
||||
button: 'bg-purple-600 hover:bg-purple-800',
|
||||
description: 'text-sm italic'
|
||||
}"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
// For update forms, we allow passing in initial values.
|
||||
// These values will be mapped onto the defaultValue field of each form input.
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
// Pass in an object with initial field values for update requests.
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachCreateProductRequest');
|
||||
|
||||
// The formInputs array was generated from our Go code.
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'description',
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
label: 'Price',
|
||||
type: 'number',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Create a computed property that maps initialValues onto each form input's defaultValue.
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
// If props.initialValues is passed in and contains a value for input.id, use it.
|
||||
// Otherwise, fall back to the defaultValue generated (or an empty string).
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
// Wrap the form values into a "payload" key.
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachCreateProduct(requestData)
|
||||
.then(response => {
|
||||
// Emit an event with the payload after a successful call.
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
// For update forms, we allow passing in initial values.
|
||||
// These values will be mapped onto the defaultValue field of each form input.
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
// Pass in an object with initial field values for update requests.
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachCreateUserRequest');
|
||||
|
||||
// The formInputs array was generated from our Go code.
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'username',
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'bio',
|
||||
label: 'Bio',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Create a computed property that maps initialValues onto each form input's defaultValue.
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
// If props.initialValues is passed in and contains a value for input.id, use it.
|
||||
// Otherwise, fall back to the defaultValue generated (or an empty string).
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
// Wrap the form values into a "payload" key.
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachCreateUser(requestData)
|
||||
.then(response => {
|
||||
// Emit an event with the payload after a successful call.
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
// For update forms, we allow passing in initial values.
|
||||
// These values will be mapped onto the defaultValue field of each form input.
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
// Pass in an object with initial field values for update requests.
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachUpdateProductRequest');
|
||||
|
||||
// The formInputs array was generated from our Go code.
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'description',
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'price',
|
||||
label: 'Price',
|
||||
type: 'number',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'id',
|
||||
label: 'Id',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'name',
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Create a computed property that maps initialValues onto each form input's defaultValue.
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
// If props.initialValues is passed in and contains a value for input.id, use it.
|
||||
// Otherwise, fall back to the defaultValue generated (or an empty string).
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
// Wrap the form values into a "payload" key.
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachUpdateProduct(requestData)
|
||||
.then(response => {
|
||||
// Emit an event with the payload after a successful call.
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<DynamicForm
|
||||
class="space-y-2 p-6 bg-white shadow-lg rounded-lg max-w-md"
|
||||
:formInputs="updatedFormInputs"
|
||||
:formTitle="formTitleComputed"
|
||||
submit-label="Submit"
|
||||
@send="handleSubmit"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { DynamicForm } from '@masonitestudios/dynamic-vue';
|
||||
import { PeachService } from '@/generated';
|
||||
|
||||
// For update forms, we allow passing in initial values.
|
||||
// These values will be mapped onto the defaultValue field of each form input.
|
||||
const props = defineProps<{
|
||||
formTitle?: string,
|
||||
// Pass in an object with initial field values for update requests.
|
||||
initialValues?: Record<string, any>
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['send']);
|
||||
|
||||
const formTitleComputed = computed(() => props.formTitle || 'peachUpdateUserRequest');
|
||||
|
||||
// The formInputs array was generated from our Go code.
|
||||
const formInputs = [
|
||||
{
|
||||
id: 'id',
|
||||
label: 'Id',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'username',
|
||||
label: 'Username',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
{
|
||||
id: 'bio',
|
||||
label: 'Bio',
|
||||
type: 'text',
|
||||
placeholder: '',
|
||||
required: false,
|
||||
defaultValue: '',
|
||||
},
|
||||
];
|
||||
|
||||
// Create a computed property that maps initialValues onto each form input's defaultValue.
|
||||
const updatedFormInputs = computed(() => {
|
||||
return formInputs.map(input => {
|
||||
// If props.initialValues is passed in and contains a value for input.id, use it.
|
||||
// Otherwise, fall back to the defaultValue generated (or an empty string).
|
||||
return {
|
||||
...input,
|
||||
defaultValue: (props.initialValues && props.initialValues[input.id]) ?? input.defaultValue ?? ''
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function handleSubmit(payload: any) {
|
||||
// Wrap the form values into a "payload" key.
|
||||
const requestData = { payload: payload };
|
||||
PeachService.peachUpdateUser(requestData)
|
||||
.then(response => {
|
||||
// Emit an event with the payload after a successful call.
|
||||
emit('send', requestData.payload);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Service error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
25
webapp/src/generated/core/ApiError.ts
Normal file
25
webapp/src/generated/core/ApiError.ts
Normal file
@ -0,0 +1,25 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly body: any;
|
||||
public readonly request: ApiRequestOptions;
|
||||
|
||||
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||
super(message);
|
||||
|
||||
this.name = 'ApiError';
|
||||
this.url = response.url;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.body = response.body;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
17
webapp/src/generated/core/ApiRequestOptions.ts
Normal file
17
webapp/src/generated/core/ApiRequestOptions.ts
Normal file
@ -0,0 +1,17 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiRequestOptions = {
|
||||
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
||||
readonly url: string;
|
||||
readonly path?: Record<string, any>;
|
||||
readonly cookies?: Record<string, any>;
|
||||
readonly headers?: Record<string, any>;
|
||||
readonly query?: Record<string, any>;
|
||||
readonly formData?: Record<string, any>;
|
||||
readonly body?: any;
|
||||
readonly mediaType?: string;
|
||||
readonly responseHeader?: string;
|
||||
readonly errors?: Record<number, string>;
|
||||
};
|
11
webapp/src/generated/core/ApiResult.ts
Normal file
11
webapp/src/generated/core/ApiResult.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiResult = {
|
||||
readonly url: string;
|
||||
readonly ok: boolean;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly body: any;
|
||||
};
|
131
webapp/src/generated/core/CancelablePromise.ts
Normal file
131
webapp/src/generated/core/CancelablePromise.ts
Normal file
@ -0,0 +1,131 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export class CancelError extends Error {
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CancelError';
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OnCancel {
|
||||
readonly isResolved: boolean;
|
||||
readonly isRejected: boolean;
|
||||
readonly isCancelled: boolean;
|
||||
|
||||
(cancelHandler: () => void): void;
|
||||
}
|
||||
|
||||
export class CancelablePromise<T> implements Promise<T> {
|
||||
#isResolved: boolean;
|
||||
#isRejected: boolean;
|
||||
#isCancelled: boolean;
|
||||
readonly #cancelHandlers: (() => void)[];
|
||||
readonly #promise: Promise<T>;
|
||||
#resolve?: (value: T | PromiseLike<T>) => void;
|
||||
#reject?: (reason?: any) => void;
|
||||
|
||||
constructor(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: any) => void,
|
||||
onCancel: OnCancel
|
||||
) => void
|
||||
) {
|
||||
this.#isResolved = false;
|
||||
this.#isRejected = false;
|
||||
this.#isCancelled = false;
|
||||
this.#cancelHandlers = [];
|
||||
this.#promise = new Promise<T>((resolve, reject) => {
|
||||
this.#resolve = resolve;
|
||||
this.#reject = reject;
|
||||
|
||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isResolved = true;
|
||||
if (this.#resolve) this.#resolve(value);
|
||||
};
|
||||
|
||||
const onReject = (reason?: any): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isRejected = true;
|
||||
if (this.#reject) this.#reject(reason);
|
||||
};
|
||||
|
||||
const onCancel = (cancelHandler: () => void): void => {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#cancelHandlers.push(cancelHandler);
|
||||
};
|
||||
|
||||
Object.defineProperty(onCancel, 'isResolved', {
|
||||
get: (): boolean => this.#isResolved,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isRejected', {
|
||||
get: (): boolean => this.#isRejected,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isCancelled', {
|
||||
get: (): boolean => this.#isCancelled,
|
||||
});
|
||||
|
||||
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||
});
|
||||
}
|
||||
|
||||
get [Symbol.toStringTag]() {
|
||||
return "Cancellable Promise";
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this.#promise.then(onFulfilled, onRejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
||||
): Promise<T | TResult> {
|
||||
return this.#promise.catch(onRejected);
|
||||
}
|
||||
|
||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||
return this.#promise.finally(onFinally);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
|
||||
return;
|
||||
}
|
||||
this.#isCancelled = true;
|
||||
if (this.#cancelHandlers.length) {
|
||||
try {
|
||||
for (const cancelHandler of this.#cancelHandlers) {
|
||||
cancelHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Cancellation threw an error', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.#cancelHandlers.length = 0;
|
||||
if (this.#reject) this.#reject(new CancelError('Request aborted'));
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return this.#isCancelled;
|
||||
}
|
||||
}
|
32
webapp/src/generated/core/OpenAPI.ts
Normal file
32
webapp/src/generated/core/OpenAPI.ts
Normal file
@ -0,0 +1,32 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||
TOKEN?: string | Resolver<string> | undefined;
|
||||
USERNAME?: string | Resolver<string> | undefined;
|
||||
PASSWORD?: string | Resolver<string> | undefined;
|
||||
HEADERS?: Headers | Resolver<Headers> | undefined;
|
||||
ENCODE_PATH?: ((path: string) => string) | undefined;
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: 'http://localhost:8080',
|
||||
VERSION: '1.0',
|
||||
WITH_CREDENTIALS: false,
|
||||
CREDENTIALS: 'include',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
ENCODE_PATH: undefined,
|
||||
};
|
322
webapp/src/generated/core/request.ts
Normal file
322
webapp/src/generated/core/request.ts
Normal file
@ -0,0 +1,322 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import { ApiError } from './ApiError';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
import { CancelablePromise } from './CancelablePromise';
|
||||
import type { OnCancel } from './CancelablePromise';
|
||||
import type { OpenAPIConfig } from './OpenAPI';
|
||||
|
||||
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
|
||||
return value !== undefined && value !== null;
|
||||
};
|
||||
|
||||
export const isString = (value: any): value is string => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
export const isStringWithValue = (value: any): value is string => {
|
||||
return isString(value) && value !== '';
|
||||
};
|
||||
|
||||
export const isBlob = (value: any): value is Blob => {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.stream === 'function' &&
|
||||
typeof value.arrayBuffer === 'function' &&
|
||||
typeof value.constructor === 'function' &&
|
||||
typeof value.constructor.name === 'string' &&
|
||||
/^(Blob|File)$/.test(value.constructor.name) &&
|
||||
/^(Blob|File)$/.test(value[Symbol.toStringTag])
|
||||
);
|
||||
};
|
||||
|
||||
export const isFormData = (value: any): value is FormData => {
|
||||
return value instanceof FormData;
|
||||
};
|
||||
|
||||
export const base64 = (str: string): string => {
|
||||
try {
|
||||
return btoa(str);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString('base64');
|
||||
}
|
||||
};
|
||||
|
||||
export const getQueryString = (params: Record<string, any>): string => {
|
||||
const qs: string[] = [];
|
||||
|
||||
const append = (key: string, value: any) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isDefined(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
process(key, v);
|
||||
});
|
||||
} else if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([k, v]) => {
|
||||
process(`${key}[${k}]`, v);
|
||||
});
|
||||
} else {
|
||||
append(key, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
process(key, value);
|
||||
});
|
||||
|
||||
if (qs.length > 0) {
|
||||
return `?${qs.join('&')}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI;
|
||||
|
||||
const path = options.url
|
||||
.replace('{api-version}', config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]));
|
||||
}
|
||||
return substring;
|
||||
});
|
||||
|
||||
const url = `${config.BASE}${path}`;
|
||||
if (options.query) {
|
||||
return `${url}${getQueryString(options.query)}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
|
||||
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
|
||||
const [token, username, password, additionalHeaders] = await Promise.all([
|
||||
resolve(options, config.TOKEN),
|
||||
resolve(options, config.USERNAME),
|
||||
resolve(options, config.PASSWORD),
|
||||
resolve(options, config.HEADERS),
|
||||
]);
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: 'application/json',
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
})
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.reduce((headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}), {} as Record<string, string>);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
if (options.body !== undefined) {
|
||||
if (options.mediaType) {
|
||||
headers['Content-Type'] = options.mediaType;
|
||||
} else if (isBlob(options.body)) {
|
||||
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||
} else if (isString(options.body)) {
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
return new Headers(headers);
|
||||
};
|
||||
|
||||
export const getRequestBody = (options: ApiRequestOptions): any => {
|
||||
if (options.body !== undefined) {
|
||||
if (options.mediaType?.includes('/json')) {
|
||||
return JSON.stringify(options.body)
|
||||
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
|
||||
return options.body;
|
||||
} else {
|
||||
return JSON.stringify(options.body);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const sendRequest = async (
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions,
|
||||
url: string,
|
||||
body: any,
|
||||
formData: FormData | undefined,
|
||||
headers: Headers,
|
||||
onCancel: OnCancel
|
||||
): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const request: RequestInit = {
|
||||
headers,
|
||||
body: body ?? formData,
|
||||
method: options.method,
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
if (config.WITH_CREDENTIALS) {
|
||||
request.credentials = config.CREDENTIALS;
|
||||
}
|
||||
|
||||
onCancel(() => controller.abort());
|
||||
|
||||
return await fetch(url, request);
|
||||
};
|
||||
|
||||
export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers.get(responseHeader);
|
||||
if (isString(content)) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getResponseBody = async (response: Response): Promise<any> => {
|
||||
if (response.status !== 204) {
|
||||
try {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType) {
|
||||
const jsonTypes = ['application/json', 'application/problem+json']
|
||||
const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type));
|
||||
if (isJSON) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return await response.text();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
500: 'Internal Server Error',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
...options.errors,
|
||||
}
|
||||
|
||||
const error = errors[result.status];
|
||||
if (error) {
|
||||
throw new ApiError(options, result, error);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
const errorStatus = result.status ?? 'unknown';
|
||||
const errorStatusText = result.statusText ?? 'unknown';
|
||||
const errorBody = (() => {
|
||||
try {
|
||||
return JSON.stringify(result.body, null, 2);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
throw new ApiError(options, result,
|
||||
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param config The OpenAPI configuration object
|
||||
* @param options The request options from the service
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
try {
|
||||
const url = getUrl(config, options);
|
||||
const formData = getFormData(options);
|
||||
const body = getRequestBody(options);
|
||||
const headers = await getHeaders(config, options);
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
|
||||
const responseBody = await getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||
|
||||
const result: ApiResult = {
|
||||
url,
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? responseBody,
|
||||
};
|
||||
|
||||
catchErrorCodes(options, result);
|
||||
|
||||
resolve(result.body);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
29
webapp/src/generated/index.ts
Normal file
29
webapp/src/generated/index.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export { ApiError } from './core/ApiError';
|
||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||
export { OpenAPI } from './core/OpenAPI';
|
||||
export type { OpenAPIConfig } from './core/OpenAPI';
|
||||
|
||||
export type { peachCreateProductRequest } from './models/peachCreateProductRequest';
|
||||
export type { peachCreateProductResponse } from './models/peachCreateProductResponse';
|
||||
export type { peachCreateUserRequest } from './models/peachCreateUserRequest';
|
||||
export type { peachCreateUserResponse } from './models/peachCreateUserResponse';
|
||||
export type { peachDeleteProductResponse } from './models/peachDeleteProductResponse';
|
||||
export type { peachDeleteUserResponse } from './models/peachDeleteUserResponse';
|
||||
export type { peachListProductsResponse } from './models/peachListProductsResponse';
|
||||
export type { peachListUsersResponse } from './models/peachListUsersResponse';
|
||||
export type { peachProduct } from './models/peachProduct';
|
||||
export type { peachReadProductResponse } from './models/peachReadProductResponse';
|
||||
export type { peachReadUserResponse } from './models/peachReadUserResponse';
|
||||
export type { peachUpdateProductRequest } from './models/peachUpdateProductRequest';
|
||||
export type { peachUpdateProductResponse } from './models/peachUpdateProductResponse';
|
||||
export type { peachUpdateUserRequest } from './models/peachUpdateUserRequest';
|
||||
export type { peachUpdateUserResponse } from './models/peachUpdateUserResponse';
|
||||
export type { peachUser } from './models/peachUser';
|
||||
export type { protobufAny } from './models/protobufAny';
|
||||
export type { rpcStatus } from './models/rpcStatus';
|
||||
|
||||
export { PeachService } from './services/PeachService';
|
9
webapp/src/generated/models/peachCreateProductRequest.ts
Normal file
9
webapp/src/generated/models/peachCreateProductRequest.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachProduct } from './peachProduct';
|
||||
export type peachCreateProductRequest = {
|
||||
payload?: peachProduct;
|
||||
};
|
||||
|
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachProduct } from './peachProduct';
|
||||
export type peachCreateProductResponse = {
|
||||
result?: peachProduct;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachCreateUserRequest.ts
Normal file
9
webapp/src/generated/models/peachCreateUserRequest.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachUser } from './peachUser';
|
||||
export type peachCreateUserRequest = {
|
||||
payload?: peachUser;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachCreateUserResponse.ts
Normal file
9
webapp/src/generated/models/peachCreateUserResponse.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachUser } from './peachUser';
|
||||
export type peachCreateUserResponse = {
|
||||
result?: peachUser;
|
||||
};
|
||||
|
@ -0,0 +1,7 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type peachDeleteProductResponse = {
|
||||
};
|
||||
|
7
webapp/src/generated/models/peachDeleteUserResponse.ts
Normal file
7
webapp/src/generated/models/peachDeleteUserResponse.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type peachDeleteUserResponse = {
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachListProductsResponse.ts
Normal file
9
webapp/src/generated/models/peachListProductsResponse.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachProduct } from './peachProduct';
|
||||
export type peachListProductsResponse = {
|
||||
results?: Array<peachProduct>;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachListUsersResponse.ts
Normal file
9
webapp/src/generated/models/peachListUsersResponse.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachUser } from './peachUser';
|
||||
export type peachListUsersResponse = {
|
||||
results?: Array<peachUser>;
|
||||
};
|
||||
|
11
webapp/src/generated/models/peachProduct.ts
Normal file
11
webapp/src/generated/models/peachProduct.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type peachProduct = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
price?: number;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachReadProductResponse.ts
Normal file
9
webapp/src/generated/models/peachReadProductResponse.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachProduct } from './peachProduct';
|
||||
export type peachReadProductResponse = {
|
||||
result?: peachProduct;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachReadUserResponse.ts
Normal file
9
webapp/src/generated/models/peachReadUserResponse.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachUser } from './peachUser';
|
||||
export type peachReadUserResponse = {
|
||||
result?: peachUser;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachUpdateProductRequest.ts
Normal file
9
webapp/src/generated/models/peachUpdateProductRequest.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachProduct } from './peachProduct';
|
||||
export type peachUpdateProductRequest = {
|
||||
payload?: peachProduct;
|
||||
};
|
||||
|
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachProduct } from './peachProduct';
|
||||
export type peachUpdateProductResponse = {
|
||||
result?: peachProduct;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachUpdateUserRequest.ts
Normal file
9
webapp/src/generated/models/peachUpdateUserRequest.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachUser } from './peachUser';
|
||||
export type peachUpdateUserRequest = {
|
||||
payload?: peachUser;
|
||||
};
|
||||
|
9
webapp/src/generated/models/peachUpdateUserResponse.ts
Normal file
9
webapp/src/generated/models/peachUpdateUserResponse.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachUser } from './peachUser';
|
||||
export type peachUpdateUserResponse = {
|
||||
result?: peachUser;
|
||||
};
|
||||
|
10
webapp/src/generated/models/peachUser.ts
Normal file
10
webapp/src/generated/models/peachUser.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type peachUser = {
|
||||
id?: string;
|
||||
username?: string;
|
||||
bio?: string;
|
||||
};
|
||||
|
5
webapp/src/generated/models/protobufAny.ts
Normal file
5
webapp/src/generated/models/protobufAny.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type protobufAny = Record<string, any>;
|
11
webapp/src/generated/models/rpcStatus.ts
Normal file
11
webapp/src/generated/models/rpcStatus.ts
Normal file
@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { protobufAny } from './protobufAny';
|
||||
export type rpcStatus = {
|
||||
code?: number;
|
||||
message?: string;
|
||||
details?: Array<protobufAny>;
|
||||
};
|
||||
|
174
webapp/src/generated/services/PeachService.ts
Normal file
174
webapp/src/generated/services/PeachService.ts
Normal file
@ -0,0 +1,174 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { peachCreateProductRequest } from '../models/peachCreateProductRequest';
|
||||
import type { peachCreateProductResponse } from '../models/peachCreateProductResponse';
|
||||
import type { peachCreateUserRequest } from '../models/peachCreateUserRequest';
|
||||
import type { peachCreateUserResponse } from '../models/peachCreateUserResponse';
|
||||
import type { peachDeleteProductResponse } from '../models/peachDeleteProductResponse';
|
||||
import type { peachDeleteUserResponse } from '../models/peachDeleteUserResponse';
|
||||
import type { peachListProductsResponse } from '../models/peachListProductsResponse';
|
||||
import type { peachListUsersResponse } from '../models/peachListUsersResponse';
|
||||
import type { peachReadProductResponse } from '../models/peachReadProductResponse';
|
||||
import type { peachReadUserResponse } from '../models/peachReadUserResponse';
|
||||
import type { peachUpdateProductRequest } from '../models/peachUpdateProductRequest';
|
||||
import type { peachUpdateProductResponse } from '../models/peachUpdateProductResponse';
|
||||
import type { peachUpdateUserRequest } from '../models/peachUpdateUserRequest';
|
||||
import type { peachUpdateUserResponse } from '../models/peachUpdateUserResponse';
|
||||
import type { rpcStatus } from '../models/rpcStatus';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class PeachService {
|
||||
/**
|
||||
* @returns peachListProductsResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachListProducts(): CancelablePromise<peachListProductsResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/v1/Product',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param body
|
||||
* @returns peachCreateProductResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachCreateProduct(
|
||||
body: peachCreateProductRequest,
|
||||
): CancelablePromise<peachCreateProductResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/v1/Product',
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param body
|
||||
* @returns peachUpdateProductResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachUpdateProduct(
|
||||
body: peachUpdateProductRequest,
|
||||
): CancelablePromise<peachUpdateProductResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/v1/Product',
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param id
|
||||
* @returns peachReadProductResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachReadProduct(
|
||||
id: string,
|
||||
): CancelablePromise<peachReadProductResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/v1/Product/{id}',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param id
|
||||
* @returns peachDeleteProductResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachDeleteProduct(
|
||||
id: string,
|
||||
): CancelablePromise<peachDeleteProductResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/v1/Product/{id}',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @returns peachListUsersResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachListUsers(): CancelablePromise<peachListUsersResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/v1/users',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param body
|
||||
* @returns peachCreateUserResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachCreateUser(
|
||||
body: peachCreateUserRequest,
|
||||
): CancelablePromise<peachCreateUserResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/v1/users',
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param body
|
||||
* @returns peachUpdateUserResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachUpdateUser(
|
||||
body: peachUpdateUserRequest,
|
||||
): CancelablePromise<peachUpdateUserResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'PUT',
|
||||
url: '/v1/users',
|
||||
body: body,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param id
|
||||
* @returns peachReadUserResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachReadUser(
|
||||
id: string,
|
||||
): CancelablePromise<peachReadUserResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/v1/users/{id}',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @param id
|
||||
* @returns peachDeleteUserResponse A successful response.
|
||||
* @returns rpcStatus An unexpected error response.
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static peachDeleteUser(
|
||||
id: string,
|
||||
): CancelablePromise<peachDeleteUserResponse | rpcStatus> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/v1/users/{id}',
|
||||
path: {
|
||||
'id': id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
12
webapp/src/layouts/Login.vue
Normal file
12
webapp/src/layouts/Login.vue
Normal file
@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<div>
|
||||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { RouterLink, RouterView } from 'vue-router';
|
||||
</script>
|
68
webapp/src/layouts/MainLayout.vue
Normal file
68
webapp/src/layouts/MainLayout.vue
Normal file
@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<!-- Pass links, a custom theme and include optional branding via the "brand" slot -->
|
||||
<SideNavBar :links="navLinks" :theme="customTheme">
|
||||
<!-- Branding slot: the logo or app name will be sized by the sidebar -->
|
||||
<template #brand>
|
||||
<!-- For example, an image with controlled size -->
|
||||
<!-- <img src="/src/assets/logo.svg" alt="App Logo" class="w-full h-full object-contain" />-->
|
||||
<!-- Alternatively, you could use a text element here -->
|
||||
<div class="flex flex-col justify-around w-full h-full">
|
||||
<span class="text-xl font-bold">Peach</span>
|
||||
</div>
|
||||
</template>
|
||||
</SideNavBar>
|
||||
|
||||
<main class="flex-1 p-4 bg-blue-50 h-screen overflow-auto">
|
||||
<Header />
|
||||
<router-view />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, markRaw } from 'vue';
|
||||
import SideNavBar from '../components/SideNavBar.vue';
|
||||
import IconHome from '../components/icons/IconHome.vue';
|
||||
import IconGear from '../components/icons/IconGear.vue';
|
||||
import IconArrowFromShapeRight from '../components/icons/IconArrowFromShapeRight.vue';
|
||||
import Header from '../components/Header.vue';
|
||||
import type { NavLink, Theme } from '../components/SideNavBar.vue';
|
||||
|
||||
const homeIcon = markRaw(IconHome);
|
||||
const gearIcon = markRaw(IconGear);
|
||||
const logoutIcon = markRaw(IconArrowFromShapeRight);
|
||||
|
||||
const navLinks = ref<NavLink[]>([
|
||||
{
|
||||
title: 'Home',
|
||||
icon: homeIcon,
|
||||
route: '/',
|
||||
subLinks: [
|
||||
{ title: 'Home2', route: '/home2' },
|
||||
{ title: 'Overview', route: '/overview' },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
icon: gearIcon,
|
||||
route: '/settings'
|
||||
},
|
||||
{
|
||||
title: 'Logout',
|
||||
icon: logoutIcon,
|
||||
route: '/logout'
|
||||
}
|
||||
]);
|
||||
|
||||
// Define a custom theme for the sidebar.
|
||||
const customTheme: Theme = {
|
||||
// sidebarBg: "bg-blue-800",
|
||||
// sidebarText: "text-gray-100",
|
||||
// sidebarLink: "rounded-md",
|
||||
// sidebarHover: "hover:bg-blue-700",
|
||||
// sidebarActive: "bg-blue-900",
|
||||
// toggleButtonBg: "bg-blue-700"
|
||||
};
|
||||
|
||||
</script>
|
15
webapp/src/main.ts
Normal file
15
webapp/src/main.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
|
||||
app.mount('#app')
|
81
webapp/src/router/index.ts
Normal file
81
webapp/src/router/index.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useAuthenticationStore } from '@/stores/authentication.ts'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../layouts/Login.vue'), // Lazy-loaded
|
||||
meta: { public: true }, // Indicates this route doesn’t require auth
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Password',
|
||||
component: () => import('../views/Password.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
component: () => import('../layouts/MainLayout.vue'), // Protected layout
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'Home',
|
||||
component: () => import('../views/HomeView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'home2',
|
||||
name: 'Home2',
|
||||
component: () => import('../views/Home2View.vue'),
|
||||
},
|
||||
{
|
||||
path: '/overview',
|
||||
name: 'Overview',
|
||||
component: () => import('../views/AboutView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'Settings',
|
||||
component: () => import('../views/HomeView.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/logout',
|
||||
name: 'Logout',
|
||||
meta: { public: true },
|
||||
redirect: '/login',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const authenticationStore = useAuthenticationStore();
|
||||
|
||||
if (to.redirectedFrom?.path === '/logout') {
|
||||
authenticationStore.logout();
|
||||
}
|
||||
|
||||
const isAuthenticated = await Promise.resolve(authenticationStore.isAuthenticated); /* logic to check auth state, e.g., from a store */
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
// save the desired route in localStorage and redirect to login
|
||||
localStorage.setItem('redirect', to.fullPath);
|
||||
return next({ path: '/login' });
|
||||
}
|
||||
// if (to.meta.public && isAuthenticated) {
|
||||
// // TODO: Decide if you want to redirect to a different page if the user is already authenticated
|
||||
// }
|
||||
const deepLink = localStorage.getItem('redirect');
|
||||
if (to.meta.requiresAuth && isAuthenticated && !!deepLink) {
|
||||
// remove redirect from localStorage and redirect to the deep link
|
||||
localStorage.removeItem('redirect');
|
||||
return next({ path: deepLink });
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router
|
12
webapp/src/services/peach-experiment.ts
Normal file
12
webapp/src/services/peach-experiment.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { PeachService } from '@/generated'
|
||||
|
||||
|
||||
export function createProduct() {
|
||||
return PeachService.peachCreateProduct({
|
||||
payload: {}
|
||||
}).then((response) => {
|
||||
console.log(response);
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
}
|
21
webapp/src/stores/authentication.ts
Normal file
21
webapp/src/stores/authentication.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export const useAuthenticationStore = defineStore('authentication', () => {
|
||||
// TODO: this should be replaced with a request to the server to check if the user is authenticated
|
||||
const isAuthenticated = ref(localStorage.getItem('isAuthenticated') === 'true');
|
||||
|
||||
function login() {
|
||||
// TODO: make a request to the server to authenticate the user
|
||||
isAuthenticated.value = true;
|
||||
localStorage.setItem('isAuthenticated', 'true');
|
||||
}
|
||||
|
||||
function logout() {
|
||||
// TODO: make a request to the server to log out the user
|
||||
isAuthenticated.value = false;
|
||||
localStorage.setItem('isAuthenticated', 'false');
|
||||
}
|
||||
|
||||
return { isAuthenticated, login, logout };
|
||||
});
|
12
webapp/src/stores/counter.ts
Normal file
12
webapp/src/stores/counter.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
11
webapp/src/views/AboutView.vue
Normal file
11
webapp/src/views/AboutView.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center justify-center h-screen">
|
||||
<h1 class="font-light font-sans text-3xl">This app was generated using the Masonry CLI tool.</h1>
|
||||
<span> </span>
|
||||
<span>Vectors and icons by <a href="https://www.svgrepo.com" target="_blank">SVG Repo</a></span>
|
||||
<span>Vectors and icons by <a href="https://www.figma.com/@thewolfkit?ref=svgrepo.com" target="_blank">Thewolfkit</a> in CC Attribution License via <a href="https://www.svgrepo.com/" target="_blank">SVG Repo</a></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
</style>
|
60
webapp/src/views/Home2View.vue
Normal file
60
webapp/src/views/Home2View.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<script setup lang="ts">
|
||||
import { type peachProduct } from '@/generated';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import PeachCreateProductRequestForm from '@/generated-sample-components/peachCreateProductRequestForm.vue';
|
||||
import PeachUpdateProductRequestForm from '@/generated-sample-components/peachUpdateProductRequestForm.vue';
|
||||
import PeachListProductsTable from '@/generated-sample-components/peachListProductsTable.vue';
|
||||
// import DynamicTableTest from '@/components/dynamicTableTest.vue'; // Commented out as in your original
|
||||
|
||||
// State to hold the selected product for editing (null means show creation form)
|
||||
const selectedProduct = ref<peachProduct | null>(null);
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
// Tells the table to update its data
|
||||
const callFetchData = () => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value.fetchData();
|
||||
}
|
||||
// Optionally reset to creation form after submission (uncomment if desired)
|
||||
selectedProduct.value = null;
|
||||
};
|
||||
|
||||
// Update selectedProduct when a table row is clicked
|
||||
const updateEditProduct = (product: peachProduct) => {
|
||||
selectedProduct.value = { ...product }; // Create a copy to avoid mutating the original
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Any initialization code can go here
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="flex flex-col md:flex-row">
|
||||
<!-- Form Container with fixed width on medium screens and up -->
|
||||
<div class="form-container w-full md:w-80 p-4">
|
||||
<peach-create-product-request-form
|
||||
v-if="!selectedProduct"
|
||||
formTitle="New Product"
|
||||
@send="callFetchData"
|
||||
></peach-create-product-request-form>
|
||||
<peach-update-product-request-form
|
||||
v-else
|
||||
:initial-values="selectedProduct"
|
||||
formTitle="Edit Product"
|
||||
@send="callFetchData"
|
||||
></peach-update-product-request-form>
|
||||
</div>
|
||||
<!-- Table Container takes remaining space -->
|
||||
<div class="table-container flex-1 p-4">
|
||||
<peach-list-products-table
|
||||
ref="tableRef"
|
||||
@row-click="updateEditProduct"
|
||||
></peach-list-products-table>
|
||||
</div>
|
||||
<!-- <dynamic-table-test></dynamic-table-test> -->
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
47
webapp/src/views/HomeView.vue
Normal file
47
webapp/src/views/HomeView.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { type peachProduct } from '@/generated'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import PeachCreateProductRequestForm from '@/generated-sample-components/peachCreateProductRequestForm.vue'
|
||||
import PeachUpdateProductRequestForm from '@/generated-sample-components/peachUpdateProductRequestForm.vue'
|
||||
import PeachListProductsTable from '@/generated-sample-components/peachListProductsTable.vue'
|
||||
import DynamicTableTest from '@/components/dynamicTableTest.vue'
|
||||
|
||||
const peachProduct = ref<peachProduct>({
|
||||
id: '123',
|
||||
name: 'Test Product',
|
||||
description: 'This is a test product',
|
||||
price: 100
|
||||
});
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
// tells the table to update its data
|
||||
const callFetchData = () => {
|
||||
if (tableRef.value) {
|
||||
tableRef.value.fetchData();
|
||||
}
|
||||
};
|
||||
|
||||
const updateEditProduct = (product: peachProduct) => {
|
||||
peachProduct.value.id = product.id;
|
||||
peachProduct.value.name = product.name;
|
||||
peachProduct.value.description = product.description;
|
||||
peachProduct.value.price = product.price;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div class="flex flex-row flex-wrap justify-around">
|
||||
<peach-create-product-request-form class="flex-auto" formTitle="New Product" @send="callFetchData"></peach-create-product-request-form>
|
||||
<peach-update-product-request-form class="flex-auto" :initial-values="peachProduct" formTitle="Edit Product" @send="callFetchData"></peach-update-product-request-form>
|
||||
<peach-list-products-table class="" ref="tableRef" @row-click="updateEditProduct"></peach-list-products-table>
|
||||
<!-- <dynamic-table-test></dynamic-table-test>-->
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
21
webapp/src/views/Password.vue
Normal file
21
webapp/src/views/Password.vue
Normal file
@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<div>
|
||||
<main>
|
||||
Username and password form <br>
|
||||
<button class="bg-blue-400 pl-4 pr-4 pt-2 pb-2 cursor-pointer rounded-2xl" @click="login">Login</button>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthenticationStore } from '@/stores/authentication.ts'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
function login() {
|
||||
const authenticationStore = useAuthenticationStore();
|
||||
authenticationStore.login();
|
||||
router.push('/');
|
||||
}
|
||||
</script>
|
12
webapp/tsconfig.app.json
Normal file
12
webapp/tsconfig.app.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
11
webapp/tsconfig.json
Normal file
11
webapp/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
}
|
||||
]
|
||||
}
|
19
webapp/tsconfig.node.json
Normal file
19
webapp/tsconfig.node.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
20
webapp/vite.config.ts
Normal file
20
webapp/vite.config.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user