# 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]) -
+
- + class="form-check-input"> + @error($name) - {{ $message }} +
{{ $message }}
@enderror
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]) -
-