Files
main-site/templates/blog/article_page.html
Mark c47a62df5c
Some checks failed
CI / nightly-e2e (pull_request) Has been skipped
CI / deploy (pull_request) Has been skipped
CI / pr-e2e (pull_request) Successful in 1m39s
CI / ci (pull_request) Failing after 2m26s
feat: redesign comments section for better UX/UI
- Redesigned comment cards with improved spacing and typography
- Added vertical line indicator for reply nesting
- Implemented native details/summary toggle for reply forms (replacing JS)
- Styled 'Join the conversation' section to be more distinct from existing comments
- Added solid-pink shadow to Tailwind configuration
- Updated E2E tests to match new UI structure and elements
2026-03-04 10:20:54 +00:00

287 lines
20 KiB
HTML

{% extends 'base.html' %}
{% load wagtailcore_tags wagtailimages_tags seo_tags core_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block head_meta %}
{% canonical_url page as canonical %}
{% article_og_image_url page as og_image %}
<link rel="canonical" href="{{ canonical }}" />
<meta name="description" content="{{ page.search_description|default:page.summary }}" />
<meta property="og:type" content="article" />
<meta property="og:title" content="{{ page.title }} | No Hype AI" />
<meta property="og:description" content="{{ page.search_description|default:page.summary }}" />
<meta property="og:url" content="{{ canonical }}" />
{% if og_image %}<meta property="og:image" content="{{ og_image }}" />{% endif %}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="{{ page.title }} | No Hype AI" />
<meta name="twitter:description" content="{{ page.search_description|default:page.summary }}" />
{% if og_image %}<meta name="twitter:image" content="{{ og_image }}" />{% endif %}
{% endblock %}
{% block content %}
<!-- Breadcrumb -->
<div class="mb-8 font-mono text-sm text-zinc-500">
<a href="/" class="hover:text-brand-cyan transition-colors">Home</a> /
<a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a> /
<span class="text-brand-dark dark:text-brand-light">{{ page.title|truncatechars:40 }}</span>
</div>
<!-- Article Header -->
<header class="mb-12 border-b border-zinc-200 dark:border-zinc-800 pb-12">
<div class="flex gap-3 mb-6 items-center flex-wrap">
{% for tag in page.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }} border {{ tag|get_tag_border_css }}">{{ tag.name }}</span>
{% endfor %}
<span class="text-sm font-mono text-zinc-500"><svg class="w-4 h-4 inline mr-1 -mt-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" /></svg> {{ page.first_published_at|date:"M j, Y" }}</span>
<span class="text-sm font-mono text-zinc-500"><svg class="w-4 h-4 inline mr-1 -mt-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg> {{ page.read_time_mins }} min read</span>
</div>
<h1 class="font-display font-black text-4xl md:text-6xl lg:text-7xl leading-tight mb-8">{{ page.title }}</h1>
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-gradient-to-tr from-brand-cyan to-brand-pink shrink-0"></div>
<div>
<div class="font-bold font-display text-lg">{{ page.author.name }}</div>
{% if page.author.role %}<div class="font-mono text-xs text-zinc-500">{{ page.author.role }}</div>{% endif %}
</div>
</div>
</header>
{% if page.hero_image %}
<div class="mb-12 border border-zinc-200 dark:border-zinc-800 overflow-hidden">
{% image page.hero_image width-1200 class="w-full h-auto" %}
</div>
{% endif %}
<!-- Main Content Layout -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<!-- Article Body -->
<article class="lg:col-span-8 prose prose-lg dark:prose-invert max-w-none
prose-headings:font-display prose-headings:font-bold
prose-a:text-brand-cyan hover:prose-a:text-brand-pink prose-a:transition-colors prose-a:no-underline hover:prose-a:underline
prose-img:border prose-img:border-zinc-200 dark:prose-img:border-zinc-800
prose-blockquote:border-l-brand-pink prose-blockquote:bg-brand-pink/5 prose-blockquote:py-2 prose-blockquote:not-italic
prose-code:font-mono prose-code:text-brand-cyan prose-code:bg-zinc-100 dark:prose-code:bg-zinc-900 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none">
{{ page.body }}
{% article_json_ld page %}
</article>
<!-- Sidebar -->
<aside class="lg:col-span-4 space-y-8">
<div class="sticky top-28">
<!-- Share -->
<section aria-label="Share this article" class="mb-8">
<h3 class="font-display font-bold text-lg mb-4 uppercase tracking-widest text-zinc-500 text-sm">Share Article</h3>
<div class="flex gap-2">
<a href="https://x.com/intent/post?url={{ request.build_absolute_uri|urlencode }}&text={{ page.title|urlencode }}" target="_blank" rel="noopener noreferrer"
class="w-10 h-10 flex items-center justify-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:border-brand-cyan transition-colors hover:text-brand-cyan" aria-label="Share on X">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.748l7.73-8.835L1.254 2.25H8.08l4.259 5.63L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"/></svg>
</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ request.build_absolute_uri|urlencode }}" target="_blank" rel="noopener noreferrer"
class="w-10 h-10 flex items-center justify-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:border-blue-600 transition-colors hover:text-blue-600" aria-label="Share on LinkedIn">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</a>
<button type="button" data-copy-link data-copy-url="{{ request.build_absolute_uri }}"
class="w-10 h-10 flex items-center justify-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:border-brand-pink transition-colors hover:text-brand-pink" aria-label="Copy link">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" /></svg>
</button>
</div>
</section>
<!-- Newsletter -->
<div class="bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark p-6 border border-transparent dark:border-zinc-700 shadow-solid-dark dark:shadow-solid-light">
<h3 class="font-display font-bold text-xl mb-2">Subscribe for Updates</h3>
<p class="text-sm opacity-80 mb-4">Get our latest articles and coding benchmarks delivered to your inbox every week.</p>
{% include 'components/newsletter_form.html' with source='article' label='Subscribe' %}
</div>
</div>
</aside>
</div>
<!-- Related Articles -->
{% if related_articles %}
<section class="mt-16 md:mt-24 pt-12 border-t border-zinc-200 dark:border-zinc-800">
<div class="flex items-center justify-between mb-8">
<h3 class="font-display font-bold text-3xl">Related Articles</h3>
<a href="/articles/" class="font-mono text-sm text-brand-cyan hover:underline">View All</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{% for article in related_articles %}
<article class="group flex flex-col h-full bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:-translate-y-2 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all duration-300">
<a href="{{ article.url }}" class="h-48 overflow-hidden relative bg-zinc-900 flex items-center justify-center border-b border-zinc-200 dark:border-zinc-800 shrink-0 block">
{% if article.hero_image %}
{% image article.hero_image fill-400x300 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500" %}
{% else %}
<svg class="w-16 h-16 text-brand-cyan opacity-40 group-hover:opacity-80 transition-all duration-500 group-hover:scale-110" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 1-6.23-.693L5 14.5m14.8.8 1.402 1.402c1 1 .03 2.798-1.442 2.798H4.24c-1.47 0-2.44-1.798-1.442-2.798L4.2 15.3" /></svg>
{% endif %}
</a>
<div class="p-6 flex flex-col flex-grow">
<div class="flex gap-2 mb-3 flex-wrap">
{% for tag in article.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }}">{{ tag.name }}</span>
{% endfor %}
</div>
<a href="{{ article.url }}">
<h4 class="font-display font-bold text-xl mb-2 group-hover:text-brand-cyan transition-colors">{{ article.title }}</h4>
</a>
<p class="text-zinc-600 dark:text-zinc-400 text-sm mb-6 line-clamp-2">{{ article.summary }}</p>
<div class="mt-auto pt-4 border-t border-zinc-100 dark:border-zinc-800 flex items-center gap-2 text-sm font-bold font-mono group-hover:text-brand-cyan transition-colors">
Read Article
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" /></svg>
</div>
</div>
</article>
{% endfor %}
</div>
</section>
{% endif %}
<!-- Comments -->
{% if page.comments_enabled %}
<section class="mt-20 pt-16 border-t border-zinc-200 dark:border-zinc-800">
<div class="flex items-baseline justify-between mb-10">
<h2 class="font-display font-bold text-4xl tracking-tight">Comments</h2>
<span class="font-mono text-sm text-zinc-500 uppercase tracking-widest">{{ approved_comments|length }} response{{ approved_comments|length|pluralize }}</span>
</div>
{% if approved_comments %}
<div class="space-y-12 mb-16">
{% for comment in approved_comments %}
<div class="group">
<!-- Top-level Comment -->
<article id="comment-{{ comment.id }}" class="relative bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6 sm:p-8 hover:border-brand-pink/30 transition-colors">
<div class="flex items-start gap-4 mb-4">
<div class="w-10 h-10 bg-gradient-to-tr from-brand-cyan to-brand-pink shrink-0 rounded-sm shadow-solid-dark/10 dark:shadow-solid-light/5"></div>
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-baseline gap-x-3 gap-y-1">
<span class="font-display font-bold text-base text-zinc-900 dark:text-zinc-100">{{ comment.author_name }}</span>
<time datetime="{{ comment.created_at|date:'c' }}" class="font-mono text-xs text-zinc-500 uppercase tracking-wider">{{ comment.created_at|date:"M j, Y" }}</time>
</div>
<div class="mt-3 prose prose-sm dark:prose-invert max-w-none text-zinc-700 dark:text-zinc-300 leading-relaxed">
{{ comment.body|linebreaks }}
</div>
</div>
</div>
<details class="group/details mt-6">
<summary class="list-none cursor-pointer flex items-center gap-2 font-mono text-xs font-bold uppercase tracking-widest text-zinc-500 hover:text-brand-pink transition-colors [&::-webkit-details-marker]:hidden">
<svg class="w-4 h-4 transition-transform group-open/details:-translate-y-0.5 group-open/details:translate-x-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
</svg>
<span class="group-open/details:hidden">Reply</span>
<span class="hidden group-open/details:inline text-brand-pink">Cancel Reply</span>
</summary>
<!-- Inline Reply Form -->
<div class="mt-8 pt-8 border-t border-zinc-100 dark:border-zinc-800 animate-in fade-in slide-in-from-top-2 duration-300">
<h4 class="font-display font-bold text-sm mb-4 uppercase tracking-wider">Reply to {{ comment.author_name }}</h4>
<form method="post" action="{% url 'comment_post' %}" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" />
<input type="hidden" name="parent_id" value="{{ comment.id }}" />
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<input type="text" name="author_name" required placeholder="Name *"
class="bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:ring-1 focus:ring-brand-pink transition-all" />
<input type="email" name="author_email" required placeholder="Email *"
class="bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:ring-1 focus:ring-brand-pink transition-all" />
</div>
<textarea name="body" required placeholder="Your reply..." rows="3"
class="w-full bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink focus:ring-1 focus:ring-brand-pink transition-all resize-none"></textarea>
<input type="text" name="honeypot" hidden />
<div class="flex justify-end gap-3">
<button type="submit" data-testid="post-reply-btn" class="px-6 py-2 bg-brand-pink text-white font-display font-bold text-sm shadow-solid-dark hover:-translate-y-0.5 hover:shadow-solid-dark/80 transition-all active:translate-y-0">Post Reply</button>
</div>
</form>
</div>
</details>
</article>
<!-- Nested Replies -->
{% if comment.replies.all %}
<div class="relative ml-6 sm:ml-12 mt-4 space-y-4 pl-6 sm:pl-8 border-l-2 border-zinc-100 dark:border-zinc-800">
{% for reply in comment.replies.all %}
<article id="comment-{{ reply.id }}" class="bg-zinc-50/50 dark:bg-zinc-900/30 border border-zinc-100 dark:border-zinc-800 p-5 sm:p-6">
<div class="flex items-start gap-3 mb-3">
<div class="w-8 h-8 bg-gradient-to-tr from-brand-pink to-brand-cyan shrink-0 rounded-sm"></div>
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-baseline gap-x-2">
<span class="font-display font-bold text-sm text-zinc-900 dark:text-zinc-100">{{ reply.author_name }}</span>
<time datetime="{{ reply.created_at|date:'c' }}" class="font-mono text-[10px] text-zinc-400 uppercase tracking-wider">{{ reply.created_at|date:"M j, Y" }}</time>
</div>
<div class="mt-2 prose prose-sm dark:prose-invert max-w-none text-zinc-600 dark:text-zinc-400 leading-relaxed text-sm">
{{ reply.body|linebreaks }}
</div>
</div>
</div>
</article>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<div class="bg-zinc-50 dark:bg-zinc-900/50 border-2 border-dashed border-zinc-200 dark:border-zinc-800 p-12 text-center mb-16">
<p class="font-mono text-sm text-zinc-500 uppercase tracking-widest">No comments yet. Be the first to join the conversation.</p>
</div>
{% endif %}
<!-- New Comment Form Section -->
<div class="relative bg-zinc-900 text-white dark:bg-white dark:text-zinc-900 p-8 sm:p-12 shadow-solid-pink">
<div class="max-w-2xl">
<h3 class="font-display font-bold text-3xl mb-2">Join the conversation</h3>
<p class="font-mono text-sm text-zinc-400 dark:text-zinc-500 mb-10 uppercase tracking-widest">Add your fresh comment below</p>
{% if comment_form and comment_form.errors %}
<div aria-label="Comment form errors" class="mb-8 p-4 bg-red-500/10 border border-red-500/20 font-mono text-sm text-red-400">
<div class="font-bold mb-2 uppercase tracking-widest text-xs">There were some errors:</div>
<ul class="list-disc list-inside">
{% if comment_form.non_field_errors %}
{% for error in comment_form.non_field_errors %}<li>{{ error }}</li>{% endfor %}
{% endif %}
{% for field in comment_form %}
{% if field.errors %}
{% for error in field.errors %}<li>{{ field.label }}: {{ error }}</li>{% endfor %}
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
<form method="post" action="{% url 'comment_post' %}" data-comment-form class="space-y-6">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-2">
<label class="block font-mono text-[10px] uppercase tracking-[0.2em] opacity-60">Full Name</label>
<input type="text" name="author_name" value="{% if comment_form %}{{ comment_form.author_name.value|default:'' }}{% endif %}" required
class="w-full bg-white/5 dark:bg-black/5 border-b-2 border-white/20 dark:border-black/20 px-0 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
<div class="space-y-2">
<label class="block font-mono text-[10px] uppercase tracking-[0.2em] opacity-60">Email Address</label>
<input type="email" name="author_email" value="{% if comment_form %}{{ comment_form.author_email.value|default:'' }}{% endif %}" required
class="w-full bg-white/5 dark:bg-black/5 border-b-2 border-white/20 dark:border-black/20 px-0 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
</div>
<div class="space-y-2">
<label class="block font-mono text-[10px] uppercase tracking-[0.2em] opacity-60">Your Thoughts</label>
<textarea name="body" required rows="5"
class="w-full bg-white/5 dark:bg-black/5 border-b-2 border-white/20 dark:border-black/20 px-0 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors resize-none">{% if comment_form %}{{ comment_form.body.value|default:'' }}{% endif %}</textarea>
</div>
<input type="text" name="honeypot" hidden />
<div class="pt-4">
<button type="submit" class="group relative inline-flex items-center gap-3 px-8 py-4 bg-brand-pink text-white font-display font-bold uppercase tracking-widest text-sm hover:-translate-y-1 transition-all active:translate-y-0">
<span>Post comment</span>
<svg class="w-4 h-4 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3" />
</svg>
</button>
</div>
</form>
</div>
</div>
</section>
{% endif %}
{% endblock %}