Skip to content

Commit 98f5d6d

Browse files
Phase 9: Robust Content Modeler with Rich Text and Image support
1 parent ecf4346 commit 98f5d6d

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{% extends get_active_back_theme()+'/base.html' %}
2+
3+
{% block content %}
4+
<div class="container-xxl flex-grow-1 container-p-y">
5+
<h4 class="fw-bold py-3 mb-4"><span class="text-muted fw-light">Content /</span> Create Type</h4>
6+
7+
<div class="row">
8+
<div class="col-xl">
9+
<div class="card mb-4">
10+
<div class="card-header d-flex justify-content-between align-items-center">
11+
<h5 class="mb-0">New Content Type</h5>
12+
</div>
13+
<div class="card-body">
14+
<form method="POST">
15+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
16+
<div class="mb-3">
17+
<label class="form-label" for="name">Type Name</label>
18+
<input type="text" class="form-label form-control" id="name" name="name" placeholder="e.g. Blog Post" required />
19+
</div>
20+
<div class="mb-3">
21+
<label class="form-label" for="description">Description</label>
22+
<textarea id="description" name="description" class="form-control" placeholder="What is this content type for?"></textarea>
23+
</div>
24+
25+
<hr>
26+
<h5>Fields</h5>
27+
<div id="fields-container">
28+
<div class="row mb-3 field-row">
29+
<div class="col-md-5">
30+
<input type="text" name="field_name[]" class="form-control" placeholder="Field Name (e.g. title)" required>
31+
</div>
32+
<div class="col-md-5">
33+
<select name="field_type[]" class="form-select">
34+
<option value="text">🔤 Text (Short)</option>
35+
<option value="richtext">📝 Rich Text (Editor)</option>
36+
<option value="textarea">📄 Textarea (Long)</option>
37+
<option value="image">🖼️ Image (URL)</option>
38+
<option value="number">🔢 Number</option>
39+
<option value="date">📅 Date</option>
40+
</select>
41+
</div>
42+
<div class="col-md-2">
43+
<button type="button" class="btn btn-danger remove-field"><i class="bx bx-trash"></i></button>
44+
</div>
45+
</div>
46+
</div>
47+
48+
<button type="button" id="add-field" class="btn btn-outline-primary mb-3">
49+
<i class="bx bx-plus me-1"></i> Add Field
50+
</button>
51+
52+
<div class="mt-4">
53+
<button type="submit" class="btn btn-primary">Create Content Type</button>
54+
<a href="{{ url_for('contenttype.dashboard') }}" class="btn btn-outline-secondary">Cancel</a>
55+
</div>
56+
</form>
57+
</div>
58+
</div>
59+
</div>
60+
</div>
61+
</div>
62+
63+
<script>
64+
document.addEventListener('DOMContentLoaded', function() {
65+
const container = document.getElementById('fields-container');
66+
const addButton = document.getElementById('add-field');
67+
68+
addButton.addEventListener('click', function() {
69+
const row = document.createElement('div');
70+
row.className = 'row mb-3 field-row';
71+
row.innerHTML = `
72+
<div class="col-md-5">
73+
<input type="text" name="field_name[]" class="form-control" placeholder="Field Name" required>
74+
</div>
75+
<div class="col-md-5">
76+
<select name="field_type[]" class="form-select">
77+
<option value="text">🔤 Text (Short)</option>
78+
<option value="richtext">📝 Rich Text (Editor)</option>
79+
<option value="textarea">📄 Textarea (Long)</option>
80+
<option value="image">🖼️ Image (URL)</option>
81+
<option value="number">🔢 Number</option>
82+
<option value="date">📅 Date</option>
83+
</select>
84+
</div>
85+
<div class="col-md-2">
86+
<button type="button" class="btn btn-danger remove-field"><i class="bx bx-trash"></i></button>
87+
</div>
88+
`;
89+
container.appendChild(row);
90+
});
91+
92+
container.addEventListener('click', function(e) {
93+
if (e.target.closest('.remove-field')) {
94+
const rows = container.querySelectorAll('.field-row');
95+
if (rows.length > 1) {
96+
e.target.closest('.field-row').remove();
97+
} else {
98+
alert('At least one field is required');
99+
}
100+
}
101+
});
102+
});
103+
</script>
104+
{% endblock %}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
{% extends get_active_back_theme()+'/base.html' %}
2+
3+
{% block content %}
4+
<div class="container-xxl flex-grow-1 container-p-y">
5+
<h4 class="fw-bold py-3 mb-4">
6+
<span class="text-muted fw-light">Content / {{ content_type.name }} /</span>
7+
{% if item %}Edit{% else %}Add{% endif %} Item
8+
</h4>
9+
10+
<div class="row">
11+
<div class="col-xl">
12+
<div class="card mb-4">
13+
<div class="card-header d-flex justify-content-between align-items-center">
14+
<h5 class="mb-0">{% if item %}Edit{% else %}New{% endif %} {{ content_type.name }}</h5>
15+
</div>
16+
<div class="card-body">
17+
<form method="POST">
18+
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
19+
20+
{% for field in content_type.schema %}
21+
<div class="mb-3">
22+
<label class="form-label" for="{{ field.name }}">{{ field.name|capitalize }}</label>
23+
24+
{% if field.type == 'text' %}
25+
<input type="text" class="form-control" id="{{ field.name }}" name="{{ field.name }}"
26+
value="{{ item.data.get(field.name, '') if item else '' }}" required />
27+
28+
{% elif field.type == 'textarea' %}
29+
<textarea id="{{ field.name }}" name="{{ field.name }}" class="form-control" rows="5" required>{{ item.data.get(field.name, '') if item else '' }}</textarea>
30+
31+
{% elif field.type == 'richtext' %}
32+
<textarea id="{{ field.name }}" name="{{ field.name }}" class="form-control richtext" rows="10">{{ item.data.get(field.name, '') if item else '' }}</textarea>
33+
34+
{% elif field.type == 'image' %}
35+
<div class="input-group">
36+
<span class="input-group-text"><i class="bx bx-image"></i></span>
37+
<input type="text" class="form-control image-input" id="{{ field.name }}" name="{{ field.name }}"
38+
placeholder="https://..." value="{{ item.data.get(field.name, '') if item else '' }}" />
39+
</div>
40+
<div class="mt-2 image-preview-container" id="preview-{{ field.name }}">
41+
{% if item and item.data.get(field.name) %}
42+
<img src="{{ item.data.get(field.name) }}" class="img-thumbnail" style="max-height: 200px;">
43+
{% endif %}
44+
</div>
45+
46+
{% elif field.type == 'number' %}
47+
<input type="number" class="form-control" id="{{ field.name }}" name="{{ field.name }}"
48+
value="{{ item.data.get(field.name, '') if item else '' }}" required />
49+
50+
{% elif field.type == 'date' %}
51+
<input type="date" class="form-control" id="{{ field.name }}" name="{{ field.name }}"
52+
value="{{ item.data.get(field.name, '') if item else '' }}" required />
53+
{% endif %}
54+
</div>
55+
{% endfor %}
56+
57+
<div class="mt-4">
58+
<button type="submit" class="btn btn-primary">Save Item</button>
59+
<a href="{{ url_for('contenttype.items', type_id=content_type.id) }}" class="btn btn-outline-secondary">Cancel</a>
60+
</div>
61+
</form>
62+
</div>
63+
</div>
64+
</div>
65+
</div>
66+
</div>
67+
68+
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
69+
<script>
70+
tinymce.init({
71+
selector: '.richtext',
72+
plugins: 'anchor autolink charmap codesample emoticons image link lists media searchreplace table visualblocks wordcount',
73+
toolbar: 'undo redo | blocks fontfamily fontsize | bold italic underline strikethrough | link image media table | align lineheight | numlist bullist indent outdent | emoticons charmap | removeformat',
74+
height: 500
75+
});
76+
77+
// Simple Image Preview
78+
document.querySelectorAll('.image-input').forEach(input => {
79+
input.addEventListener('input', function() {
80+
const previewId = 'preview-' + this.id;
81+
const container = document.getElementById(previewId);
82+
if (this.value) {
83+
container.innerHTML = `<img src="${this.value}" class="img-thumbnail" style="max-height: 200px;" onerror="this.style.display='none'">`;
84+
} else {
85+
container.innerHTML = '';
86+
}
87+
});
88+
});
89+
</script>
90+
{% endblock %}

0 commit comments

Comments
 (0)