simplify list ownership & invitations

This commit is contained in:
2026-08-01 15:46:38 -04:00
parent 65886903cd
commit 5da5fee942
3 changed files with 138 additions and 223 deletions
+42 -49
View File
@@ -1,7 +1,7 @@
use maud::{DOCTYPE, Markup, html};
use crate::{
db::{Category, GroceryList, InvitationInfo, Item, ListAccess, ListSummary, User},
db::{Category, GroceryList, Item, User},
hub::PresenceUser,
};
@@ -13,7 +13,7 @@ pub fn login_page(error: Option<&str>, invite: Option<&str>) -> Markup {
div class="auth-card" {
p class="eyebrow" { "SUSTENANCE" }
h1 { "Welcome back" }
p class="lede" { "Keep the household running, one item at a time." }
p class="lede" { "Keep the shopping list in sync." }
@if let Some(error) = error {
div class="alert alert-error" role="alert" { (error) }
}
@@ -69,23 +69,23 @@ pub fn registration_closed_page() -> Markup {
None,
html! {
div class="auth-card" {
p class="eyebrow" { "PRIVATE HOUSEHOLD" }
p class="eyebrow" { "PRIVATE LISTS" }
h1 { "Registration is invite-only" }
p class="lede" { "Ask someone who owns a list to send you an invitation link." }
p class="lede" { "Ask someone who uses the app to send you an invitation link." }
a class="button button-primary" href="/login" { "Back to sign in" }
}
},
)
}
pub fn lists_page(user: &User, lists: &[ListSummary], csrf_token: &str) -> Markup {
pub fn lists_page(user: &User, lists: &[GroceryList], csrf_token: &str) -> Markup {
page(
"Your lists",
Some(user),
html! {
div class="page-heading" {
div {
p class="eyebrow" { "YOUR HOUSEHOLD" }
p class="eyebrow" { "SHARED LISTS" }
h1 { "Grocery lists" }
p class="lede" { "Everything you need, in one place." }
}
@@ -104,12 +104,11 @@ pub fn lists_page(user: &User, lists: &[ListSummary], csrf_token: &str) -> Marku
}
} @else {
div class="list-cards" {
@for summary in lists {
a class="list-card" href=(format!("/lists/{}", summary.list.id)) {
@for list in lists {
a class="list-card" href=(format!("/lists/{}", list.id)) {
span class="list-card-icon" { "" }
span class="list-card-copy" {
strong { (summary.list.name) }
small { @if summary.role == "owner" { "Owner" } @else { "Member" } }
strong { (list.name) }
}
span class="list-card-arrow" { "" }
}
@@ -126,6 +125,20 @@ pub fn lists_page(user: &User, lists: &[ListSummary], csrf_token: &str) -> Marku
button class="button button-primary" type="submit" { "Create list" }
}
}
section id="sharing" class="panel sharing-panel" {
div class="panel-heading" { h2 { "Invite someone" } }
p { "Create a one-time invite link so a new person can join." }
form
hx-post="/invitations"
hx-target="#invite-result"
hx-swap="innerHTML"
class="stack"
{
input type="hidden" name="csrf" value=(csrf_token);
button class="button button-secondary" type="submit" { "Create invite link" }
}
div id="invite-result" class="invite-result" {}
}
}
},
)
@@ -133,24 +146,20 @@ pub fn lists_page(user: &User, lists: &[ListSummary], csrf_token: &str) -> Marku
pub fn list_page(
user: &User,
access: &ListAccess,
list: &GroceryList,
items: &[Item],
categories: &[Category],
presence: &[PresenceUser],
csrf_token: &str,
) -> Markup {
let is_owner = access.role == "owner";
page(
&access.list.name,
&list.name,
Some(user),
html! {
div class="list-topbar" {
a class="back-link" href="/lists" { "← All lists" }
div class="list-topbar-actions" {
span class="live-pill" { span class="live-dot" {} "Live" }
@if is_owner {
a class="button button-small button-quiet" href="#sharing" { "Share list" }
}
}
}
div class="list-layout" {
@@ -158,38 +167,22 @@ pub fn list_page(
div class="list-heading" {
div {
p class="eyebrow" { "SHARED LIST" }
h1 { (access.list.name) }
h1 { (list.name) }
p class="list-meta" { (items.iter().filter(|item| !item.checked).count()) " items to get" }
}
}
(list_content_fragment(&access.list, items, categories, csrf_token, false))
(list_content_fragment(list, items, categories, csrf_token, false))
}
aside class="side-column" {
(presence_panel(presence, false))
(categories_panel(&access.list, categories, csrf_token, false))
@if is_owner {
section id="sharing" class="panel sharing-panel" {
div class="panel-heading" { h2 { "Share this list" } }
p { "Create a one-time invite link for someone you shop with." }
form
hx-post=(format!("/lists/{}/invitations", access.list.id))
hx-target="#invite-result"
hx-swap="innerHTML"
class="stack"
{
input type="hidden" name="csrf" value=(csrf_token);
button class="button button-secondary" type="submit" { "Create invite link" }
}
div id="invite-result" class="invite-result" {}
}
}
(categories_panel(list, categories, csrf_token, false))
section class="panel tip-panel" {
span class="tip-label" { "TIP" }
p { "Check items off as you go. Everyone viewing this list will see it instantly." }
}
}
}
div id="live-stream" class="live-stream" hx-ext="ws" ws-connect=(format!("/lists/{}/stream", access.list.id)) {}
div id="live-stream" class="live-stream" hx-ext="ws" ws-connect=(format!("/lists/{}/stream", list.id)) {}
},
)
}
@@ -448,26 +441,26 @@ pub fn categories_panel(
}
pub fn live_list_fragments(
access: &ListAccess,
list: &GroceryList,
items: &[Item],
categories: &[Category],
csrf_token: &str,
) -> Markup {
html! {
(list_content_fragment(&access.list, items, categories, csrf_token, true))
(categories_panel(&access.list, categories, csrf_token, true))
(list_content_fragment(list, items, categories, csrf_token, true))
(categories_panel(list, categories, csrf_token, true))
}
}
pub fn category_created(
access: &ListAccess,
list: &GroceryList,
items: &[Item],
categories: &[Category],
csrf_token: &str,
) -> Markup {
html! {
p class="category-success" { "Category added." }
(live_list_fragments(access, items, categories, csrf_token))
(live_list_fragments(list, items, categories, csrf_token))
}
}
@@ -509,7 +502,6 @@ fn presence_content(presence: &[PresenceUser]) -> Markup {
}
pub fn invite_page(
info: &InvitationInfo,
user: Option<&User>,
token: &str,
error: Option<&str>,
@@ -521,8 +513,8 @@ pub fn invite_page(
html! {
div class="auth-card invite-card" {
p class="eyebrow" { "YOU'RE INVITED" }
h1 { "Join " (info.list_name) }
p class="lede" { "Shop together and keep the list in sync." }
h1 { "Join the shared lists" }
p class="lede" { "Shop together and keep all the lists in sync." }
@if let Some(error) = error {
div class="alert alert-error" role="alert" { (error) }
}
@@ -532,14 +524,15 @@ pub fn invite_page(
@if let Some(csrf_token) = csrf_token {
input type="hidden" name="csrf" value=(csrf_token);
}
button class="button button-primary" type="submit" { "Join list" }
button class="button button-primary" type="submit" { "Join lists" }
}
p class="muted" { "Want to join as a different person? Create a new account below." }
} @else {
p { "Sign in or create an account to accept this invite." }
div class="invite-actions" {
a class="button button-primary" href=(format!("/login?invite={}", token)) { "Sign in" }
a class="button button-secondary" href=(format!("/register?invite={}", token)) { "Create account" }
}
}
div class="invite-actions" {
a class="button button-primary" href=(format!("/register?invite={}", token)) { "Create account" }
a class="button button-secondary" href=(format!("/login?invite={}", token)) { "Sign in" }
}
}
},