# HG changeset patch
# User luka
# Date 1755650015 14400
# Node ID 84c75d9d90be7ba61a11294bd287ee92808e0465
# Parent e107504fa22c92ccd9673f023ccb4186cc9decf7
Changing usage to be bootstrap 5, not everything is reviewed but it's been started
diff -r e107504fa22c -r 84c75d9d90be composer.json
--- a/composer.json Mon Jun 23 20:20:31 2025 -0400
+++ b/composer.json Tue Aug 19 20:33:35 2025 -0400
@@ -1,22 +1,23 @@
{
- "name": "wizard/framework",
- "version": "1.0.0",
- "description": "A reliable and repeatable base framework.",
- "autoload": {
- "psr-4": {
- "Wizard\\Framework\\":"src/"
- }
- },
- "minimum-stability": "stable",
- "require": {
- "laravel/framework": ">=12",
- "doctrine/dbal": "*"
- },
- "extra": {
- "laravel": {
- "providers": [
- "Wizard\\Framework\\FrameworkServiceProvider"
- ]
- }
- }
+ "name": "wizard/framework",
+ "version": "1.0.0",
+ "description": "A reliable and repeatable base framework.",
+ "autoload": {
+ "psr-4": {
+ "Wizard\\Framework\\": "src/",
+ "Wizard\\Framework\\Components\\": "publishable/resources/views/components/"
+ }
+ },
+ "minimum-stability": "stable",
+ "require": {
+ "laravel/framework": ">=12",
+ "doctrine/dbal": "*"
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Wizard\\Framework\\FrameworkServiceProvider"
+ ]
+ }
+ }
}
diff -r e107504fa22c -r 84c75d9d90be publishable/package.json
--- a/publishable/package.json Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/package.json Tue Aug 19 20:33:35 2025 -0400
@@ -13,6 +13,7 @@
"laravel-vite-plugin": "^1.2.0",
"postcss": "^8.4.31",
"prettier": "^3.5.3",
+ "sass-embedded": "^1.90.0",
"vite": "^6.2.4"
},
"prettier": {
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/css/app.css
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/js/ServerTable.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/publishable/resources/js/ServerTable.js Tue Aug 19 20:33:35 2025 -0400
@@ -0,0 +1,247 @@
+class ServerTable {
+ /**
+ * @param {HTMLElement} rootEl - The container element for the table
+ * @param {Object} options - Configuration options
+ * @param {string} options.endpoint - API endpoint for data
+ * @param {Array} options.columns - Array of column configs: [{name, label, ...}]
+ * @param {number} [options.pageSize=10] - Default rows per page
+ * @param {Array} [options.initialSort=[]] - Default sort: [{col, dir}]
+ * @param {Object} [options.headers={}] - Additional headers for requests
+ * @param {string} [options.groupBy={}] - Which column to group by
+ * @param {Function} [options.groupRender={}] - Function to render the grouping
+ */
+ constructor(rootEl, options) {
+ this.rootEl = rootEl;
+ this.endpoint = options.endpoint;
+ this.columns = options.columns || [];
+ this.pageSize = options.pageSize || 10;
+ this.sort = options.initialSort || [];
+ this.filters = options.filters || {};
+ this.currentPage = 1;
+ this.headers = options.headers || {};
+ this.groupBy = options.groupBy;
+ this.groupRender =
+ options.groupRender ||
+ ((g, rows) =>
+ `
| Group: ${g} |
`);
+
+ this.skeleton = options.skeleton || `
+
+ `;
+
+ this.state = {
+ loading: false,
+ error: null,
+ totalRecords: 0,
+ records: [],
+ };
+
+ // Render initial skeleton and fetch initial data
+ this.renderSkeleton();
+ this.fetchData();
+ }
+
+ renderSkeleton() {
+ // Build base structure: table shell, controls area
+ this.rootEl.innerHTML = this.skeleton;
+ // Store references
+ this.head = this.rootEl.querySelector("thead");
+ this.tbody = this.rootEl.querySelector("tbody");
+ this.statusEl = this.rootEl.querySelector(".st-status");
+ this.paginationEl = this.rootEl.querySelector(".st-pagination");
+
+ // Draw the header
+ this.head.innerHTML = `
+
+ ${this.columns.map((col) => `| ${col.label || col.name} | `).join("")}
+
+ `;
+ }
+
+ async fetchData() {
+ this.setLoading(true);
+ const payload = {
+ page: this.currentPage,
+ page_size: this.pageSize,
+ sort: this.sort,
+ filters: this.filters,
+ };
+
+ try {
+ const res = await fetch(this.endpoint, {
+ method: "POST",
+ headers: {
+ ...this.headers,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(payload),
+ });
+
+ if (!res.ok) {
+ throw new Error(`Server responded with ${res.status}`);
+ }
+
+ const data = await res.json();
+ // Minimal shape validation
+ if (
+ !Array.isArray(data.records) ||
+ typeof data.total_records !== "number"
+ ) {
+ throw new Error("Malformed server response");
+ }
+
+ // Save data in state
+ this.state.records = data.records;
+ this.state.totalRecords = data.total_records;
+ this.state.error = null;
+
+ this.renderRows();
+ this.updateControls();
+ } catch (err) {
+ this.state.error = err.message;
+ this.renderError();
+ } finally {
+ this.setLoading(false);
+ }
+ }
+
+ setLoading(loading) {
+ this.state.loading = loading;
+ this.statusEl.textContent = loading ? "Loading..." : "";
+ }
+
+ renderRows() {
+ const { records } = this.state;
+ const cols = this.columns;
+ const groupBy = this.groupBy;
+ const groupRender =
+ typeof this.groupRender === "function"
+ ? this.groupRender
+ : (g, rows) =>
+ `| Project: ${g} |
`;
+
+ if (!records || records.length === 0) {
+ this.tbody.innerHTML = `| No data |
`;
+ return;
+ }
+
+ // Group if needed
+ if (groupBy) {
+ // Find grouping field or function
+ const getGroupValue =
+ typeof groupBy === "function" ? groupBy : (row) => row[groupBy];
+ let lastGroup = undefined;
+ let out = "";
+ let groupRows = [];
+
+ for (let i = 0; i < records.length; i++) {
+ const row = records[i];
+ const groupVal = getGroupValue(row);
+
+ // On group transition, flush previous group
+ if (i === 0 || groupVal !== lastGroup) {
+ if (i > 0) {
+ // Optionally do something with groupRows if groupRender wants it
+ }
+ // Insert group header row
+ out += groupRender(groupVal, []);
+ lastGroup = groupVal;
+ groupRows = [];
+ }
+
+ groupRows.push(row);
+
+ out += `${cols
+ .map((col, ci) => {
+ // If grouping by this column, suppress repeated values (leave blank except for first row in group)
+ if (
+ typeof groupBy === "string" &&
+ col.name === groupBy &&
+ groupVal === lastGroup &&
+ groupRows.length > 1
+ ) {
+ return ` | `;
+ }
+ return `${
+ typeof col.render === "function"
+ ? col.render(row, col, i)
+ : row[col.name]
+ } | `;
+ })
+ .join("")}
`;
+ }
+ this.tbody.innerHTML = out;
+ } else {
+ // No grouping
+ this.tbody.innerHTML = records
+ .map(
+ (row, i) =>
+ `${cols
+ .map(
+ (col) =>
+ `| ${
+ typeof col.render === "function"
+ ? col.render(row, col, i)
+ : row[col.name]
+ } | `,
+ )
+ .join("")}
`,
+ )
+ .join("");
+ }
+ }
+ renderError() {
+ this.tbody.innerHTML = `| ${this.state.error} |
`;
+ }
+
+ updateControls() {
+ // Basic pagination info (full controls to come later)
+ const from = 1 + (this.currentPage - 1) * this.pageSize;
+ const to = Math.min(
+ this.currentPage * this.pageSize,
+ this.state.totalRecords,
+ );
+ this.paginationEl.textContent = `Showing ${from}-${to} of ${this.state.totalRecords}`;
+ }
+
+ // PUBLIC: force reload
+ reload() {
+ this.fetchData();
+ }
+
+ // PUBLIC: update filters, resets to page 1
+ setFilters(newFilters) {
+ this.filters = newFilters;
+ this.currentPage = 1;
+ this.fetchData();
+ }
+}
+
+// Example usage (not part of module export):
+/*
+const table = new ServerTable(document.getElementById('my-table'), {
+ endpoint: '/tickets/get_data',
+ columns: [
+ {name: 'id', label: 'ID'},
+ {name: 'subject', label: 'Subject'},
+ {name: 'project', label: 'Project'},
+ {name: 'created_at', label: 'Created At'},
+ ],
+ pageSize: 10,
+ initialSort: [{col: 'created_at', dir: 'desc'}]
+});
+*/
+
+window.ServerTable = ServerTable;
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/js/bootstrap.js
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/publishable/resources/js/bootstrap.js Tue Aug 19 20:33:35 2025 -0400
@@ -0,0 +1,4 @@
+import axios from 'axios';
+window.axios = axios;
+
+window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/sass/app.scss
--- a/publishable/resources/sass/app.scss Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/sass/app.scss Tue Aug 19 20:33:35 2025 -0400
@@ -1,1 +1,4 @@
-@import 'bootstrap/scss/bootstrap'
+$border-radius: 0.75rem;
+@import 'bootstrap/scss/bootstrap';
+@import './dashboard';
+@import './badges';
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/sass/badges.scss
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/publishable/resources/sass/badges.scss Tue Aug 19 20:33:35 2025 -0400
@@ -0,0 +1,30 @@
+.badge{
+ &.yellow {
+ background-color: $yellow-100;
+ color: $yellow-700;
+ }
+
+ &.orange {
+ background-color: $orange-100;
+ color: $orange-700;
+ }
+ &.green {
+ background-color: $green-100;
+ color: $green-700;
+ }
+
+ &.blue {
+ background-color: $blue-100;
+ color: $blue-700;
+ }
+
+ &.gray {
+ background-color: $gray-200;
+ color: $gray-700;
+ }
+
+ &.red {
+ background-color: $red-100;
+ color: $red-700;
+ }
+}
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/sass/dashboard.scss
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/publishable/resources/sass/dashboard.scss Tue Aug 19 20:33:35 2025 -0400
@@ -0,0 +1,36 @@
+.dashboard-card{
+ min-width: 170px;
+ padding: 1.5rem;
+ border-radius: var(--bs-border-radius, 12px);
+ background-color: var(--bs-white, #fff);
+ display: flex;
+ align-items: center;
+ gap: var(--bs-gap-4, 24px);
+ height: 100%;
+ box-shadow: var(--bs-box-shadow);
+
+ .icon {
+ border-radius: 99px;
+ display: inline-flex;
+ flex-wrap: wrap;
+ height: 3em;
+ width: 3em;
+ justify-content: center;
+ align-content: center;
+
+ &.blue {
+ background-color: $blue-100;
+ color: $blue-600;
+ }
+
+ &.green {
+ background-color: $teal-100;
+ color: $teal-600;
+ }
+
+ &.purple {
+ background-color: $purple-100;
+ color: $purple-600;
+ }
+ }
+}
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/badge.blade.php
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/publishable/resources/views/components/badge.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -0,0 +1,6 @@
+@props([
+ 'colour' => 'gray'
+])
+
+ {{ $slot }}
+
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/card.blade.php
--- a/publishable/resources/views/components/card.blade.php Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/views/components/card.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -6,18 +6,5 @@
'headerButton' => null, //button or link for the right side of the header
])
-
-
-
- {!! $icon ?? '' !!}
- {{ $title }}
-
- {!! $headerButton !!}
-
- {{ $slot }}
- @if ($footer)
-
- {!! $footer !!}
-
- @endif
+
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/dashboard-card.blade.php
--- a/publishable/resources/views/components/dashboard-card.blade.php Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/views/components/dashboard-card.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -9,25 +9,25 @@
@php
$bgColor =
[
- 'green' => 'bg-green-100 text-green-600',
- 'blue' => 'bg-blue-100 text-blue-600',
- 'purple' => 'bg-purple-100 text-purple-600',
- 'yellow' => 'bg-yellow-100 text-yellow-600',
- 'gray' => 'bg-gray-100 text-gray-600',
- ][$color] ?? 'bg-blue-100 text-blue-600';
+ 'green' => 'green',
+ 'blue' => 'blue',
+ 'purple' => 'purple',
+ 'yellow' => 'yellow',
+ 'gray' => 'gray',
+ ][$color] ?? 'blue';
@endphp
-
-
-
+
+
+
{!! $icon !!}
-
{{ $value }}
-
{{ $title }}
+
{{ $value }}
+
{{ $title }}
@if ($subtitle)
-
{{ $subtitle }}
+
{{ $subtitle }}
@endif
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/form/checkbox.blade.php
--- a/publishable/resources/views/components/form/checkbox.blade.php Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/views/components/form/checkbox.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -1,11 +1,11 @@
@props(['name', 'label', 'checked' => false])
-
+
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/form/date.blade.php
--- a/publishable/resources/views/components/form/date.blade.php Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/views/components/form/date.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -1,11 +1,10 @@
@props(['name', 'label', 'value' => '', 'required' => false])
-
-
+
+
+ @if ($required) required @endif class="form-control">
@error($name)
-
{{ $message }}
+
{{ $message }}
@enderror
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/form/select.blade.php
--- a/publishable/resources/views/components/form/select.blade.php Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/views/components/form/select.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -1,8 +1,8 @@
@props(['name', 'label', 'options' => [], 'value' => '', 'required' => false])
-
-
+
+
@error($name)
-
{{ $message }}
+
{{ $message }}
@enderror
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/form/text.blade.php
--- a/publishable/resources/views/components/form/text.blade.php Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/views/components/form/text.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -1,11 +1,11 @@
@props(['name', 'label', 'value' => '', 'required' => false])
-
-
+
+
+ class="form-control">
@error($name)
-
{{ $message }}
+
{{ $message }}
@enderror
diff -r e107504fa22c -r 84c75d9d90be publishable/resources/views/components/form/textarea.blade.php
--- a/publishable/resources/views/components/form/textarea.blade.php Mon Jun 23 20:20:31 2025 -0400
+++ b/publishable/resources/views/components/form/textarea.blade.php Tue Aug 19 20:33:35 2025 -0400
@@ -1,10 +1,10 @@
@props(['name', 'label', 'value' => '', 'required' => false])
-
-