From dbce932ce2cd38dff3d0ca342cbcc371c7457701 Mon Sep 17 00:00:00 2001 From: Floriansylvain Date: Tue, 16 Dec 2025 21:09:53 +0100 Subject: [PATCH] feat: initial impl w/ styles, scripts, & server-side logic (steps 1, 2, 3) --- assessment/assets/css/styles.css | 49 ++++++++++++++ assessment/assets/css/toolbar.css | 33 ++++++++++ assessment/assets/js/apiClient.js | 25 ++++++++ assessment/assets/js/domModule.js | 19 ++++++ assessment/assets/js/main.js | 44 +++++++++++++ assessment/assets/js/toolbar.js | 64 +++++++++++++++++++ .../customs/clienta/modules/cars/ajax.php | 36 ++++++++++- .../customs/clienta/modules/cars/edit.php | 31 +++++++++ .../customs/clientb/modules/cars/ajax.php | 25 +++++++- .../customs/clientb/modules/cars/edit.php | 31 +++++++++ .../customs/clientc/modules/cars/ajax.php | 22 ++++++- .../customs/clientc/modules/cars/edit.php | 31 +++++++++ assessment/index.php | 13 ++-- assessment/src/ajax_handler.php | 48 ++++++++++++++ assessment/src/utils.php | 37 +++++++++++ 15 files changed, 501 insertions(+), 7 deletions(-) create mode 100644 assessment/assets/css/styles.css create mode 100644 assessment/assets/css/toolbar.css create mode 100644 assessment/assets/js/apiClient.js create mode 100644 assessment/assets/js/domModule.js create mode 100644 assessment/assets/js/main.js create mode 100644 assessment/assets/js/toolbar.js create mode 100644 assessment/customs/clienta/modules/cars/edit.php create mode 100644 assessment/customs/clientb/modules/cars/edit.php create mode 100644 assessment/customs/clientc/modules/cars/edit.php create mode 100644 assessment/src/ajax_handler.php create mode 100644 assessment/src/utils.php diff --git a/assessment/assets/css/styles.css b/assessment/assets/css/styles.css new file mode 100644 index 0000000..c62890a --- /dev/null +++ b/assessment/assets/css/styles.css @@ -0,0 +1,49 @@ +html { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + margin: 0; + padding: 0; +} + +.dynamic-div ul { + list-style: none; + padding: 0; + margin: 0; +} + +.dynamic-div li { + margin: 6px 0; +} + +.garage-bullet { + display: flex; + align-items: center; + gap: 8px; +} + +.garage-color { + width: 12px; + height: 12px; + border-radius: 50%; + display: inline-block; + border: 1px solid rgba(0, 0, 0, 0.12); +} + +.car-item { + cursor: pointer; +} +.dynamic-div .car-item:hover { + background-color: rgba(59, 130, 246, 0.06); +} +.car-item:focus { + outline: 2px solid rgba(59,130,246,0.18); + outline-offset: 2px; +} + +.error-message { + color: crimson; +} diff --git a/assessment/assets/css/toolbar.css b/assessment/assets/css/toolbar.css new file mode 100644 index 0000000..b969058 --- /dev/null +++ b/assessment/assets/css/toolbar.css @@ -0,0 +1,33 @@ +.toolbar { + background: #1f2937; + color: #fff; + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + font-family: Inter, system-ui, Arial, sans-serif +} + +.toolbar button { + background: #fff; + color: #111; + border: 0; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer +} + +.toolbar button.active { + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15) +} + +.toolbar button.toolbar-close { + margin-left: auto; + background: transparent; + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.08) +} + +.error-message { + color: crimson; +} diff --git a/assessment/assets/js/apiClient.js b/assessment/assets/js/apiClient.js new file mode 100644 index 0000000..53f4b26 --- /dev/null +++ b/assessment/assets/js/apiClient.js @@ -0,0 +1,25 @@ +export async function fetchHtml(moduleName, scriptName, clientName = '', extraParams = {}) { + if (!moduleName || !scriptName) throw new Error('module/script required'); + + const params = new URLSearchParams({ module: moduleName, script: scriptName }); + + if (clientName) params.set('client', clientName); + + Object.keys(extraParams || {}).forEach(k => { + if (extraParams[k] !== undefined && extraParams[k] !== null) params.set(k, String(extraParams[k])); + }); + + const url = 'src/ajax_handler.php?' + params.toString(); + const response = await fetch(url, { method: 'GET', credentials: 'same-origin' }); + + if (!response.ok) { + const text = await response.text(); + const message = text || `Request failed: ${response.status}`; + const err = new Error(message); + err.httpStatus = response.status; + throw err; + } + + return response.text(); +} + diff --git a/assessment/assets/js/domModule.js b/assessment/assets/js/domModule.js new file mode 100644 index 0000000..8578c28 --- /dev/null +++ b/assessment/assets/js/domModule.js @@ -0,0 +1,19 @@ +export function renderError(container, message) { + if (!container) return; + try { + const $c = (container.jquery) ? container : (typeof container === 'string' ? $(container) : $(container)); + $c.html(`
${String(message)}
`); + } catch (e) { + if (container instanceof Element) container.innerHTML = `
${String(message)}
`; + } +} + +export function insertHtml(container, html) { + if (!container) return; + try { + const $c = (container.jquery) ? container : (typeof container === 'string' ? $(container) : $(container)); + $c.html(html); + } catch (e) { + if (container instanceof Element) container.innerHTML = html; + } +} diff --git a/assessment/assets/js/main.js b/assessment/assets/js/main.js new file mode 100644 index 0000000..b0123cf --- /dev/null +++ b/assessment/assets/js/main.js @@ -0,0 +1,44 @@ +import { fetchHtml } from './apiClient.js'; +import { renderError, insertHtml } from './domModule.js'; + +function getCookie(name) { + const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); + return match ? decodeURIComponent(match[2]) : null; +} + +async function loadDynamic() { + const $container = $('.dynamic-div').first(); + if (!$container.length) return; + const moduleName = $container.data('module') || ''; + const scriptName = $container.data('script') || ''; + const clientName = getCookie('client') || ''; + if (!moduleName || !scriptName) return renderError($container, 'Missing data-module or data-script.'); + try { + const html = await fetchHtml(moduleName, scriptName, clientName); + insertHtml($container, html); + attachCarClickHandlers($container, clientName); + } catch (err) { + renderError($container, err.message || 'Network error while loading dynamic content.'); + } +} + +$(document).ready(loadDynamic); + +window.loadDynamic = loadDynamic; + +function attachCarClickHandlers($container, clientName) { + if (!$container || !$container.length) return; + if ($container.data('_carHandlerAttached')) return; + $container.on('click', '.car-item', async function (ev) { + const $el = $(this); + const id = $el.data('car-id') || $el.attr('data-car-id'); + if (!id) return; + try { + const html = await fetchHtml('cars', 'edit', clientName, { id }); + insertHtml($container, html); + } catch (err) { + renderError($container, err.message || 'Network error while loading details.'); + } + }); + $container.data('_carHandlerAttached', '1'); +} diff --git a/assessment/assets/js/toolbar.js b/assessment/assets/js/toolbar.js new file mode 100644 index 0000000..d3a4565 --- /dev/null +++ b/assessment/assets/js/toolbar.js @@ -0,0 +1,64 @@ +class Toolbar { + constructor(containerClass = 'toolbar') { + this.$container = $('
').addClass(containerClass); + this.clientMapping = [ + { label: 'Client A', value: 'clienta' }, + { label: 'Client B', value: 'clientb' }, + { label: 'Client C', value: 'clientc' } + ]; + this.injectStylesheet('assets/css/toolbar.css'); + this.render(); + this.activateExistingClient(); + } + + injectStylesheet(href) { + const $link = $('', { rel: 'stylesheet', href }); + $('head').append($link); + } + + setCookieValue(name, value, maxAgeSeconds = 86400) { + document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAgeSeconds}; SameSite=Lax`; + } + + markActiveButton($button) { + this.$container.find('button').removeClass('active'); + if ($button && $button.length) $button.addClass('active'); + } + + render() { + this.clientMapping.forEach(mapping => { + const $button = $('