fix whack formatting

This commit is contained in:
Jono Targett 2026-03-18 23:29:16 +10:30
parent 1dac5e35f6
commit 93d7e9e5d1
21 changed files with 5956 additions and 5916 deletions

8
console/.prettierrc Normal file
View File

@ -0,0 +1,8 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"printWidth": 100,
"vueIndentScriptAndStyle": true
}

View File

@ -1,13 +1,13 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<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.js"></script>
</body>
<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.js"></script>
</body>
</html>

8727
console/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,42 @@
{
"name": "primevue-vite-quickstart",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.40.0",
"@jsonforms/core": "^3.7.0",
"@jsonforms/material-renderers": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@jsonforms/vue": "^3.7.0",
"@jsonforms/vue-vanilla": "^3.7.0",
"@primeuix/themes": "^1.2.5",
"@primevue/core": "^4.2.5",
"@tailwindcss/vite": "^4.2.1",
"chart.js": "^4.4.7",
"codemirror": "^6.0.2",
"jsonforms-primevue": "github:kobbejager/jsonforms-primevue",
"mqtt": "^5.15.0",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"tailwindcss": "^4.2.1",
"tailwindcss-primeui": "^0.6.1",
"vue": "^3.4.27"
},
"devDependencies": {
"@primevue/auto-import-resolver": "^4.3.1",
"@vitejs/plugin-vue": "^5.0.5",
"unplugin-vue-components": "^28.4.0",
"vite": "^5.2.13"
}
"name": "primevue-vite-quickstart",
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@codemirror/autocomplete": "^6.20.1",
"@codemirror/commands": "^6.10.3",
"@codemirror/lang-json": "^6.0.2",
"@codemirror/state": "^6.6.0",
"@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.40.0",
"@jsonforms/core": "^3.7.0",
"@jsonforms/material-renderers": "^3.7.0",
"@jsonforms/vanilla-renderers": "^3.7.0",
"@jsonforms/vue": "^3.7.0",
"@jsonforms/vue-vanilla": "^3.7.0",
"@primeuix/themes": "^1.2.5",
"@primevue/core": "^4.2.5",
"@tailwindcss/vite": "^4.2.1",
"chart.js": "^4.4.7",
"codemirror": "^6.0.2",
"jsonforms-primevue": "github:kobbejager/jsonforms-primevue",
"mqtt": "^5.15.0",
"primeicons": "^7.0.0",
"primevue": "^4.2.5",
"tailwindcss": "^4.2.1",
"tailwindcss-primeui": "^0.6.1",
"vue": "^3.4.27"
},
"devDependencies": {
"@primevue/auto-import-resolver": "^4.3.1",
"@vitejs/plugin-vue": "^5.0.5",
"prettier": "^3.8.1",
"unplugin-vue-components": "^28.4.0",
"vite": "^5.2.13"
}
}

View File

@ -1,47 +1,47 @@
<script setup>
import AppTopbar from "./components/AppTopbar.vue";
import AppFooter from "./components/AppFooter.vue";
import StatsWidget from "./components/dashboard/StatsWidget.vue";
import SalesTrendWidget from "./components/dashboard/SalesTrendWidget.vue";
import RecentActivityWidget from "./components/dashboard/RecentActivityWidget.vue";
import ProductOverviewWidget from "./components/dashboard/ProductOverviewWidget.vue";
import AppTopbar from './components/AppTopbar.vue'
import AppFooter from './components/AppFooter.vue'
import StatsWidget from './components/dashboard/StatsWidget.vue'
import SalesTrendWidget from './components/dashboard/SalesTrendWidget.vue'
import RecentActivityWidget from './components/dashboard/RecentActivityWidget.vue'
import ProductOverviewWidget from './components/dashboard/ProductOverviewWidget.vue'
import PropertiesWidget from "./components/dashboard/PropertiesWidget.vue";
import CommandsWidget from "./components/dashboard/CommandsWidget.vue";
import PropertiesWidget from './components/dashboard/PropertiesWidget.vue'
import CommandsWidget from './components/dashboard/CommandsWidget.vue'
import { ref } from 'vue'
import DevicesWidget from './components/dashboard/DevicesWidget.vue'
import { ref } from 'vue'
import DevicesWidget from './components/dashboard/DevicesWidget.vue'
const selectedDevice = ref(null)
const selectedDevice = ref(null)
</script>
<template>
<Toast position="bottom-right" />
<Toast position="bottom-right" />
<div class="layout-container">
<AppTopbar />
<div class="layout-grid">
<DevicesWidget @select="selectedDevice = $event" />
<div v-if="selectedDevice" class="layout-grid-row">
<PropertiesWidget
v-if="selectedDevice"
:key="'props-' + selectedDevice"
:device-id="selectedDevice"
/>
<div class="layout-container">
<AppTopbar />
<div class="layout-grid">
<DevicesWidget @select="selectedDevice = $event" />
<div v-if="selectedDevice" class="layout-grid-row">
<PropertiesWidget
v-if="selectedDevice"
:key="'props-' + selectedDevice"
:device-id="selectedDevice"
/>
<CommandsWidget
v-if="selectedDevice"
:key="'cmds-' + selectedDevice"
:device-id="selectedDevice"
/>
</div>
<!-- <StatsWidget /> -->
<!-- <div class="layout-grid-row">
<CommandsWidget
v-if="selectedDevice"
:key="'cmds-' + selectedDevice"
:device-id="selectedDevice"
/>
</div>
<!-- <StatsWidget /> -->
<!-- <div class="layout-grid-row">
<SalesTrendWidget />
<RecentActivityWidget />
</div>-->
<!-- <ProductOverviewWidget /> -->
</div>
<AppFooter />
<!-- <ProductOverviewWidget /> -->
</div>
<AppFooter />
</div>
</template>

View File

@ -1,598 +1,602 @@
@import "primeicons/primeicons.css";
@import 'primeicons/primeicons.css';
@import "tailwindcss";
@import 'tailwindcss';
html {
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: "Inter", sans-serif;
font-optical-sizing: auto;
font-variation-settings: normal;
font-weight: 400;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-family: 'Inter', sans-serif;
font-optical-sizing: auto;
font-variation-settings: normal;
font-weight: 400;
}
body {
background-color: var(--p-surface-50);
min-height: 100vh;
background-color: var(--p-surface-50);
min-height: 100vh;
}
.p-dark body {
background-color: var(--p-surface-950);
background-color: var(--p-surface-950);
}
.layout-container {
background-color: var(--p-surface-50);
color: var(--p-surface-950);
min-height: 100vh;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
background-color: var(--p-surface-50);
color: var(--p-surface-950);
min-height: 100vh;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.p-dark .layout-container {
background-color: var(--p-surface-950);
color: var(--p-surface-0);
background-color: var(--p-surface-950);
color: var(--p-surface-0);
}
.layout-grid {
display: flex;
flex-direction: column;
flex: 1;
max-width: 1400px;
width: 100%;
margin: 0 auto;
gap: 1.5rem;
display: flex;
flex-direction: column;
flex: 1;
max-width: 1400px;
width: 100%;
margin: 0 auto;
gap: 1.5rem;
}
.layout-grid-row {
display: flex;
flex-direction: row;
gap: 1.5rem;
width: 100%;
display: flex;
flex-direction: row;
gap: 1.5rem;
width: 100%;
}
.layout-card {
background-color: var(--p-surface-0);
color: var(--p-surface-950);
padding: 1.5rem;
border-radius: 0.5rem;
border: 1px solid var(--p-surface-200);
display: flex;
flex-direction: column;
gap: 0.5rem;
background-color: var(--p-surface-0);
color: var(--p-surface-950);
padding: 1.5rem;
border-radius: 0.5rem;
border: 1px solid var(--p-surface-200);
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.p-dark .layout-card {
background-color: var(--p-surface-900);
color: var(--p-surface-0);
border-color: var(--p-surface-700);
background-color: var(--p-surface-900);
color: var(--p-surface-0);
border-color: var(--p-surface-700);
}
.stats {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 1.714rem;
position: relative;
z-index: 0;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
gap: 1.714rem;
position: relative;
z-index: 0;
}
.stats-icon-box {
flex-shrink: 0;
background-color: var(--p-primary-100);
color: var(--p-primary-600);
border-radius: 0.5rem;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--p-primary-200);
flex-shrink: 0;
background-color: var(--p-primary-100);
color: var(--p-primary-600);
border-radius: 0.5rem;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--p-primary-200);
}
.p-dark .stats-icon-box {
background-color: color-mix(in srgb, var(--p-primary-400), transparent 80%);
border-color: color-mix(in srgb, var(--p-primary-400), transparent 70%);
color: var(--p-primary-200);
background-color: color-mix(in srgb, var(--p-primary-400), transparent 80%);
border-color: color-mix(in srgb, var(--p-primary-400), transparent 70%);
color: var(--p-primary-200);
}
.stats-header {
display: flex;
align-items: flex-start;
gap: 0.5rem;
justify-content: space-between;
display: flex;
align-items: flex-start;
gap: 0.5rem;
justify-content: space-between;
}
.stats-title {
font-size: 1.25rem;
font-weight: 300;
line-height: 1.25;
color: var(--p-surface-900);
font-size: 1.25rem;
font-weight: 300;
line-height: 1.25;
color: var(--p-surface-900);
}
.p-dark .stats-title {
color: var(--p-surface-0);
color: var(--p-surface-0);
}
.stats-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
width: 100%;
display: flex;
flex-direction: column;
gap: 0.25rem;
width: 100%;
}
.stats-value {
font-size: 1.875rem;
font-weight: 500;
line-height: 1.25;
color: var(--p-surface-900);
font-size: 1.875rem;
font-weight: 500;
line-height: 1.25;
color: var(--p-surface-900);
}
.p-dark .stats-value {
color: var(--p-surface-0);
color: var(--p-surface-0);
}
.stats-subtitle {
color: var(--p-surface-600);
font-size: 0.875rem;
line-height: 1.25;
color: var(--p-surface-600);
font-size: 0.875rem;
line-height: 1.25;
}
.p-dark .stats-subtitle {
color: var(--p-surface-400);
color: var(--p-surface-400);
}
.col-item-2 {
width: 50%;
width: 50%;
}
.chart-header {
display: flex;
flex-direction: column;
gap: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.chart-title {
font-size: 1rem;
font-weight: 500;
color: var(--p-surface-900);
font-size: 1rem;
font-weight: 500;
color: var(--p-surface-900);
}
.p-dark .chart-title {
color: var(--p-surface-0);
color: var(--p-surface-0);
}
.chart-content {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.5rem 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.5rem 0;
}
.activity-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.5rem 0;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.5rem 0;
}
.activity-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border: 1px solid var(--p-surface-200);
border-radius: 0.5rem;
background-color: var(--p-surface-50);
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border: 1px solid var(--p-surface-200);
border-radius: 0.5rem;
background-color: var(--p-surface-50);
}
.p-dark .activity-item {
background-color: var(--p-surface-800);
border-color: var(--p-surface-700);
background-color: var(--p-surface-800);
border-color: var(--p-surface-700);
}
.activity-icon {
font-size: 1.125rem !important;
font-size: 1.125rem !important;
}
.activity-icon.green {
color: #22c55e;
color: #22c55e;
}
.activity-icon.blue {
color: #3b82f6;
color: #3b82f6;
}
.activity-icon.yellow {
color: #eab308;
color: #eab308;
}
.activity-icon.pink {
color: #ec4899;
color: #ec4899;
}
.activity-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.activity-text {
font-size: 0.875rem;
font-weight: 500;
font-size: 0.875rem;
font-weight: 500;
}
.activity-time {
font-size: 0.75rem;
color: var(--p-surface-600);
font-size: 0.75rem;
color: var(--p-surface-600);
}
.p-dark .activity-time {
color: var(--p-surface-400);
color: var(--p-surface-400);
}
.products-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
}
@media (max-width: 640px) {
.products-header {
flex-direction: column;
gap: 0.5rem;
}
.products-header {
flex-direction: column;
gap: 0.5rem;
}
.products-header .search-field {
width: 100%;
}
.products-header .search-field {
width: 100%;
}
}
.products-title {
font-size: 1rem;
font-weight: 500;
color: var(--p-surface-900);
font-size: 1rem;
font-weight: 500;
color: var(--p-surface-900);
}
.p-dark .products-title {
color: var(--p-surface-0);
color: var(--p-surface-0);
}
.products-table-container {
display: flex;
flex-direction: column;
gap: 0.5rem;
background-color: var(--p-surface-0);
display: flex;
flex-direction: column;
gap: 0.5rem;
background-color: var(--p-surface-0);
}
.p-dark .products-table-container {
background-color: var(--p-surface-900);
background-color: var(--p-surface-900);
}
.products-table {
width: 100%;
color: var(--p-surface-900);
width: 100%;
color: var(--p-surface-900);
}
.p-dark .products-table {
color: var(--p-surface-0);
color: var(--p-surface-0);
}
.products-table-mask {
backdrop-filter: blur(4px) !important;
background-color: color-mix(in srgb, var(--p-surface-0), transparent 80%) !important;
backdrop-filter: blur(4px) !important;
background-color: color-mix(in srgb, var(--p-surface-0), transparent 80%) !important;
}
.p-dark .products-table-mask {
background-color: color-mix(in srgb, var(--p-surface-900), transparent 80%) !important;
background-color: color-mix(in srgb, var(--p-surface-900), transparent 80%) !important;
}
.products-table-loading {
color: var(--p-primary-500) !important;
color: var(--p-primary-500) !important;
}
.products-search {
font-size: 0.875rem;
padding: 0.5rem;
background-color: var(--p-surface-0);
color: var(--p-surface-900);
border: 1px solid var(--p-surface-200);
font-size: 0.875rem;
padding: 0.5rem;
background-color: var(--p-surface-0);
color: var(--p-surface-900);
border: 1px solid var(--p-surface-200);
}
.p-dark .products-search {
background-color: var(--p-surface-900);
color: var(--p-surface-0);
border-color: var(--p-surface-700);
background-color: var(--p-surface-900);
color: var(--p-surface-0);
border-color: var(--p-surface-700);
}
@media (min-width: 768px) {
.products-search {
width: auto !important;
}
.products-search {
width: auto !important;
}
}
@media (max-width: 767px) {
.products-search {
width: 100% !important;
}
.products-search {
width: 100% !important;
}
}
@media (max-width: 991px) {
.stats {
grid-template-columns: 1fr 1fr;
}
.layout-grid-row {
flex-direction: column;
}
.col-item-2 {
width: 100%;
}
.stats {
grid-template-columns: 1fr 1fr;
}
.layout-grid-row {
flex-direction: column;
}
.col-item-2 {
width: 100%;
}
}
@media (max-width: 480px) {
.stats {
grid-template-columns: 1fr;
}
.stats {
grid-template-columns: 1fr;
}
}
.topbar {
background-color: var(--p-surface-0);
padding: 1.5rem;
border-radius: 1rem;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
border: 1px solid var(--p-surface-200);
width: 100%;
background-color: var(--p-surface-0);
padding: 1.5rem;
border-radius: 1rem;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
border: 1px solid var(--p-surface-200);
width: 100%;
}
.p-dark .topbar {
background-color: var(--p-surface-900);
border-color: var(--p-surface-700);
background-color: var(--p-surface-900);
border-color: var(--p-surface-700);
}
.topbar-container {
display: flex;
justify-content: space-between;
align-items: center;
display: flex;
justify-content: space-between;
align-items: center;
}
.topbar-brand {
display: flex;
gap: 0.75rem;
align-items: center;
display: flex;
gap: 0.75rem;
align-items: center;
}
.topbar-brand-text {
display: none;
display: none;
}
@media (min-width: 640px) {
.topbar-brand-text {
display: flex;
flex-direction: column;
}
.topbar-brand-text {
display: flex;
flex-direction: column;
}
}
.topbar-title {
font-size: 1.25rem;
font-weight: 300;
color: var(--p-surface-700);
line-height: 1;
font-size: 1.25rem;
font-weight: 300;
color: var(--p-surface-700);
line-height: 1;
}
.p-dark .topbar-title {
color: var(--p-surface-100);
color: var(--p-surface-100);
}
.topbar-subtitle {
font-size: 0.875rem;
font-weight: 500;
color: var(--p-primary-500);
line-height: 1.25;
font-size: 0.875rem;
font-weight: 500;
color: var(--p-primary-500);
line-height: 1.25;
}
.topbar-actions {
display: flex;
align-items: center;
gap: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.topbar-theme-button {
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
transition: all 0.2s;
color: var(--p-surface-900);
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
transition: all 0.2s;
color: var(--p-surface-900);
}
.p-dark .topbar-theme-button {
color: var(--p-surface-0);
color: var(--p-surface-0);
}
.topbar-theme-button:hover {
background-color: var(--p-surface-100);
background-color: var(--p-surface-100);
}
.p-dark .topbar-theme-button:hover {
background-color: var(--p-surface-800);
background-color: var(--p-surface-800);
}
.fill-primary {
fill: var(--p-primary-500);
fill: var(--p-primary-500);
}
.p-dark .fill-primary {
fill: var(--p-primary-400);
fill: var(--p-primary-400);
}
.fill-surface {
fill: var(--p-surface-900);
fill: var(--p-surface-900);
}
.p-dark .fill-surface {
fill: var(--p-surface-0);
fill: var(--p-surface-0);
}
.config-panel {
position: absolute;
top: 4rem;
right: 0;
width: 16rem;
padding: 1rem;
background-color: var(--p-surface-0);
border-radius: 0.375rem;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
border: 1px solid var(--p-surface-200);
transform-origin: top;
z-index: 50;
position: absolute;
top: 4rem;
right: 0;
width: 16rem;
padding: 1rem;
background-color: var(--p-surface-0);
border-radius: 0.375rem;
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
border: 1px solid var(--p-surface-200);
transform-origin: top;
z-index: 50;
}
.p-dark .config-panel {
background-color: var(--p-surface-900);
border-color: var(--p-surface-700);
background-color: var(--p-surface-900);
border-color: var(--p-surface-700);
}
.config-section {
display: flex;
flex-direction: column;
gap: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.config-label {
font-size: 0.875rem;
color: var(--p-surface-600);
font-weight: 600;
font-size: 0.875rem;
color: var(--p-surface-600);
font-weight: 600;
}
.p-dark .config-label {
color: var(--p-surface-400);
color: var(--p-surface-400);
}
.config-colors {
padding-top: 0.5rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: space-between;
padding-top: 0.5rem;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
justify-content: space-between;
}
.color-button {
border: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 9999px;
padding: 0;
cursor: pointer;
border: none;
width: 1.25rem;
height: 1.25rem;
border-radius: 9999px;
padding: 0;
cursor: pointer;
}
.selected {
--ring-offset-shadow: 0 0 0 var(--ring-offset-width) var(--ring-offset-color);
--ring-shadow: 0 0 0 calc(var(--ring-width) + var(--ring-offset-width)) var(--ring-color);
--ring-width: 2px;
--ring-offset-width: 2px;
--ring-color: var(--p-primary-500);
--ring-offset-color: #ffffff;
box-shadow: var(--ring-offset-shadow), var(--ring-shadow);
--ring-offset-shadow: 0 0 0 var(--ring-offset-width) var(--ring-offset-color);
--ring-shadow: 0 0 0 calc(var(--ring-width) + var(--ring-offset-width)) var(--ring-color);
--ring-width: 2px;
--ring-offset-width: 2px;
--ring-color: var(--p-primary-500);
--ring-offset-color: #ffffff;
box-shadow: var(--ring-offset-shadow), var(--ring-shadow);
}
.hidden {
display: none;
display: none;
}
.footer {
background-color: var(--p-surface-0);
padding: 1.5rem;
border-radius: 1rem;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
border: 1px solid var(--p-surface-200);
width: 100%;
background-color: var(--p-surface-0);
padding: 1.5rem;
border-radius: 1rem;
max-width: 1400px;
margin-left: auto;
margin-right: auto;
border: 1px solid var(--p-surface-200);
width: 100%;
}
.p-dark .footer {
background-color: var(--p-surface-900);
border-color: var(--p-surface-700);
background-color: var(--p-surface-900);
border-color: var(--p-surface-700);
}
.footer-container {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
}
@media (max-width: 640px) {
.footer-container {
flex-direction: column;
}
.footer-container {
flex-direction: column;
}
}
.footer-copyright {
font-size: 0.875rem;
color: var(--p-surface-600);
font-size: 0.875rem;
color: var(--p-surface-600);
}
.p-dark .footer-copyright {
color: var(--p-surface-400);
color: var(--p-surface-400);
}
.footer-links {
display: flex;
gap: 1rem;
display: flex;
gap: 1rem;
}
.footer-link {
color: var(--p-surface-600);
font-size: 0.875rem;
transition: color 0.2s;
color: var(--p-surface-600);
font-size: 0.875rem;
transition: color 0.2s;
}
.p-dark .footer-link {
color: var(--p-surface-400);
color: var(--p-surface-400);
}
.footer-link:hover {
color: var(--p-primary-500);
color: var(--p-primary-500);
}
.footer-icon {
font-size: 1.25rem;
font-size: 1.25rem;
}
.relative {
position: relative;
position: relative;
}
.animate-fadeout {
animation: fadeout 0.15s linear;
animation: fadeout 0.15s linear;
}
.animate-scalein {
animation: scalein 0.15s linear;
animation: scalein 0.15s linear;
}
@keyframes fadeout {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
@keyframes scalein {
0% {
opacity: 0;
transform: scaleY(0.8);
transition: transform 0.12s cubic-bezier(0, 0, 0.2, 1), opacity 0.12s cubic-bezier(0, 0, 0.2, 1);
}
100% {
opacity: 1;
transform: scaleY(1);
}
0% {
opacity: 0;
transform: scaleY(0.8);
transition:
transform 0.12s cubic-bezier(0, 0, 0.2, 1),
opacity 0.12s cubic-bezier(0, 0, 0.2, 1);
}
100% {
opacity: 1;
transform: scaleY(1);
}
}

View File

@ -1,55 +1,55 @@
<script setup>
import { useLayout } from "../composables/useLayout";
import { onMounted } from 'vue'
import { useLayout } from '../composables/useLayout'
import { onMounted } from 'vue'
const { primaryColors, surfaces, primary, surface, isDarkMode, updateColors, toggleDarkMode } = useLayout();
const { primaryColors, surfaces, primary, surface, isDarkMode, updateColors, toggleDarkMode } =
useLayout()
onMounted(() => {
onMounted(() => {
toggleDarkMode()
})
})
</script>
<template>
<div class="config-panel hidden">
<div class="config-section">
<div>
<span class="config-label">Primary</span>
<div class="config-colors">
<button
v-for="pc of primaryColors"
:key="pc.name"
type="button"
:title="pc.name"
:class="['color-button', { selected: primary === pc.name }]"
:style="{ backgroundColor: pc.palette['500'] }"
@click="updateColors('primary', pc.name)"
/>
</div>
</div>
<div>
<span class="config-label">Surface</span>
<div class="config-colors">
<button
v-for="s of surfaces"
:key="s.name"
type="button"
:title="s.name"
:class="[
'color-button',
{
selected: surface
? surface === s.name
: isDarkMode
? s.name === 'zinc'
: s.name === 'slate',
},
]"
:style="{ backgroundColor: s.palette['500'] }"
@click="updateColors('surface', s.name)"
/>
</div>
</div>
<div class="config-panel hidden">
<div class="config-section">
<div>
<span class="config-label">Primary</span>
<div class="config-colors">
<button
v-for="pc of primaryColors"
:key="pc.name"
type="button"
:title="pc.name"
:class="['color-button', { selected: primary === pc.name }]"
:style="{ backgroundColor: pc.palette['500'] }"
@click="updateColors('primary', pc.name)"
/>
</div>
</div>
<div>
<span class="config-label">Surface</span>
<div class="config-colors">
<button
v-for="s of surfaces"
:key="s.name"
type="button"
:title="s.name"
:class="[
'color-button',
{
selected: surface
? surface === s.name
: isDarkMode
? s.name === 'zinc'
: s.name === 'slate',
},
]"
:style="{ backgroundColor: s.palette['500'] }"
@click="updateColors('surface', s.name)"
/>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,19 +1,19 @@
<template>
<div class="footer">
<div class="footer-container">
<div class="footer-copyright">Jono made this by blatantly copy-pasting from the example projects and vibe-coding the rest</div>
<div class="footer-links">
<a
href="https://git.jonotargett.com/jono/mqttdevicemanager"
target="_blank"
rel="noopener noreferrer"
class="footer-link"
>
<i class="pi pi-github footer-icon"> gitea</i>
</a>
</div>
</div>
<div class="footer">
<div class="footer-container">
<div class="footer-copyright">
Jono made this by blatantly copy-pasting from the example projects and vibe-coding the rest
</div>
<div class="footer-links">
<a
href="https://git.jonotargett.com/jono/mqttdevicemanager"
target="_blank"
rel="noopener noreferrer"
class="footer-link"
>
<i class="pi pi-github footer-icon"> gitea</i>
</a>
</div>
</div>
</div>
</template>

View File

@ -1,48 +1,65 @@
<script setup>
import { useLayout } from "../composables/useLayout";
import AppConfig from "./AppConfig.vue";
import { useLayout } from '../composables/useLayout'
import AppConfig from './AppConfig.vue'
const { isDarkMode, toggleDarkMode } = useLayout();
const { isDarkMode, toggleDarkMode } = useLayout()
</script>
<template>
<div class="topbar">
<div class="topbar-container">
<div class="topbar-brand">
<!-- Stolen from logoipsum -->
<svg width="59" height="36" viewBox="0 0 59 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.0898 18C20.0898 19.1046 19.1944 20 18.0898 20C16.9853 20 16.0898 19.1046 16.0898 18C16.0898 16.8954 16.9853 16 18.0898 16C19.1944 16 20.0898 16.8954 20.0898 18Z" class="fill-surface"></path>
<path d="M42.0898 18C42.0898 19.1046 41.1944 20 40.0898 20C38.9853 20 38.0898 19.1046 38.0898 18C38.0898 16.8954 38.9853 16 40.0898 16C41.1944 16 42.0898 16.8954 42.0898 18Z" class="fill-surface"></path>
<path class="fill-primary" fill-rule="evenodd" clip-rule="evenodd" d="M40.0899 2.0365e-06C40.7659 2.1547e-06 41.4332 0.0372686 42.0898 0.109859V2.12379C41.4347 2.04209 40.7672 2 40.0899 2C36.5517 2 33.2835 3.14743 30.6345 5.09135C31.1162 5.55951 31.5717 6.0544 31.9988 6.57362C34.2835 4.95312 37.0764 4 40.0899 4C40.7689 4 41.4367 4.04834 42.0898 4.14177V6.16591C41.4394 6.0568 40.7713 6 40.0899 6C37.5171 6 35.1352 6.80868 33.1819 8.18644C33.547 8.74673 33.8817 9.3286 34.1837 9.92973C35.8384 8.71698 37.8811 8 40.0899 8C40.7748 8 41.4436 8.06886 42.0898 8.20003V10.252C41.4506 10.0875 40.7805 10 40.0899 10C38.1572 10 36.386 10.6843 35.0029 11.8253C35.2578 12.5233 35.4707 13.2415 35.6386 13.9768C36.7365 12.7628 38.3241 12 40.0899 12C40.7911 12 41.4643 12.1203 42.0898 12.3414V14.5351C41.5015 14.1948 40.8185 14 40.0899 14C37.8808 14 36.0899 15.7909 36.0899 18C36.0899 20.2091 37.8808 22 40.0899 22C42.299 22 44.0899 20.2091 44.0899 18L44.0898 17.9791V0.446033C52.1058 2.26495 58.0899 9.43365 58.0899 18C58.0899 27.9411 50.031 36 40.0899 36C35.9474 36 32.1317 34.6006 29.0898 32.2488C26.0482 34.6002 22.2313 36 18.0898 36C17.4138 36 16.7465 35.9627 16.0898 35.8901V33.8762C16.745 33.9579 17.4125 34 18.0898 34C21.6281 34 24.8962 32.8525 27.5452 30.9086C27.0636 30.4405 26.6081 29.9456 26.181 29.4264C23.8963 31.0469 21.1033 32 18.0899 32C17.4108 32 16.7431 31.9517 16.0898 31.8582V29.8341C16.7403 29.9432 17.4085 30 18.0899 30C20.6628 30 23.0446 29.1913 24.9979 27.8136C24.6328 27.2533 24.2981 26.6714 23.996 26.0703C22.3414 27.283 20.2987 28 18.0899 28C17.405 28 16.7361 27.9311 16.0898 27.7999V25.7479C16.7291 25.9125 17.3993 26 18.0899 26C20.0226 26 21.7938 25.3157 23.1769 24.1747C22.922 23.4767 22.7091 22.7585 22.5412 22.0232C21.4433 23.2372 19.8557 24 18.0899 24C17.3886 24 16.7154 23.8797 16.0898 23.6586V21.4648C16.6782 21.8052 17.3613 22 18.0899 22C20.299 22 22.0899 20.2091 22.0899 18C22.0899 15.7909 20.299 14 18.0899 14C15.8808 14 14.0899 15.7909 14.0899 18L14.0898 35.554C6.07389 33.7351 0.0898438 26.5663 0.0898438 18C0.0898438 8.05887 8.14877 0 18.0899 0C22.2324 0 26.048 1.39934 29.0899 3.75111C32.1316 1.3998 35.9483 0 40.0899 2.0365e-06ZM46.0898 3.16303V5.34723C50.8198 7.59414 54.0899 12.4152 54.0899 18C54.0899 25.732 47.8219 32 40.0899 32C32.3579 32 26.0899 25.732 26.0899 18C26.0899 13.5817 22.5082 10 18.0899 10C13.6716 10 10.0899 13.5817 10.0899 18C10.0899 20.0289 10.8452 21.8814 12.0899 23.2916V18C12.0899 14.6863 14.7762 12 18.0899 12C21.4036 12 24.0899 14.6863 24.0899 18C24.0899 26.8366 31.2533 34 40.0899 34C48.9265 34 56.0898 26.8366 56.0898 18C56.0898 11.2852 51.9535 5.53658 46.0898 3.16303ZM12.0899 26.0007L12.0898 28.3946C8.50306 26.3197 6.0899 22.4417 6.0899 18C6.0899 11.3726 11.4625 6 18.0899 6C24.7173 6 30.0899 11.3726 30.0899 18C30.0899 23.5228 34.5671 28 40.0899 28C45.6127 28 50.0899 23.5228 50.0899 18C50.0899 14.7284 48.5188 11.8237 46.0899 9.99929V7.60538C49.6767 9.68023 52.0899 13.5583 52.0899 18C52.0899 24.6274 46.7173 30 40.0899 30C33.4625 30 28.0899 24.6274 28.0899 18C28.0899 12.4772 23.6127 8 18.0899 8C12.567 8 8.08992 12.4772 8.08992 18C8.08992 21.2716 9.66101 24.1763 12.0899 26.0007ZM4.0899 18C4.0899 23.5848 7.35998 28.4058 12.0898 30.6527V32.837C6.2262 30.4634 2.08989 24.7148 2.0899 18C2.0899 9.16344 9.25334 2 18.0899 2C26.9265 2 34.0899 9.16344 34.0899 18C34.0899 21.3137 36.7762 24 40.0899 24C43.4036 24 46.0899 21.3137 46.0899 18L46.0899 12.7084C47.3346 14.1186 48.0899 15.9711 48.0899 18C48.0899 22.4183 44.5082 26 40.0899 26C35.6716 26 32.0899 22.4183 32.0899 18C32.0899 10.268 25.8219 4 18.0899 4C10.3579 4 4.0899 10.268 4.0899 18Z"></path>
</svg>
<div class="topbar">
<div class="topbar-container">
<div class="topbar-brand">
<!-- Stolen from logoipsum -->
<svg
width="59"
height="36"
viewBox="0 0 59 36"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.0898 18C20.0898 19.1046 19.1944 20 18.0898 20C16.9853 20 16.0898 19.1046 16.0898 18C16.0898 16.8954 16.9853 16 18.0898 16C19.1944 16 20.0898 16.8954 20.0898 18Z"
class="fill-surface"
></path>
<path
d="M42.0898 18C42.0898 19.1046 41.1944 20 40.0898 20C38.9853 20 38.0898 19.1046 38.0898 18C38.0898 16.8954 38.9853 16 40.0898 16C41.1944 16 42.0898 16.8954 42.0898 18Z"
class="fill-surface"
></path>
<path
class="fill-primary"
fill-rule="evenodd"
clip-rule="evenodd"
d="M40.0899 2.0365e-06C40.7659 2.1547e-06 41.4332 0.0372686 42.0898 0.109859V2.12379C41.4347 2.04209 40.7672 2 40.0899 2C36.5517 2 33.2835 3.14743 30.6345 5.09135C31.1162 5.55951 31.5717 6.0544 31.9988 6.57362C34.2835 4.95312 37.0764 4 40.0899 4C40.7689 4 41.4367 4.04834 42.0898 4.14177V6.16591C41.4394 6.0568 40.7713 6 40.0899 6C37.5171 6 35.1352 6.80868 33.1819 8.18644C33.547 8.74673 33.8817 9.3286 34.1837 9.92973C35.8384 8.71698 37.8811 8 40.0899 8C40.7748 8 41.4436 8.06886 42.0898 8.20003V10.252C41.4506 10.0875 40.7805 10 40.0899 10C38.1572 10 36.386 10.6843 35.0029 11.8253C35.2578 12.5233 35.4707 13.2415 35.6386 13.9768C36.7365 12.7628 38.3241 12 40.0899 12C40.7911 12 41.4643 12.1203 42.0898 12.3414V14.5351C41.5015 14.1948 40.8185 14 40.0899 14C37.8808 14 36.0899 15.7909 36.0899 18C36.0899 20.2091 37.8808 22 40.0899 22C42.299 22 44.0899 20.2091 44.0899 18L44.0898 17.9791V0.446033C52.1058 2.26495 58.0899 9.43365 58.0899 18C58.0899 27.9411 50.031 36 40.0899 36C35.9474 36 32.1317 34.6006 29.0898 32.2488C26.0482 34.6002 22.2313 36 18.0898 36C17.4138 36 16.7465 35.9627 16.0898 35.8901V33.8762C16.745 33.9579 17.4125 34 18.0898 34C21.6281 34 24.8962 32.8525 27.5452 30.9086C27.0636 30.4405 26.6081 29.9456 26.181 29.4264C23.8963 31.0469 21.1033 32 18.0899 32C17.4108 32 16.7431 31.9517 16.0898 31.8582V29.8341C16.7403 29.9432 17.4085 30 18.0899 30C20.6628 30 23.0446 29.1913 24.9979 27.8136C24.6328 27.2533 24.2981 26.6714 23.996 26.0703C22.3414 27.283 20.2987 28 18.0899 28C17.405 28 16.7361 27.9311 16.0898 27.7999V25.7479C16.7291 25.9125 17.3993 26 18.0899 26C20.0226 26 21.7938 25.3157 23.1769 24.1747C22.922 23.4767 22.7091 22.7585 22.5412 22.0232C21.4433 23.2372 19.8557 24 18.0899 24C17.3886 24 16.7154 23.8797 16.0898 23.6586V21.4648C16.6782 21.8052 17.3613 22 18.0899 22C20.299 22 22.0899 20.2091 22.0899 18C22.0899 15.7909 20.299 14 18.0899 14C15.8808 14 14.0899 15.7909 14.0899 18L14.0898 35.554C6.07389 33.7351 0.0898438 26.5663 0.0898438 18C0.0898438 8.05887 8.14877 0 18.0899 0C22.2324 0 26.048 1.39934 29.0899 3.75111C32.1316 1.3998 35.9483 0 40.0899 2.0365e-06ZM46.0898 3.16303V5.34723C50.8198 7.59414 54.0899 12.4152 54.0899 18C54.0899 25.732 47.8219 32 40.0899 32C32.3579 32 26.0899 25.732 26.0899 18C26.0899 13.5817 22.5082 10 18.0899 10C13.6716 10 10.0899 13.5817 10.0899 18C10.0899 20.0289 10.8452 21.8814 12.0899 23.2916V18C12.0899 14.6863 14.7762 12 18.0899 12C21.4036 12 24.0899 14.6863 24.0899 18C24.0899 26.8366 31.2533 34 40.0899 34C48.9265 34 56.0898 26.8366 56.0898 18C56.0898 11.2852 51.9535 5.53658 46.0898 3.16303ZM12.0899 26.0007L12.0898 28.3946C8.50306 26.3197 6.0899 22.4417 6.0899 18C6.0899 11.3726 11.4625 6 18.0899 6C24.7173 6 30.0899 11.3726 30.0899 18C30.0899 23.5228 34.5671 28 40.0899 28C45.6127 28 50.0899 23.5228 50.0899 18C50.0899 14.7284 48.5188 11.8237 46.0899 9.99929V7.60538C49.6767 9.68023 52.0899 13.5583 52.0899 18C52.0899 24.6274 46.7173 30 40.0899 30C33.4625 30 28.0899 24.6274 28.0899 18C28.0899 12.4772 23.6127 8 18.0899 8C12.567 8 8.08992 12.4772 8.08992 18C8.08992 21.2716 9.66101 24.1763 12.0899 26.0007ZM4.0899 18C4.0899 23.5848 7.35998 28.4058 12.0898 30.6527V32.837C6.2262 30.4634 2.08989 24.7148 2.0899 18C2.0899 9.16344 9.25334 2 18.0899 2C26.9265 2 34.0899 9.16344 34.0899 18C34.0899 21.3137 36.7762 24 40.0899 24C43.4036 24 46.0899 21.3137 46.0899 18L46.0899 12.7084C47.3346 14.1186 48.0899 15.9711 48.0899 18C48.0899 22.4183 44.5082 26 40.0899 26C35.6716 26 32.0899 22.4183 32.0899 18C32.0899 10.268 25.8219 4 18.0899 4C10.3579 4 4.0899 10.268 4.0899 18Z"
></path>
</svg>
<span class="topbar-brand-text">
<span class="topbar-title">Device Management Console</span>
<span class="topbar-subtitle">Shitty Edition</span>
</span>
</div>
<div class="topbar-actions">
<Button type="button" class="topbar-theme-button" @click="toggleDarkMode" text rounded>
<i :class="['pi ', 'pi ', { 'pi-moon': isDarkMode, 'pi-sun': !isDarkMode }]" />
</Button>
<div class="relative">
<Button
v-styleclass="{
selector: '@next',
enterFromClass: 'hidden',
enterActiveClass: 'animate-scalein',
leaveToClass: 'hidden',
leaveActiveClass: 'animate-fadeout',
hideOnOutsideClick: true,
}"
icon="pi pi-palette"
text
rounded
aria-label="Settings"
/>
<AppConfig />
</div>
</div>
<span class="topbar-brand-text">
<span class="topbar-title">Device Management Console</span>
<span class="topbar-subtitle">Shitty Edition</span>
</span>
</div>
<div class="topbar-actions">
<Button type="button" class="topbar-theme-button" @click="toggleDarkMode" text rounded>
<i :class="['pi ', 'pi ', { 'pi-moon': isDarkMode, 'pi-sun': !isDarkMode }]" />
</Button>
<div class="relative">
<Button
v-styleclass="{
selector: '@next',
enterFromClass: 'hidden',
enterActiveClass: 'animate-scalein',
leaveToClass: 'hidden',
leaveActiveClass: 'animate-fadeout',
hideOnOutsideClick: true,
}"
icon="pi pi-palette"
text
rounded
aria-label="Settings"
/>
<AppConfig />
</div>
</div>
</div>
</div>
</template>

View File

@ -1,264 +1,254 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import MQTTService from '../../services/mqtt.js'
import { ref, reactive, onMounted } from 'vue'
import MQTTService from '../../services/mqtt.js'
import Accordion from 'primevue/accordion';
import AccordionPanel from 'primevue/accordionpanel';
import AccordionHeader from 'primevue/accordionheader';
import AccordionContent from 'primevue/accordioncontent';
import Dialog from 'primevue/dialog'
import Accordion from 'primevue/accordion'
import AccordionPanel from 'primevue/accordionpanel'
import AccordionHeader from 'primevue/accordionheader'
import AccordionContent from 'primevue/accordioncontent'
import Dialog from 'primevue/dialog'
import { JsonForms } from "@jsonforms/vue";
import { createAjv } from '@jsonforms/core'
import { primeVueRenderers } from 'jsonforms-primevue'
import { JsonForms } from '@jsonforms/vue'
import { createAjv } from '@jsonforms/core'
import { primeVueRenderers } from 'jsonforms-primevue'
import { useToast } from 'primevue/usetoast'
import { useToast } from 'primevue/usetoast'
const toast = useToast()
const toast = useToast()
const renderers = Object.freeze([
...primeVueRenderers,
// here you can add custom renderers
]);
const renderers = Object.freeze([
...primeVueRenderers,
// here you can add custom renderers
])
const props = defineProps({
deviceId: String
})
const props = defineProps({
deviceId: String,
})
const mqtt2 = new MQTTService()
const mqtt2 = new MQTTService()
const filters = ref({})
const commands = ref([])
const commandMap = reactive({})
const commandByResponseId = reactive({})
const filters = ref({})
const commands = ref([])
const commandMap = reactive({})
const commandByResponseId = reactive({})
const defaultsAjv = createAjv({ useDefaults: true })
// Suppress Ajv unknown format warning by registering a no-op validator for "textarea"
defaultsAjv.addFormat('textarea', true)
const defaultsAjv = createAjv({ useDefaults: true })
// Suppress Ajv unknown format warning by registering a no-op validator for "textarea"
defaultsAjv.addFormat('textarea', true)
function rebuildList() {
commands.value = Object.values(commandMap)
}
function rebuildList() {
commands.value = Object.values(commandMap)
}
function updateCommand(device, name, field, value) {
if (!commandMap[name]) {
commandMap[name] = {
function updateCommand(device, name, field, value) {
if (!commandMap[name]) {
commandMap[name] = {
name,
description: "No description provided.",
description: 'No description provided.',
schema: {},
input: {},
response: null
}
response: null,
}
}
commandMap[name][field] = value
rebuildList()
}
commandMap[name][field] = value
rebuildList()
}
function sendCommand(cmd) {
cmd.responseId = null
cmd.response = {}
function sendCommand(cmd) {
cmd.responseId = null
cmd.response = {}
const topic = `device/${props.deviceId}/command/${cmd.name}`
var payload = JSON.stringify(cmd.input, null, 2)
const topic = `device/${props.deviceId}/command/${cmd.name}`
var payload = JSON.stringify(cmd.input, null, 2)
var responseId = generateId()
commandByResponseId[responseId] = cmd
var responseId = generateId()
commandByResponseId[responseId] = cmd
mqtt2.publish(topic, payload || '{}', {
qos: 1,
properties: {
responseTopic: `client/${mqtt2.clientId}/responses`,
correlationData: new TextEncoder().encode(responseId),
},
})
}
mqtt2.publish(topic, payload || '{}', {
qos: 1,
properties: {
responseTopic: `client/${mqtt2.clientId}/responses`,
correlationData: new TextEncoder().encode(responseId)
}
function generateId() {
return Math.random().toString(16).substr(2, 8)
}
const schemaDialog = reactive({
visible: false,
content: '',
})
}
function generateId() {
return Math.random().toString(16).substr(2, 8)
}
const schemaDialog = reactive({
visible: false,
content: ''
})
function showSchema(cmd) {
try {
schemaDialog.content = JSON.stringify(cmd.schema, null, 2)
} catch {
schemaDialog.content = cmd.schema || 'No schema available'
}
schemaDialog.visible = true
}
onMounted(() => {
mqtt2.subscribe('device/+/command/#', (payload, topic) => {
const parts = topic.split('/')
const device = parts[1]
if (device !== props.deviceId) return
const command = parts[3]
const field = parts[4]
if (field === '$description') {
updateCommand(device, command, 'description', payload)
function showSchema(cmd) {
try {
schemaDialog.content = JSON.stringify(cmd.schema, null, 2)
} catch {
schemaDialog.content = cmd.schema || 'No schema available'
}
if (field === '$schema') {
schemaDialog.visible = true
}
onMounted(() => {
mqtt2.subscribe('device/+/command/#', (payload, topic) => {
const parts = topic.split('/')
const device = parts[1]
if (device !== props.deviceId) return
const command = parts[3]
const field = parts[4]
if (field === '$description') {
updateCommand(device, command, 'description', payload)
}
if (field === '$schema') {
const schema = JSON.parse(payload)
updateCommand(device, command, 'schema', schema)
}
updateCommand(device, command, 'schema', schema)
}
})
mqtt2.subscribe(`client/${mqtt2.clientId}/responses`, (payload, topic) => {
let response = JSON.parse(payload)
const responseId = response.correlation
const cmd = commandByResponseId[responseId]
if (cmd) {
cmd.response = response
toast.add({
severity: response.success ? 'success' : 'error',
summary: cmd.name,
detail:
response.message !== 'None'
? response.message
: response.success
? 'Success'
: 'Failed',
life: 4000,
})
delete commandByResponseId[responseId]
}
})
})
mqtt2.subscribe(`client/${mqtt2.clientId}/responses`, (payload, topic) => {
let response = JSON.parse(payload)
const responseId = response.correlation
const cmd = commandByResponseId[responseId]
if (cmd) {
cmd.response = response
toast.add({
severity: response.success ? 'success' : 'error',
summary: cmd.name,
detail: response.message !== 'None' ? response.message : (response.success ? 'Success' : 'Failed'),
life: 4000
})
delete commandByResponseId[responseId]
}
})
})
const jsonFormsConfig = {
const jsonFormsConfig = {
validationMode: 'ValidateOnTouched',
showAllErrors: false,
showErrorsOnTouched: true,
}
}
</script>
<template>
<Dialog
v-model:visible="schemaDialog.visible"
header="Command Schema"
:modal="true"
:closable="true"
:style="{width: '700px'}"
>
<pre>{{ schemaDialog.content }}</pre>
</Dialog>
<Dialog
v-model:visible="schemaDialog.visible"
header="Command Schema"
:modal="true"
:closable="true"
:style="{ width: '700px' }"
>
<pre>{{ schemaDialog.content }}</pre>
</Dialog>
<div class="layout-card col-item-2">
<DataView :value="commands">
<template #header>
<div class="flex products-header">
<span class="products-title">Commands</span>
<IconField class="search-field">
<InputIcon class="pi pi-search" />
<InputText v-model="filters['global']" placeholder="Filter by..." />
</IconField>
</div>
</template>
<template #header>
<div class="flex products-header">
<span class="products-title">Commands</span>
<IconField class="search-field">
<InputIcon class="pi pi-search" />
<InputText v-model="filters['global']" placeholder="Filter by..." />
</IconField>
</div>
</template>
<template #empty> No commands available.</template>
<template #list="slotProps">
<Accordion >
<AccordionPanel
v-for="cmd in slotProps.items"
:key="cmd.name"
:value="cmd.name"
>
<Accordion>
<AccordionPanel v-for="cmd in slotProps.items" :key="cmd.name" :value="cmd.name">
<AccordionHeader>
<div class="command-header">
<div class="command-header">
<span class="command-name">{{ cmd.name }}</span>
<span class="command-desc">{{ cmd.description }}</span>
</div>
</div>
</AccordionHeader>
<AccordionContent>
<div class="command-content">
<div class="command-content">
<!-- Left: Code editor -->
<JsonForms
:data="cmd.input"
:schema="cmd.schema"
:renderers="renderers"
:config="jsonFormsConfig"
:onChange="({ data, errors }) => cmd.input = data"
:ajv="defaultsAjv"
:data="cmd.input"
:schema="cmd.schema"
:renderers="renderers"
:config="jsonFormsConfig"
:onChange="({ data, errors }) => (cmd.input = data)"
:ajv="defaultsAjv"
/>
<!-- Right: Buttons -->
<div class="button-container">
<Button
v-if="cmd.response"
:icon="cmd.response.success ? 'pi pi-check' : 'pi pi-times'"
class="p-button-rounded p-button-text"
:class="cmd.response.success ? 'p-button-success' : 'p-button-danger'"
v-tooltip="cmd.response.message !== 'None' ? cmd.response.message : null"
/>
<Button
<Button
v-if="cmd.response"
:icon="cmd.response.success ? 'pi pi-check' : 'pi pi-times'"
class="p-button-rounded p-button-text"
:class="cmd.response.success ? 'p-button-success' : 'p-button-danger'"
v-tooltip="cmd.response.message !== 'None' ? cmd.response.message : null"
/>
<Button
icon="pi pi-question-circle"
class="p-mb-2"
label="Help"
severity="secondary"
@click="showSchema(cmd)"
tooltip="Show schema"
/>
<Button
/>
<Button
icon="pi pi-send"
label="Send"
@click="sendCommand(cmd)"
tooltip="Send command"
/>
</div>
/>
</div>
</div>
</AccordionContent>
</AccordionPanel>
</AccordionPanel>
</Accordion>
</template>
</template>
</DataView>
</div>
</div>
</template>
<style scoped>
.command-header {
display: flex;
gap: 10px;
align-items: center;
}
.command-header {
display: flex;
gap: 10px;
align-items: center;
}
.command-name {
font-weight: 600;
}
.command-name {
font-weight: 600;
}
.command-desc {
font-size: 0.8rem;
}
.command-desc {
font-size: 0.8rem;
}
.command-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.editor-container {
flex: 1;
}
.command-content {
display: flex;
flex-direction: column;
gap: 1rem;
}
.editor-container {
flex: 1;
}
.button-container {
display: flex;
flex-direction: row;
gap: 0.5rem;
justify-content: flex-end;
}
</style>
.button-container {
display: flex;
flex-direction: row;
gap: 0.5rem;
justify-content: flex-end;
}
</style>

View File

@ -1,48 +1,48 @@
<script setup>
import { ref, onMounted } from 'vue'
import MQTTService from '../../services/mqtt.js'
import { ref, onMounted } from 'vue'
import MQTTService from '../../services/mqtt.js'
const emit = defineEmits(['select'])
const emit = defineEmits(['select'])
const mqtt = new MQTTService()
const mqtt = new MQTTService()
const devices = ref([])
const deviceSet = new Set()
const selected = ref(null)
const devices = ref([])
const deviceSet = new Set()
const selected = ref(null)
function upsertDevice(device, status) {
const existing = devices.value.find(d => d.id === device)
function upsertDevice(device, status) {
const existing = devices.value.find((d) => d.id === device)
if (existing) {
// update reactively
existing.value = status
} else {
deviceSet.add(device)
if (existing) {
// update reactively
existing.value = status
} else {
deviceSet.add(device)
devices.value = [
...devices.value,
{
id: device,
title: device,
value: status,
subtitle: 'MQTT Device',
icon: status === "ONLINE" ? 'pi-check-circle' : 'pi-times-circle'
}
]
devices.value = [
...devices.value,
{
id: device,
title: device,
value: status,
subtitle: 'MQTT Device',
icon: status === 'ONLINE' ? 'pi-check-circle' : 'pi-times-circle',
},
]
}
}
function selectDevice(device) {
selected.value = device.id
emit('select', device.id)
}
}
function selectDevice(device) {
selected.value = device.id
emit('select', device.id)
}
onMounted(() => {
mqtt.subscribe('device/+/property/status', (payload, topic) => {
const parts = topic.split('/')
const device = parts[1]
upsertDevice(device, payload)
onMounted(() => {
mqtt.subscribe('device/+/property/status', (payload, topic) => {
const parts = topic.split('/')
const device = parts[1]
upsertDevice(device, payload)
})
})
})
</script>
<template>
@ -57,11 +57,13 @@ onMounted(() => {
<div class="stats-header">
<span class="stats-title">{{ device.title }}</span>
<span class="stats-icon-box">
<i :class="[
'pi',
device.value === 'ONLINE' ? 'pi-check-circle' : 'pi-times-circle',
device.value === 'ONLINE' ? '' : 'text-red-500'
]"></i>
<i
:class="[
'pi',
device.value === 'ONLINE' ? 'pi-check-circle' : 'pi-times-circle',
device.value === 'ONLINE' ? '' : 'text-red-500',
]"
></i>
</span>
</div>
@ -74,18 +76,18 @@ onMounted(() => {
</template>
<style scoped>
.device-card {
cursor: pointer;
transition: all 0.2s ease;
}
.device-card {
cursor: pointer;
transition: all 0.2s ease;
}
.device-card:hover {
transform: translateY(-2px);
}
.device-card:hover {
transform: translateY(-2px);
}
/* 🔥 selected state */
.selected-device {
border: 2px solid var(--p-primary-color);
box-shadow: 0 0 8px var(--p-primary-color);
}
</style>
/* 🔥 selected state */
.selected-device {
border: 2px solid var(--p-primary-color);
box-shadow: 0 0 8px var(--p-primary-color);
}
</style>

View File

@ -1,103 +1,107 @@
<script setup>
import { ref, watch, onMounted } from "vue";
import { ref, watch, onMounted } from 'vue'
const products = ref([
const products = ref([
{
name: "Laptop Pro",
category: "Electronics",
price: 2499,
status: "In Stock",
name: 'Laptop Pro',
category: 'Electronics',
price: 2499,
status: 'In Stock',
},
{
name: "Wireless Mouse",
category: "Accessories",
price: 49,
status: "Low Stock",
name: 'Wireless Mouse',
category: 'Accessories',
price: 49,
status: 'Low Stock',
},
{
name: "Monitor 4K",
category: "Electronics",
price: 699,
status: "Out of Stock",
name: 'Monitor 4K',
category: 'Electronics',
price: 699,
status: 'Out of Stock',
},
{ name: "Keyboard", category: "Accessories", price: 149, status: "In Stock" },
]);
{ name: 'Keyboard', category: 'Accessories', price: 149, status: 'In Stock' },
])
const selectedProduct = ref(null);
const searchQuery = ref("");
const loading = ref(false);
const filteredProducts = ref([]);
const selectedProduct = ref(null)
const searchQuery = ref('')
const loading = ref(false)
const filteredProducts = ref([])
const searchProducts = () => {
loading.value = true;
const searchProducts = () => {
loading.value = true
filteredProducts.value = products.value.filter(
(product) =>
product.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
product.category.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
product.status.toLowerCase().includes(searchQuery.value.toLowerCase())
);
(product) =>
product.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
product.category.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
product.status.toLowerCase().includes(searchQuery.value.toLowerCase()),
)
setTimeout(() => {
loading.value = false;
}, 300);
};
loading.value = false
}, 300)
}
watch(searchQuery, () => {
searchProducts();
});
watch(searchQuery, () => {
searchProducts()
})
onMounted(() => {
filteredProducts.value = [...products.value];
});
onMounted(() => {
filteredProducts.value = [...products.value]
})
</script>
<template>
<div class="layout-card">
<div class="products-header">
<span class="products-title">Products Overview</span>
<IconField class="search-field">
<InputIcon class="pi pi-search" />
<InputText
v-model="searchQuery"
placeholder="Search products..."
class="products-search"
@keyup.enter="searchProducts"
/>
</IconField>
</div>
<div class="products-table-container">
<DataTable
:value="filteredProducts"
v-model:selection="selectedProduct"
selectionMode="single"
:loading="loading"
:rows="5"
class="products-table"
:pt="{
mask: {
class: 'products-table-mask',
},
loadingIcon: {
class: 'products-table-loading',
},
}"
>
<Column field="name" header="Name" sortable></Column>
<Column field="category" header="Category" sortable></Column>
<Column field="price" header="Price" sortable>
<template #body="{ data }"> ${{ data.price }} </template>
</Column>
<Column field="status" header="Status">
<template #body="{ data }">
<Tag
:severity="
data.status === 'In Stock' ? 'success' : data.status === 'Low Stock' ? 'warn' : 'danger'
"
>
{{ data.status }}
</Tag>
</template>
</Column>
</DataTable>
</div>
<div class="layout-card">
<div class="products-header">
<span class="products-title">Products Overview</span>
<IconField class="search-field">
<InputIcon class="pi pi-search" />
<InputText
v-model="searchQuery"
placeholder="Search products..."
class="products-search"
@keyup.enter="searchProducts"
/>
</IconField>
</div>
<div class="products-table-container">
<DataTable
:value="filteredProducts"
v-model:selection="selectedProduct"
selectionMode="single"
:loading="loading"
:rows="5"
class="products-table"
:pt="{
mask: {
class: 'products-table-mask',
},
loadingIcon: {
class: 'products-table-loading',
},
}"
>
<Column field="name" header="Name" sortable></Column>
<Column field="category" header="Category" sortable></Column>
<Column field="price" header="Price" sortable>
<template #body="{ data }"> ${{ data.price }} </template>
</Column>
<Column field="status" header="Status">
<template #body="{ data }">
<Tag
:severity="
data.status === 'In Stock'
? 'success'
: data.status === 'Low Stock'
? 'warn'
: 'danger'
"
>
{{ data.status }}
</Tag>
</template>
</Column>
</DataTable>
</div>
</div>
</template>

View File

@ -1,142 +1,144 @@
<script setup>
import { ref, watch, onMounted } from 'vue'
import MQTTService from '../../services/mqtt.js'
import { ref, watch, onMounted } from 'vue'
import MQTTService from '../../services/mqtt.js'
const props = defineProps({
deviceId: String
})
const nodes = ref([])
const filters = ref({})
const filterMode = ref({ label: 'Lenient', value: 'lenient' });
const mqtt = new MQTTService()
const changedKeys = ref({})
let propertyTree = {}
function buildNodes(obj, path='') {
return Object.entries(obj).map(([key, val]) => {
const fullKey = path ? `${path}/${key}` : key
const hasChildren = Object.keys(val).some(k => k !== 'value' && k !== '_value')
return {
key: fullKey,
data: {
name: key,
value: val.value ?? val._value
},
children: hasChildren ? buildNodes(
Object.fromEntries(
Object.entries(val).filter(([k]) => k !== 'value' && k !== '_value')
),
fullKey
) : undefined
}
})
}
function insertProperty(pathParts, value) {
let node = propertyTree
const fullKey = pathParts.join('/')
pathParts.forEach((part, i) => {
const isLeaf = i === pathParts.length - 1
if (!node[part]) {
node[part] = {}
}
// If this node used to be a leaf, convert it into a branch
if (node[part].value !== undefined && !isLeaf) {
node[part] = { _value: node[part].value }
}
if (isLeaf) {
node[part].value = value
} else {
node = node[part]
}
const props = defineProps({
deviceId: String,
})
// flash logic
changedKeys.value[fullKey] = true
setTimeout(() => {
delete changedKeys.value[fullKey]
}, 600)
const nodes = ref([])
const filters = ref({})
const filterMode = ref({ label: 'Lenient', value: 'lenient' })
nodes.value = buildNodes(propertyTree)
}
const mqtt = new MQTTService()
watch(() => props.deviceId, () => {
propertyTree = {}
nodes.value = []
})
const changedKeys = ref({})
let propertyTree = {}
onMounted(() => {
mqtt.subscribe('device/+/property/#', (payload, topic) => {
const parts = topic.split('/')
function buildNodes(obj, path = '') {
return Object.entries(obj).map(([key, val]) => {
const fullKey = path ? `${path}/${key}` : key
const device = parts[1]
const propertyPath = parts.slice(3)
const hasChildren = Object.keys(val).some((k) => k !== 'value' && k !== '_value')
if (device !== props.deviceId) return
return {
key: fullKey,
data: {
name: key,
value: val.value ?? val._value,
},
children: hasChildren
? buildNodes(
Object.fromEntries(
Object.entries(val).filter(([k]) => k !== 'value' && k !== '_value'),
),
fullKey,
)
: undefined,
}
})
}
insertProperty(propertyPath, payload)
function insertProperty(pathParts, value) {
let node = propertyTree
const fullKey = pathParts.join('/')
pathParts.forEach((part, i) => {
const isLeaf = i === pathParts.length - 1
if (!node[part]) {
node[part] = {}
}
// If this node used to be a leaf, convert it into a branch
if (node[part].value !== undefined && !isLeaf) {
node[part] = { _value: node[part].value }
}
if (isLeaf) {
node[part].value = value
} else {
node = node[part]
}
})
// flash logic
changedKeys.value[fullKey] = true
setTimeout(() => {
delete changedKeys.value[fullKey]
}, 600)
nodes.value = buildNodes(propertyTree)
}
watch(
() => props.deviceId,
() => {
propertyTree = {}
nodes.value = []
},
)
onMounted(() => {
mqtt.subscribe('device/+/property/#', (payload, topic) => {
const parts = topic.split('/')
const device = parts[1]
const propertyPath = parts.slice(3)
if (device !== props.deviceId) return
insertProperty(propertyPath, payload)
})
})
})
</script>
<template>
<div class="layout-card col-item-2">
<TreeTable :value="nodes" :filters="filters" :filterMode="filterMode.value">
<template #header>
<div class="flex products-header">
<span class="products-title">Properties</span>
<IconField class="search-field">
<InputIcon class="pi pi-search" />
<InputText v-model="filters['global']" placeholder="Filter by..." />
</IconField>
</div>
</template>
<template #empty> No properties available.</template>
<Column field="name" header="Name" expander style="min-width: 12rem">
<template #filter>
<InputText v-model="filters['name']" type="text" placeholder="Filter by name" />
</template>
</Column>
<Column field="value" header="Value" style="min-width: 12rem">
<template #filter>
<InputText v-model="filters['value']" type="text" placeholder="Filter by value" />
</template>
<template #body="{ node }">
<span :class="{ 'flash-cell': changedKeys[node.key] }">
{{ node.data.value }}
</span>
</template>
</Column>
</TreeTable>
</div>
<div class="layout-card col-item-2">
<TreeTable :value="nodes" :filters="filters" :filterMode="filterMode.value">
<template #header>
<div class="flex products-header">
<span class="products-title">Properties</span>
<IconField class="search-field">
<InputIcon class="pi pi-search" />
<InputText v-model="filters['global']" placeholder="Filter by..." />
</IconField>
</div>
</template>
<template #empty> No properties available.</template>
<Column field="name" header="Name" expander style="min-width: 12rem">
<!-- <template #filter>
<InputText v-model="filters['name']" type="text" placeholder="Filter by name" />
</template> -->
</Column>
<Column field="value" header="Value" style="min-width: 12rem">
<!-- <template #filter>
<InputText v-model="filters['value']" type="text" placeholder="Filter by value" />
</template> -->
<template #body="{ node }">
<span :class="{ 'flash-cell': changedKeys[node.key] }">
{{ node.data.value }}
</span>
</template>
</Column>
</TreeTable>
</div>
</template>
<style scoped>
.flash-cell {
animation: flash-bg 0.6s ease;
border-radius: 6px;
}
.flash-cell {
animation: flash-bg 0.6s ease;
border-radius: 6px;
}
@keyframes flash-bg {
0% {
background-color: var(--p-primary-color);
color: var(--p-primary-color-text);
@keyframes flash-bg {
0% {
background-color: var(--p-primary-color);
color: var(--p-primary-color-text);
}
100% {
background-color: transparent;
color: inherit;
}
}
100% {
background-color: transparent;
color: inherit;
}
}
</style>
</style>

View File

@ -1,43 +1,43 @@
<script setup>
const activities = [
const activities = [
{
icon: "pi-shopping-cart",
text: "New order #1123",
time: "2 minutes ago",
color: "pink",
icon: 'pi-shopping-cart',
text: 'New order #1123',
time: '2 minutes ago',
color: 'pink',
},
{
icon: "pi-user-plus",
text: "New customer registered",
time: "15 minutes ago",
color: "green",
icon: 'pi-user-plus',
text: 'New customer registered',
time: '15 minutes ago',
color: 'green',
},
{
icon: "pi-check-circle",
text: "Payment processed",
time: "25 minutes ago",
color: "blue",
icon: 'pi-check-circle',
text: 'Payment processed',
time: '25 minutes ago',
color: 'blue',
},
{
icon: "pi-inbox",
text: "Inventory updated",
time: "40 minutes ago",
color: "yellow",
icon: 'pi-inbox',
text: 'Inventory updated',
time: '40 minutes ago',
color: 'yellow',
},
];
]
</script>
<template>
<div class="layout-card col-item-2">
<span class="chart-title">Recent Activity</span>
<div class="activity-list">
<div v-for="(activity, index) in activities" :key="index" class="activity-item">
<i :class="['activity-icon', activity.color, 'pi', activity.icon]"></i>
<div class="activity-content">
<span class="activity-text">{{ activity.text }}</span>
<span class="activity-time">{{ activity.time }}</span>
</div>
</div>
<div class="layout-card col-item-2">
<span class="chart-title">Recent Activity</span>
<div class="activity-list">
<div v-for="(activity, index) in activities" :key="index" class="activity-item">
<i :class="['activity-icon', activity.color, 'pi', activity.icon]"></i>
<div class="activity-content">
<span class="activity-text">{{ activity.text }}</span>
<span class="activity-time">{{ activity.time }}</span>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,94 +1,94 @@
<script setup>
import { ref, watch, onMounted } from "vue";
import { useLayout } from "../../composables/useLayout";
import { ref, watch, onMounted } from 'vue'
import { useLayout } from '../../composables/useLayout'
const { primary, surface, isDarkMode } = useLayout();
const { primary, surface, isDarkMode } = useLayout()
const chartData = ref(null);
const chartOptions = ref(null);
const chartData = ref(null)
const chartOptions = ref(null)
function setChartData() {
const documentStyle = getComputedStyle(document.documentElement);
function setChartData() {
const documentStyle = getComputedStyle(document.documentElement)
return {
labels: ["Q1", "Q2", "Q3", "Q4"],
datasets: [
{
type: "bar",
label: "Subscriptions",
backgroundColor: documentStyle.getPropertyValue("--p-primary-400"),
data: [4000, 10000, 15000, 4000],
barThickness: 32,
},
{
type: "bar",
label: "Advertising",
backgroundColor: documentStyle.getPropertyValue("--p-primary-300"),
data: [2100, 8400, 2400, 7500],
barThickness: 32,
},
{
type: "bar",
label: "Affiliate",
backgroundColor: documentStyle.getPropertyValue("--p-primary-200"),
data: [4100, 5200, 3400, 7400],
borderRadius: {
topLeft: 8,
topRight: 8,
},
barThickness: 32,
},
],
};
}
labels: ['Q1', 'Q2', 'Q3', 'Q4'],
datasets: [
{
type: 'bar',
label: 'Subscriptions',
backgroundColor: documentStyle.getPropertyValue('--p-primary-400'),
data: [4000, 10000, 15000, 4000],
barThickness: 32,
},
{
type: 'bar',
label: 'Advertising',
backgroundColor: documentStyle.getPropertyValue('--p-primary-300'),
data: [2100, 8400, 2400, 7500],
barThickness: 32,
},
{
type: 'bar',
label: 'Affiliate',
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
data: [4100, 5200, 3400, 7400],
borderRadius: {
topLeft: 8,
topRight: 8,
},
barThickness: 32,
},
],
}
}
function setChartOptions() {
function setChartOptions() {
return {
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
position: "top",
},
maintainAspectRatio: false,
responsive: true,
plugins: {
legend: {
position: 'top',
},
scales: {
x: {
stacked: true,
grid: {
color: "transparent",
borderColor: "transparent",
},
},
y: {
stacked: true,
grid: {
color: "transparent",
borderColor: "transparent",
drawTicks: false,
},
},
},
scales: {
x: {
stacked: true,
grid: {
color: 'transparent',
borderColor: 'transparent',
},
},
};
}
y: {
stacked: true,
grid: {
color: 'transparent',
borderColor: 'transparent',
drawTicks: false,
},
},
},
}
}
watch([primary, surface, isDarkMode], () => {
chartData.value = setChartData();
chartOptions.value = setChartOptions();
});
watch([primary, surface, isDarkMode], () => {
chartData.value = setChartData()
chartOptions.value = setChartOptions()
})
onMounted(() => {
chartData.value = setChartData();
chartOptions.value = setChartOptions();
});
onMounted(() => {
chartData.value = setChartData()
chartOptions.value = setChartOptions()
})
</script>
<template>
<div class="layout-card col-item-2">
<div class="chart-header">
<span class="chart-title">Sales Trend</span>
</div>
<div class="chart-content">
<Chart type="bar" :data="chartData" :options="chartOptions" style="height: 300px" />
</div>
<div class="layout-card col-item-2">
<div class="chart-header">
<span class="chart-title">Sales Trend</span>
</div>
<div class="chart-content">
<Chart type="bar" :data="chartData" :options="chartOptions" style="height: 300px" />
</div>
</div>
</template>

View File

@ -1,45 +1,45 @@
<script setup>
const stats = [
const stats = [
{
title: "Total Orders",
icon: "pi-shopping-cart",
value: "1,234",
subtitle: "Last 7 days",
title: 'Total Orders',
icon: 'pi-shopping-cart',
value: '1,234',
subtitle: 'Last 7 days',
},
{
title: "Active Users",
icon: "pi-users",
value: "2,573",
subtitle: "Last 7 days",
title: 'Active Users',
icon: 'pi-users',
value: '2,573',
subtitle: 'Last 7 days',
},
{
title: "Revenue",
icon: "pi-dollar",
value: "$45,200",
subtitle: "Last 7 days",
title: 'Revenue',
icon: 'pi-dollar',
value: '$45,200',
subtitle: 'Last 7 days',
},
{
title: "Success Rate",
icon: "pi-chart-line",
value: "95%",
subtitle: "Last 7 days",
title: 'Success Rate',
icon: 'pi-chart-line',
value: '95%',
subtitle: 'Last 7 days',
},
];
]
</script>
<template>
<div class="stats">
<div v-for="(stat, index) in stats" :key="index" class="layout-card">
<div class="stats-header">
<span class="stats-title">{{ stat.title }}</span>
<span class="stats-icon-box">
<i :class="['pi', stat.icon]"></i>
</span>
</div>
<div class="stats-content">
<div class="stats-value">{{ stat.value }}</div>
<div class="stats-subtitle">{{ stat.subtitle }}</div>
</div>
</div>
<div class="stats">
<div v-for="(stat, index) in stats" :key="index" class="layout-card">
<div class="stats-header">
<span class="stats-title">{{ stat.title }}</span>
<span class="stats-icon-box">
<i :class="['pi', stat.icon]"></i>
</span>
</div>
<div class="stats-content">
<div class="stats-value">{{ stat.value }}</div>
<div class="stats-subtitle">{{ stat.subtitle }}</div>
</div>
</div>
</div>
</template>

View File

@ -1,459 +1,459 @@
import { updatePrimaryPalette, updateSurfacePalette } from "@primeuix/themes";
import { computed, ref } from "vue";
import { updatePrimaryPalette, updateSurfacePalette } from '@primeuix/themes'
import { computed, ref } from 'vue'
const appState = ref({
primary: "emerald",
surface: null,
darkMode: false
});
primary: 'emerald',
surface: null,
darkMode: false,
})
const primaryColors = ref([
{
name: "emerald",
palette: {
50: "#ecfdf5",
100: "#d1fae5",
200: "#a7f3d0",
300: "#6ee7b7",
400: "#34d399",
500: "#10b981",
600: "#059669",
700: "#047857",
800: "#065f46",
900: "#064e3b",
950: "#022c22"
}
},
{
name: "green",
palette: {
50: "#f0fdf4",
100: "#dcfce7",
200: "#bbf7d0",
300: "#86efac",
400: "#4ade80",
500: "#22c55e",
600: "#16a34a",
700: "#15803d",
800: "#166534",
900: "#14532d",
950: "#052e16"
}
},
{
name: "lime",
palette: {
50: "#f7fee7",
100: "#ecfccb",
200: "#d9f99d",
300: "#bef264",
400: "#a3e635",
500: "#84cc16",
600: "#65a30d",
700: "#4d7c0f",
800: "#3f6212",
900: "#365314",
950: "#1a2e05"
}
},
{
name: "orange",
palette: {
50: "#fff7ed",
100: "#ffedd5",
200: "#fed7aa",
300: "#fdba74",
400: "#fb923c",
500: "#f97316",
600: "#ea580c",
700: "#c2410c",
800: "#9a3412",
900: "#7c2d12",
950: "#431407"
}
},
{
name: "amber",
palette: {
50: "#fffbeb",
100: "#fef3c7",
200: "#fde68a",
300: "#fcd34d",
400: "#fbbf24",
500: "#f59e0b",
600: "#d97706",
700: "#b45309",
800: "#92400e",
900: "#78350f",
950: "#451a03"
}
},
{
name: "yellow",
palette: {
50: "#fefce8",
100: "#fef9c3",
200: "#fef08a",
300: "#fde047",
400: "#facc15",
500: "#eab308",
600: "#ca8a04",
700: "#a16207",
800: "#854d0e",
900: "#713f12",
950: "#422006"
}
},
{
name: "teal",
palette: {
50: "#f0fdfa",
100: "#ccfbf1",
200: "#99f6e4",
300: "#5eead4",
400: "#2dd4bf",
500: "#14b8a6",
600: "#0d9488",
700: "#0f766e",
800: "#115e59",
900: "#134e4a",
950: "#042f2e"
}
},
{
name: "cyan",
palette: {
50: "#ecfeff",
100: "#cffafe",
200: "#a5f3fc",
300: "#67e8f9",
400: "#22d3ee",
500: "#06b6d4",
600: "#0891b2",
700: "#0e7490",
800: "#155e75",
900: "#164e63",
950: "#083344"
}
},
{
name: "sky",
palette: {
50: "#f0f9ff",
100: "#e0f2fe",
200: "#bae6fd",
300: "#7dd3fc",
400: "#38bdf8",
500: "#0ea5e9",
600: "#0284c7",
700: "#0369a1",
800: "#075985",
900: "#0c4a6e",
950: "#082f49"
}
},
{
name: "blue",
palette: {
50: "#eff6ff",
100: "#dbeafe",
200: "#bfdbfe",
300: "#93c5fd",
400: "#60a5fa",
500: "#3b82f6",
600: "#2563eb",
700: "#1d4ed8",
800: "#1e40af",
900: "#1e3a8a",
950: "#172554"
}
},
{
name: "indigo",
palette: {
50: "#eef2ff",
100: "#e0e7ff",
200: "#c7d2fe",
300: "#a5b4fc",
400: "#818cf8",
500: "#6366f1",
600: "#4f46e5",
700: "#4338ca",
800: "#3730a3",
900: "#312e81",
950: "#1e1b4b"
}
},
{
name: "violet",
palette: {
50: "#f5f3ff",
100: "#ede9fe",
200: "#ddd6fe",
300: "#c4b5fd",
400: "#a78bfa",
500: "#8b5cf6",
600: "#7c3aed",
700: "#6d28d9",
800: "#5b21b6",
900: "#4c1d95",
950: "#2e1065"
}
},
{
name: "purple",
palette: {
50: "#faf5ff",
100: "#f3e8ff",
200: "#e9d5ff",
300: "#d8b4fe",
400: "#c084fc",
500: "#a855f7",
600: "#9333ea",
700: "#7e22ce",
800: "#6b21a8",
900: "#581c87",
950: "#3b0764"
}
},
{
name: "fuchsia",
palette: {
50: "#fdf4ff",
100: "#fae8ff",
200: "#f5d0fe",
300: "#f0abfc",
400: "#e879f9",
500: "#d946ef",
600: "#c026d3",
700: "#a21caf",
800: "#86198f",
900: "#701a75",
950: "#4a044e"
}
},
{
name: "pink",
palette: {
50: "#fdf2f8",
100: "#fce7f3",
200: "#fbcfe8",
300: "#f9a8d4",
400: "#f472b6",
500: "#ec4899",
600: "#db2777",
700: "#be185d",
800: "#9d174d",
900: "#831843",
950: "#500724"
}
},
{
name: "rose",
palette: {
50: "#fff1f2",
100: "#ffe4e6",
200: "#fecdd3",
300: "#fda4af",
400: "#fb7185",
500: "#f43f5e",
600: "#e11d48",
700: "#be123c",
800: "#9f1239",
900: "#881337",
950: "#4c0519"
}
}
]);
{
name: 'emerald',
palette: {
50: '#ecfdf5',
100: '#d1fae5',
200: '#a7f3d0',
300: '#6ee7b7',
400: '#34d399',
500: '#10b981',
600: '#059669',
700: '#047857',
800: '#065f46',
900: '#064e3b',
950: '#022c22',
},
},
{
name: 'green',
palette: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
},
{
name: 'lime',
palette: {
50: '#f7fee7',
100: '#ecfccb',
200: '#d9f99d',
300: '#bef264',
400: '#a3e635',
500: '#84cc16',
600: '#65a30d',
700: '#4d7c0f',
800: '#3f6212',
900: '#365314',
950: '#1a2e05',
},
},
{
name: 'orange',
palette: {
50: '#fff7ed',
100: '#ffedd5',
200: '#fed7aa',
300: '#fdba74',
400: '#fb923c',
500: '#f97316',
600: '#ea580c',
700: '#c2410c',
800: '#9a3412',
900: '#7c2d12',
950: '#431407',
},
},
{
name: 'amber',
palette: {
50: '#fffbeb',
100: '#fef3c7',
200: '#fde68a',
300: '#fcd34d',
400: '#fbbf24',
500: '#f59e0b',
600: '#d97706',
700: '#b45309',
800: '#92400e',
900: '#78350f',
950: '#451a03',
},
},
{
name: 'yellow',
palette: {
50: '#fefce8',
100: '#fef9c3',
200: '#fef08a',
300: '#fde047',
400: '#facc15',
500: '#eab308',
600: '#ca8a04',
700: '#a16207',
800: '#854d0e',
900: '#713f12',
950: '#422006',
},
},
{
name: 'teal',
palette: {
50: '#f0fdfa',
100: '#ccfbf1',
200: '#99f6e4',
300: '#5eead4',
400: '#2dd4bf',
500: '#14b8a6',
600: '#0d9488',
700: '#0f766e',
800: '#115e59',
900: '#134e4a',
950: '#042f2e',
},
},
{
name: 'cyan',
palette: {
50: '#ecfeff',
100: '#cffafe',
200: '#a5f3fc',
300: '#67e8f9',
400: '#22d3ee',
500: '#06b6d4',
600: '#0891b2',
700: '#0e7490',
800: '#155e75',
900: '#164e63',
950: '#083344',
},
},
{
name: 'sky',
palette: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
950: '#082f49',
},
},
{
name: 'blue',
palette: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
},
{
name: 'indigo',
palette: {
50: '#eef2ff',
100: '#e0e7ff',
200: '#c7d2fe',
300: '#a5b4fc',
400: '#818cf8',
500: '#6366f1',
600: '#4f46e5',
700: '#4338ca',
800: '#3730a3',
900: '#312e81',
950: '#1e1b4b',
},
},
{
name: 'violet',
palette: {
50: '#f5f3ff',
100: '#ede9fe',
200: '#ddd6fe',
300: '#c4b5fd',
400: '#a78bfa',
500: '#8b5cf6',
600: '#7c3aed',
700: '#6d28d9',
800: '#5b21b6',
900: '#4c1d95',
950: '#2e1065',
},
},
{
name: 'purple',
palette: {
50: '#faf5ff',
100: '#f3e8ff',
200: '#e9d5ff',
300: '#d8b4fe',
400: '#c084fc',
500: '#a855f7',
600: '#9333ea',
700: '#7e22ce',
800: '#6b21a8',
900: '#581c87',
950: '#3b0764',
},
},
{
name: 'fuchsia',
palette: {
50: '#fdf4ff',
100: '#fae8ff',
200: '#f5d0fe',
300: '#f0abfc',
400: '#e879f9',
500: '#d946ef',
600: '#c026d3',
700: '#a21caf',
800: '#86198f',
900: '#701a75',
950: '#4a044e',
},
},
{
name: 'pink',
palette: {
50: '#fdf2f8',
100: '#fce7f3',
200: '#fbcfe8',
300: '#f9a8d4',
400: '#f472b6',
500: '#ec4899',
600: '#db2777',
700: '#be185d',
800: '#9d174d',
900: '#831843',
950: '#500724',
},
},
{
name: 'rose',
palette: {
50: '#fff1f2',
100: '#ffe4e6',
200: '#fecdd3',
300: '#fda4af',
400: '#fb7185',
500: '#f43f5e',
600: '#e11d48',
700: '#be123c',
800: '#9f1239',
900: '#881337',
950: '#4c0519',
},
},
])
const surfaces = ref([
{
name: "slate",
palette: {
0: "#ffffff",
50: "#f8fafc",
100: "#f1f5f9",
200: "#e2e8f0",
300: "#cbd5e1",
400: "#94a3b8",
500: "#64748b",
600: "#475569",
700: "#334155",
800: "#1e293b",
900: "#0f172a",
950: "#020617"
}
},
{
name: "gray",
palette: {
0: "#ffffff",
50: "#f9fafb",
100: "#f3f4f6",
200: "#e5e7eb",
300: "#d1d5db",
400: "#9ca3af",
500: "#6b7280",
600: "#4b5563",
700: "#374151",
800: "#1f2937",
900: "#111827",
950: "#030712"
}
},
{
name: "zinc",
palette: {
0: "#ffffff",
50: "#fafafa",
100: "#f4f4f5",
200: "#e4e4e7",
300: "#d4d4d8",
400: "#a1a1aa",
500: "#71717a",
600: "#52525b",
700: "#3f3f46",
800: "#27272a",
900: "#18181b",
950: "#09090b"
}
},
{
name: "neutral",
palette: {
0: "#ffffff",
50: "#fafafa",
100: "#f5f5f5",
200: "#e5e5e5",
300: "#d4d4d4",
400: "#a3a3a3",
500: "#737373",
600: "#525252",
700: "#404040",
800: "#262626",
900: "#171717",
950: "#0a0a0a"
}
},
{
name: "stone",
palette: {
0: "#ffffff",
50: "#fafaf9",
100: "#f5f5f4",
200: "#e7e5e4",
300: "#d6d3d1",
400: "#a8a29e",
500: "#78716c",
600: "#57534e",
700: "#44403c",
800: "#292524",
900: "#1c1917",
950: "#0c0a09"
}
},
{
name: "soho",
palette: {
0: "#ffffff",
50: "#f4f4f4",
100: "#e8e9e9",
200: "#d2d2d4",
300: "#bbbcbe",
400: "#a5a5a9",
500: "#8e8f93",
600: "#77787d",
700: "#616268",
800: "#4a4b52",
900: "#34343d",
950: "#1d1e27"
}
},
{
name: "viva",
palette: {
0: "#ffffff",
50: "#f3f3f3",
100: "#e7e7e8",
200: "#cfd0d0",
300: "#b7b8b9",
400: "#9fa1a1",
500: "#87898a",
600: "#6e7173",
700: "#565a5b",
800: "#3e4244",
900: "#262b2c",
950: "#0e1315"
}
},
{
name: "ocean",
palette: {
0: "#ffffff",
50: "#fbfcfc",
100: "#F7F9F8",
200: "#EFF3F2",
300: "#DADEDD",
400: "#B1B7B6",
500: "#828787",
600: "#5F7274",
700: "#415B61",
800: "#29444E",
900: "#183240",
950: "#0c1920"
}
}
]);
{
name: 'slate',
palette: {
0: '#ffffff',
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
},
{
name: 'gray',
palette: {
0: '#ffffff',
50: '#f9fafb',
100: '#f3f4f6',
200: '#e5e7eb',
300: '#d1d5db',
400: '#9ca3af',
500: '#6b7280',
600: '#4b5563',
700: '#374151',
800: '#1f2937',
900: '#111827',
950: '#030712',
},
},
{
name: 'zinc',
palette: {
0: '#ffffff',
50: '#fafafa',
100: '#f4f4f5',
200: '#e4e4e7',
300: '#d4d4d8',
400: '#a1a1aa',
500: '#71717a',
600: '#52525b',
700: '#3f3f46',
800: '#27272a',
900: '#18181b',
950: '#09090b',
},
},
{
name: 'neutral',
palette: {
0: '#ffffff',
50: '#fafafa',
100: '#f5f5f5',
200: '#e5e5e5',
300: '#d4d4d4',
400: '#a3a3a3',
500: '#737373',
600: '#525252',
700: '#404040',
800: '#262626',
900: '#171717',
950: '#0a0a0a',
},
},
{
name: 'stone',
palette: {
0: '#ffffff',
50: '#fafaf9',
100: '#f5f5f4',
200: '#e7e5e4',
300: '#d6d3d1',
400: '#a8a29e',
500: '#78716c',
600: '#57534e',
700: '#44403c',
800: '#292524',
900: '#1c1917',
950: '#0c0a09',
},
},
{
name: 'soho',
palette: {
0: '#ffffff',
50: '#f4f4f4',
100: '#e8e9e9',
200: '#d2d2d4',
300: '#bbbcbe',
400: '#a5a5a9',
500: '#8e8f93',
600: '#77787d',
700: '#616268',
800: '#4a4b52',
900: '#34343d',
950: '#1d1e27',
},
},
{
name: 'viva',
palette: {
0: '#ffffff',
50: '#f3f3f3',
100: '#e7e7e8',
200: '#cfd0d0',
300: '#b7b8b9',
400: '#9fa1a1',
500: '#87898a',
600: '#6e7173',
700: '#565a5b',
800: '#3e4244',
900: '#262b2c',
950: '#0e1315',
},
},
{
name: 'ocean',
palette: {
0: '#ffffff',
50: '#fbfcfc',
100: '#F7F9F8',
200: '#EFF3F2',
300: '#DADEDD',
400: '#B1B7B6',
500: '#828787',
600: '#5F7274',
700: '#415B61',
800: '#29444E',
900: '#183240',
950: '#0c1920',
},
},
])
export function useLayout() {
function setPrimary(value) {
appState.value.primary = value;
}
function setPrimary(value) {
appState.value.primary = value
}
function setSurface(value) {
appState.value.surface = value;
}
function setSurface(value) {
appState.value.surface = value
}
function setDarkMode(value) {
appState.value.darkMode = value;
if (value) {
document.documentElement.classList.add("p-dark");
} else {
document.documentElement.classList.remove("p-dark");
}
}
function setDarkMode(value) {
appState.value.darkMode = value
if (value) {
document.documentElement.classList.add('p-dark')
} else {
document.documentElement.classList.remove('p-dark')
}
}
function toggleDarkMode() {
appState.value.darkMode = !appState.value.darkMode;
document.documentElement.classList.toggle("p-dark");
}
function toggleDarkMode() {
appState.value.darkMode = !appState.value.darkMode
document.documentElement.classList.toggle('p-dark')
}
function updateColors(type, colorName) {
if (type === "primary") {
setPrimary(colorName);
const color = primaryColors.value.find((c) => c.name === colorName);
updatePrimaryPalette(color.palette);
} else if (type === "surface") {
setSurface(colorName);
const surfaceColor = surfaces.value.find((s) => s.name === colorName);
updateSurfacePalette(surfaceColor.palette);
}
}
function updateColors(type, colorName) {
if (type === 'primary') {
setPrimary(colorName)
const color = primaryColors.value.find((c) => c.name === colorName)
updatePrimaryPalette(color.palette)
} else if (type === 'surface') {
setSurface(colorName)
const surfaceColor = surfaces.value.find((s) => s.name === colorName)
updateSurfacePalette(surfaceColor.palette)
}
}
const isDarkMode = computed(() => appState.value.darkMode);
const primary = computed(() => appState.value.primary);
const surface = computed(() => appState.value.surface);
const isDarkMode = computed(() => appState.value.darkMode)
const primary = computed(() => appState.value.primary)
const surface = computed(() => appState.value.surface)
return {
primaryColors,
surfaces,
isDarkMode,
primary,
surface,
toggleDarkMode,
setDarkMode,
setPrimary,
setSurface,
updateColors
};
return {
primaryColors,
surfaces,
isDarkMode,
primary,
surface,
toggleDarkMode,
setDarkMode,
setPrimary,
setSurface,
updateColors,
}
}

View File

@ -1,23 +1,23 @@
import "./assets/styles/main.css";
import './assets/styles/main.css'
import { createApp } from "vue";
import PrimeVue from "primevue/config";
import App from "./App.vue";
import Aura from "@primeuix/themes/aura";
import { createApp } from 'vue'
import PrimeVue from 'primevue/config'
import App from './App.vue'
import Aura from '@primeuix/themes/aura'
import ToastService from 'primevue/toastservice'
const app = createApp(App);
const app = createApp(App)
app.use(PrimeVue, {
theme: {
preset: Aura,
options: {
darkModeSelector: ".p-dark",
}
},
});
theme: {
preset: Aura,
options: {
darkModeSelector: '.p-dark',
},
},
})
app.use(ToastService)
app.mount("#app");
app.mount('#app')

View File

@ -3,13 +3,13 @@ import mqtt from 'mqtt'
export default class MQTTService {
constructor(brokerUrl = 'ws://127.0.0.1:8083/mqtt', clientId = null) {
this.clientId = clientId || 'vue-client-' + Math.random().toString(16).substr(2, 8)
this.client = mqtt.connect(brokerUrl, { clientId: this.clientId, protocolVersion: 5})
this.subscriptions = [] // array of {topic, callback}
this.client = mqtt.connect(brokerUrl, { clientId: this.clientId, protocolVersion: 5 })
this.subscriptions = [] // array of {topic, callback}
this.client.on('connect', () => console.log('Connected to MQTT broker'))
this.client.on('message', (topic, payload) => {
// iterate over subscriptions and check for matches
this.subscriptions.forEach(({topic: subTopic, callback}) => {
this.subscriptions.forEach(({ topic: subTopic, callback }) => {
if (mqttMatch(subTopic, topic)) {
callback(payload.toString(), topic)
}
@ -18,7 +18,7 @@ export default class MQTTService {
}
subscribe(topic, callback) {
this.subscriptions.push({topic, callback})
this.subscriptions.push({ topic, callback })
this.client.subscribe(topic)
}
@ -32,4 +32,4 @@ function mqttMatch(subTopic, topic) {
// replace MQTT wildcards with RegExp
const regex = '^' + subTopic.replace('+', '[^/]+').replace('#', '.+') + '$'
return new RegExp(regex).test(topic)
}
}

View File

@ -1,9 +1,6 @@
/** tailwind.config.js */
module.exports = {
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}"
],
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
theme: {
extend: {},
},
@ -31,4 +28,4 @@ module.exports = {
// Add any other classes you know jsonforms-primevue uses
],
plugins: [],
}
}

View File

@ -1,18 +1,16 @@
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import Components from 'unplugin-vue-components/vite';
import { PrimeVueResolver } from '@primevue/auto-import-resolver';
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { PrimeVueResolver } from '@primevue/auto-import-resolver'
import tailwindcss from '@tailwindcss/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
tailwindcss(),
Components({
resolvers: [
PrimeVueResolver()
]
})
]
});
plugins: [
vue(),
tailwindcss(),
Components({
resolvers: [PrimeVueResolver()],
}),
],
})