Skip to content
This repository was archived by the owner on Apr 21, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions app/config/routes.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,20 @@

.post(name = "blog-store", pattern = "blog/store", to = "web.BlogController##store")
.post(name = "blog-comment", pattern = "blog/comment", to = "web.BlogController##comment")
.post(name = "check-title", pattern = "blog/check-title", to = "web.BlogController##checkTitle")

.get(name = "user-changePassword", pattern = "user/change-password", to = "web.userController##changePassword")
.post(name = "user-updatePassword", pattern = "user/update-Password", to = "web.userController##updatePassword")
.get(name = "user-update-profile-pic", pattern = "user/update-profile-pic", to = "web.userController##updateProfilePic")
.post(name = "user-upload-profile-pic", pattern = "user/upload-profile-pic", to = "web.userController##uploadProfilePic")

// Testimonial-specific routes
.get(name="check_testimonial", pattern="testimonials/check", to="web.testimonials##check")
.get(name="approve_testimonial", pattern="testimonials/approve/[key]", to="web.testimonials##approve")
.get(name="feature_testimonial", pattern="testimonials/feature/[key]", to="web.testimonials##feature")
.get(name="delete_testimonial", pattern="testimonials/delete/[key]", to="web.testimonials##delete")
.get(name="check_testimonial", pattern="testimonial/check", to="web.testimonials##check")
.get(name="approve_testimonial", pattern="testimonial/approve/[key]", to="web.testimonials##approve")
.get(name="feature_testimonial", pattern="testimonial/feature/[key]", to="web.testimonials##feature")
.get(name="delete_testimonial", pattern="testimonial/delete/[key]", to="web.testimonials##delete")

.post(name="clear_testimonial_prompt", pattern="testimonial/clear-prompt", to="web.Testimonial##clearPromptFlag") // Use POST to indicate an action

// routes for testimonials
.resources("web.testimonial")
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/web/AdminController.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ component extends="app.Controllers.Controller" {
function config() {
verifies(except="index,dashboard,checkAdminAccess,blog,BlogList,approve,reject,showBlog", params="key", paramsTypes="integer", handler="dashboard");

usesLayout("/web/AdminController/layout");
usesLayout(template="/web/AdminController/layout", except="BlogList");
filters(through="checkAdminAccess");
}

Expand Down
1 change: 1 addition & 0 deletions app/controllers/web/AuthController.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ component extends="app.Controllers.Controller" {
// Set a flag in the flash scope.
// Flash scope persists only for the next request, which is perfect for this.
session.promptForTestimonial = true;
cfheader(name="HX-Trigger" value="showTestimonialModal");
}
// Redirect to the intended page or default to dashboard
redirectTo(url=redirectUrl);
Expand Down
21 changes: 18 additions & 3 deletions app/controllers/web/BlogController.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ component extends="app.Controllers.Controller" {

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


// function to check title is unique
function checkTitle() {
try{
if(structKeyExists(form, "title")){
var blogModel = model("Blog").findAll(where="title='#form.title#'");
if(blogModel.recordCount != 0){
renderText('<p class="text-danger">Blog title already exist!</p>');
}else{
renderText("");
}
}
}catch (any e) {
// Handle error
renderText("Error: " & e);
}
}
// Function to update an existing blog
function update() {
var blogModel = model("Blog"); // Get model instance
Expand Down Expand Up @@ -228,7 +243,7 @@ component extends="app.Controllers.Controller" {

// Get blog posts with matching IDs
return model("Blog").findAll(
where="id IN (#arrayToList(blogIds)#)",
where="id IN (#arrayToList(blogIds)#) AND category_id = '#category.id#'",
order="createdAt DESC",
include="User,BlogCategory",
returnAs="query"
Expand Down
19 changes: 18 additions & 1 deletion app/controllers/web/TestimonialController.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ component extends="app.Controllers.Controller" {

// Configuration function
function config() {
verifies(except="index,publicList,new,create,edit,update,check,error", params="key", paramsTypes="integer", handler="error");
verifies(except="index,publicList,new,create,edit,update,check,error,clearPromptFlag", params="key", paramsTypes="integer", handler="error");
// Apply filters for security
filters(through="restrictAccess", only="new,create,edit,update,check");
filters(through="requireAdmin", only="index,approve,feature,delete");
Expand Down Expand Up @@ -624,4 +624,21 @@ component extends="app.Controllers.Controller" {
}
return true;
}

/**
* Endpoint to clear the promptForTestimonial session flag.
*/
function clearPromptFlag() {
try {
if (session.keyExists("promptForTestimonial")) {
session.delete("promptForTestimonial");
}
// Return a success response
renderWith(data={"success"=true});
} catch (any e) {
// Log error;
// Return an error response
renderWith(data={"success"=false, "message"="Error clearing flag."}, status=500);
}
}
}
2 changes: 1 addition & 1 deletion app/migrator/migrations/20250306112302_insert_records.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ component extends="wheels.migrator.Migration" hint="insert records" {
addRecord(table='Roles',name = "user");

// users
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);
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);

// categories
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.');
Expand Down
84 changes: 52 additions & 32 deletions app/views/layout.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -386,49 +386,69 @@
</div>
</div>

<script>
document.addEventListener('htmx:load', function() {
var shouldPromptForTestimonial = <cfoutput>#session.keyExists("promptForTestimonial") ? 'true' : 'false'#</cfoutput>;
var clearFlagUrl = "<cfoutput>#session.delete('promptForTestimonial')#</cfoutput>";
<cfoutput>
<cfif session.keyExists("promptForTestimonial")>
<!--- This div triggers the clearing of the session flag --->
<div id="clear-prompt-trigger"
hx-post="#urlFor(route='clear_testimonial_prompt')#"
hx-trigger="load delay:50ms" <!--- Trigger on load --->
hx-swap="none" <!--- No HTML swap needed --->
style="display: none;">
</div>

<script>
// This script block only renders if the session flag exists.

if (shouldPromptForTestimonial) {
// Get modal element immediately
var testimonialModalElement = document.getElementById('testimonialPromptModal');
var testimonialModalInstance = null;
// Flag to ensure the 'shown.bs.modal' listener is attached only once
let formLoadListenerAttached = false;

if (testimonialModalElement) {
// Get the Bootstrap Modal instance
var testimonialModal = bootstrap.Modal.getOrCreateInstance(testimonialModalElement);
// Get or create the Bootstrap modal instance right away
testimonialModalInstance = bootstrap.Modal.getOrCreateInstance(testimonialModalElement);

testimonialModalElement.addEventListener('shown.bs.modal', function () {

var formContainer = testimonialModalElement.querySelector('#testimonial-form-container');
if (formContainer) {
// Check if content is already loaded (e.g., if modal was opened, closed, reopened quickly)
// Simple check: see if it still contains the spinner/loading text
var isLoadingIndicatorPresent = formContainer.querySelector('.spinner-border');

if(isLoadingIndicatorPresent) {
console.log('Form container found, processing HTMX.'); // Debug log
// Manually process the container with HTMX to trigger the hx-get
htmx.process(formContainer);
document.body.addEventListener('showTestimonialModal', function handleShowTrigger() {
console.log('Received showTestimonialModal trigger from backend.');
if (testimonialModalInstance) {
testimonialModalInstance.show();
} else {
var currentModalInstance = bootstrap.Modal.getInstance(testimonialModalElement);
if (currentModalInstance) {
currentModalInstance.show();
} else {
console.log('Form container already has content, skipping HTMX process.'); // Debug log
console.error("Modal instance not found when trying to show via HX-Trigger.");
}
} else {
console.error('Form container #testimonial-form-container not found inside modal.');
}
}, { once: true });

}, { once: true }); // Use { once: true } so this listener only fires ONCE per modal instance show

//Show the modal
testimonialModal.show();

if (!formLoadListenerAttached && testimonialModalInstance) {
testimonialModalElement.addEventListener('shown.bs.modal', function handleModalShown() {
var formContainer = testimonialModalElement.querySelector('##testimonial-form-container');
if (formContainer) {
var isLoadingIndicatorPresent = formContainer.querySelector('.spinner-border');
if (isLoadingIndicatorPresent || formContainer.innerHTML.trim() === '') {
console.log('Modal shown, processing HTMX for form container.');
htmx.process(formContainer); // Trigger the hx-get on the container
} else {
console.log('Modal shown, form container already has content.');
}
} else {
console.error('Form container ##testimonial-form-container not found inside modal.');
}
});
formLoadListenerAttached = true; // Mark that this listener has been attached.
console.log('Attached shown.bs.modal listener.');
}
} else {
console.error("Testimonial prompt modal element not found.");
console.error("Testimonial prompt modal element not found when initializing script.");
}
}
});
</script>

</script>

</cfif>
</cfoutput>

<script src="/javascripts/swiper.js"></script>
<script src="/javascripts/custom.js"></script>
<script src="/javascripts/infinite-scroll.pkgd.min.js"></script>
Expand Down
91 changes: 43 additions & 48 deletions app/views/web/AdminController/partials/_blogs.cfm
Original file line number Diff line number Diff line change
@@ -1,53 +1,48 @@
<cfoutput>
<cfscript>
// writeDump(blogs); abort;
</cfscript>
<cfloop from="1" to="#blogs.recordCount#" index="i">
<cfset blogId = blogs.id[i]>
<cfset truncatedContent = left(blogs.content[i], 100) & "...">

<cfloop from="1" to="#blogs.recordCount#" index="i">
<cfset blogId = blogs.id[i]>
<cfset truncatedContent = left(blogs.content[i], 100) & "...">
<tr id="blog-#blogId#">
<td>#blogId#</td>
<td>#blogs.title[i]#</td>
<td>#blogs.slug[i]#</td>
<td>#blogs.name[i]#</td>
<td>#blogs.NAME[i]#</td>
<td>#blogs.POSTTYPENAME[i]#</td>
<td>#blogs.fullName[i]#</td>

<!-- Truncated content with "View More" link -->
<td>#truncatedContent#</td>

<td>
<!-- Approve Button -->
<button
hx-post="approve"
hx-vals='{"id": "#blogId#"}'
hx-target="##blog-#blogId#"
hx-confirm="Are you sure you want to approve this blog?"
class="fw-semibold bg--iris py-1 rounded-2 text-white"
>Approve</button>

<tr id="blog-#blogId#">
<td>#blogId#</td>
<td>#blogs.title[i]#</td>
<td>#blogs.slug[i]#</td>
<td>#blogs.name[i]#</td>
<td>#blogs.NAME[i]#</td>
<td>#blogs.POSTTYPENAME[i]#</td>
<td>#blogs.fullName[i]#</td>

<!-- Truncated content with "View More" link -->
<td>#truncatedContent#</td>

<td>
<!-- Approve Button -->
<button
hx-post="approve"
hx-vals='{"id": "#blogId#"}'
hx-target="##blog-#blogId#"
hx-confirm="Are you sure you want to approve this blog?"
class="fw-semibold bg--iris py-1 rounded-2 text-white"
>Approve</button>
&nbsp;&nbsp; | &nbsp;&nbsp;

&nbsp;&nbsp; | &nbsp;&nbsp;

<!-- Reject Button -->
<button
class="fw-semibold bg--primary py-1 rounded-2 text-white"
hx-post="reject"
hx-vals='{"id": "#blogId#"}'
hx-target="##blog-#blogId#"
hx-confirm="Are you sure you want to reject this blog?"
>Reject</button>
</td>
<td>
<a href="blog/#blogs.slug[i]#">
<button class="fw-semibold bg--secondary py-1 rounded-2 text-white">
View More
</button>
</a>
</td>
</tr>
</cfloop>
</table>
<!-- Reject Button -->
<button
class="fw-semibold bg--primary py-1 rounded-2 text-white"
hx-post="reject"
hx-vals='{"id": "#blogId#"}'
hx-target="##blog-#blogId#"
hx-confirm="Are you sure you want to reject this blog?"
>Reject</button>
</td>
<td>
<a href="blog/#blogs.slug[i]#">
<button class="fw-semibold bg--secondary py-1 rounded-2 text-white">
View More
</button>
</a>
</td>
</tr>
</cfloop>
</cfoutput>
Loading