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

Commit 5252f73

Browse files
authored
Merge pull request #36 from paiindustries/feature/testimonial
testimonial working update
2 parents 3362016 + 6c33136 commit 5252f73

6 files changed

Lines changed: 234 additions & 18 deletions

File tree

app/config/routes.cfm

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@
8080
.post(name = "user-upload-profile-pic", pattern = "user/upload-profile-pic", to = "web.userController##uploadProfilePic")
8181
8282
// Testimonial-specific routes
83-
.get(name="check_testimonial", pattern="testimonials/check", to="testimonials##check")
84-
.get(name="approve_testimonial", pattern="testimonials/approve/[key]", to="testimonials##approve")
85-
.get(name="feature_testimonial", pattern="testimonials/feature/[key]", to="testimonials##feature")
86-
.get(name="delete_testimonial", pattern="testimonials/delete/[key]", to="testimonials##delete")
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")
8787
8888
// routes for testimonials
89-
.resources("testimonials")
89+
.resources("web.testimonial")
9090
.end()
9191
9292
.namespace("")

app/controllers/web/AuthController.cfc

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,28 @@ component extends="app.Controllers.Controller" {
4040
}
4141

4242
private function handleLoginSuccess() {
43-
var redirectUrl = session.keyExists("redirectAfterLogin") ? session.redirectAfterLogin : urlFor(route="home");
44-
45-
// Clear the session variable after use
46-
structDelete(session, "redirectAfterLogin");
43+
try {
44+
45+
// Get the full user object (assuming session.user might just have the ID)
46+
var redirectUrl = session.keyExists("redirectAfterLogin") ? session.redirectAfterLogin : urlFor(route="home");
47+
48+
// Clear the session variable after use
49+
structDelete(session, "redirectAfterLogin");
50+
51+
var userToCheck = model("User").findByKey(session.userID);
4752

48-
// Redirect to the intended page or default to dashboard
49-
redirectTo(url=redirectUrl);
53+
// Check if the user has submitted a testimonial
54+
if (IsObject(userToCheck) && !userToCheck.hasSubmittedTestimonial()) {
55+
// Set a flag in the flash scope.
56+
// Flash scope persists only for the next request, which is perfect for this.
57+
session.promptForTestimonial = true;
58+
}
59+
// Redirect to the intended page or default to dashboard
60+
redirectTo(url=redirectUrl);
61+
} catch (any e) {
62+
// Log the error, but don't prevent login
63+
}
64+
redirectTo(route="home");
5065
}
5166

5267
function logout() {

app/controllers/web/TestimonialController.cfc

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ component extends="app.Controllers.Controller" {
4646
// Check if user already has a testimonial
4747
user = model("User").findByKey(GetSignedInUserId());
4848
if (user.hasSubmittedTestimonial()) {
49-
flashInsert(info="You have already submitted a testimonial. You can edit it below.");
50-
redirectTo(action="edit");
49+
// flashInsert(info="You have already submitted a testimonial. You can edit it below.");
50+
// redirectTo(action="edit");
5151
return;
5252
}
5353

@@ -568,11 +568,11 @@ component extends="app.Controllers.Controller" {
568568
}
569569

570570
/**
571-
* Helper method to check if request is AJAX
572-
*/
573-
private function isHtmx() {
574-
return StructKeyExists(cgi, "HTTP_X_REQUESTED_WITH") &&
575-
cgi.HTTP_X_REQUESTED_WITH == "XMLHttpRequest";
571+
* Helper method to check if the request was initiated by HTMX.
572+
*/
573+
private boolean function isHtmx() {
574+
// HTMX requests include the HX-Request header set to "true"
575+
return StructKeyExists(cgi, "HTTP_HX_REQUEST") && cgi.HTTP_HX_REQUEST == "true";
576576
}
577577

578578
/**

app/views/layout.cfm

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,82 @@
352352
</div>
353353
</footer>
354354

355+
<div class="modal fade" id="testimonialPromptModal" tabindex="-1" aria-labelledby="testimonialPromptModalLabel" aria-hidden="true">
356+
<div class="modal-dialog">
357+
<div class="modal-content">
358+
<div class="modal-header">
359+
<h5 class="modal-title" id="testimonialPromptModalLabel">Share Your Experience!</h5>
360+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
361+
</div>
362+
<div class="modal-body">
363+
<p>We'd love to hear about your experience! Would you take a moment to share a testimonial?</p>
364+
365+
<!--- HTMX Target Area --->
366+
<div id="testimonial-form-container"
367+
hx-get="<cfoutput>#urlFor(route='newWeb.testimonial')#</cfoutput>"
368+
hx-target="this"
369+
hx-swap="innerHTML">
370+
<!--- Optional: Loading indicator --->
371+
<div class="text-center p-3">
372+
<div class="spinner-border text-primary" role="status">
373+
<span class="visually-hidden">Loading form...</span>
374+
</div>
375+
<p class="mt-2">Loading form...</p>
376+
</div>
377+
<!--- The form content from TestimonialController::new will be loaded here --->
378+
</div>
379+
380+
</div>
381+
<div class="modal-footer">
382+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
383+
<!--- The submit button will be part of the loaded form --->
384+
</div>
385+
</div>
386+
</div>
387+
</div>
388+
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>";
393+
394+
if (shouldPromptForTestimonial) {
395+
var testimonialModalElement = document.getElementById('testimonialPromptModal');
396+
397+
if (testimonialModalElement) {
398+
// Get the Bootstrap Modal instance
399+
var testimonialModal = bootstrap.Modal.getOrCreateInstance(testimonialModalElement);
400+
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);
413+
} else {
414+
console.log('Form container already has content, skipping HTMX process.'); // Debug log
415+
}
416+
} else {
417+
console.error('Form container #testimonial-form-container not found inside modal.');
418+
}
419+
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+
425+
} else {
426+
console.error("Testimonial prompt modal element not found.");
427+
}
428+
}
429+
});
430+
</script>
355431

356432
<script src="/javascripts/swiper.js"></script>
357433
<script src="/javascripts/custom.js"></script>
File renamed without changes.

app/views/web/Testimonial/new.cfm

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<form method="post"
2+
action="/wb.testimonial/create" <!--- Standard form action (fallback) --->
3+
enctype="multipart/form-data"
4+
hx-post="/web.testimonials/create" <!--- HTMX action --->
5+
hx-target="#testimonial-form-container" <!--- Target the container holding this form --->
6+
hx-swap="outerHTML" <!--- Replace the container with the response --->
7+
hx-encoding="multipart/form-data"> <!--- Needed for file uploads --->
8+
9+
<!--- Placeholder for server-side validation messages (HTMX will replace this whole form on error/success) --->
10+
<div id="form-messages"></div>
11+
12+
<!--- Company Name --->
13+
<div class="mb-3">
14+
<label for="companyName" class="form-label">Company Name</label>
15+
<input type="text"
16+
name="companyName"
17+
id="companyName"
18+
class="form-control"
19+
required>
20+
</div>
21+
22+
<!--- Testimonial Text --->
23+
<div class="mb-3">
24+
<label for="testimonialText" class="form-label">Your Testimonial</label>
25+
<textarea name="testimonialText"
26+
id="testimonialText"
27+
class="form-control"
28+
rows="5"
29+
required
30+
minlength="20"
31+
maxlength="500"></textarea>
32+
<div class="form-text">Please share your experience (20-500 characters).</div>
33+
</div>
34+
35+
<!--- Rating --->
36+
<div class="mb-3">
37+
<label class="form-label">Rating (1-5 Stars)</label>
38+
<div class="d-flex">
39+
<!-- Loop from 1 to 5 -->
40+
<div class="form-check form-check-inline">
41+
<input class="form-check-input" type="radio" name="rating" id="rating1" value="1" required>
42+
<label class="form-check-label" for="rating1">1 Star</label>
43+
</div>
44+
<div class="form-check form-check-inline">
45+
<input class="form-check-input" type="radio" name="rating" id="rating2" value="2">
46+
<label class="form-check-label" for="rating2">2 Stars</label>
47+
</div>
48+
<div class="form-check form-check-inline">
49+
<input class="form-check-input" type="radio" name="rating" id="rating3" value="3">
50+
<label class="form-check-label" for="rating3">3 Stars</label>
51+
</div>
52+
<div class="form-check form-check-inline">
53+
<input class="form-check-input" type="radio" name="rating" id="rating4" value="4">
54+
<label class="form-check-label" for="rating4">4 Stars</label>
55+
</div>
56+
<div class="form-check form-check-inline">
57+
<input class="form-check-input" type="radio" name="rating" id="rating5" value="5">
58+
<label class="form-check-label" for="rating5">5 Stars</label>
59+
</div>
60+
</div>
61+
</div>
62+
63+
<!--- Experience Level --->
64+
<div class="mb-3">
65+
<label for="experienceLevel" class="form-label">Your Experience Level with Wheels.dev</label>
66+
<select name="experienceLevel"
67+
id="experienceLevel"
68+
class="form-select"
69+
required>
70+
<option value="" selected disabled>-- Select Level --</option>
71+
<option value="Beginner">Beginner</option>
72+
<option value="Intermediate">Intermediate</option>
73+
<option value="Advanced">Advanced</option>
74+
<option value="Expert">Expert</option>
75+
</select>
76+
</div>
77+
78+
<!--- Website URL --->
79+
<div class="mb-3">
80+
<label for="websiteUrl" class="form-label">Company Website (Optional)</label>
81+
<input type="url"
82+
name="websiteUrl"
83+
id="websiteUrl"
84+
class="form-control"
85+
placeholder="https://...">
86+
</div>
87+
88+
<!--- Social Media Links --->
89+
<div class="mb-3">
90+
<label for="socialMediaLinks" class="form-label">Social Media Link (Optional)</label>
91+
<input type="url"
92+
name="socialMediaLinks"
93+
id="socialMediaLinks"
94+
class="form-control"
95+
placeholder="https://linkedin.com/in/...">
96+
</div>
97+
98+
<!--- Logo Upload --->
99+
<div class="mb-3">
100+
<label for="logo" class="form-label">Company Logo (Optional)</label>
101+
<input type="file"
102+
name="logo"
103+
id="logo"
104+
class="form-control"
105+
accept="image/jpeg, image/png, image/gif, image/webp"> <!--- Hint for file types --->
106+
<div class="form-text">Upload JPG, PNG, GIF, or WEBP.</div>
107+
</div>
108+
109+
<!--- Display Permission --->
110+
<div class="mb-3 form-check">
111+
<input type="checkbox"
112+
name="displayPermission"
113+
id="displayPermission"
114+
value="true" <!--- Value sent when checked --->
115+
class="form-check-input">
116+
<label class="form-check-label" for="displayPermission">I grant permission to display this testimonial publicly.</label>
117+
<input type="hidden" name="displayPermission" value="false">
118+
</div>
119+
120+
<!--- Submit Button --->
121+
<div class="d-grid">
122+
<button type="submit" class="btn btn-primary">Submit Testimonial</button>
123+
</div>
124+
125+
</form>

0 commit comments

Comments
 (0)