Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.

Commit d8a231b

Browse files
committed
Merge branch 'fix/feature-crud' into develop
2 parents aa692d1 + 43d5866 commit d8a231b

4 files changed

Lines changed: 255 additions & 0 deletions

File tree

app/config/routes.cfm

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@
103103
.namespace("admin")
104104
// Admin Controls
105105
.get(name = "dashboard", pattern="/", to="AdminController##dashboard")
106+
107+
// FeatureController routes
108+
.get(name = "feature", pattern = "feature", to = "FeatureController##index")
109+
.get(name = "addFeature", pattern = "feature/add", to = "FeatureController##addFeature")
110+
.get(name = "editFeature", pattern = "feature/edit/[id]", to = "FeatureController##addFeature")
111+
.post(name = "storeFeature", pattern = "feature/store", to = "FeatureController##store")
112+
.get(name = "deleteFeature", pattern = "feature/delete/[id]", to = "FeatureController##delete")
106113
.get(name = "blog", pattern = "blog", to = "AdminController##blog")
107114
.get(name = "blogEdit", pattern = "blog/edit/[id]", to = "AdminController##editBlog")
108115
.put(name = "blog-update", pattern = "blog/blogUpdate/[id]", to = "AdminController##update")
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
component extends="app.Controllers.Controller" {
2+
3+
function config() {
4+
verifies(except="index,addFeature,store", params="key", paramsTypes="integer", handler="index");
5+
usesLayout(template="/admin/AdminController/layout");
6+
filters(through="checkAdminAccess");
7+
}
8+
9+
// List all features
10+
function index() {
11+
features = model("Feature").getAllFeatures();
12+
}
13+
14+
// Add or edit feature
15+
function addFeature() {
16+
param name="id" default=0;
17+
feature;
18+
if (id > 0) {
19+
feature = findById(params.id);
20+
} else {
21+
feature = model("Feature");
22+
}
23+
return feature;
24+
}
25+
26+
// Save feature (create or update)
27+
function store() {
28+
try {
29+
var message = saveFeature(params);
30+
flashInsert(success=message);
31+
redirectTo(route="adminFeature");
32+
} catch (any e) {
33+
flashInsert(error="Failed to save feature: " & e.message);
34+
redirectTo(route="adminFeature");
35+
}
36+
}
37+
38+
// Delete feature (soft delete)
39+
function delete() {
40+
try {
41+
var message = softDelete(params.id);
42+
flashInsert(success=message.message);
43+
renderText('');
44+
} catch (any e) {
45+
flashInsert(error="Failed to delete feature: " & e.message);
46+
cfheader(statusCode=500);
47+
}
48+
}
49+
50+
// Find feature by ID
51+
private function findById(id) {
52+
return model("Feature").findByKey(arguments.id);
53+
}
54+
55+
// Save or update feature
56+
private function saveFeature(featureData) {
57+
var message = "";
58+
if (featureData.id > 0) {
59+
var feature = model("Feature").findByKey(featureData.id);
60+
if (!isNull(feature)) {
61+
feature.title = featureData.title;
62+
feature.image = featureData.image;
63+
feature.description = featureData.description;
64+
feature.updatedAt = now();
65+
feature.save();
66+
message = "Feature updated successfully.";
67+
} else {
68+
message = "Feature not found for editing.";
69+
}
70+
} else {
71+
var newFeature = model("Feature").new();
72+
newFeature.title = featureData.title;
73+
newFeature.image = featureData.image;
74+
newFeature.description = featureData.description;
75+
newFeature.createdAt = now();
76+
newFeature.updatedAt = now();
77+
newFeature.save();
78+
message = "Feature created successfully.";
79+
}
80+
return message;
81+
}
82+
83+
// Soft delete feature
84+
private function softDelete(id) {
85+
var feature = model("Feature").findByKey(arguments.id);
86+
if (!isNull(feature)) {
87+
feature.deletedAt = now();
88+
feature.save();
89+
return { success = true, message = "Feature deleted successfully." };
90+
}
91+
return { success = false, message = "Feature not found." };
92+
}
93+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<cfoutput>
2+
<div class="bg-white p-4 box-shadow col-xl-12 rounded-4 mt-3">
3+
<div class="row gx-6 align-items-center">
4+
<div class="col-auto">
5+
<h1 class="fs-24 fw-bold">
6+
<cfparam name="id" default=0>
7+
<cfparam name="title" default=''>
8+
<cfparam name="image" default=''>
9+
<cfparam name="description" default=''>
10+
<cfif structKeyExists(params, "id") OR id gt 0>
11+
<cfset feature = findById(params.id)>
12+
<cfset action = "Edit">
13+
<cfset id = feature.id>
14+
<cfset title = feature.title>
15+
<cfset image = feature.image>
16+
<cfset description = feature.description>
17+
<cfelse>
18+
<cfset action = "Add">
19+
</cfif>
20+
#action# Feature
21+
</h1>
22+
</div>
23+
</div>
24+
<div class="row">
25+
<form class="row g-3 mb-6 needs-validation" id="featureForm" novalidate action="/admin/feature/store" method="post" hx-validate="true">
26+
<input name="id" type="hidden" id="id" value="#id#">
27+
28+
<div class="col-sm-6 col-md-6 mb-3">
29+
<div class="form-floating">
30+
<input name="title" type="text" placeholder="Enter title" class="form-control fs-18" id="title"
31+
aria-describedby="titleHelp" required minlength="3" maxlength="100" value="#title#">
32+
<label for="title" class="form-label fs-18 fw-medium">Title <span class="text-danger">*</span></label>
33+
<div class="invalid-feedback">Title must be between 3 and 100 characters.</div>
34+
</div>
35+
</div>
36+
<div class="col-sm-6 col-md-6 mb-3">
37+
<div class="form-floating">
38+
<input name="image" type="text" placeholder="Enter image URL" class="form-control fs-18" id="image"
39+
aria-describedby="imageHelp" value="#image#">
40+
<label for="image" class="form-label fs-18 fw-medium">Image URL</label>
41+
</div>
42+
</div>
43+
<div class="col-sm-12 mb-3">
44+
<div class="form-floating">
45+
<textarea name="description" id="description" class="form-control" placeholder="Enter description" rows="5">#description#</textarea>
46+
<label for="description" class="form-label fs-18 fw-medium">Description</label>
47+
</div>
48+
</div>
49+
50+
<div class="col-12 gy-6">
51+
<div class="row g-3 justify-content-end">
52+
<div class="col-auto">
53+
<button type="submit" class="btn bg--primary text-white px-sm-5 fs-14">Save</button>
54+
</div>
55+
<div class="col-auto">
56+
<button hx-get="/admin/feature" hx-trigger="click" hx-swap="innerHTML" hx-target="body" class="btn btn-dark px-sm-5 fs-14">Cancel</button>
57+
</div>
58+
</div>
59+
</div>
60+
</form>
61+
</div>
62+
</div>
63+
<script>
64+
(function () {
65+
'use strict'
66+
var form = document.querySelector('.needs-validation')
67+
form.addEventListener('submit', function (event) {
68+
if (!form.checkValidity()) {
69+
event.preventDefault()
70+
event.stopPropagation()
71+
}
72+
form.classList.add('was-validated')
73+
}, false)
74+
})()
75+
</script>
76+
</cfoutput>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<div class="container">
2+
<div class="bg-white p-4 box-shadow rounded-4 mt-3">
3+
<div id="flash-message" class="my-3"></div>
4+
<div class="d-flex mb-3 justify-content-between align-items-center">
5+
<div class="col-auto">
6+
<h1 class="text-center fs-24 fw-bold">Features List</h1>
7+
</div>
8+
</div>
9+
<div class="table-responsive">
10+
<table id="featureTable" class="table table-hover table-striped">
11+
<thead>
12+
<tr>
13+
<th>ID</th>
14+
<th>Title</th>
15+
<th>Description</th>
16+
<th></th>
17+
</tr>
18+
</thead>
19+
<tbody id="features-container">
20+
<cfscript>
21+
for (var i = 1; i <= features.recordCount; i++) {
22+
writeOutput('<tr> <td>' & i & '</td>');
23+
writeOutput('<td>' & features.title[i] & '</td>');
24+
writeOutput('<td>' & features.description[i] & '</td>');
25+
writeOutput('<td>
26+
<div class="dropdown">
27+
<div class="fw-bold cursor-pointer me-2" data-bs-toggle="dropdown" aria-expanded="false">
28+
...
29+
</div>
30+
<ul class="dropdown-menu">
31+
<li>
32+
<a hx-get="/admin/feature/edit/#features.id[i]#" hx-target="body" hx-trigger="click" hx-swap="innerHTML" class="dropdown-item text-success fs-16">Edit</a>
33+
</li>
34+
</ul>
35+
</div>
36+
</td>');
37+
}
38+
</cfscript>
39+
</tbody>
40+
</table>
41+
</div>
42+
</div>
43+
</div>
44+
<script type="text/javascript">
45+
var table = new DataTable('#featureTable', {
46+
columnDefs: [
47+
{
48+
targets: [3],
49+
orderable: false
50+
}
51+
]
52+
});
53+
table.on('draw', function() {
54+
htmx.process(document.body);
55+
});
56+
57+
document.body.addEventListener("htmx:afterRequest", function(event) {
58+
const xhr = event.detail.xhr;
59+
if (xhr.status === 200 && xhr.responseURL.includes("/admin/feature/deleteFeature/")) {
60+
document.querySelector("#flash-message").innerHTML = `
61+
<div class="alert alert-subtle-success alert-dismissible fade show" role="alert">
62+
Feature deleted successfully!
63+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
64+
</div>`;
65+
66+
// Auto-hide after 5 seconds
67+
setTimeout(() => {
68+
const alert = document.querySelector("#flash-message .alert");
69+
if (alert) alert.classList.remove("show");
70+
}, 5000);
71+
} else if(xhr.status === 500 && xhr.responseURL.includes("/admin/feature/deleteFeature/")){
72+
document.querySelector("#flash-message").innerHTML = `
73+
<div class="alert alert-subtle-danger alert-dismissible fade show" role="alert">
74+
Error: Feature not deleted!
75+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
76+
</div>`;
77+
}
78+
});
79+
</script>

0 commit comments

Comments
 (0)