Skip to content

Commit 076e109

Browse files
Michael Borckclaude
andcommitted
Refresh landing/app copy and improve auth + dashboard UX
The landing page still advertised three question types and never mentioned cohort mode or image choice, so it described the app as it was several features ago. The same stale "multiple choice, word cloud, or a rating scale" line had also been copied into the dashboard's empty state. Landing: cover all nine question types (grouped into four cards rather than enumerated), add cohort mode and AI drafting to the feature grid, and update the meta description and hero copy to match. Logout: land on the public page instead of the login form — signing out is not a prompt to sign back in. The flash is dropped rather than moved: the landing renders no flash block, so the message would have sat queued and surfaced on some later page. Its test asserted the flash string, so it now asserts the redirect target and that the session is actually cleared. Login: split panel carrying the landing's visual system, with the join link for audience members who land here by mistake, and the autocomplete hints the form was missing. Dashboard: the session list had no heading and used the same card treatment as the detail panel, so nothing distinguished navigator from canvas — it is now a labelled list of flat rows on one surface. The empty state told a brand-new user to "select a session from the list" when the list was empty; it now branches on whether there is anything to select. The list's empty message is also contextual, so filtering to an empty Archived tab explains itself instead of showing a blank box. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 7ea85d6 commit 076e109

7 files changed

Lines changed: 160 additions & 61 deletions

File tree

classpulse/auth.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,10 @@ def login():
194194
@login_required
195195
def logout():
196196
session.clear()
197-
flash("You have been logged out.", "info")
198-
return redirect(url_for('login'))
197+
# Land on the public page rather than the login form: signing out is not
198+
# a prompt to sign back in. No flash — the landing template renders no
199+
# flash block, so a queued message would surface on some later page.
200+
return redirect(url_for('index'))
199201

200202
@app.route('/register', methods=['GET', 'POST'])
201203
@limiter.limit("20 per hour", methods=["POST"])

static/css/tailwind.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

static/src/input.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ body { font-family: 'Inter', sans-serif; }
151151
.cp-landing .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1.2rem; }
152152
.cp-landing .cp-card .ico { width: 2.6rem; height: 2.6rem; border-radius: 10px; display: grid; place-items: center; font-size: 1.3rem; margin-bottom: 0.9rem; background-image: theme('backgroundImage.brand-gradient-soft'); }
153153
.cp-landing .cp-card h3 { margin: 0 0 0.35rem; font-size: 1.1rem; }
154+
.cp-landing .cp-card h3 .opt { font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; vertical-align: middle; margin-left: 0.4rem; padding: 0.15rem 0.45rem; border-radius: 999px; color: #5b6178; background: #eef0f7; }
154155
.cp-landing .cp-card p { margin: 0; color: #5b6178; font-size: 0.95rem; }
155156

156157
/* question types */
@@ -161,6 +162,7 @@ body { font-family: 'Inter', sans-serif; }
161162
.cp-landing .qchip.mc { color: #4f46e5; background: rgba(79,70,229,0.1); }
162163
.cp-landing .qchip.wc { color: #db2777; background: rgba(236,72,153,0.1); }
163164
.cp-landing .qchip.rt { color: #7c3aed; background: rgba(139,92,246,0.1); }
165+
.cp-landing .qchip.im { color: #0d9488; background: rgba(13,148,136,0.1); }
164166
.cp-landing .qtype h3 { margin: 0 0 0.3rem; font-size: 1.05rem; }
165167
.cp-landing .qtype p { margin: 0; color: #5b6178; font-size: 0.95rem; }
166168

@@ -187,3 +189,36 @@ body { font-family: 'Inter', sans-serif; }
187189
.cp-landing .nav .links .lnk.hide-sm { display: none; }
188190
.cp-landing .band { padding: 2rem 1.3rem; }
189191
}
192+
193+
/* auth (login / register) — split panel carrying the landing's visual system */
194+
.cp-auth { display: grid; grid-template-columns: 1.05fr 1fr; max-width: 56rem; margin: 0 auto; background: #fff; border: 1px solid #e6e8f2; border-radius: 20px; overflow: hidden; box-shadow: 0 18px 40px -24px rgba(11,15,36,0.35); }
195+
.cp-auth .side { position: relative; overflow: hidden; background: #0b0f24; background-image: theme('backgroundImage.brand-gradient'); color: #eef1ff; padding: 2.6rem 2.4rem; display: flex; flex-direction: column; }
196+
.cp-auth .side .brand { display: inline-flex; align-items: center; gap: 0.55rem; font-weight: 800; font-size: 1.15rem; color: #fff; letter-spacing: -0.01em; }
197+
.cp-auth .side h2 { font-size: 1.6rem; line-height: 1.25; letter-spacing: -0.02em; margin: 1.6rem 0 0.6rem; color: #fff; }
198+
.cp-auth .side h2 em { font-style: normal; color: #f0abfc; }
199+
.cp-auth .side p { color: rgba(255,255,255,0.8); font-size: 0.95rem; margin: 0; }
200+
.cp-auth .side ul { list-style: none; margin: 1.6rem 0 0; padding: 0; display: grid; gap: 0.6rem; }
201+
.cp-auth .side li { position: relative; padding-left: 1.5rem; font-size: 0.92rem; color: rgba(255,255,255,0.85); }
202+
.cp-auth .side li::before { content: '✓'; position: absolute; left: 0; color: #f0abfc; font-weight: 700; }
203+
.cp-auth .side .side-foot { margin-top: auto; padding-top: 1.8rem; font-size: 0.88rem; color: rgba(255,255,255,0.75); }
204+
.cp-auth .side .side-foot a { color: #fff; text-decoration: underline; text-underline-offset: 2px; }
205+
.cp-auth .panel { padding: 2.6rem 2.4rem; display: flex; flex-direction: column; justify-content: center; }
206+
.cp-auth .panel h1 { font-size: 1.4rem; letter-spacing: -0.02em; margin: 0 0 0.35rem; }
207+
.cp-auth .panel .panel-sub { color: #5b6178; font-size: 0.93rem; margin: 0 0 1.6rem; }
208+
.cp-auth .field { margin-bottom: 1rem; }
209+
.cp-auth .field label { display: block; font-size: 0.85rem; font-weight: 600; color: #3b4055; margin-bottom: 0.35rem; }
210+
.cp-auth .field input { display: block; width: 100%; padding: 0.6rem 0.8rem; border: 1px solid #d7dae8; border-radius: 10px; font-size: 0.95rem; background: #fff; }
211+
.cp-auth .field input:focus { outline: none; border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79,70,229,0.15); }
212+
.cp-auth .panel .alt { color: #5b6178; font-size: 0.88rem; text-align: center; margin: 0.55rem 0 0; }
213+
.cp-auth .panel .alt a { color: #4f46e5; font-weight: 600; }
214+
.cp-auth .panel .alt a:hover { color: #3730a3; }
215+
.cp-auth .panel .rule { display: flex; align-items: center; gap: 0.75rem; margin: 1.4rem 0 1rem; color: #9aa0b5; font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.06em; }
216+
.cp-auth .panel .rule::before, .cp-auth .panel .rule::after { content: ''; height: 1px; flex: 1; background: #e6e8f2; }
217+
218+
@media (max-width: 820px) {
219+
.cp-auth { grid-template-columns: 1fr; }
220+
.cp-auth .side { padding: 2rem 1.6rem; }
221+
.cp-auth .side ul, .cp-auth .side .side-foot { display: none; }
222+
.cp-auth .side h2 { font-size: 1.35rem; margin-top: 1.1rem; }
223+
.cp-auth .panel { padding: 2rem 1.6rem; }
224+
}

templates/dashboard.html

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@
66

77
{# ===== Left: sessions list ===== #}
88
<aside class="md:col-span-1 {{ 'hidden md:block' if selected else 'block' }}">
9-
<button id="new-session" class="btn btn-primary w-full mb-3">+ New session</button>
9+
<h2 class="text-xs font-bold uppercase tracking-wider text-gray-500 mb-2">Sessions</h2>
10+
<button id="new-session" class="js-new-session btn btn-primary w-full mb-3">+ New session</button>
1011
<input id="session-search" placeholder="Search sessions…"
1112
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm mb-2">
1213
<div class="flex gap-2 text-xs mb-3">
1314
<button data-tab="active" class="tab-btn px-2 py-1 rounded bg-indigo-600 text-white">Active</button>
1415
<button data-tab="archived" class="tab-btn px-2 py-1 rounded bg-white border">Archived</button>
1516
</div>
16-
<div id="session-list" class="space-y-2">
17+
{# One bordered surface with flat rows — the list reads as a navigator,
18+
distinct from the card stack in the detail panel on the right. #}
19+
<div id="session-list" class="bg-white border border-gray-200 rounded-lg overflow-hidden divide-y divide-gray-100">
1720
{% for s in sessions %}
1821
<a href="{{ url_for('manage_session', session_id=s.id) }}"
19-
class="session-row block bg-white rounded-lg shadow p-3 {{ 'ring-2 ring-indigo-500' if selected and selected.id == s.id }}"
22+
class="session-row block px-3 py-2.5 border-l-4 hover:bg-gray-50 {{ 'border-indigo-500 bg-indigo-50' if selected and selected.id == s.id else 'border-transparent' }}"
2023
data-name="{{ s.name|lower }}" data-archived="{{ '1' if s.archived else '0' }}">
2124
<div class="flex items-center justify-between">
2225
<span class="font-medium text-gray-800 truncate">{{ s.name }}</span>
@@ -25,7 +28,7 @@
2528
<div class="text-xs text-gray-500 mt-0.5">{{ s.question_count }} question{{ '' if s.question_count == 1 else 's' }} · {{ s.code }}</div>
2629
</a>
2730
{% endfor %}
28-
<p id="list-empty" class="text-sm text-gray-400 px-1 {{ 'hidden' if sessions }}">No sessions yet — create your first one.</p>
31+
<p id="list-empty" class="text-sm text-gray-400 px-3 py-4 {{ 'hidden' if sessions }}">No sessions yet — create your first one.</p>
2932
</div>
3033
</aside>
3134

@@ -67,7 +70,7 @@ <h3 class="text-lg font-semibold">Questions <span id="q-count" class="text-gray-
6770
<button id="add-question" class="btn btn-primary text-sm">+ Add question</button>
6871
</div>
6972
<div id="question-list" class="space-y-2"></div>
70-
<p id="q-empty" class="text-sm text-gray-400 hidden">No questions yet. Click <span class="font-medium">+ Add question</span> to create your first one — multiple choice, word cloud, or a rating scale.</p>
73+
<p id="q-empty" class="text-sm text-gray-400 hidden">No questions yet. Click <span class="font-medium">+ Add question</span> to create your first one — a poll, word cloud, rating scale, ranking, image choice, and more.</p>
7174
</div>
7275

7376
{# Cohort mode: audience-proposed questions #}
@@ -85,8 +88,19 @@ <h3 class="text-lg font-semibold">Audience proposals <span id="pp-count" class="
8588
</div>
8689
</div>
8790
{% else %}
88-
<div class="cp-card p-10 text-center text-gray-400">
89-
Select a session from the list, or create one to get started.
91+
{# Branch on whether there is anything to select — telling someone to pick
92+
from an empty list is a dead end for a brand-new account. #}
93+
<div class="cp-card p-10 text-center">
94+
{% if sessions %}
95+
<div class="text-3xl mb-3">👈</div>
96+
<h3 class="text-lg font-semibold text-gray-700">No session selected</h3>
97+
<p class="text-sm text-gray-500 mt-1">Pick a session from the list to add questions, present it, or see results.</p>
98+
{% else %}
99+
<div class="text-3xl mb-3"></div>
100+
<h3 class="text-lg font-semibold text-gray-700">Create your first session</h3>
101+
<p class="text-sm text-gray-500 mt-1 mb-4">A session holds your questions and gives your audience a join code and QR to scan.</p>
102+
<button class="js-new-session btn btn-primary">+ New session</button>
103+
{% endif %}
90104
</div>
91105
{% endif %}
92106
</section>
@@ -147,12 +161,26 @@ <h3 class="text-lg font-semibold">Audience proposals <span id="pp-count" class="
147161
let tab = 'active';
148162
function filterList() {
149163
const term = (document.getElementById('session-search')?.value || '').toLowerCase();
150-
document.querySelectorAll('.session-row').forEach(row => {
164+
const rows = document.querySelectorAll('.session-row');
165+
let visible = 0;
166+
rows.forEach(row => {
151167
const archived = row.dataset.archived === '1';
152168
const matchTab = (tab === 'archived') === archived;
153169
const matchTerm = row.dataset.name.includes(term);
154-
row.style.display = (matchTab && matchTerm) ? '' : 'none';
170+
const show = matchTab && matchTerm;
171+
row.style.display = show ? '' : 'none';
172+
if (show) visible++;
155173
});
174+
// Say why the list is empty — filtered out reads very differently from "none yet".
175+
const empty = document.getElementById('list-empty');
176+
if (!empty) return;
177+
empty.classList.toggle('hidden', visible > 0);
178+
if (!visible) {
179+
empty.textContent = rows.length === 0 ? 'No sessions yet — create your first one.'
180+
: term ? 'No sessions match your search.'
181+
: tab === 'archived' ? 'No archived sessions.'
182+
: 'No active sessions.';
183+
}
156184
}
157185
document.getElementById('session-search')?.addEventListener('input', filterList);
158186
document.querySelectorAll('.tab-btn').forEach(b => b.addEventListener('click', () => {
@@ -166,11 +194,11 @@ <h3 class="text-lg font-semibold">Audience proposals <span id="pp-count" class="
166194
}));
167195
filterList();
168196

169-
document.getElementById('new-session').addEventListener('click', async () => {
197+
document.querySelectorAll('.js-new-session').forEach(btn => btn.addEventListener('click', async () => {
170198
const { ok, data } = await api('{{ url_for("api_create_session") }}', { name: 'Untitled session' });
171199
if (ok && data.success) window.location.href = '/sessions/' + data.session.id;
172200
else alert((data && data.message) || 'Could not create session.');
173-
});
201+
}));
174202

175203
/* ---------- per-session actions ---------- */
176204
if (SID !== null) {

templates/landing.html

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>ClassPulse — real-time audience engagement for live sessions</title>
7-
<meta name="description" content="ClassPulse turns any room into a live conversation. Build interactive sessions with polls, word clouds, and rating scales; your audience joins with a QR code and results stream in real time.">
7+
<meta name="description" content="ClassPulse turns any room into a live conversation. Build interactive sessions with nine question types — polls, word clouds, rankings, rating scales, and image choice; your audience joins with a QR code and results stream in real time.">
88
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
99
<link href="{{ url_for('static', filename='css/tailwind.css') }}" rel="stylesheet">
1010
</head>
@@ -32,7 +32,7 @@
3232
<div>
3333
<span class="pill"><span class="pulse-dot animate-pulse-dot" style="background:#f0abfc"></span> Real-time audience engagement</span>
3434
<h1>Turn any room into a <em>live conversation</em>.</h1>
35-
<p class="lead">ClassPulse lets you run instant polls, word clouds, and rating scales. Your audience joins with a QR code or session link and responses stream in, live, as you present.</p>
35+
<p class="lead">ClassPulse lets you run instant polls, word clouds, rankings, and rating scales — and even let the room propose its own questions. Your audience joins with a QR code or session link, and responses stream in, live, as you present.</p>
3636
<div class="cta">
3737
{% if session.get('user_id') %}
3838
<a class="btn btn-primary" href="{{ url_for('dashboard') }}">Open your dashboard →</a>
@@ -92,12 +92,22 @@ <h3>Join in seconds</h3>
9292
<div class="cp-card">
9393
<div class="ico">📊</div>
9494
<h3>Live result visualizations</h3>
95-
<p>Bar charts, word clouds, and rating summaries render instantly and stay in sync as responses arrive.</p>
95+
<p>Bar charts, word clouds, rating breakdowns, ranking order, and side-by-side image tallies render instantly and stay in sync as responses arrive.</p>
9696
</div>
9797
<div class="cp-card">
9898
<div class="ico">🗂️</div>
99-
<h3>Question types that fit</h3>
100-
<p>Multiple choice for quick polls, word clouds for open reflection, and rating scales for structured feedback.</p>
99+
<h3>Nine question types</h3>
100+
<p>Multiple choice, select-all, ranking, numeric, rating, short answer, word cloud, image choice, and poll-with-other — mix them freely in one session.</p>
101+
</div>
102+
<div class="cp-card">
103+
<div class="ico">🙋</div>
104+
<h3>Let the room ask</h3>
105+
<p>In cohort mode your audience anonymously proposes questions and upvotes each other's. Proposals pass a keyword filter before they surface, and flagged ones wait for your approval.</p>
106+
</div>
107+
<div class="cp-card">
108+
<div class="ico"></div>
109+
<h3>AI-assisted drafting <span class="opt">optional</span></h3>
110+
<p>Stuck on wording? Draft and refine questions with an AI provider you configure — or leave it switched off entirely.</p>
101111
</div>
102112
<div class="cp-card">
103113
<div class="ico">⬇️</div>
@@ -115,24 +125,29 @@ <h3>Sessions &amp; archive</h3>
115125

116126
<section id="types">
117127
<div class="wrap">
118-
<p class="eyebrow">Three question types, endless uses</p>
128+
<p class="eyebrow">Nine question types, endless uses</p>
119129
<h2>The right question for the moment</h2>
120130
<p class="sub">Mix and match within a single session to keep your audience engaged and surface exactly the insight you need.</p>
121131
<div class="grid">
122132
<div class="qtype">
123-
<span class="tag"><span class="qchip mc">Multiple choice</span></span>
133+
<span class="tag"><span class="qchip mc">Choice</span></span>
124134
<h3>Polls &amp; comprehension checks</h3>
125-
<p>Present options and collect structured responses — perfect for quick votes, knowledge checks, and guided discussion.</p>
135+
<p>Multiple choice for quick votes and knowledge checks, select-all when more than one answer fits, or poll-with-other to catch the option you didn't think of.</p>
126136
</div>
127137
<div class="qtype">
128-
<span class="tag"><span class="qchip wc">Word cloud</span></span>
138+
<span class="tag"><span class="qchip wc">Open text</span></span>
129139
<h3>Open reflection</h3>
130-
<p>Collect free-text responses and watch a word cloud form in real time — great for brainstorming and sentiment.</p>
140+
<p>Collect free-text responses and read them as a list, or watch a word cloud form in real time — great for brainstorming and sentiment.</p>
131141
</div>
132142
<div class="qtype">
133-
<span class="tag"><span class="qchip rt">Rating</span></span>
143+
<span class="tag"><span class="qchip rt">Scales</span></span>
134144
<h3>Structured feedback</h3>
135-
<p>Collect numerical ratings on a defined scale to gauge confidence, satisfaction, or agreement at a glance.</p>
145+
<p>Rating scales to gauge confidence or agreement, numeric answers with an optional range, and ranking to have the room order a set of items.</p>
146+
</div>
147+
<div class="qtype">
148+
<span class="tag"><span class="qchip im">Image choice</span></span>
149+
<h3>Visual decisions</h3>
150+
<p>Upload images or link to your own, then let the audience vote. Results show the images themselves with live tallies — ideal for design critique and spot-the-difference.</p>
136151
</div>
137152
</div>
138153
</div>
@@ -146,7 +161,7 @@ <h2>How it works</h2>
146161
<div class="steps">
147162
<div class="step">
148163
<h3>Create a session</h3>
149-
<p>From your dashboard, spin up a session and add a few questions — multiple choice, word cloud, or rating.</p>
164+
<p>From your dashboard, spin up a session and add a few questions — a poll, a word cloud, a rating scale, or any of the nine types.</p>
150165
</div>
151166
<div class="step">
152167
<h3>Audience joins</h3>

0 commit comments

Comments
 (0)