initial commit

This commit is contained in:
2025-04-06 10:25:26 -06:00
commit 05e9970008
225 changed files with 35329 additions and 0 deletions

9
webapp/.editorconfig Normal file
View 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
View File

@ -0,0 +1 @@
* text=auto eol=lf

30
webapp/.gitignore vendored Normal file
View 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
View 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
View File

@ -0,0 +1,8 @@
{
"recommendations": [
"Vue.volar",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

39
webapp/README.md Normal file
View 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
View File

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

24
webapp/eslint.config.ts Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

43
webapp/package.json Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

11
webapp/src/App.vue Normal file
View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import { RouterLink, RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style scoped>
</style>

View File

@ -0,0 +1 @@
@import "tailwindcss";

View 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

View File

@ -0,0 +1 @@
@import './base.css';

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

View File

@ -0,0 +1,5 @@
<template>
<div class="flex justify-around">Peach</div>
</template>
<script setup lang="ts">
</script>

View 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">
Youve 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>

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

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

View 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>
Vues
<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>

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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;
}
}

View 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>;
};

View 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;
};

View 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;
}
}

View 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,
};

View 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);
}
});
};

View 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';

View 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;
};

View 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 peachCreateProductResponse = {
result?: peachProduct;
};

View 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;
};

View 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;
};

View File

@ -0,0 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type peachDeleteProductResponse = {
};

View File

@ -0,0 +1,7 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type peachDeleteUserResponse = {
};

View 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>;
};

View 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>;
};

View 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;
};

View 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;
};

View 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;
};

View 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;
};

View 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 peachUpdateProductResponse = {
result?: peachProduct;
};

View 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;
};

View 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;
};

View 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;
};

View 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>;

View 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>;
};

View 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,
},
});
}
}

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

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

View 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 doesnt 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

View 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);
});
}

View 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 };
});

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

View 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>&nbsp;</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>

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

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

View 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
View 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
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
webapp/tsconfig.node.json Normal file
View 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
View 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))
},
},
})