summaryrefslogtreecommitdiff
path: root/templates
diff options
context:
space:
mode:
authorTyler Hoang <tyler@tylerhoang.xyz>2026-05-06 15:25:36 -0700
committerTyler Hoang <tyler@tylerhoang.xyz>2026-05-06 15:25:36 -0700
commit1bdf4ca8c0f51718124ffe5247ac133973d4f251 (patch)
tree2904145bdbc5d2a2164cd3cb6c95346e48afd4a5 /templates
parent051775337251b0c7036959901eacb58471100862 (diff)
Add authentication, public profile, and infinite scroll
- Implement session-based auth with argon2 password hashing - Add login form and logout button in nav - Create public /tyler profile page with curated stats - Implement infinite scroll for film lists (load 20 at a time) - Add lazy loading for poster images - Fix profile page CSS to use dark theme variables - Use consistent star character (✦) across all pages - Add /films/partial endpoint for pagination Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Diffstat (limited to 'templates')
-rw-r--r--templates/_feed_partial.html14
-rw-r--r--templates/_film_card.html2
-rw-r--r--templates/base.html3
-rw-r--r--templates/detail.html2
-rw-r--r--templates/index.html7
-rw-r--r--templates/login.html60
-rw-r--r--templates/profile.html124
-rw-r--r--templates/year_review.html8
8 files changed, 212 insertions, 8 deletions
diff --git a/templates/_feed_partial.html b/templates/_feed_partial.html
new file mode 100644
index 0000000..e22cdde
--- /dev/null
+++ b/templates/_feed_partial.html
@@ -0,0 +1,14 @@
+{% if active_shelf == 'diary' and grouped_films %}
+ {% for group in grouped_films %}
+ <div class="month-group" data-month="{{ group.month }}">
+ <p class="month-label">{{ group.month }}</p>
+ {% for film in group.films %}
+ {% include "_film_card.html" %}
+ {% endfor %}
+ </div>
+ {% endfor %}
+{% else %}
+ {% for film in films %}
+ {% include "_film_card.html" %}
+ {% endfor %}
+{% endif %}
diff --git a/templates/_film_card.html b/templates/_film_card.html
index 7147a1e..5e8e6e4 100644
--- a/templates/_film_card.html
+++ b/templates/_film_card.html
@@ -1,7 +1,7 @@
<article class="film-card">
<a class="poster-frame" href="/films/{{ film.id }}" aria-label="{{ film.title }}">
{% if film.poster_url %}
- <img src="{{ film.poster_url }}" alt="{{ film.title }} poster">
+ <img src="{{ film.poster_url }}" alt="{{ film.title }} poster" loading="lazy">
{% else %}
<span>{{ film.title[:1] }}</span>
{% endif %}
diff --git a/templates/base.html b/templates/base.html
index 168efde..1041c25 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -18,6 +18,9 @@
<a class="{% if active_page == 'stats' %}is-active{% endif %}" href="/stats">Stats</a>
<a class="{% if active_page == 'import' %}is-active{% endif %}" href="/import">Import</a>
<a class="button-link" href="/films/new">Add Film</a>
+ <form method="post" action="/logout" style="display: contents;">
+ <button type="submit" style="background: none; border: none; color: var(--muted); cursor: pointer; font-size: inherit; padding: 0;">Logout</button>
+ </form>
</nav>
</header>
diff --git a/templates/detail.html b/templates/detail.html
index df3f96a..59779d8 100644
--- a/templates/detail.html
+++ b/templates/detail.html
@@ -7,7 +7,7 @@
<aside class="detail-poster">
<div class="poster-frame poster-large">
{% if film.poster_url %}
- <img src="{{ film.poster_url }}" alt="{{ film.title }} poster">
+ <img src="{{ film.poster_url }}" alt="{{ film.title }} poster" loading="lazy">
{% else %}
<span>{{ film.title[:1] }}</span>
{% endif %}
diff --git a/templates/index.html b/templates/index.html
index 52c633f..cb0e1fc 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -38,10 +38,10 @@
{% endif %}
{% if films %}
- <section class="diary-feed" aria-label="Diary entries">
+ <section class="diary-feed" id="film-feed" aria-label="Diary entries">
{% if active_shelf == 'diary' and grouped_films %}
{% for group in grouped_films %}
- <div class="month-group">
+ <div class="month-group" data-month="{{ group.month }}">
<p class="month-label">{{ group.month }}</p>
{% for film in group.films %}
{% include "_film_card.html" %}
@@ -54,6 +54,9 @@
{% endfor %}
{% endif %}
</section>
+ {% if has_more %}
+ <div id="feed-sentinel" data-shelf="{{ active_shelf }}" data-offset="20" data-total="{{ total_films }}" style="height: 1px; margin: 20px 0;"></div>
+ {% endif %}
{% else %}
<section class="empty-state">
<h2>{{ shelf_meta.empty_title }}</h2>
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..c164531
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,60 @@
+{% extends "base.html" %}
+
+{% block title %}Login - Lumière{% endblock %}
+
+{% block content %}
+<div style="max-width: 320px; margin: 60px auto; padding: 0 20px;">
+ <div style="text-align: center; margin-bottom: 40px;">
+ <h1 style="margin: 0; font-size: 32px; color: var(--text);">Lumière</h1>
+ <p style="color: var(--muted); margin: 8px 0 0 0;">Film Diary</p>
+ </div>
+
+ <form method="post" style="display: flex; flex-direction: column; gap: 16px;">
+ {% if error %}
+ <div style="
+ background: rgba(223, 110, 98, 0.1);
+ border: 1px solid rgba(223, 110, 98, 0.4);
+ color: #ffc7c0;
+ padding: 12px;
+ border-radius: 6px;
+ font-size: 14px;
+ ">{{ error }}</div>
+ {% endif %}
+
+ <div style="display: flex; flex-direction: column; gap: 6px;">
+ <label for="password" style="font-weight: 500; font-size: 14px; color: var(--text);">Password</label>
+ <input
+ type="password"
+ id="password"
+ name="password"
+ required
+ autofocus
+ style="
+ padding: 11px 12px;
+ border: 1px solid var(--line);
+ border-radius: 6px;
+ font-size: 16px;
+ background: #11100e;
+ color: var(--text);
+ "
+ />
+ </div>
+
+ <button
+ type="submit"
+ style="
+ padding: 11px 12px;
+ background: var(--accent);
+ color: #0e0a04;
+ border: none;
+ border-radius: 6px;
+ font-size: 16px;
+ font-weight: 800;
+ cursor: pointer;
+ "
+ >
+ Sign In
+ </button>
+ </form>
+</div>
+{% endblock %}
diff --git a/templates/profile.html b/templates/profile.html
new file mode 100644
index 0000000..9fac27f
--- /dev/null
+++ b/templates/profile.html
@@ -0,0 +1,124 @@
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <title>Tyler's Film Diary - Lumière</title>
+ <link rel="stylesheet" href="/static/styles.css" />
+ </head>
+ <body>
+ <div class="shell">
+ <header class="topbar">
+ <a class="brand" href="/">Lumière</a>
+ <nav class="nav-actions" aria-label="Primary">
+ <a href="/tyler">Profile</a>
+ </nav>
+ </header>
+
+ <main>
+ <div style="max-width: 1200px; margin: 0 auto; padding: 40px 20px;">
+ <!-- Hero Section -->
+ <div style="margin-bottom: 60px; padding-bottom: 40px; border-bottom: 1px solid var(--line);">
+ <h1 style="margin: 0 0 8px 0; font-size: 40px; color: var(--text);">Tyler's Film Diary</h1>
+ <p style="margin: 0; color: var(--muted); font-size: 18px;">A curated collection of films watched and loved</p>
+
+ <!-- Summary Stats -->
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 24px; margin-top: 32px;">
+ <div style="padding: 20px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px;">
+ <div style="font-size: 32px; font-weight: bold; color: var(--text);">{{ total_watched }}</div>
+ <div style="color: var(--muted); margin-top: 4px;">Films Watched</div>
+ </div>
+ <div style="padding: 20px; background: var(--panel); border: 1px solid var(--line); border-radius: 8px;">
+ <div style="font-size: 32px; font-weight: bold; color: var(--text);"><span class="rating">{% for _ in range(average_stars|int) %}✦{% endfor %}</span> {{ average_stars }}</div>
+ <div style="color: var(--muted); margin-top: 4px;">Average Rating</div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Top Directors -->
+ {% if most_watched_directors %}
+ <div style="margin-bottom: 60px;">
+ <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Top Directors</h2>
+ <div style="display: grid; gap: 12px;">
+ {% for item in most_watched_directors %}
+ <div style="padding: 12px 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; display: flex; justify-content: space-between; align-items: center;">
+ <span style="color: var(--text);">{{ item.director }}</span>
+ <span style="color: var(--muted); font-size: 14px;">{{ item.count }} film{{ 's' if item.count > 1 else '' }}</span>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+
+ <!-- Star Distribution -->
+ <div style="margin-bottom: 60px;">
+ <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Rating Distribution</h2>
+ <div style="display: grid; gap: 16px;">
+ {% for item in star_distribution %}
+ <div>
+ <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
+ <span style="color: var(--text);">
+ {% if item.stars == 0 %}No rating{% elif item.stars == 1 %}<span class="rating">✦</span>{% elif item.stars == 2 %}<span class="rating">✦✦</span>{% elif item.stars == 3 %}<span class="rating">✦✦✦</span>{% endif %}
+ </span>
+ <span style="color: var(--muted);">{{ item.count }}</span>
+ </div>
+ <div style="background: var(--panel-soft); height: 24px; border-radius: 4px; overflow: hidden;">
+ {% if total_watched > 0 %}
+ <div style="background: var(--accent); height: 100%; width: {{ (item.count / total_watched * 100) }}%; border-radius: 4px;"></div>
+ {% endif %}
+ </div>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+
+ <!-- Top Countries -->
+ {% if films_per_country %}
+ <div style="margin-bottom: 60px;">
+ <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Top Countries</h2>
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 16px;">
+ {% for item in films_per_country %}
+ <div style="padding: 16px; background: var(--panel); border: 1px solid var(--line); border-radius: 6px; text-align: center;">
+ <div style="font-size: 20px; font-weight: bold; color: var(--text);">{{ item.count }}</div>
+ <div style="color: var(--muted); margin-top: 4px; font-size: 14px;">{{ item.country }}</div>
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+
+ <!-- Recent Films -->
+ {% if recent_films %}
+ <div>
+ <h2 style="margin: 0 0 24px 0; font-size: 24px; color: var(--text);">Recently Watched</h2>
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 24px;">
+ {% for film in recent_films %}
+ <div style="text-align: center;">
+ {% if film.poster_url %}
+ <img src="{{ film.poster_url }}" alt="{{ film.title }}" loading="lazy" style="width: 100%; border-radius: 8px; margin-bottom: 12px; aspect-ratio: 2/3; object-fit: cover;" />
+ {% else %}
+ <div style="width: 100%; aspect-ratio: 2/3; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; margin-bottom: 12px; display: flex; align-items: center; justify-content: center; color: var(--muted);">No poster</div>
+ {% endif %}
+ <h3 style="margin: 0; font-size: 14px; line-height: 1.4; color: var(--text);">{{ film.title }}</h3>
+ {% if film.stars %}
+ <div style="color: var(--accent); font-size: 12px; margin-top: 4px; font-weight: 800;">
+ <span class="rating">{% for i in range(film.stars) %}✦{% endfor %}</span>
+ </div>
+ {% endif %}
+ {% if film.date_watched %}
+ <div style="color: var(--muted); font-size: 12px; margin-top: 4px;">{{ film.date_watched[:10] }}</div>
+ {% endif %}
+ </div>
+ {% endfor %}
+ </div>
+ </div>
+ {% endif %}
+ </div>
+ </main>
+
+ <footer style="text-align: center; padding: 40px 20px; color: var(--muted); font-size: 12px; border-top: 1px solid var(--line); margin-top: 60px;">
+ <p style="margin: 0;">Made with <a href="https://github.com/tylerhoang/lumiere" style="color: var(--accent); text-decoration: none;">Lumière</a></p>
+ </footer>
+ </div>
+ </body>
+</html>
diff --git a/templates/year_review.html b/templates/year_review.html
index 5339c34..1234e80 100644
--- a/templates/year_review.html
+++ b/templates/year_review.html
@@ -99,7 +99,7 @@
<article class="highlight-card">
<a class="poster-frame" href="/films/{{ film.id }}">
{% if film.poster_url %}
- <img src="{{ film.poster_url }}" alt="{{ film.title }} poster">
+ <img src="{{ film.poster_url }}" alt="{{ film.title }} poster" loading="lazy">
{% else %}
<span>{{ film.title[:1] }}</span>
{% endif %}
@@ -123,7 +123,7 @@
<article class="highlight-card">
<a class="poster-frame" href="/films/{{ highlight_films.first_watch.id }}">
{% if highlight_films.first_watch.poster_url %}
- <img src="{{ highlight_films.first_watch.poster_url }}" alt="{{ highlight_films.first_watch.title }} poster">
+ <img src="{{ highlight_films.first_watch.poster_url }}" alt="{{ highlight_films.first_watch.title }} poster" loading="lazy">
{% else %}
<span>{{ highlight_films.first_watch.title[:1] }}</span>
{% endif %}
@@ -140,7 +140,7 @@
<article class="highlight-card">
<a class="poster-frame" href="/films/{{ highlight_films.last_watch.id }}">
{% if highlight_films.last_watch.poster_url %}
- <img src="{{ highlight_films.last_watch.poster_url }}" alt="{{ highlight_films.last_watch.title }} poster">
+ <img src="{{ highlight_films.last_watch.poster_url }}" alt="{{ highlight_films.last_watch.title }} poster" loading="lazy">
{% else %}
<span>{{ highlight_films.last_watch.title[:1] }}</span>
{% endif %}
@@ -157,7 +157,7 @@
<article class="highlight-card">
<a class="poster-frame" href="/films/{{ highlight_films.most_rewatched.id }}">
{% if highlight_films.most_rewatched.poster_url %}
- <img src="{{ highlight_films.most_rewatched.poster_url }}" alt="{{ highlight_films.most_rewatched.title }} poster">
+ <img src="{{ highlight_films.most_rewatched.poster_url }}" alt="{{ highlight_films.most_rewatched.title }} poster" loading="lazy">
{% else %}
<span>{{ highlight_films.most_rewatched.title[:1] }}</span>
{% endif %}