1 Commits

Author SHA1 Message Date
codex_a
bf1187bc65 fix: use plain StaticFilesStorage in dev settings
All checks were successful
CI / nightly-e2e (pull_request) Has been skipped
CI / pr-e2e (pull_request) Successful in 1m4s
CI / ci (pull_request) Successful in 1m22s
CompressedManifestStaticFilesStorage requires collectstatic to generate a
manifest before it can serve anything. Dev containers never run collectstatic
so every static asset 404s. Override to StaticFilesStorage in dev so Django
serves files directly from STATICFILES_DIRS and app static directories.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-28 20:48:56 +00:00
30 changed files with 176 additions and 926 deletions

View File

@@ -2,9 +2,6 @@ name: CI
on: on:
pull_request: pull_request:
push:
branches:
- main
schedule: schedule:
- cron: "0 2 * * *" - cron: "0 2 * * *"
@@ -191,15 +188,3 @@ jobs:
- name: Remove CI image - name: Remove CI image
if: always() if: always()
run: docker image rm -f "$CI_IMAGE" || true run: docker image rm -f "$CI_IMAGE" || true
deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to lintel-prod-01
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_SSH_HOST }}
username: deploy
key: ${{ secrets.PROD_SSH_KEY }}
script: bash /srv/sum/nohype/app/deploy/deploy.sh

View File

@@ -25,9 +25,9 @@ class SecurityHeadersMiddleware:
response["Content-Security-Policy"] = ( response["Content-Security-Policy"] = (
f"default-src 'self'; " f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; " f"script-src 'self' 'nonce-{nonce}'; "
"style-src 'self' https://fonts.googleapis.com; " "style-src 'self'; "
"img-src 'self' data: blob:; " "img-src 'self' data: blob:; "
"font-src 'self' https://fonts.gstatic.com; " "font-src 'self'; "
"connect-src 'self'; " "connect-src 'self'; "
"object-src 'none'; " "object-src 'none'; "
"base-uri 'self'; " "base-uri 'self'; "

View File

@@ -142,13 +142,6 @@ X_CONTENT_TYPE_OPTIONS = "nosniff"
CSRF_TRUSTED_ORIGINS = [u for u in os.getenv("CSRF_TRUSTED_ORIGINS", "http://localhost:8035").split(",") if u] CSRF_TRUSTED_ORIGINS = [u for u in os.getenv("CSRF_TRUSTED_ORIGINS", "http://localhost:8035").split(",") if u]
TRUSTED_PROXY_IPS = [ip.strip() for ip in os.getenv("TRUSTED_PROXY_IPS", "").split(",") if ip.strip()] TRUSTED_PROXY_IPS = [ip.strip() for ip in os.getenv("TRUSTED_PROXY_IPS", "").split(",") if ip.strip()]
STORAGES = { STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
TAILWIND_APP_NAME = "theme" TAILWIND_APP_NAME = "theme"

View File

@@ -4,20 +4,10 @@ DEBUG = True
INTERNAL_IPS = ["127.0.0.1"] INTERNAL_IPS = ["127.0.0.1"]
# Drop WhiteNoise in dev — it serves from STATIC_ROOT which is empty without # Use plain static file storage in dev — CompressedManifestStaticFilesStorage
# collectstatic, so it 404s every asset. Django's runserver serves static and # (set in base.py) requires collectstatic to have been run and will 404 on
# media files natively when DEBUG=True (via django.contrib.staticfiles + the # every asset otherwise.
# media URL pattern in urls.py). STATICFILES_STORAGE = "django.contrib.staticfiles.storage.StaticFilesStorage"
MIDDLEWARE = [m for m in MIDDLEWARE if m != "whitenoise.middleware.WhiteNoiseMiddleware"]
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
try: try:
import debug_toolbar # noqa: F401 import debug_toolbar # noqa: F401

View File

@@ -2,16 +2,8 @@ from .base import * # noqa
DEBUG = False DEBUG = False
# Behind Caddy: trust the forwarded proto header so Django knows it's HTTPS.
# SECURE_SSL_REDIRECT is intentionally off — Caddy handles HTTPS redirects
# before the request reaches Django; enabling it here causes redirect loops.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True USE_X_FORWARDED_HOST = True
SECURE_SSL_REDIRECT = False SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = [
"https://nohypeai.net",
"https://www.nohypeai.net",
]

View File

@@ -1,5 +1,3 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import RedirectView from django.views.generic import RedirectView
@@ -23,6 +21,3 @@ urlpatterns = [
path("admin/", RedirectView.as_view(url="/cms/", permanent=False)), path("admin/", RedirectView.as_view(url="/cms/", permanent=False)),
path("", include(wagtail_urls)), path("", include(wagtail_urls)),
] ]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,23 +0,0 @@
nohypeai.net, www.nohypeai.net {
encode gzip zstd
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "geolocation=(), microphone=(), camera=()"
X-Forwarded-Proto https
}
handle_path /static/* {
root * /srv/sum/nohype/static
file_server
}
handle_path /media/* {
root * /srv/sum/nohype/media
file_server
}
reverse_proxy localhost:8001
}

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
# Deploy script for No Hype AI — runs on lintel-prod-01 as deploy user.
# Called by CI after a successful push to main.
set -euo pipefail
SITE_DIR=/srv/sum/nohype
APP_DIR=${SITE_DIR}/app
cd "${SITE_DIR}"
echo "==> Pulling latest code"
git -C "${APP_DIR}" pull origin main
echo "==> Updating compose file"
cp "${APP_DIR}/docker-compose.prod.yml" "${SITE_DIR}/docker-compose.prod.yml"
echo "==> Ensuring static/media directories exist"
mkdir -p "${SITE_DIR}/static" "${SITE_DIR}/media"
echo "==> Rebuilding and recreating web container"
docker compose -f "${SITE_DIR}/docker-compose.prod.yml" up -d --no-deps --build --force-recreate web
echo "==> Waiting for health check"
for i in $(seq 1 30); do
if curl -fsS -H "Host: nohypeai.net" http://localhost:8001/ >/dev/null 2>&1; then
echo "==> Site is up"
exit 0
fi
sleep 3
done
echo "ERROR: site did not come up after 90s" >&2
docker compose -f "${SITE_DIR}/docker-compose.prod.yml" logs --tail=50 web
exit 1

View File

@@ -1,22 +0,0 @@
#!/bin/sh
set -e
python manage.py tailwind install --no-input
python manage.py tailwind build
python manage.py migrate --noinput
python manage.py collectstatic --noinput
# Set Wagtail site hostname from first entry in ALLOWED_HOSTS
python manage.py shell -c "
from wagtail.models import Site
import os
hostname = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')[0].strip()
Site.objects.update(hostname=hostname, port=443, site_name='No Hype AI')
"
exec gunicorn config.wsgi:application \
--workers 3 \
--bind 0.0.0.0:8000 \
--access-logfile - \
--error-logfile - \
--capture-output

View File

@@ -1,26 +0,0 @@
[Unit]
Description=No Hype AI (Docker Compose)
Requires=docker.service
After=docker.service network-online.target
[Service]
Type=simple
User=deploy
Group=www-data
WorkingDirectory=/srv/sum/nohype
ExecStartPre=docker compose -f docker-compose.prod.yml pull --ignore-pull-failures
ExecStart=docker compose -f docker-compose.prod.yml up --build
ExecStop=docker compose -f docker-compose.prod.yml down
Restart=always
RestartSec=10
TimeoutStartSec=300
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=sum-nohype
[Install]
WantedBy=multi-user.target

View File

@@ -1,36 +0,0 @@
services:
web:
build: app
working_dir: /app
command: /app/deploy/entrypoint.prod.sh
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
volumes:
- /srv/sum/nohype/static:/app/staticfiles
- /srv/sum/nohype/media:/app/media
ports:
- "127.0.0.1:8001:8000"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
env_file: .env
environment:
POSTGRES_DB: nohype
POSTGRES_USER: nohype
volumes:
- nohype_pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nohype -d nohype"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
restart: unless-stopped
volumes:
nohype_pg:

View File

@@ -3,9 +3,7 @@ services:
build: . build: .
working_dir: /app working_dir: /app
command: > command: >
sh -c "python manage.py tailwind install --no-input && sh -c "python manage.py migrate --noinput &&
python manage.py tailwind build &&
python manage.py migrate --noinput &&
python manage.py seed_e2e_content && python manage.py seed_e2e_content &&
python manage.py runserver 0.0.0.0:8000" python manage.py runserver 0.0.0.0:8000"
volumes: volumes:

View File

@@ -6,9 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}No Hype AI{% endblock %}</title> <title>{% block title %}No Hype AI{% endblock %}</title>
{% block head_meta %}{% endblock %} {% block head_meta %}{% endblock %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/styles.css' %}" /> <link rel="stylesheet" href="{% static 'css/styles.css' %}" />
<script nonce="{{ request.csp_nonce|default:'' }}"> <script nonce="{{ request.csp_nonce|default:'' }}">
(function(){try{if(localStorage.getItem('theme')==='light'){document.documentElement.classList.remove('dark');}}catch(e){}})(); (function(){try{if(localStorage.getItem('theme')==='light'){document.documentElement.classList.remove('dark');}}catch(e){}})();
@@ -18,18 +15,17 @@
<script src="{% static 'js/prism.js' %}" defer></script> <script src="{% static 'js/prism.js' %}" defer></script>
<script src="{% static 'js/newsletter.js' %}" defer></script> <script src="{% static 'js/newsletter.js' %}" defer></script>
</head> </head>
<body class="bg-brand-light dark:bg-brand-dark text-brand-dark dark:text-brand-light antialiased min-h-screen flex flex-col relative"> <body>
<div class="fixed inset-0 bg-grid-pattern pointer-events-none z-[-1]"></div>
{% include 'components/nav.html' %} {% include 'components/nav.html' %}
{% include 'components/cookie_banner.html' %} {% include 'components/cookie_banner.html' %}
{% if messages %} {% if messages %}
<section aria-label="Messages" class="max-w-7xl mx-auto px-6 py-2"> <section aria-label="Messages">
{% for message in messages %} {% for message in messages %}
<p class="font-mono text-sm py-2 px-4 bg-brand-cyan/10 text-brand-cyan border border-brand-cyan/20 mb-2">{{ message }}</p> <p>{{ message }}</p>
{% endfor %} {% endfor %}
</section> </section>
{% endif %} {% endif %}
<main class="flex-grow w-full max-w-7xl mx-auto px-6 py-8">{% block content %}{% endblock %}</main> <main>{% block content %}{% endblock %}</main>
{% include 'components/footer.html' %} {% include 'components/footer.html' %}
</body> </body>
</html> </html>

View File

@@ -1,40 +1,14 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load wagtailimages_tags wagtailcore_tags %} {% load wagtailimages_tags wagtailcore_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block content %} {% block content %}
<h1>{{ page.title }}</h1>
<!-- Page Header --> <p>{{ page.mission_statement }}</p>
<div class="py-8 md:py-12 border-b border-zinc-200 dark:border-zinc-800 mb-12">
<h1 class="font-display font-black text-4xl md:text-6xl mb-4">{{ page.title }}</h1>
{% if page.mission_statement %}
<p class="text-xl md:text-2xl text-zinc-600 dark:text-zinc-400 font-medium max-w-2xl">{{ page.mission_statement }}</p>
{% endif %}
</div>
<!-- Body -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<div 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">
{{ page.body|richtext }} {{ page.body|richtext }}
</div>
{% if page.featured_author %} {% if page.featured_author %}
<aside class="lg:col-span-4"> <h2>{{ page.featured_author.name }}</h2>
<div class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6 relative overflow-hidden"> <p>{{ page.featured_author.bio }}</p>
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-brand-cyan to-brand-pink"></div>
{% if page.featured_author.avatar %} {% if page.featured_author.avatar %}
<div class="w-20 h-20 mb-4 overflow-hidden border border-zinc-200 dark:border-zinc-800"> {% image page.featured_author.avatar fill-200x200 %}
{% image page.featured_author.avatar fill-80x80 class="w-full h-full object-cover" %}
</div>
{% else %}
<div class="w-20 h-20 mb-4 bg-gradient-to-tr from-brand-cyan to-brand-pink"></div>
{% endif %} {% endif %}
<h2 class="font-display font-bold text-xl mb-1">{{ page.featured_author.name }}</h2>
{% if page.featured_author.role %}<p class="font-mono text-xs text-zinc-500 mb-3">{{ page.featured_author.role }}</p>{% endif %}
{% if page.featured_author.bio %}<p class="text-sm text-zinc-600 dark:text-zinc-400">{{ page.featured_author.bio }}</p>{% endif %}
</div>
</aside>
{% endif %} {% endif %}
</div>
{% endblock %} {% endblock %}

View File

@@ -10,39 +10,26 @@
<meta property="og:url" content="{{ canonical }}" /> <meta property="og:url" content="{{ canonical }}" />
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<h1>{{ page.title }}</h1>
<!-- Page Header --> <section>
<div class="py-8 md:py-12 border-b border-zinc-200 dark:border-zinc-800 mb-12"> <h2>Filter by tag</h2>
<h1 class="font-display font-black text-4xl md:text-6xl mb-6">{{ page.title }}</h1> <a href="/articles/" {% if not active_tag %}aria-current="page"{% endif %}>All</a>
<!-- Tag Filters -->
<div class="flex flex-wrap gap-3">
<a href="/articles/" class="px-4 py-2 font-mono text-sm font-bold border transition-colors {% if not active_tag %}bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark border-transparent{% else %}bg-transparent text-zinc-600 dark:text-zinc-400 border-zinc-300 dark:border-zinc-700 hover:text-brand-dark dark:hover:text-brand-light hover:border-brand-dark dark:hover:border-brand-light{% endif %}" {% if not active_tag %}aria-current="page"{% endif %}>All</a>
{% for tag in available_tags %} {% for tag in available_tags %}
<a href="/articles/?tag={{ tag.slug }}" class="px-4 py-2 font-mono text-sm font-bold border transition-colors {% if active_tag == tag.slug %}bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark border-transparent{% else %}bg-transparent text-zinc-600 dark:text-zinc-400 border-zinc-300 dark:border-zinc-700 hover:text-brand-dark dark:hover:text-brand-light hover:border-brand-dark dark:hover:border-brand-light{% endif %}" {% if active_tag == tag.slug %}aria-current="page"{% endif %}>{{ tag.name }}</a> <a href="/articles/?tag={{ tag.slug }}" {% if active_tag == tag.slug %}aria-current="page"{% endif %}>{{ tag.name }}</a>
{% endfor %} {% endfor %}
</div> </section>
</div>
<!-- Article List -->
<div class="space-y-8">
{% for article in articles %} {% for article in articles %}
{% include 'components/article_card.html' with article=article %} {% include 'components/article_card.html' with article=article %}
{% empty %} {% empty %}
<p class="font-mono text-zinc-500 py-12 text-center">No articles found.</p> <p>No articles found.</p>
{% endfor %} {% endfor %}
</div> <nav aria-label="Pagination">
<!-- Pagination -->
{% if articles.has_previous or articles.has_next %}
<nav aria-label="Pagination" class="mt-12 flex justify-center items-center gap-4 font-mono text-sm">
{% if articles.has_previous %} {% if articles.has_previous %}
<a href="?page={{ articles.previous_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}" class="px-6 py-3 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">Previous</a> <a href="?page={{ articles.previous_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}">Previous</a>
{% endif %} {% endif %}
<span class="text-zinc-500">Page {{ articles.number }} of {{ paginator.num_pages }}</span> <span>Page {{ articles.number }} of {{ paginator.num_pages }}</span>
{% if articles.has_next %} {% if articles.has_next %}
<a href="?page={{ articles.next_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}" class="px-6 py-3 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">Next</a> <a href="?page={{ articles.next_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}">Next</a>
{% endif %} {% endif %}
</nav> </nav>
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load wagtailcore_tags wagtailimages_tags seo_tags core_tags %} {% load wagtailcore_tags wagtailimages_tags seo_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %} {% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block head_meta %} {% block head_meta %}
{% canonical_url page as canonical %} {% canonical_url page as canonical %}
@@ -17,210 +17,75 @@
{% if og_image %}<meta name="twitter:image" content="{{ og_image }}" />{% endif %} {% if og_image %}<meta name="twitter:image" content="{{ og_image }}" />{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<article>
<!-- Breadcrumb --> <h1>{{ page.title }}</h1>
<div class="mb-8 font-mono text-sm text-zinc-500"> <p>{{ page.read_time_mins }} min read</p>
<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 border-current/20">{{ tag.name }}</span>
{% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ page.first_published_at|date:"M j, Y" }}</span>
<span class="text-sm font-mono text-zinc-500">{{ 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 %} {% if page.hero_image %}
<div class="mb-12 border border-zinc-200 dark:border-zinc-800 overflow-hidden"> {% image page.hero_image fill-1200x630 %}
{% image page.hero_image width-1200 class="w-full h-auto" %}
</div>
{% endif %} {% 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 }} {{ page.body }}
{% article_json_ld page %} {% article_json_ld page %}
</article> </article>
<section aria-label="Share this article">
<!-- Sidebar --> <h2>Share</h2>
<aside class="lg:col-span-4 space-y-8"> <a href="https://x.com/intent/post?url={{ request.build_absolute_uri|urlencode }}&text={{ page.title|urlencode }}" target="_blank" rel="noopener noreferrer">Share on X</a>
<div class="sticky top-28"> <a href="https://www.linkedin.com/sharing/share-offsite/?url={{ request.build_absolute_uri|urlencode }}" target="_blank" rel="noopener noreferrer">Share on LinkedIn</a>
<button type="button" data-copy-link data-copy-url="{{ request.build_absolute_uri }}">Copy link</button>
<!-- 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> </section>
<section>
<!-- Newsletter --> <h2>Related</h2>
<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 %} {% 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 }}">{{ article.title }}</a>
<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 %} {% 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> </section>
{% endif %} <aside>
<h2>Newsletter</h2>
<!-- Comments --> {% include 'components/newsletter_form.html' with source='article' label='Never miss a post' %}
</aside>
{% if page.comments_enabled %} {% if page.comments_enabled %}
<section class="mt-16 pt-12 border-t border-zinc-200 dark:border-zinc-800"> <section>
<h2 class="font-display font-bold text-3xl mb-8">Comments</h2> <h2>Comments</h2>
{% if approved_comments %}
<div class="space-y-8 mb-12">
{% for comment in approved_comments %} {% for comment in approved_comments %}
<article id="comment-{{ comment.id }}" class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6"> <article id="comment-{{ comment.id }}">
<div class="flex items-center gap-3 mb-3"> <p><strong>{{ comment.author_name }}</strong></p>
<div class="w-8 h-8 bg-gradient-to-tr from-brand-cyan to-brand-pink shrink-0"></div> <p>{{ comment.body }}</p>
<div>
<div class="font-display font-bold text-sm">{{ comment.author_name }}</div>
<div class="font-mono text-xs text-zinc-500">{{ comment.created_at|date:"M j, Y" }}</div>
</div>
</div>
<p class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ comment.body }}</p>
{% for reply in comment.replies.all %} {% for reply in comment.replies.all %}
<article id="comment-{{ reply.id }}" class="mt-6 ml-8 bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-4"> <article id="comment-{{ reply.id }}">
<div class="flex items-center gap-3 mb-2"> <p><strong>{{ reply.author_name }}</strong></p>
<div class="w-6 h-6 bg-gradient-to-tr from-brand-pink to-brand-cyan shrink-0"></div> <p>{{ reply.body }}</p>
<div>
<div class="font-display font-bold text-sm">{{ reply.author_name }}</div>
<div class="font-mono text-xs text-zinc-500">{{ reply.created_at|date:"M j, Y" }}</div>
</div>
</div>
<p class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ reply.body }}</p>
</article> </article>
{% endfor %} {% endfor %}
<form method="post" action="{% url 'comment_post' %}" class="mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800"> <form method="post" action="{% url 'comment_post' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" /> <input type="hidden" name="article_id" value="{{ page.id }}" />
<input type="hidden" name="parent_id" value="{{ comment.id }}" /> <input type="hidden" name="parent_id" value="{{ comment.id }}" />
<div class="flex gap-3 mb-3"> <input type="text" name="author_name" required />
<input type="text" name="author_name" required placeholder="Your name" <input type="email" name="author_email" required />
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" /> <textarea name="body" required></textarea>
<input type="email" name="author_email" required placeholder="your@email.com"
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
<textarea name="body" required placeholder="Write a reply..." rows="2"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors mb-3 resize-none"></textarea>
<input type="text" name="honeypot" style="display:none" /> <input type="text" name="honeypot" style="display:none" />
<button type="submit" class="px-4 py-2 bg-zinc-200 dark:bg-zinc-800 font-display font-bold text-sm hover:bg-brand-pink hover:text-white transition-colors">Reply</button> <button type="submit">Reply</button>
</form> </form>
</article> </article>
{% empty %}
<p>No comments yet.</p>
{% endfor %}
{% if comment_form and comment_form.errors %}
<div aria-label="Comment form errors">
{{ comment_form.non_field_errors }}
{% for field in comment_form %}
{{ field.errors }}
{% endfor %} {% endfor %}
</div> </div>
{% else %}
<p class="font-mono text-sm text-zinc-500 mb-12">No comments yet. Be the first to comment.</p>
{% endif %} {% endif %}
<form method="post" action="{% url 'comment_post' %}">
{% if comment_form and comment_form.errors %}
<div aria-label="Comment form errors" class="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 font-mono text-sm text-red-600 dark:text-red-400">
{{ comment_form.non_field_errors }}
{% for field in comment_form %}{{ field.errors }}{% endfor %}
</div>
{% endif %}
<div class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="font-display font-bold text-xl mb-6">Post a Comment</h3>
<form method="post" action="{% url 'comment_post' %}" class="space-y-4">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" /> <input type="hidden" name="article_id" value="{{ page.id }}" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <input type="text" name="author_name" value="{% if comment_form %}{{ comment_form.author_name.value|default:'' }}{% endif %}" required />
<div> <input type="email" name="author_email" value="{% if comment_form %}{{ comment_form.author_email.value|default:'' }}{% endif %}" required />
<label class="block font-mono text-xs text-zinc-500 mb-1 uppercase tracking-wider">Name *</label> <textarea name="body" required>{% if comment_form %}{{ comment_form.body.value|default:'' }}{% endif %}</textarea>
<input type="text" name="author_name" value="{% if comment_form %}{{ comment_form.author_name.value|default:'' }}{% endif %}" required
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
<div>
<label class="block font-mono text-xs text-zinc-500 mb-1 uppercase tracking-wider">Email *</label>
<input type="email" name="author_email" value="{% if comment_form %}{{ comment_form.author_email.value|default:'' }}{% endif %}" required
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
</div>
<div>
<label class="block font-mono text-xs text-zinc-500 mb-1 uppercase tracking-wider">Comment *</label>
<textarea name="body" required rows="5"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 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" style="display:none" /> <input type="text" name="honeypot" style="display:none" />
<button type="submit" class="px-6 py-3 bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold hover:-translate-y-1 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all">Post comment</button> <button type="submit">Post comment</button>
</form> </form>
</div>
</section> </section>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -1,20 +1,4 @@
<div class="bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-6 my-8 flex items-start gap-4 <div class="callout icon-{{ value.icon }}">
{% if value.icon == 'warning' %}border-l-4 border-l-yellow-400{% elif value.icon == 'trophy' %}border-l-4 border-l-brand-pink{% elif value.icon == 'tip' %}border-l-4 border-l-green-500{% else %}border-l-4 border-l-brand-cyan{% endif %}"> <h3>{{ value.heading }}</h3>
<div class="shrink-0 mt-0.5"> {{ value.body }}
{% if value.icon == 'warning' %}
<svg class="w-6 h-6 text-yellow-400" 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 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>
{% elif value.icon == 'trophy' %}
<svg class="w-6 h-6 text-brand-pink" 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="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0" /></svg>
{% elif value.icon == 'tip' %}
<svg class="w-6 h-6 text-green-500" 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 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" /></svg>
{% else %}
<svg class="w-6 h-6 text-brand-cyan" 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="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /></svg>
{% endif %}
</div>
<div class="min-w-0">
{% if value.heading %}
<h4 class="font-display font-bold text-lg mb-2">{{ value.heading }}</h4>
{% endif %}
<div class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ value.body }}</div>
</div>
</div> </div>

View File

@@ -1,19 +1,5 @@
{% load wagtailcore_tags %} {% load wagtailcore_tags %}
<div class="my-8 overflow-hidden bg-[#0d1117] border border-zinc-800 shadow-xl"> <div class="code-block">
<div class="flex items-center justify-between px-4 py-2 bg-[#161b22] border-b border-zinc-800"> {% if value.filename %}<div>{{ value.filename }}</div>{% endif %}
<div class="flex gap-2"> <pre data-lang="{{ value.language }}"><code class="language-{{ value.language }}">{{ value.raw_code }}</code></pre>
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
</div>
{% if value.filename %}
<div class="font-mono text-xs text-zinc-500">{{ value.filename }}</div>
{% else %}
<div class="font-mono text-xs text-zinc-500">{{ value.language }}</div>
{% endif %}
<div class="w-8"></div>
</div>
<div class="overflow-x-auto">
<pre data-lang="{{ value.language }}" class="p-6 text-sm"><code class="language-{{ value.language }} font-mono text-zinc-300">{{ value.raw_code }}</code></pre>
</div>
</div> </div>

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load wagtailimages_tags seo_tags core_tags %} {% load seo_tags %}
{% block title %}No Hype AI{% endblock %} {% block title %}No Hype AI{% endblock %}
{% block head_meta %} {% block head_meta %}
{% canonical_url page as canonical %} {% canonical_url page as canonical %}
@@ -11,146 +11,21 @@
<meta property="og:url" content="{{ canonical }}" /> <meta property="og:url" content="{{ canonical }}" />
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<section>
<!-- Featured Article -->
{% if featured_article %} {% if featured_article %}
<section class="mb-12 md:mb-16"> <h2>{{ featured_article.title }}</h2>
<div class="flex items-center gap-2 mb-6"> <p>{{ featured_article.author.name }}</p>
<span class="w-2 h-2 rounded-full bg-brand-pink animate-pulse"></span> <p>{{ featured_article.read_time_mins }} min read</p>
<span class="font-mono text-sm font-bold uppercase tracking-widest text-zinc-500">Featured Article</span>
</div>
<article class="group cursor-pointer grid grid-cols-1 lg:grid-cols-2 gap-8 items-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-4 md:p-6 hover:border-brand-cyan dark:hover:border-brand-cyan hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all duration-300">
<a href="{{ featured_article.url }}" class="w-full h-64 md:h-[400px] overflow-hidden relative bg-zinc-100 dark:bg-zinc-900 order-2 lg:order-1 border border-zinc-200 dark:border-zinc-800 block">
{% if featured_article.hero_image %}
{% image featured_article.hero_image fill-800x600 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-700 group-hover:scale-105" %}
{% else %}
<div class="absolute inset-0 flex items-center justify-center">
<svg class="w-20 h-20 text-brand-cyan opacity-30" 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>
</div>
{% endif %} {% endif %}
</a>
<div class="flex flex-col py-2 order-1 lg:order-2">
<div class="flex gap-3 mb-4 items-center flex-wrap">
{% for tag in featured_article.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }}">{{ tag.name }}</span>
{% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ featured_article.read_time_mins }} min read</span>
</div>
<a href="{{ featured_article.url }}">
<h2 class="font-display font-black text-3xl md:text-5xl mb-4 group-hover:text-brand-cyan transition-colors leading-[1.1]">{{ featured_article.title }}</h2>
</a>
<p class="text-zinc-600 dark:text-zinc-400 mb-8 text-lg md:text-xl line-clamp-3">{{ featured_article.summary }}</p>
<div class="mt-auto flex items-center justify-between pt-4 border-t border-zinc-200 dark:border-zinc-800">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-tr from-brand-cyan to-brand-pink"></div>
<div>
<div class="text-sm font-bold font-display">{{ featured_article.author.name }}</div>
<div class="text-xs font-mono text-zinc-500">{{ featured_article.first_published_at|date:"M j, Y" }}</div>
</div>
</div>
<a href="{{ featured_article.url }}" class="text-sm font-bold font-mono group-hover:text-brand-cyan transition-colors flex items-center gap-1">
Read
<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.5 4.5 21 12m0 0-7.5 7.5M21 12H3" /></svg>
</a>
</div>
</div>
</article>
</section> </section>
{% endif %} <section>
<!-- 2-Column Editorial Layout -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<!-- Main Feed -->
<div class="lg:col-span-8">
<div class="flex items-center justify-between mb-8 pb-4 border-b border-zinc-200 dark:border-zinc-800">
<h3 class="font-display font-bold text-3xl">Latest Articles</h3>
<a href="/articles/" class="font-mono text-sm text-brand-cyan hover:underline flex items-center gap-1">View All</a>
</div>
<div class="space-y-8">
{% for article in latest_articles %} {% for article in latest_articles %}
<article class="group flex flex-col md:flex-row gap-6 items-start pb-8 border-b border-zinc-200 dark:border-zinc-800 last:border-0"> {% include 'components/article_card.html' with article=article %}
<a href="{{ article.url }}" class="w-full md:w-48 h-48 md:h-32 shrink-0 overflow-hidden relative bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 flex items-center justify-center block">
{% if article.hero_image %}
{% image article.hero_image fill-200x130 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500" %}
{% else %}
<svg class="w-12 h-12 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="flex flex-col w-full">
<div class="flex gap-3 mb-2 items-center 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 %} {% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ article.first_published_at|date:"M j" }}</span> </section>
</div> <section>
<a href="{{ article.url }}">
<h4 class="font-display font-bold text-2xl mb-2 group-hover:text-brand-cyan transition-colors">{{ article.title }}</h4>
</a>
<p class="text-zinc-600 dark:text-zinc-400 text-sm line-clamp-2 mb-3">{{ article.summary }}</p>
<a href="{{ article.url }}" class="text-xs font-mono font-bold group-hover:text-brand-cyan transition-colors mt-auto">Read article →</a>
</div>
</article>
{% endfor %}
</div>
{% if more_articles %}
<div class="mt-10">
<div class="flex items-center justify-between mb-6 pb-4 border-b border-zinc-200 dark:border-zinc-800">
<h3 class="font-display font-bold text-2xl">More Articles</h3>
</div>
<div class="space-y-6">
{% for article in more_articles %} {% for article in more_articles %}
{% include 'components/article_card.html' with article=article %} {% include 'components/article_card.html' with article=article %}
{% endfor %} {% endfor %}
</div> </section>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<aside class="lg:col-span-4 space-y-8">
<!-- Newsletter Widget -->
<div class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-brand-cyan to-brand-pink"></div>
<h4 class="font-display font-bold text-xl mb-2">Weekly Newsletter</h4>
<p class="text-zinc-600 dark:text-zinc-400 text-sm mb-4">Get our latest articles and coding benchmarks delivered to your inbox every week.</p>
{% include 'components/newsletter_form.html' with source='sidebar' label='Subscribe' %}
</div>
<!-- Popular Articles -->
{% if latest_articles %}
<div>
<h4 class="font-display font-bold mb-4 uppercase tracking-widest text-zinc-500 text-sm">Popular Articles</h4>
<ul class="space-y-4">
{% for article in latest_articles %}
<li class="group">
<a href="{{ article.url }}" class="flex gap-4 items-start">
<span class="font-display font-black text-2xl text-zinc-300 dark:text-zinc-800 group-hover:text-brand-{% cycle 'cyan' 'pink' 'cyan' 'pink' 'cyan' %} transition-colors">0{{ forloop.counter }}</span>
<div>
<h5 class="font-display font-bold text-sm leading-tight group-hover:text-brand-cyan transition-colors">{{ article.title }}</h5>
<div class="text-xs font-mono text-zinc-500 mt-1">{{ article.read_time_mins }} min read</div>
</div>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if available_tags %}
<div>
<h4 class="font-display font-bold mb-4 uppercase tracking-widest text-zinc-500 text-sm">Explore Topics</h4>
<div class="flex flex-wrap gap-2">
{% for tag in available_tags %}
<a href="/articles/?tag={{ tag.slug }}" class="px-3 py-1.5 border border-zinc-200 dark:border-zinc-800 text-sm font-mono hover:border-brand-cyan hover:text-brand-cyan transition-colors">#{{ tag.name }}</a>
{% endfor %}
</div>
</div>
{% endif %}
</aside>
</div>
{% endblock %} {% endblock %}

View File

@@ -1,31 +1,8 @@
{% load core_tags wagtailimages_tags %} {% load core_tags %}
<article class="group flex flex-col md:flex-row gap-8 items-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-4 hover:border-brand-cyan dark:hover:border-brand-cyan transition-colors"> <article>
<a href="{{ article.url }}" class="w-full md:w-1/3 h-48 md:h-full min-h-[200px] overflow-hidden relative bg-zinc-100 dark:bg-zinc-900 shrink-0 block"> <a href="{{ article.url }}">{{ article.title }}</a>
{% if article.hero_image %} <p>{{ article.summary|truncatewords:20 }}</p>
{% image article.hero_image fill-600x400 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500" %}
{% else %}
<div class="w-full h-full min-h-[200px] flex items-center justify-center bg-zinc-900">
<svg class="w-16 h-16 text-brand-cyan opacity-40 group-hover:opacity-80 transition-opacity" 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>
</div>
{% endif %}
</a>
<div class="flex flex-col py-2 w-full">
<div class="flex gap-3 mb-4 items-center flex-wrap">
{% for tag in article.tags.all %} {% 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> <span class="{{ tag|get_tag_css }}">{{ tag.name }}</span>
{% endfor %} {% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ article.first_published_at|date:"M j, Y" }}</span>
</div>
<a href="{{ article.url }}">
<h2 class="font-display font-bold text-2xl md:text-3xl mb-3 group-hover:text-brand-cyan transition-colors">{{ article.title }}</h2>
</a>
<p class="text-zinc-600 dark:text-zinc-400 mb-6 max-w-2xl line-clamp-2">{{ article.summary }}</p>
<div class="flex items-center justify-between mt-auto">
<span class="text-sm font-mono text-zinc-500">{{ article.read_time_mins }} min read</span>
<a href="{{ article.url }}" class="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>
</a>
</div>
</div>
</article> </article>

View File

@@ -1,43 +1,27 @@
{% if request.consent.requires_prompt %} {% if request.consent.requires_prompt %}
<div id="cookie-banner" class="fixed bottom-0 left-0 right-0 z-50 bg-brand-surfaceLight dark:bg-brand-surfaceDark border-t border-zinc-200 dark:border-zinc-800 shadow-lg"> <div id="cookie-banner">
<div class="max-w-7xl mx-auto px-6 py-4 flex flex-col md:flex-row items-start md:items-center justify-between gap-4"> <form method="post" action="{% url 'consent' %}">
<div class="flex-1">
<p class="font-mono text-sm text-zinc-600 dark:text-zinc-400">
We use cookies to improve your experience.
{% if site_settings and site_settings.privacy_policy_page %}
<a href="{{ site_settings.privacy_policy_page.url }}" class="text-brand-cyan hover:underline">Privacy Policy</a>
{% endif %}
</p>
</div>
<div class="flex items-center gap-3 shrink-0">
<form method="post" action="{% url 'consent' %}" class="inline">
{% csrf_token %} {% csrf_token %}
<button type="submit" name="reject_all" value="1" <button type="submit" name="accept_all" value="1">Accept all</button>
class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 font-mono text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">Reject all</button> <button type="submit" name="reject_all" value="1">Reject all</button>
</form> </form>
<form method="post" action="{% url 'consent' %}" class="inline"> <details>
<summary>Manage preferences</summary>
<form method="post" action="{% url 'consent' %}">
{% csrf_token %} {% csrf_token %}
<button type="submit" name="accept_all" value="1" <label>
class="px-4 py-2 bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold text-sm hover:bg-brand-cyan transition-colors">Accept all</button> <input type="checkbox" name="analytics" value="1" />
</form>
<details class="relative">
<summary class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 font-mono text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer list-none">Manage</summary>
<div class="absolute bottom-full right-0 mb-2 w-72 bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-4 shadow-lg">
<form method="post" action="{% url 'consent' %}" class="space-y-3">
{% csrf_token %}
<label class="flex items-center gap-3 font-mono text-sm cursor-pointer">
<input type="checkbox" name="analytics" value="1" class="accent-brand-cyan" />
Analytics cookies Analytics cookies
</label> </label>
<label class="flex items-center gap-3 font-mono text-sm cursor-pointer"> <label>
<input type="checkbox" name="advertising" value="1" class="accent-brand-pink" /> <input type="checkbox" name="advertising" value="1" />
Advertising cookies Advertising cookies
</label> </label>
<button type="submit" class="w-full px-4 py-2 bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold text-sm hover:bg-brand-cyan transition-colors">Save preferences</button> <button type="submit">Save preferences</button>
</form> </form>
</div>
</details> </details>
</div> {% if site_settings and site_settings.privacy_policy_page %}
</div> <a href="{{ site_settings.privacy_policy_page.url }}">Privacy Policy</a>
{% endif %}
</div> </div>
{% endif %} {% endif %}

View File

@@ -1,33 +1,8 @@
{% load core_tags %} {% load core_tags %}
<footer class="border-t border-zinc-200 dark:border-zinc-800 bg-brand-light dark:bg-brand-dark mt-12 py-12 text-center md:text-left"> <footer>
<div class="max-w-7xl mx-auto px-6 grid grid-cols-1 md:grid-cols-4 gap-8">
<div class="md:col-span-2">
<a href="/" class="font-display font-bold text-2xl tracking-tight mb-4 inline-block">NO HYPE AI</a>
<p class="text-zinc-500 font-mono text-sm max-w-sm mx-auto md:mx-0">
In-depth reviews and benchmarks of the latest AI coding tools.<br>
Honest analysis for developers.
</p>
</div>
<div>
<h4 class="font-display font-bold mb-4 uppercase text-sm tracking-widest text-zinc-400">Navigation</h4>
<ul class="space-y-2 font-mono text-sm text-zinc-500">
<li><a href="/" class="hover:text-brand-cyan transition-colors">Home</a></li>
<li><a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a></li>
<li><a href="/about/" class="hover:text-brand-pink transition-colors">About</a></li>
{% get_legal_pages as legal_pages %} {% get_legal_pages as legal_pages %}
{% for page in legal_pages %}
<li><a href="{{ page.url }}" class="hover:text-brand-pink transition-colors">{{ page.title }}</a></li>
{% endfor %}
</ul>
</div>
<div>
<h4 class="font-display font-bold mb-4 uppercase text-sm tracking-widest text-zinc-400">Newsletter</h4>
<p class="text-zinc-500 font-mono text-sm mb-4">Get weekly AI tool reviews.</p>
{% include 'components/newsletter_form.html' with source='footer' label='Newsletter' %} {% include 'components/newsletter_form.html' with source='footer' label='Newsletter' %}
</div> {% for page in legal_pages %}
</div> <a href="{{ page.url }}">{{ page.title }}</a>
<div class="max-w-7xl mx-auto px-6 mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800 text-center font-mono text-xs text-zinc-500 flex flex-col md:flex-row justify-between items-center gap-4"> {% endfor %}
<p>&copy; {% now "Y" %} No Hype AI. All rights reserved.</p>
<p>Honest AI tool reviews for developers.</p>
</div>
</footer> </footer>

View File

@@ -1,71 +1,7 @@
{% load static %} <nav>
<nav class="sticky top-0 z-50 backdrop-blur-md bg-brand-light/80 dark:bg-brand-dark/80 border-b border-zinc-200 dark:border-zinc-800 transition-colors"> <a href="/">Home</a>
<div class="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between"> <a href="/articles/">Articles</a>
<!-- Logo --> <a href="/about/">About</a>
<a href="/" class="group flex items-center gap-2"> <button type="button" data-theme-toggle>Toggle theme</button>
<div class="w-8 h-8 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark flex items-center justify-center font-display font-bold text-xl group-hover:rotate-12 transition-transform"> {% include 'components/newsletter_form.html' with source='nav' label='Get updates' %}
/
</div>
<span class="font-display font-bold text-2xl tracking-tight">NO HYPE AI</span>
</a>
<!-- Desktop Links -->
<div class="hidden md:flex items-center gap-8 font-medium">
<a href="/" class="hover:text-brand-cyan transition-colors">Home</a>
<a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a>
<a href="/about/" class="hover:text-brand-pink transition-colors">About</a>
<form method="post" action="/newsletter/subscribe/" data-newsletter-form class="flex items-center gap-2" id="nav-newsletter">
{% csrf_token %}
<input type="hidden" name="source" value="nav" />
<input type="email" name="email" required placeholder="dev@example.com"
class="bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-brand-dark dark:text-brand-light font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors w-44" />
<input type="text" name="honeypot" style="display:none" />
<button type="submit" class="px-5 py-2.5 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark font-display font-bold hover:-translate-y-1 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all border border-transparent dark:border-zinc-700 whitespace-nowrap">Subscribe</button>
<p data-newsletter-message aria-live="polite" class="font-mono text-xs text-brand-cyan min-h-[1rem]"></p>
</form>
</div>
<!-- Theme Toggle + Hamburger -->
<div class="flex items-center gap-4">
<button type="button" data-theme-toggle class="p-2 rounded-full hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors" aria-label="Toggle theme">
<svg class="w-5 h-5 hidden dark:block text-yellow-400" 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 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" /></svg>
<svg class="w-5 h-5 block dark:hidden text-zinc-700" 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="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" /></svg>
</button>
<button type="button" data-mobile-menu-toggle class="md:hidden p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded transition-colors" aria-label="Open menu" aria-expanded="false" aria-controls="mobile-menu">
<svg class="w-6 h-6" 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="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>
</button>
</div>
</div>
</nav> </nav>
<!-- Mobile Menu (outside <nav> to avoid duplicate form[data-newsletter-form] in nav scope) -->
<div id="mobile-menu" class="md:hidden hidden sticky top-20 z-40 border-b border-zinc-200 dark:border-zinc-800 bg-brand-light/95 dark:bg-brand-dark/95 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-6 py-4 flex flex-col gap-4">
<a href="/" class="font-medium py-2 hover:text-brand-cyan transition-colors">Home</a>
<a href="/articles/" class="font-medium py-2 hover:text-brand-cyan transition-colors">Articles</a>
<a href="/about/" class="font-medium py-2 hover:text-brand-pink transition-colors">About</a>
<form method="post" action="/newsletter/subscribe/" data-newsletter-form class="space-y-2 pt-2 border-t border-zinc-200 dark:border-zinc-800" id="mobile-newsletter">
{% csrf_token %}
<input type="hidden" name="source" value="nav-mobile" />
<input type="email" name="email" required placeholder="dev@example.com"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-brand-dark dark:text-brand-light font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
<input type="text" name="honeypot" style="display:none" />
<button type="submit" class="w-full px-4 py-2.5 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark font-display font-bold transition-colors">Subscribe</button>
<p data-newsletter-message aria-live="polite" class="font-mono text-xs text-brand-cyan min-h-[1rem]"></p>
</form>
</div>
</div>
<script nonce="{{ request.csp_nonce|default:'' }}">
(function(){
var btn = document.querySelector('[data-mobile-menu-toggle]');
var menu = document.getElementById('mobile-menu');
if (btn && menu) {
btn.addEventListener('click', function(){
var isOpen = !menu.classList.contains('hidden');
menu.classList.toggle('hidden');
btn.setAttribute('aria-expanded', String(!isOpen));
});
}
})();
</script>

View File

@@ -1,12 +1,11 @@
<form method="post" action="/newsletter/subscribe/" data-newsletter-form class="space-y-3" id="newsletter"> <form method="post" action="/newsletter/subscribe/" data-newsletter-form>
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="source" value="{{ source|default:'unknown' }}" /> <input type="hidden" name="source" value="{{ source|default:'unknown' }}" />
<input type="email" name="email" required placeholder="dev@example.com" <label>
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 py-2 text-brand-dark dark:text-brand-light font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" /> <span>{{ label|default:"Newsletter" }}</span>
<input type="email" name="email" required />
</label>
<input type="text" name="honeypot" style="display:none" /> <input type="text" name="honeypot" style="display:none" />
<button type="submit" <button type="submit">Subscribe</button>
class="w-full bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold py-2 hover:bg-brand-pink dark:hover:bg-brand-pink hover:text-white transition-colors"> <p data-newsletter-message aria-live="polite"></p>
{{ label|default:"Subscribe" }}
</button>
<p data-newsletter-message aria-live="polite" class="font-mono text-xs text-brand-cyan min-h-[1rem]"></p>
</form> </form>

View File

@@ -1,18 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load wagtailcore_tags %} {% load wagtailcore_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block content %} {% block content %}
<h1>{{ page.title }}</h1>
<!-- Page Header --> <p>Last updated: {{ page.last_updated|date:'F Y' }}</p>
<div class="py-8 md:py-12 border-b border-zinc-200 dark:border-zinc-800 mb-12">
<h1 class="font-display font-black text-4xl md:text-5xl mb-3">{{ page.title }}</h1>
<p class="font-mono text-sm text-zinc-500">Last updated: {{ page.last_updated|date:'F Y' }}</p>
</div>
<!-- Body -->
<div class="max-w-3xl prose prose-lg dark:prose-invert
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">
{{ page.body|richtext }} {{ page.body|richtext }}
</div>
{% endblock %} {% endblock %}

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,6 @@
"name": "nohype-theme", "name": "nohype-theme",
"version": "1.0.0", "version": "1.0.0",
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"tailwindcss": "^3.4.17" "tailwindcss": "^3.4.17"
} }
}, },
@@ -94,33 +93,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/any-promise": { "node_modules/any-promise": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",

View File

@@ -7,7 +7,6 @@
"dev": "tailwindcss -i ./src/input.css -o ../static/css/styles.css --watch" "dev": "tailwindcss -i ./src/input.css -o ../static/css/styles.css --watch"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"tailwindcss": "^3.4.17" "tailwindcss": "^3.4.17"
} }
} }

View File

@@ -1,40 +1,3 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer utilities {
.bg-grid-pattern {
background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px);
background-size: 24px 24px;
}
.dark .bg-grid-pattern {
background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px);
}
.text-gradient {
background: linear-gradient(135deg, #06b6d4, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
::selection {
background-color: #ec4899;
color: #ffffff;
}
pre {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
pre::-webkit-scrollbar {
height: 6px;
}
pre::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
body {
transition: background-color 0.3s ease, color 0.3s ease;
}

View File

@@ -1,34 +1,10 @@
module.exports = { module.exports = {
darkMode: 'class',
content: [ content: [
"../../templates/**/*.html", "../../templates/**/*.html",
"../../apps/**/templates/**/*.html", "../../apps/**/templates/**/*.html"
], ],
theme: { theme: {
extend: { extend: {}
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Space Grotesk', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
}, },
colors: { plugins: []
brand: {
cyan: '#06b6d4',
cyanGlow: 'rgba(6,182,212,0.4)',
pink: '#ec4899',
dark: '#09090b',
light: '#fafafa',
surfaceDark: '#18181b',
surfaceLight: '#ffffff',
},
},
boxShadow: {
'neon-cyan': '0 0 20px rgba(6, 182, 212, 0.3)',
'neon-pink': '0 0 20px rgba(236, 72, 153, 0.3)',
'solid-dark': '6px 6px 0px 0px #09090b',
'solid-light': '6px 6px 0px 0px #e4e4e7',
},
},
},
plugins: [require('@tailwindcss/typography')],
}; };