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

Commit ead095a

Browse files
authored
Merge pull request #40 from paiindustries/feature/testimonial
Feature/testimonial
2 parents 5790572 + 5ec4881 commit ead095a

17 files changed

Lines changed: 596 additions & 263 deletions

File tree

app/config/routes.cfm

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,17 +73,20 @@
7373
7474
.post(name = "blog-store", pattern = "blog/store", to = "web.BlogController##store")
7575
.post(name = "blog-comment", pattern = "blog/comment", to = "web.BlogController##comment")
76+
.post(name = "check-title", pattern = "blog/check-title", to = "web.BlogController##checkTitle")
7677
7778
.get(name = "user-changePassword", pattern = "user/change-password", to = "web.userController##changePassword")
7879
.post(name = "user-updatePassword", pattern = "user/update-Password", to = "web.userController##updatePassword")
7980
.get(name = "user-update-profile-pic", pattern = "user/update-profile-pic", to = "web.userController##updateProfilePic")
8081
.post(name = "user-upload-profile-pic", pattern = "user/upload-profile-pic", to = "web.userController##uploadProfilePic")
8182
8283
// Testimonial-specific routes
83-
.get(name="check_testimonial", pattern="testimonials/check", to="web.testimonials##check")
84-
.get(name="approve_testimonial", pattern="testimonials/approve/[key]", to="web.testimonials##approve")
85-
.get(name="feature_testimonial", pattern="testimonials/feature/[key]", to="web.testimonials##feature")
86-
.get(name="delete_testimonial", pattern="testimonials/delete/[key]", to="web.testimonials##delete")
84+
.get(name="check_testimonial", pattern="testimonial/check", to="web.testimonials##check")
85+
.get(name="approve_testimonial", pattern="testimonial/approve/[key]", to="web.testimonials##approve")
86+
.get(name="feature_testimonial", pattern="testimonial/feature/[key]", to="web.testimonials##feature")
87+
.get(name="delete_testimonial", pattern="testimonial/delete/[key]", to="web.testimonials##delete")
88+
89+
.post(name="clear_testimonial_prompt", pattern="testimonial/clear-prompt", to="web.Testimonial##clearPromptFlag") // Use POST to indicate an action
8790
8891
// routes for testimonials
8992
.resources("web.testimonial")

app/controllers/web/AdminController.cfc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ component extends="app.Controllers.Controller" {
44
function config() {
55
verifies(except="index,dashboard,checkAdminAccess,blog,BlogList,approve,reject,showBlog", params="key", paramsTypes="integer", handler="dashboard");
66

7-
usesLayout("/web/AdminController/layout");
7+
usesLayout(template="/web/AdminController/layout", except="BlogList");
88
filters(through="checkAdminAccess");
99
}
1010

app/controllers/web/AuthController.cfc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ component extends="app.Controllers.Controller" {
5555
// Set a flag in the flash scope.
5656
// Flash scope persists only for the next request, which is perfect for this.
5757
session.promptForTestimonial = true;
58+
cfheader(name="HX-Trigger" value="showTestimonialModal");
5859
}
5960
// Redirect to the intended page or default to dashboard
6061
redirectTo(url=redirectUrl);

app/controllers/web/BlogController.cfc

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ component extends="app.Controllers.Controller" {
33

44
// Configuration function
55
function config() {
6-
verifies(except="index,create,store,show,update,destroy,loadCategories,loadStatuses,loadPostTypes,Categories,blogs,comment,feed,error", params="key", paramsTypes="integer", handler="index");
6+
verifies(except="index,create,store,show,update,destroy,loadCategories,loadStatuses,loadPostTypes,Categories,blogs,comment,feed,error,checkTitle", params="key", paramsTypes="integer", handler="index");
77
filters(through="restrictAccess", only="create,store,comment");
88
usesLayout("/layout");
99
}
@@ -128,7 +128,22 @@ component extends="app.Controllers.Controller" {
128128
}
129129
}
130130

131-
131+
// function to check title is unique
132+
function checkTitle() {
133+
try{
134+
if(structKeyExists(form, "title")){
135+
var blogModel = model("Blog").findAll(where="title='#form.title#'");
136+
if(blogModel.recordCount != 0){
137+
renderText('<p class="text-danger">Blog title already exist!</p>');
138+
}else{
139+
renderText("");
140+
}
141+
}
142+
}catch (any e) {
143+
// Handle error
144+
renderText("Error: " & e);
145+
}
146+
}
132147
// Function to update an existing blog
133148
function update() {
134149
var blogModel = model("Blog"); // Get model instance
@@ -228,7 +243,7 @@ component extends="app.Controllers.Controller" {
228243

229244
// Get blog posts with matching IDs
230245
return model("Blog").findAll(
231-
where="id IN (#arrayToList(blogIds)#)",
246+
where="id IN (#arrayToList(blogIds)#) AND category_id = '#category.id#'",
232247
order="createdAt DESC",
233248
include="User,BlogCategory",
234249
returnAs="query"

app/controllers/web/TestimonialController.cfc

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ component extends="app.Controllers.Controller" {
33

44
// Configuration function
55
function config() {
6-
verifies(except="index,publicList,new,create,edit,update,check,error", params="key", paramsTypes="integer", handler="error");
6+
verifies(except="index,publicList,new,create,edit,update,check,error,clearPromptFlag", params="key", paramsTypes="integer", handler="error");
77
// Apply filters for security
88
filters(through="restrictAccess", only="new,create,edit,update,check");
99
filters(through="requireAdmin", only="index,approve,feature,delete");
@@ -624,4 +624,21 @@ component extends="app.Controllers.Controller" {
624624
}
625625
return true;
626626
}
627+
628+
/**
629+
* Endpoint to clear the promptForTestimonial session flag.
630+
*/
631+
function clearPromptFlag() {
632+
try {
633+
if (session.keyExists("promptForTestimonial")) {
634+
session.delete("promptForTestimonial");
635+
}
636+
// Return a success response
637+
renderWith(data={"success"=true});
638+
} catch (any e) {
639+
// Log error;
640+
// Return an error response
641+
renderWith(data={"success"=false, "message"="Error clearing flag."}, status=500);
642+
}
643+
}
627644
}

app/migrator/migrations/20250306112302_insert_records.cfc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ component extends="wheels.migrator.Migration" hint="insert records" {
99
addRecord(table='Roles',name = "user");
1010

1111
// users
12-
addRecord(table="users", first_name="Peter", last_name="Amiri", email="petera@pai.com", password_hash="$2a$10$P27CV/m.aramHhIxJTmzzu4dxIGfNqHWzLgVGJJTLDpXymnt4jPZu", profile_picture='/images/avatar-rounded.webp', profile_url='', status=1, role_id=1);
12+
addRecord(table="users", first_name="Peter", last_name="Amiri", email="petera@pai.com", password_hash="$2a$10$P27CV/m.aramHhIxJTmzzu4dxIGfNqHWzLgVGJJTLDpXymnt4jPZu", profile_picture='avatar-rounded.webp', profile_url='', status=1, role_id=1);
1313

1414
// categories
1515
addRecord(table='categories', name='CLI', parent_id='', description='Learn about command-line tools, tips, and tricks for enhancing your development workflow using the command line.');

app/views/layout.cfm

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -386,49 +386,69 @@
386386
</div>
387387
</div>
388388

389-
<script>
390-
document.addEventListener('htmx:load', function() {
391-
var shouldPromptForTestimonial = <cfoutput>#session.keyExists("promptForTestimonial") ? 'true' : 'false'#</cfoutput>;
392-
var clearFlagUrl = "<cfoutput>#session.delete('promptForTestimonial')#</cfoutput>";
389+
<cfoutput>
390+
<cfif session.keyExists("promptForTestimonial")>
391+
<!--- This div triggers the clearing of the session flag --->
392+
<div id="clear-prompt-trigger"
393+
hx-post="#urlFor(route='clear_testimonial_prompt')#"
394+
hx-trigger="load delay:50ms" <!--- Trigger on load --->
395+
hx-swap="none" <!--- No HTML swap needed --->
396+
style="display: none;">
397+
</div>
398+
399+
<script>
400+
// This script block only renders if the session flag exists.
393401
394-
if (shouldPromptForTestimonial) {
402+
// Get modal element immediately
395403
var testimonialModalElement = document.getElementById('testimonialPromptModal');
404+
var testimonialModalInstance = null;
405+
// Flag to ensure the 'shown.bs.modal' listener is attached only once
406+
let formLoadListenerAttached = false;
396407
397408
if (testimonialModalElement) {
398-
// Get the Bootstrap Modal instance
399-
var testimonialModal = bootstrap.Modal.getOrCreateInstance(testimonialModalElement);
409+
// Get or create the Bootstrap modal instance right away
410+
testimonialModalInstance = bootstrap.Modal.getOrCreateInstance(testimonialModalElement);
400411
401-
testimonialModalElement.addEventListener('shown.bs.modal', function () {
402-
403-
var formContainer = testimonialModalElement.querySelector('#testimonial-form-container');
404-
if (formContainer) {
405-
// Check if content is already loaded (e.g., if modal was opened, closed, reopened quickly)
406-
// Simple check: see if it still contains the spinner/loading text
407-
var isLoadingIndicatorPresent = formContainer.querySelector('.spinner-border');
408-
409-
if(isLoadingIndicatorPresent) {
410-
console.log('Form container found, processing HTMX.'); // Debug log
411-
// Manually process the container with HTMX to trigger the hx-get
412-
htmx.process(formContainer);
412+
document.body.addEventListener('showTestimonialModal', function handleShowTrigger() {
413+
console.log('Received showTestimonialModal trigger from backend.');
414+
if (testimonialModalInstance) {
415+
testimonialModalInstance.show();
416+
} else {
417+
var currentModalInstance = bootstrap.Modal.getInstance(testimonialModalElement);
418+
if (currentModalInstance) {
419+
currentModalInstance.show();
413420
} else {
414-
console.log('Form container already has content, skipping HTMX process.'); // Debug log
421+
console.error("Modal instance not found when trying to show via HX-Trigger.");
415422
}
416-
} else {
417-
console.error('Form container #testimonial-form-container not found inside modal.');
418423
}
424+
}, { once: true });
419425
420-
}, { once: true }); // Use { once: true } so this listener only fires ONCE per modal instance show
421-
422-
//Show the modal
423-
testimonialModal.show();
424-
426+
if (!formLoadListenerAttached && testimonialModalInstance) {
427+
testimonialModalElement.addEventListener('shown.bs.modal', function handleModalShown() {
428+
var formContainer = testimonialModalElement.querySelector('##testimonial-form-container');
429+
if (formContainer) {
430+
var isLoadingIndicatorPresent = formContainer.querySelector('.spinner-border');
431+
if (isLoadingIndicatorPresent || formContainer.innerHTML.trim() === '') {
432+
console.log('Modal shown, processing HTMX for form container.');
433+
htmx.process(formContainer); // Trigger the hx-get on the container
434+
} else {
435+
console.log('Modal shown, form container already has content.');
436+
}
437+
} else {
438+
console.error('Form container ##testimonial-form-container not found inside modal.');
439+
}
440+
});
441+
formLoadListenerAttached = true; // Mark that this listener has been attached.
442+
console.log('Attached shown.bs.modal listener.');
443+
}
425444
} else {
426-
console.error("Testimonial prompt modal element not found.");
445+
console.error("Testimonial prompt modal element not found when initializing script.");
427446
}
428-
}
429-
});
430-
</script>
431-
447+
</script>
448+
449+
</cfif>
450+
</cfoutput>
451+
432452
<script src="/javascripts/swiper.js"></script>
433453
<script src="/javascripts/custom.js"></script>
434454
<script src="/javascripts/infinite-scroll.pkgd.min.js"></script>
Lines changed: 43 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,48 @@
11
<cfoutput>
2-
<cfscript>
3-
// writeDump(blogs); abort;
4-
</cfscript>
2+
<cfloop from="1" to="#blogs.recordCount#" index="i">
3+
<cfset blogId = blogs.id[i]>
4+
<cfset truncatedContent = left(blogs.content[i], 100) & "...">
55

6-
<cfloop from="1" to="#blogs.recordCount#" index="i">
7-
<cfset blogId = blogs.id[i]>
8-
<cfset truncatedContent = left(blogs.content[i], 100) & "...">
6+
<tr id="blog-#blogId#">
7+
<td>#blogId#</td>
8+
<td>#blogs.title[i]#</td>
9+
<td>#blogs.slug[i]#</td>
10+
<td>#blogs.name[i]#</td>
11+
<td>#blogs.NAME[i]#</td>
12+
<td>#blogs.POSTTYPENAME[i]#</td>
13+
<td>#blogs.fullName[i]#</td>
14+
15+
<!-- Truncated content with "View More" link -->
16+
<td>#truncatedContent#</td>
17+
18+
<td>
19+
<!-- Approve Button -->
20+
<button
21+
hx-post="approve"
22+
hx-vals='{"id": "#blogId#"}'
23+
hx-target="##blog-#blogId#"
24+
hx-confirm="Are you sure you want to approve this blog?"
25+
class="fw-semibold bg--iris py-1 rounded-2 text-white"
26+
>Approve</button>
927

10-
<tr id="blog-#blogId#">
11-
<td>#blogId#</td>
12-
<td>#blogs.title[i]#</td>
13-
<td>#blogs.slug[i]#</td>
14-
<td>#blogs.name[i]#</td>
15-
<td>#blogs.NAME[i]#</td>
16-
<td>#blogs.POSTTYPENAME[i]#</td>
17-
<td>#blogs.fullName[i]#</td>
18-
19-
<!-- Truncated content with "View More" link -->
20-
<td>#truncatedContent#</td>
21-
22-
<td>
23-
<!-- Approve Button -->
24-
<button
25-
hx-post="approve"
26-
hx-vals='{"id": "#blogId#"}'
27-
hx-target="##blog-#blogId#"
28-
hx-confirm="Are you sure you want to approve this blog?"
29-
class="fw-semibold bg--iris py-1 rounded-2 text-white"
30-
>Approve</button>
28+
&nbsp;&nbsp; | &nbsp;&nbsp;
3129

32-
&nbsp;&nbsp; | &nbsp;&nbsp;
33-
34-
<!-- Reject Button -->
35-
<button
36-
class="fw-semibold bg--primary py-1 rounded-2 text-white"
37-
hx-post="reject"
38-
hx-vals='{"id": "#blogId#"}'
39-
hx-target="##blog-#blogId#"
40-
hx-confirm="Are you sure you want to reject this blog?"
41-
>Reject</button>
42-
</td>
43-
<td>
44-
<a href="blog/#blogs.slug[i]#">
45-
<button class="fw-semibold bg--secondary py-1 rounded-2 text-white">
46-
View More
47-
</button>
48-
</a>
49-
</td>
50-
</tr>
51-
</cfloop>
52-
</table>
30+
<!-- Reject Button -->
31+
<button
32+
class="fw-semibold bg--primary py-1 rounded-2 text-white"
33+
hx-post="reject"
34+
hx-vals='{"id": "#blogId#"}'
35+
hx-target="##blog-#blogId#"
36+
hx-confirm="Are you sure you want to reject this blog?"
37+
>Reject</button>
38+
</td>
39+
<td>
40+
<a href="blog/#blogs.slug[i]#">
41+
<button class="fw-semibold bg--secondary py-1 rounded-2 text-white">
42+
View More
43+
</button>
44+
</a>
45+
</td>
46+
</tr>
47+
</cfloop>
5348
</cfoutput>

0 commit comments

Comments
 (0)