Improve comment system UX and product page layout

- **Comment Form Styling**: Unified button styling across reply/edit forms with new CSS classes
- **Comment Actions**: Improved spacing, consistent button styling, and proper grid layout
- **Comment Navigation**: Added anchor redirects for delete/approve/unapprove actions
- **Product Layout**: Enhanced desktop two-column grid (2fr 1fr) with better proportions
- **Mobile Optimization**: Fixed button width and section ordering for mobile view
- **Template Consolidation**: Moved comments and description into main grid layout
- **Auto-refresh Removal**: Removed disruptive timers from content pages
This commit is contained in:
Russell Ballestrini 2025-10-04 11:45:35 -04:00
parent e67178abdb
commit 6745824fc3
7 changed files with 186 additions and 126 deletions

View file

@ -299,6 +299,49 @@ button.mps-button-red {
opacity: 0.8;
}
.mps-cancel-button,
.mps-comment-form-button {
display: inline-block;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f9f9f9;
color: #333;
font-size: 14px;
cursor: pointer;
margin: 0;
text-decoration: none;
vertical-align: top;
box-sizing: border-box;
}
.comment-actions {
margin-bottom: 42px;
display: grid;
grid-auto-flow: column;
grid-template-columns: repeat(auto-fit, max-content);
justify-content: start;
gap: 5px;
}
.reply-link,
.edit-link,
.delete-link,
.moderate-link {
display: inline-block;
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #f9f9f9;
color: #333;
font-size: 14px;
cursor: pointer;
margin: 0;
text-decoration: none;
vertical-align: top;
box-sizing: border-box;
}
a.product-edit-button {
background-color: #98b6fa;
}
@ -685,10 +728,28 @@ textarea.markup-editor-textarea {
@media (max-width: 800px) {
/* the two-column is stacked by default. */
section.two-column {
display: grid;
grid-template-columns: 1fr;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
/* On mobile, show price section before description */
section.product-left {
order: 2;
}
section.product-right {
order: 1;
width: 100%;
}
/* Ensure buttons get full width on mobile */
section.product-right .mps-button {
width: 100%;
min-width: 100%;
}
/* the cart-grid is stacked by default. */
section.cart-grid {
@ -725,13 +786,14 @@ textarea.markup-editor-textarea {
/* We have the room to really break into 2 columns */
section.two-column {
display: grid;
grid-template-columns: 1fr 1fr;
grid-auto-columns: max-content;
grid-auto-flow: dense;
gap: 0px 60px;
padding-left: 60px;
padding-right: 60px;
justify-items: center;
grid-template-columns: 2fr 1fr;
gap: 40px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
padding-left: 40px;
padding-right: 40px;
align-items: start;
}
/* if we have room, break markup-editor into 2 columns */

View file

@ -1,29 +1,31 @@
{% extends "base.j2" -%}
{% block content -%}
<section class="one-column">
<section class="well">
<h3>Edit Comment</h3>
{% block title %}Edit Comment{% endblock %}
{% block content %}
<div class="container">
<h2>Edit Comment</h2>
<div class="original-comment" style="background-color: #f5f5f5; padding: 15px; border-left: 4px solid #007cba; margin-bottom: 20px;">
<div class="comment-header">
<strong>{{ comment.user.name if comment.user else "Anonymous" }}</strong>
<span class="comment-date">{{ comment.ago_string }}</span>
</div>
<div class="comment-content">
{{ comment.data_html | safe }}
</div>
</div>
<form method="post" action="/comments/{{ comment.id }}/edit">
<label for="comment_data">Comment:</label>
<textarea name="data" id="comment_data" rows="6" cols="60" required>{{ comment.data }}</textarea>
<br />
<br />
<input type="submit" name="submit" class="mps-submit" value="Update Comment" />
<a href="{{ product.absolute_url(request) }}" class="mps-button">Cancel</a>
<br />
<br />
<div class="form-group">
<label for="comment_data">Edit Your Comment:</label>
<textarea name="data" id="comment_data" rows="6" cols="80" required placeholder="Edit your comment... (You can use Markdown formatting)">{{ comment.data }}</textarea>
</div>
<div class="form-actions">
<a href="{{ product.absolute_url(request) }}" class="mps-cancel-button">Cancel</a>
<input type="submit" value="Update Comment" class="mps-comment-form-button" />
</div>
</form>
</section>
</section>
{%- endblock -%}
</div>
{% endblock %}

View file

@ -19,13 +19,12 @@
<form method="post" action="{{ request.route_url('comment_reply', comment_id=parent_comment.id) }}">
<div class="form-group">
<label for="comment_data">Your Reply:</label>
<textarea name="data" id="comment_data" rows="6" cols="80" required placeholder="Write your reply..."></textarea>
<small>You can use Markdown formatting.</small>
<textarea name="data" id="comment_data" rows="6" cols="80" required placeholder="Write your reply... (You can use Markdown formatting)"></textarea>
</div>
<div class="form-actions">
<input type="submit" value="Post Reply" class="mps-submit" />
<a href="{{ product.absolute_url(request) }}" class="mps-cancel">Cancel</a>
<a href="{{ product.absolute_url(request) }}" class="mps-cancel-button">Cancel</a>
<input type="submit" value="Post Reply" class="mps-comment-form-button" />
</div>
</form>
</div>

View file

@ -17,16 +17,7 @@
<meta property="og:image" content="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/{{ og_thumbnail }}?ts={{ product.updated_timestamp }}" />
{%- endif %}
{% if product.has_product_file and signed_get_object_url is not none %}
<script>
// Simple web 1.5 refresh mechanism:
// redirects to self every 9 minutes to get fresh signed & secure download URIs.
// 540000 is 9 minutes in milliseconds.
setTimeout(function() {
window.location.href = window.location.href;
}, 540000);
</script>
{% endif %}
{# Removed auto-refresh timer - better UX to let download links expire than interrupt reading #}
{%- endblock append_to_head_tag_section -%}

View file

@ -17,16 +17,7 @@
<meta property="og:image" content="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/{{ og_thumbnail }}?ts={{ product.updated_timestamp }}" />
{%- endif %}
{% if product.has_product_file and signed_get_object_url is not none %}
<script>
// Simple web 1.5 refresh mechanism:
// redirects to self every 9 minutes to get fresh signed & secure download URIs.
// 540000 is 9 minutes in milliseconds.
setTimeout(function() {
window.location.href = window.location.href;
}, 540000);
</script>
{% endif %}
{# Removed auto-refresh timer - better UX to let download links expire than interrupt reading #}
{%- endblock append_to_head_tag_section -%}
{%- block call_to_action -%}
@ -41,11 +32,7 @@
<section class="product-left">
<h1>{{ product.title }}</h1>
<h3>
sold by
<a href="{{ product.shop.absolute_about_url(request) }}" rel="nofollow" class="shop-theme-link-color">{{ product.shop.name }}</a>
</h3>
<h1>{{ product.title }} <span style="font-size: 0.6em; font-weight: normal;">sold by <a href="{{ product.shop.absolute_about_url(request) }}" rel="nofollow" class="shop-theme-link-color">{{ product.shop.name }}</a></span></h1>
{% if "thumbnail1" in product.extensions %}
<img src="{{ request.app["bucket.secure_uploads.get_endpoint"] }}/{{ product.s3_path }}/thumbnail1?ts={{ product.updated_timestamp }}" class="product-main" />
@ -59,14 +46,47 @@
{% endif %}
{% endfor %}
<br/>
<br/>
<b>Description</b>
<section>{{ product.description_html | safe }}</section>
<br/>
<br/>
{% if product.is_bundle %}
{% if product.products_in_this_bundle %}
<b>Bundle Contents</b>
<ul>
{% for p in product.products_in_this_bundle %}
<li>
{{p.title}}
</li>
{% endfor %}
</ul>
{% endif %}
{% elif product.has_product_file %}
<b>File Type</b>
<br/>
{{ product.get_content_type("product") }} {{ product_size }}
<br/>
Please make sure you have an application to open this file type.
{% endif %}
<br/>
<br/>
<!-- Comments Section -->
{% include 'snippets/comments.j2' %}
<br/>
<a href="/" class="product-edit-button mps-button">Back to shop</a>
</section>
<section class="product-right">
<br>
<br>
<br>
<div class="well">
<h1>${{ '{:,.2f}'.format(product.price) }}</h1>
@ -132,46 +152,5 @@
</section>
<section class="one-column" style="max-width: 960px;">
<br/>
<b>Description</b>
<section>{{ product.description_html | safe }}</section>
<br/>
<br/>
{% if product.is_bundle %}
{% if product.products_in_this_bundle %}
<ul>
{% for p in product.products_in_this_bundle %}
<li>
{{p.title}}
</li>
{% endfor %}
</ul>
{% endif %}
{% elif product.has_product_file %}
<b>File Type</b>
<br/>
{{ product.get_content_type("product") }} {{ product_size }}
<br/>
Please make sure you have an application to open this file type.
{% endif %}
<br/>
<br/>
<!-- Comments Section -->
{% include 'snippets/comments.j2' %}
<br/>
<a href="/" class="product-edit-button mps-button">Back to shop</a>
</section>
{%- endblock -%}

View file

@ -17,30 +17,29 @@
</div>
<div class="comment-actions">
{% if request.user.authenticated and not comment.is_locked %}
{% set can_comment, error_msg = comment.can_user_comment(request.user, shop) %}
{% if can_comment %}
<a href="/comments/{{ comment.id }}/reply" class="reply-link">Reply</a>
{% if request.user.authenticated and (request.user.id == comment.user_id or comment.can_user_moderate(request.user, shop)) %}
<input type="submit" value="Delete" class="delete-link" onclick="return confirm('Are you sure you want to delete this comment?')" form="delete-{{ comment.id }}" />
<form id="delete-{{ comment.id }}" method="post" action="/comments/{{ comment.id }}/delete" style="display: none;"></form>
{% endif %}
{% if request.user.authenticated and comment.can_user_moderate(request.user, shop) %}
{% if comment.approved %}
<input type="submit" value="Unapprove" class="moderate-link" form="unapprove-{{ comment.id }}" />
<form id="unapprove-{{ comment.id }}" method="post" action="/comments/{{ comment.id }}/unapprove" style="display: none;"></form>
{% else %}
<input type="submit" value="Approve" class="moderate-link" form="approve-{{ comment.id }}" />
<form id="approve-{{ comment.id }}" method="post" action="/comments/{{ comment.id }}/approve" style="display: none;"></form>
{% endif %}
{% endif %}
{% if request.user.authenticated and (request.user.id == comment.user_id or comment.can_user_moderate(request.user, shop)) %}
<a href="/comments/{{ comment.id }}/edit" class="edit-link">Edit</a>
<form method="post" action="/comments/{{ comment.id }}/delete" style="display: inline;">
<input type="submit" value="Delete" class="delete-link" onclick="return confirm('Are you sure you want to delete this comment?')" />
</form>
{% endif %}
{% if request.user.authenticated and comment.can_user_moderate(request.user, shop) %}
{% if comment.approved %}
<form method="post" action="/comments/{{ comment.id }}/unapprove" style="display: inline;">
<input type="submit" value="Unapprove" class="moderate-link" />
</form>
{% else %}
<form method="post" action="/comments/{{ comment.id }}/approve" style="display: inline;">
<input type="submit" value="Approve" class="moderate-link" />
</form>
{% if request.user.authenticated and not comment.is_locked %}
{% set can_comment, error_msg = comment.can_user_comment(request.user, shop) %}
{% if can_comment %}
<a href="/comments/{{ comment.id }}/reply" class="reply-link">Reply</a>
{% endif %}
{% endif %}
</div>
@ -84,7 +83,7 @@
</div>
<div>
<input type="submit" value="Post Comment" />
<input type="submit" value="Post Comment" class="mps-comment-form-button" />
</div>
</form>
</div>

View file

@ -268,11 +268,23 @@ def comment_delete(request):
product_url = comment.product.absolute_url(request)
# Determine which comment to anchor to after deletion
if comment.parent_id:
# For replies, anchor to the parent comment
anchor_comment_id = comment.parent_id
else:
# For root comments, just go to the product page
anchor_comment_id = None
# Soft delete using disable method
comment.disable()
request.session.flash(("Comment deleted successfully", "success"))
return HTTPFound(location=product_url)
if anchor_comment_id:
return HTTPFound(location=f"{product_url}#comment-{anchor_comment_id}")
else:
return HTTPFound(location=product_url)
@view_config(route_name="comment_approve", request_method="POST")
@ -294,7 +306,15 @@ def comment_approve(request):
comment.stamp_updated_timestamp()
request.session.flash(("Comment approved", "success"))
return HTTPFound(location=get_referer_or_home(request))
# Determine which comment to anchor to after approval
product_url = comment.product.absolute_url(request)
if comment.parent_id:
# For replies, anchor to the parent comment
return HTTPFound(location=f"{product_url}#comment-{comment.parent_id}")
else:
# For root comments, anchor to the comment itself
return HTTPFound(location=f"{product_url}#comment-{comment.id}")
@view_config(route_name="comment_unapprove", request_method="POST")
@ -316,7 +336,15 @@ def comment_unapprove(request):
comment.stamp_updated_timestamp()
request.session.flash(("Comment unapproved", "success"))
return HTTPFound(location=get_referer_or_home(request))
# Determine which comment to anchor to after unapproval
product_url = comment.product.absolute_url(request)
if comment.parent_id:
# For replies, anchor to the parent comment
return HTTPFound(location=f"{product_url}#comment-{comment.parent_id}")
else:
# For root comments, anchor to the comment itself
return HTTPFound(location=f"{product_url}#comment-{comment.id}")
@view_config(route_name="comment_undelete", request_method="POST")