feat(web): in-feed paste box + onboarding-aware connect confirmation
5b: connected users get a 'Summarize any video' URL input on the feed; submit posts to /paste (HTMX) and swaps the resulting card / inline error into the feed. 7: the connect flash now sets expectations for the async onboarding burst — 'finding your subscriptions, your newest videos will appear below as summarized'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
package web
|
package web
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestParseYouTubeVideoID(t *testing.T) {
|
func TestParseYouTubeVideoID(t *testing.T) {
|
||||||
const id = "dQw4w9WgXcQ"
|
const id = "dQw4w9WgXcQ"
|
||||||
@@ -55,3 +60,21 @@ func TestParseYouTubeVideoID(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListPageShowsPasteFormOnlyWhenConnected(t *testing.T) {
|
||||||
|
render := func(connected bool) string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := ListPage(listBuckets{}, Filter{}, PipelineStats{}, "", connected).Render(context.Background(), &buf); err != nil {
|
||||||
|
t.Fatalf("render: %v", err)
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
html := render(true)
|
||||||
|
if !strings.Contains(html, `name="url"`) || !strings.Contains(html, `action="/paste"`) {
|
||||||
|
t.Errorf("connected feed must show the paste form")
|
||||||
|
}
|
||||||
|
if strings.Contains(render(false), `name="url"`) {
|
||||||
|
t.Errorf("disconnected feed must not show the paste form")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -333,7 +333,7 @@ type flashView struct {
|
|||||||
// flashMessages maps each flash code to its banner. An unknown code renders no
|
// flashMessages maps each flash code to its banner. An unknown code renders no
|
||||||
// banner (flashFor returns ok=false), so a forged cookie value is inert.
|
// banner (flashFor returns ok=false), so a forged cookie value is inert.
|
||||||
var flashMessages = map[string]flashView{
|
var flashMessages = map[string]flashView{
|
||||||
flashConnected: {"success", "YouTube account connected."},
|
flashConnected: {"success", "YouTube account connected — finding your subscriptions. Your newest videos will appear below as they're summarized."},
|
||||||
flashConnectFailed: {"error", "Could not connect your YouTube account. Please try again."},
|
flashConnectFailed: {"error", "Could not connect your YouTube account. Please try again."},
|
||||||
flashDisconnected: {"success", "Account disconnected."},
|
flashDisconnected: {"success", "Account disconnected."},
|
||||||
flashDeleted: {"success", "Your account and all its data were deleted."},
|
flashDeleted: {"success", "Your account and all its data were deleted."},
|
||||||
|
|||||||
@@ -107,6 +107,9 @@ templ flashBanner(code string) {
|
|||||||
templ ListPage(b listBuckets, f Filter, stats PipelineStats, flash string, hasConnected bool) {
|
templ ListPage(b listBuckets, f Filter, stats PipelineStats, flash string, hasConnected bool) {
|
||||||
@Layout("Tapir — Summaries") {
|
@Layout("Tapir — Summaries") {
|
||||||
@flashBanner(flash)
|
@flashBanner(flash)
|
||||||
|
if hasConnected {
|
||||||
|
@pasteForm()
|
||||||
|
}
|
||||||
if !b.empty() || f.active() {
|
if !b.empty() || f.active() {
|
||||||
@filterForm(f)
|
@filterForm(f)
|
||||||
}
|
}
|
||||||
@@ -143,6 +146,28 @@ templ pipelineBar(s PipelineStats) {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pasteForm lets a connected user summarize any YouTube video by pasting its URL
|
||||||
|
// (Feature 2). The result (a video card, or an inline error) swaps into
|
||||||
|
// #paste-result; the next list refresh shows it inline. Summarization runs
|
||||||
|
// through the shared caption rate gate like every other fetch.
|
||||||
|
templ pasteForm() {
|
||||||
|
<form
|
||||||
|
class="paste"
|
||||||
|
method="post"
|
||||||
|
action="/paste"
|
||||||
|
hx-post="/paste"
|
||||||
|
hx-target="#paste-result"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
>
|
||||||
|
<label>
|
||||||
|
Summarize any video
|
||||||
|
<input type="url" name="url" placeholder="Paste a YouTube link…" required/>
|
||||||
|
</label>
|
||||||
|
<button type="submit">Add</button>
|
||||||
|
</form>
|
||||||
|
<div id="paste-result"></div>
|
||||||
|
}
|
||||||
|
|
||||||
templ filterForm(f Filter) {
|
templ filterForm(f Filter) {
|
||||||
<form
|
<form
|
||||||
class="filters"
|
class="filters"
|
||||||
|
|||||||
+599
-556
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user