feat: implement dynamic component template engine with data source integration and management UI
This commit is contained in:
@@ -201,8 +201,8 @@
|
||||
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
|
||||
<div class="bg-white py-2 collapse-inner rounded">
|
||||
<h6 class="collapse-header">Custom Components:</h6>
|
||||
<a class="collapse-item" href="#">Buttons</a>
|
||||
<a class="collapse-item" href="#">Cards</a>
|
||||
<a class="collapse-item" th:href="@{/manage/components}">Component Templates</a>
|
||||
<a class="collapse-item" th:href="@{/manage/snippets}">HTML Snippets</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
<head>
|
||||
<title th:text="${isNew} ? 'New Component Template' : 'Edit Component Template'">Edit Component Template</title>
|
||||
<style>
|
||||
#monaco-container {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
border: 1px solid #d1d3e2;
|
||||
border-radius: 0.35rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.placeholder-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
margin: 2px;
|
||||
border-radius: 3px;
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #90caf9;
|
||||
color: #1565c0;
|
||||
cursor: pointer;
|
||||
font-family: monospace;
|
||||
font-size: 0.85rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.placeholder-tag:hover {
|
||||
background: #bbdefb;
|
||||
border-color: #64b5f6;
|
||||
}
|
||||
.placeholder-section {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
.placeholder-section h6 {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: #858796;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800" th:text="${isNew} ? 'New Component Template' : 'Edit Component Template'">Component Template</h1>
|
||||
<a th:href="@{/manage/components}" class="d-none d-sm-inline-block btn btn-sm btn-secondary shadow-sm">
|
||||
<i class="fas fa-arrow-left fa-sm text-white-50"></i> Back to Templates
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form th:action="${isNew} ? @{/manage/components/create} : @{/manage/components/{id}(id=${template.id})}" method="post" id="templateForm">
|
||||
<div class="row">
|
||||
<!-- Left Column: Properties -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Template Properties</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-group">
|
||||
<label for="name">Name <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="name" name="name" th:value="${template.name}" required placeholder="e.g. CTA Buttons">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="slug">Slug <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="slug" name="slug" th:value="${template.slug}" required placeholder="e.g. cta-buttons"
|
||||
pattern="[a-zA-Z0-9_-]+" title="Only letters, numbers, hyphens, and underscores">
|
||||
<small class="form-text text-muted">Used in shortcode: <code>[component:<span id="slugPreview">slug</span>]</code></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="description">Description</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" th:text="${template.description}" placeholder="Brief description of what this template renders"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" class="custom-control-input" id="active" name="active" th:checked="${template.active}">
|
||||
<label class="custom-control-label" for="active">Active</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available Placeholders -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">Available Placeholders</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="placeholder-section">
|
||||
<h6>Data Source Fields</h6>
|
||||
<span class="placeholder-tag" onclick="insertPlaceholder('{{name}}')">{{name}}</span>
|
||||
</div>
|
||||
<div class="placeholder-section">
|
||||
<h6>Loop (repeats for each item)</h6>
|
||||
<span class="placeholder-tag" onclick="insertPlaceholder('{{#each items}}\n\n{{/each}}')">{{#each items}}</span>
|
||||
</div>
|
||||
<div class="placeholder-section">
|
||||
<h6>Item Fields (inside loop)</h6>
|
||||
<span class="placeholder-tag" onclick="insertPlaceholder('{{label}}')">{{label}}</span>
|
||||
<span class="placeholder-tag" onclick="insertPlaceholder('{{url}}')">{{url}}</span>
|
||||
<span class="placeholder-tag" onclick="insertPlaceholder('{{index}}')">{{index}}</span>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="small text-muted">
|
||||
<strong>Tip:</strong> Click a placeholder tag to insert it at the cursor position in the editor.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block mb-4">
|
||||
<i class="fas fa-save mr-1"></i>
|
||||
<span th:text="${isNew} ? 'Create Template' : 'Update Template'">Save</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: HTML Template Editor -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">HTML Template</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<textarea id="htmlTemplate" name="htmlTemplate" style="display:none;" th:text="${template.htmlTemplate}"></textarea>
|
||||
<div id="monaco-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Example -->
|
||||
<div class="card border-left-success shadow mb-4" th:unless="${isNew}">
|
||||
<div class="card-body">
|
||||
<div class="text-xs font-weight-bold text-success text-uppercase mb-2">Usage in Snippets</div>
|
||||
<div class="mb-2">
|
||||
<strong>Static (no data):</strong><br>
|
||||
<code>[component:<span th:text="${template.slug}">slug</span>]</code>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<strong>With Menu data:</strong><br>
|
||||
<code>[component:<span th:text="${template.slug}">slug</span> data-source="menu:LOCATION"]</code>
|
||||
</div>
|
||||
<div class="small text-muted mt-2">
|
||||
Replace <code>LOCATION</code> with the menu location code (e.g. CTA, INFO, PRIMARY, FOOTER).
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section layout:fragment="scripts">
|
||||
<!-- Monaco Editor -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
||||
<script th:inline="javascript">
|
||||
var monacoEditor = null;
|
||||
|
||||
require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs' } });
|
||||
require(['vs/editor/editor.main'], function () {
|
||||
var textArea = document.getElementById('htmlTemplate');
|
||||
var initialValue = textArea.value || '';
|
||||
|
||||
monacoEditor = monaco.editor.create(document.getElementById('monaco-container'), {
|
||||
value: initialValue,
|
||||
language: 'html',
|
||||
theme: 'vs-dark',
|
||||
minimap: { enabled: false },
|
||||
automaticLayout: true,
|
||||
wordWrap: 'on',
|
||||
fontSize: 14,
|
||||
lineNumbers: 'on',
|
||||
renderWhitespace: 'selection',
|
||||
scrollBeyondLastLine: false,
|
||||
tabSize: 2
|
||||
});
|
||||
|
||||
// Sync editor content to textarea on form submit
|
||||
document.getElementById('templateForm').addEventListener('submit', function () {
|
||||
textArea.value = monacoEditor.getValue();
|
||||
});
|
||||
});
|
||||
|
||||
// Insert placeholder at cursor position in Monaco
|
||||
function insertPlaceholder(text) {
|
||||
if (monacoEditor) {
|
||||
var selection = monacoEditor.getSelection();
|
||||
monacoEditor.executeEdits('placeholder', [{
|
||||
range: selection,
|
||||
text: text,
|
||||
forceMoveMarkers: true
|
||||
}]);
|
||||
monacoEditor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Update slug preview
|
||||
document.getElementById('slug').addEventListener('input', function () {
|
||||
document.getElementById('slugPreview').textContent = this.value || 'slug';
|
||||
});
|
||||
// Initialize preview
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var slugInput = document.getElementById('slug');
|
||||
if (slugInput.value) {
|
||||
document.getElementById('slugPreview').textContent = slugInput.value;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{fragments/manage-layout}">
|
||||
<head>
|
||||
<title>Component Templates</title>
|
||||
</head>
|
||||
<body>
|
||||
<div layout:fragment="content">
|
||||
<!-- Page Heading -->
|
||||
<div class="d-sm-flex align-items-center justify-content-between mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">Component Templates</h1>
|
||||
<a th:href="@{/manage/components/new}" class="d-none d-sm-inline-block btn btn-sm btn-primary shadow-sm">
|
||||
<i class="fas fa-plus fa-sm text-white-50"></i> Add New Template
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Alerts -->
|
||||
<div th:if="${successMessage}" class="alert alert-success alert-dismissible fade show" role="alert">
|
||||
<span th:text="${successMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div th:if="${errorMessage}" class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<span th:text="${errorMessage}"></span>
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Info Card -->
|
||||
<div class="card border-left-info shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row no-gutters align-items-center">
|
||||
<div class="col mr-2">
|
||||
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">How to use</div>
|
||||
<div class="text-gray-800">
|
||||
Use shortcode <code>[component:SLUG data-source="menu:LOCATION"]</code> inside any Snippet to render a Component Template with dynamic data.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
<i class="fas fa-puzzle-piece fa-2x text-gray-300"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered" width="100%" cellspacing="0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Slug</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="t : ${templates}">
|
||||
<td th:text="${t.name}">Name</td>
|
||||
<td>
|
||||
<code th:text="${t.slug}">slug</code>
|
||||
<div class="small text-muted mt-1">
|
||||
Shortcode: <code>[component:<span th:text="${t.slug}">slug</span>]</code>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<small th:text="${t.description}" class="text-muted">Description</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge badge-success" th:if="${t.active}">Active</span>
|
||||
<span class="badge badge-secondary" th:unless="${t.active}">Inactive</span>
|
||||
</td>
|
||||
<td>
|
||||
<a th:href="@{/manage/components/{id}/edit(id=${t.id})}" class="btn btn-sm btn-outline-info mr-1" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<form th:action="@{/manage/components/{id}/duplicate(id=${t.id})}" method="post" style="display:inline;">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-warning mr-1" title="Duplicate">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</form>
|
||||
<form th:action="@{/manage/components/{id}/delete(id=${t.id})}" method="post" style="display:inline;" onsubmit="return confirm('Are you sure you want to delete this component template?');">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(templates)}">
|
||||
<td colspan="5" class="text-center">No Component Templates found.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
<div data-component-id="umass_base:block-multiple-ctas" data-background="solid" data-layout-variant="default">
|
||||
<div class="container" th:if="${ctaMenu != null and not #lists.isEmpty(ctaMenu.items)}">
|
||||
<div class="text-container">
|
||||
<div data-component-id="umass_base:section-title" class="f--section-title">
|
||||
<h2><span th:utext="${ctaMenu.name}">TÌM HIỂU CHUYÊN KHOA</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="links-container">
|
||||
<div class="f--field f--button" th:each="item : ${ctaMenu.items}">
|
||||
<a th:href="${item.url}" class="button-text-link button-context-light " th:aria-label="${item.label}" target="_self" data-component-id="umass_base:button">
|
||||
<span class="button-text" th:text="${item.label}"> Apply </span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Fallback if menu is empty, show default CTA block -->
|
||||
<div class="container" th:unless="${ctaMenu != null and not #lists.isEmpty(ctaMenu.items)}">
|
||||
<div class="text-container">
|
||||
<div data-component-id="umass_base:section-title" class="f--section-title">
|
||||
<h2>TÌM HIỂU<span class="highlight">CHUYÊN KHOA</span></h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="links-container">
|
||||
<div class="f--field f--button">
|
||||
<a href="https://www.umass.edu/admissions/apply" class="button-text-link button-context-light " aria-label="Apply" target="_self" data-component-id="umass_base:button">
|
||||
<span class="button-text"> Apply </span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="f--field f--button">
|
||||
<a href="https://www.umass.edu/gateway/academics/explore-our-programs" class="button-text-link button-context-light " aria-label="Majors & Minors" target="_self" data-component-id="umass_base:button">
|
||||
<span class="button-text"> Majors & Minors </span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="f--field f--button">
|
||||
<a href="https://www.umass.edu/admissions/undergraduate-admissions/costs-aid" class="button-text-link button-context-light " aria-label="Tuition & Costs" target="_self" data-component-id="umass_base:button">
|
||||
<span class="button-text"> Tuition & Costs </span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="f--field f--button">
|
||||
<a href="https://www.umass.edu/admissions/visit" class="button-text-link button-context-light " aria-label="Take a Tour" target="_self" data-component-id="umass_base:button">
|
||||
<span class="button-text"> Take a Tour </span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user