feat(detail): add roast detail page, simplify lists and actions
- Add roast detail page at /roasters/{slug}/roasts/{slug}
- Remove expand/collapse detail rows from all 7 list views
- List rows now navigate directly to entity detail pages
- Replace three-dots action button with chevron-right link
- Add delete buttons to brew and cup detail pages
- Restyle all delete buttons: outlined with red text
- Remove share buttons from all detail pages
- Update timeline card links to point at detail pages
- Make homepage activity cards clickable with hover effect
- Replace all vanilla JS delete/close with Datastar actions
- Extract render_redirect_script helper for Datastar redirects
- Update delete macro with referer-based routing for detail pages
This commit is contained in:
parent
958c8de6cc
commit
f0eb346086
35 changed files with 379 additions and 454 deletions
|
|
@ -85,16 +85,8 @@ pub(crate) async fn create_bag(
|
|||
.await
|
||||
.map_err(ApiError::from)
|
||||
} else {
|
||||
use axum::http::header::HeaderValue;
|
||||
let script = format!("<script>window.location.href='{detail_url}'</script>");
|
||||
let mut response = axum::response::Html(script).into_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-selector", HeaderValue::from_static("body"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-mode", HeaderValue::from_static("append"));
|
||||
Ok(response)
|
||||
crate::application::routes::support::render_redirect_script(&detail_url)
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
Ok(Redirect::to(&detail_url).into_response())
|
||||
|
|
@ -175,9 +167,20 @@ pub(crate) async fn update_bag(
|
|||
state.stats_invalidator.invalidate();
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
let from_bag_page = headers
|
||||
.get("referer")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.is_some_and(|r| r.contains("type=bags"));
|
||||
|
||||
if from_bag_page {
|
||||
render_bag_list_fragment(state, request, search, true)
|
||||
.await
|
||||
.map_err(ApiError::from)
|
||||
} else {
|
||||
let detail_url = format!("/bags/{id}");
|
||||
crate::application::routes::support::render_redirect_script(&detail_url)
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
} else {
|
||||
let enriched = state
|
||||
.bag_repo
|
||||
|
|
@ -193,7 +196,9 @@ define_delete_handler!(
|
|||
BagId,
|
||||
BagSortKey,
|
||||
bag_repo,
|
||||
render_bag_list_fragment
|
||||
render_bag_list_fragment,
|
||||
"type=bags",
|
||||
"/data?type=bags"
|
||||
);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -269,16 +269,8 @@ pub(crate) async fn create_brew(
|
|||
.await
|
||||
.map_err(ApiError::from)
|
||||
} else {
|
||||
use axum::http::header::HeaderValue;
|
||||
let script = format!("<script>window.location.href='{detail_url}'</script>");
|
||||
let mut response = axum::response::Html(script).into_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-selector", HeaderValue::from_static("body"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-mode", HeaderValue::from_static("append"));
|
||||
Ok(response)
|
||||
crate::application::routes::support::render_redirect_script(&detail_url)
|
||||
.map_err(ApiError::from)
|
||||
}
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
Ok(Redirect::to(&detail_url).into_response())
|
||||
|
|
@ -323,7 +315,9 @@ define_delete_handler!(
|
|||
BrewId,
|
||||
BrewSortKey,
|
||||
brew_repo,
|
||||
render_brew_list_fragment
|
||||
render_brew_list_fragment,
|
||||
"type=brews",
|
||||
"/data?type=brews"
|
||||
);
|
||||
|
||||
async fn render_brew_list_fragment(
|
||||
|
|
|
|||
|
|
@ -141,7 +141,9 @@ define_delete_handler!(
|
|||
CafeId,
|
||||
CafeSortKey,
|
||||
cafe_repo,
|
||||
render_cafe_list_fragment
|
||||
render_cafe_list_fragment,
|
||||
"type=cafes",
|
||||
"/data?type=cafes"
|
||||
);
|
||||
|
||||
define_list_fragment_renderer!(
|
||||
|
|
|
|||
|
|
@ -104,7 +104,9 @@ define_delete_handler!(
|
|||
CupId,
|
||||
CupSortKey,
|
||||
cup_repo,
|
||||
render_cup_list_fragment
|
||||
render_cup_list_fragment,
|
||||
"type=cups",
|
||||
"/data?type=cups"
|
||||
);
|
||||
|
||||
define_list_fragment_renderer!(
|
||||
|
|
|
|||
|
|
@ -156,7 +156,9 @@ define_delete_handler!(
|
|||
GearId,
|
||||
GearSortKey,
|
||||
gear_repo,
|
||||
render_gear_list_fragment
|
||||
render_gear_list_fragment,
|
||||
"type=gear",
|
||||
"/data?type=gear"
|
||||
);
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -59,12 +59,19 @@ macro_rules! define_enriched_get_handler {
|
|||
|
||||
/// Generates a DELETE handler with Datastar fragment re-rendering support.
|
||||
///
|
||||
/// When a Datastar request arrives from the data/list page (detected via referer
|
||||
/// containing `$referer_match`), the handler re-renders the list fragment. When the
|
||||
/// request comes from elsewhere (e.g. a detail page), it returns a redirect script
|
||||
/// pointing at `$redirect_url`. Non-Datastar requests get a 204 No Content.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `$fn_name` - Name of the generated handler function
|
||||
/// * `$id_type` - Type of the ID path parameter (e.g., `RoasterId`)
|
||||
/// * `$sort_key` - Sort key type for list requests (e.g., `RoasterSortKey`)
|
||||
/// * `$repo_field` - Name of the repository field on `AppState` (e.g., `roaster_repo`)
|
||||
/// * `$render_fragment` - Path to the fragment render function
|
||||
/// * `$referer_match` - String to look for in `Referer` header (e.g., `"type=roasters"`)
|
||||
/// * `$redirect_url` - URL for the redirect script (e.g., `"/data?type=roasters"`)
|
||||
///
|
||||
/// # Example
|
||||
/// ```ignore
|
||||
|
|
@ -73,11 +80,13 @@ macro_rules! define_enriched_get_handler {
|
|||
/// RoasterId,
|
||||
/// RoasterSortKey,
|
||||
/// roaster_repo,
|
||||
/// render_roaster_list_fragment
|
||||
/// render_roaster_list_fragment,
|
||||
/// "type=roasters",
|
||||
/// "/data?type=roasters"
|
||||
/// );
|
||||
/// ```
|
||||
macro_rules! define_delete_handler {
|
||||
($fn_name:ident, $id_type:ty, $sort_key:ty, $repo_field:ident, $render_fragment:path) => {
|
||||
($fn_name:ident, $id_type:ty, $sort_key:ty, $repo_field:ident, $render_fragment:path, $referer_match:literal, $redirect_url:literal) => {
|
||||
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||
pub(crate) async fn $fn_name(
|
||||
axum::extract::State(state): axum::extract::State<crate::application::state::AppState>,
|
||||
|
|
@ -99,9 +108,19 @@ macro_rules! define_delete_handler {
|
|||
state.stats_invalidator.invalidate();
|
||||
|
||||
if crate::application::routes::support::is_datastar_request(&headers) {
|
||||
let from_data_page = headers
|
||||
.get("referer")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.is_some_and(|r| r.contains($referer_match));
|
||||
|
||||
if from_data_page {
|
||||
$render_fragment(state, request, search, true)
|
||||
.await
|
||||
.map_err(crate::application::errors::ApiError::from)
|
||||
} else {
|
||||
crate::application::routes::support::render_redirect_script($redirect_url)
|
||||
.map_err(crate::application::errors::ApiError::from)
|
||||
}
|
||||
} else {
|
||||
Ok(axum::http::StatusCode::NO_CONTENT.into_response())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,9 @@ define_delete_handler!(
|
|||
RoasterId,
|
||||
RoasterSortKey,
|
||||
roaster_repo,
|
||||
render_roaster_list_fragment
|
||||
render_roaster_list_fragment,
|
||||
"type=roasters",
|
||||
"/data?type=roasters"
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, auth_user, headers, payload))]
|
||||
|
|
|
|||
|
|
@ -74,14 +74,37 @@ pub(crate) async fn create_roast(
|
|||
info!(roast_id = %roast.id, name = %roast.name, "roast created");
|
||||
state.stats_invalidator.invalidate();
|
||||
|
||||
let roaster = state
|
||||
.roaster_repo
|
||||
.get(roast.roaster_id)
|
||||
.await
|
||||
.map_err(AppError::from)?;
|
||||
let detail_url = format!("/roasters/{}/roasts/{}", roaster.slug, roast.slug);
|
||||
|
||||
if is_datastar_request(&headers) {
|
||||
let from_data_page = headers
|
||||
.get("referer")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.is_some_and(|r| r.contains("type=roasts"));
|
||||
|
||||
if from_data_page {
|
||||
render_roast_list_fragment(state, request, search, true)
|
||||
.await
|
||||
.map_err(ApiError::from)
|
||||
} else {
|
||||
use axum::http::header::HeaderValue;
|
||||
let script = format!("<script>window.location.href='{detail_url}'</script>");
|
||||
let mut response = axum::response::Html(script).into_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-selector", HeaderValue::from_static("body"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-mode", HeaderValue::from_static("append"));
|
||||
Ok(response)
|
||||
}
|
||||
} else if matches!(source, PayloadSource::Form) {
|
||||
let target =
|
||||
ListNavigator::new(ROAST_PAGE_PATH, ROAST_FRAGMENT_PATH, request, search).page_href(1);
|
||||
Ok(Redirect::to(&target).into_response())
|
||||
Ok(Redirect::to(&detail_url).into_response())
|
||||
} else {
|
||||
let enriched = state
|
||||
.roast_repo
|
||||
|
|
@ -146,7 +169,9 @@ define_delete_handler!(
|
|||
RoastId,
|
||||
RoastSortKey,
|
||||
roast_repo,
|
||||
render_roast_list_fragment
|
||||
render_roast_list_fragment,
|
||||
"type=roasts",
|
||||
"/data?type=roasts"
|
||||
);
|
||||
|
||||
#[tracing::instrument(skip(state, _auth_user))]
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ mod data;
|
|||
mod gear;
|
||||
mod home;
|
||||
mod roasters;
|
||||
mod roasts;
|
||||
mod stats;
|
||||
mod timeline;
|
||||
mod webauthn;
|
||||
|
|
@ -39,6 +40,10 @@ pub(super) fn router() -> axum::Router<AppState> {
|
|||
.route("/cups/{id}", get(cups::cup_detail_page))
|
||||
.route("/gear/{id}", get(gear::gear_detail_page))
|
||||
.route("/roasters/{slug}", get(roasters::roaster_detail_page))
|
||||
.route(
|
||||
"/roasters/{roaster_slug}/roasts/{roast_slug}",
|
||||
get(roasts::roast_detail_page),
|
||||
)
|
||||
.route("/styles.css", get(styles))
|
||||
.route("/webauthn.js", get(webauthn_js))
|
||||
.route("/components/photo-capture.js", get(photo_capture_js))
|
||||
|
|
|
|||
43
src/application/routes/app/roasts.rs
Normal file
43
src/application/routes/app/roasts.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use axum::extract::{Path, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use tower_cookies::Cookies;
|
||||
|
||||
use crate::application::errors::map_app_error;
|
||||
use crate::application::routes::render_html;
|
||||
use crate::application::state::AppState;
|
||||
use crate::presentation::web::templates::RoastDetailTemplate;
|
||||
use crate::presentation::web::views::RoastDetailView;
|
||||
|
||||
#[tracing::instrument(skip(state, cookies))]
|
||||
pub(crate) async fn roast_detail_page(
|
||||
State(state): State<AppState>,
|
||||
cookies: Cookies,
|
||||
Path((roaster_slug, roast_slug)): Path<(String, String)>,
|
||||
) -> Result<Response, StatusCode> {
|
||||
let is_authenticated = crate::application::routes::is_authenticated(&state, &cookies).await;
|
||||
|
||||
let roaster = state
|
||||
.roaster_repo
|
||||
.get_by_slug(&roaster_slug)
|
||||
.await
|
||||
.map_err(|e| map_app_error(e.into()))?;
|
||||
|
||||
let roast = state
|
||||
.roast_repo
|
||||
.get_by_slug(roaster.id, &roast_slug)
|
||||
.await
|
||||
.map_err(|e| map_app_error(e.into()))?;
|
||||
|
||||
let view = RoastDetailView::from_parts(roast, &roaster);
|
||||
|
||||
let template = RoastDetailTemplate {
|
||||
nav_active: "",
|
||||
is_authenticated,
|
||||
version_info: &crate::VERSION_INFO,
|
||||
base_url: crate::base_url(),
|
||||
roast: view,
|
||||
};
|
||||
|
||||
render_html(template).map(IntoResponse::into_response)
|
||||
}
|
||||
|
|
@ -131,6 +131,21 @@ where
|
|||
(view_page, navigator)
|
||||
}
|
||||
|
||||
/// Return a Datastar response that redirects the browser to `url`.
|
||||
///
|
||||
/// Works by appending a `<script>` tag to `<body>` that sets `window.location.href`.
|
||||
pub fn render_redirect_script(url: &str) -> Result<Response, AppError> {
|
||||
let script = format!("<script>window.location.href='{url}'</script>");
|
||||
let mut response = Html(script).into_response();
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-selector", HeaderValue::from_static("body"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("datastar-mode", HeaderValue::from_static("append"));
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn render_fragment<T: Template>(
|
||||
template: T,
|
||||
selector: &'static str,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ use super::views::{
|
|||
BagDetailView, BagOptionView, BagView, BrewDefaultsView, BrewDetailView, BrewView,
|
||||
CafeDetailView, CafeOptionView, CafeView, CupDetailView, CupView, GearDetailView,
|
||||
GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, QuickNoteView,
|
||||
RoastOptionView, RoastView, RoasterDetailView, RoasterOptionView, RoasterView, StatCard,
|
||||
StatsView, TimelineEventView, TimelineMonthView,
|
||||
RoastDetailView, RoastOptionView, RoastView, RoasterDetailView, RoasterOptionView, RoasterView,
|
||||
StatCard, StatsView, TimelineEventView, TimelineMonthView,
|
||||
};
|
||||
use crate::domain::bags::BagSortKey;
|
||||
use crate::domain::brews::BrewSortKey;
|
||||
|
|
@ -241,6 +241,16 @@ pub struct CupDetailTemplate {
|
|||
pub cup: CupDetailView,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/roast.html")]
|
||||
pub struct RoastDetailTemplate {
|
||||
pub nav_active: &'static str,
|
||||
pub is_authenticated: bool,
|
||||
pub version_info: &'static crate::VersionInfo,
|
||||
pub base_url: &'static str,
|
||||
pub roast: RoastDetailView,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "pages/roaster.html")]
|
||||
pub struct RoasterDetailTemplate {
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ impl Default for BrewDefaultsView {
|
|||
}
|
||||
|
||||
pub struct BrewDetailView {
|
||||
pub id: String,
|
||||
// Coffee info
|
||||
pub roast_name: String,
|
||||
pub roaster_name: String,
|
||||
|
|
@ -252,6 +253,7 @@ impl BrewDetailView {
|
|||
.join(", ");
|
||||
|
||||
Self {
|
||||
id: brew.brew.id.to_string(),
|
||||
roast_name: brew.roast_name,
|
||||
roaster_name: brew.roaster_name,
|
||||
origin: coffee.origin,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ impl CupView {
|
|||
}
|
||||
|
||||
pub struct CupDetailView {
|
||||
pub id: String,
|
||||
// Coffee info
|
||||
pub roast_name: String,
|
||||
pub roaster_name: String,
|
||||
|
|
@ -87,6 +88,7 @@ impl CupDetailView {
|
|||
let (map_countries, map_max) = build_map_data(&map_entries);
|
||||
|
||||
Self {
|
||||
id: cup.cup.id.to_string(),
|
||||
roast_name: cup.roast_name,
|
||||
roaster_name: cup.roaster_name,
|
||||
origin: coffee.origin,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub use cafes::{CafeDetailView, CafeOptionView, CafeView, NearbyCafeView};
|
|||
pub use cups::{CupDetailView, CupView};
|
||||
pub use gear::{GearDetailView, GearOptionView, GearView};
|
||||
pub use roasters::{RoasterDetailView, RoasterOptionView, RoasterView};
|
||||
pub use roasts::{RoastOptionView, RoastView};
|
||||
pub use roasts::{RoastDetailView, RoastOptionView, RoastView};
|
||||
pub use tasting_notes::TastingNoteView;
|
||||
pub use timeline::{
|
||||
TimelineBrewDataView, TimelineEventDetailView, TimelineEventView, TimelineMonthView,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
use crate::domain::roasters::Roaster;
|
||||
use crate::domain::roasts::{Roast, RoastWithRoaster};
|
||||
|
||||
use super::tasting_notes::{self, TastingNoteView};
|
||||
use super::{build_coffee_info, build_map_data, build_roaster_info};
|
||||
|
||||
pub struct RoastView {
|
||||
pub id: String,
|
||||
|
|
@ -90,6 +92,68 @@ impl RoastView {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct RoastDetailView {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub roaster_name: String,
|
||||
pub roaster_slug: String,
|
||||
// Coffee info
|
||||
pub origin: String,
|
||||
pub origin_flag: String,
|
||||
pub region: String,
|
||||
pub producer: String,
|
||||
pub process: String,
|
||||
pub tasting_notes: Vec<TastingNoteView>,
|
||||
// Roaster info
|
||||
pub roaster_country: String,
|
||||
pub roaster_country_flag: String,
|
||||
pub roaster_city: Option<String>,
|
||||
pub roaster_homepage: Option<String>,
|
||||
// Map
|
||||
pub map_countries: String,
|
||||
pub map_max: u32,
|
||||
// Dates
|
||||
pub created_date: String,
|
||||
pub created_time: String,
|
||||
}
|
||||
|
||||
impl RoastDetailView {
|
||||
pub fn from_parts(roast: Roast, roaster: &Roaster) -> Self {
|
||||
let coffee = build_coffee_info(&roast);
|
||||
let roaster_info = build_roaster_info(roaster);
|
||||
|
||||
let mut map_entries: Vec<(&str, u32)> = Vec::new();
|
||||
if let Some(ref o) = roast.origin
|
||||
&& !o.is_empty()
|
||||
{
|
||||
map_entries.push((o.as_str(), 2));
|
||||
}
|
||||
map_entries.push((roaster.country.as_str(), 1));
|
||||
let (map_countries, map_max) = build_map_data(&map_entries);
|
||||
|
||||
Self {
|
||||
id: roast.id.to_string(),
|
||||
name: roast.name,
|
||||
roaster_name: roaster.name.clone(),
|
||||
roaster_slug: roaster.slug.clone(),
|
||||
origin: coffee.origin,
|
||||
origin_flag: coffee.origin_flag,
|
||||
region: coffee.region,
|
||||
producer: coffee.producer,
|
||||
process: coffee.process,
|
||||
tasting_notes: coffee.tasting_notes,
|
||||
roaster_country: roaster_info.country,
|
||||
roaster_country_flag: roaster_info.country_flag,
|
||||
roaster_city: roaster_info.city,
|
||||
roaster_homepage: roaster_info.homepage,
|
||||
map_countries,
|
||||
map_max,
|
||||
created_date: roast.created_at.format("%Y-%m-%d").to_string(),
|
||||
created_time: roast.created_at.format("%H:%M").to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RoastOptionView {
|
||||
pub id: String,
|
||||
pub label: String,
|
||||
|
|
|
|||
|
|
@ -72,14 +72,14 @@ impl TimelineEventView {
|
|||
let TimelineEvent {
|
||||
id,
|
||||
entity_type,
|
||||
entity_id: _,
|
||||
entity_id,
|
||||
action,
|
||||
occurred_at,
|
||||
title,
|
||||
details,
|
||||
tasting_notes,
|
||||
slug: _,
|
||||
roaster_slug: _,
|
||||
slug,
|
||||
roaster_slug,
|
||||
brew_data,
|
||||
} = event;
|
||||
|
||||
|
|
@ -96,11 +96,21 @@ impl TimelineEventView {
|
|||
};
|
||||
|
||||
let link = match entity_type.as_str() {
|
||||
"roaster" => "/data?type=roasters".to_string(),
|
||||
"roast" | "bag" | "brew" => format!("/data?type={entity_type}s"),
|
||||
"cafe" => "/data?type=cafes".to_string(),
|
||||
"cup" => "/data?type=cups".to_string(),
|
||||
"gear" => "/data?type=gear".to_string(),
|
||||
"brew" => format!("/brews/{entity_id}"),
|
||||
"cup" => format!("/cups/{entity_id}"),
|
||||
"bag" => format!("/bags/{entity_id}"),
|
||||
"gear" => format!("/gear/{entity_id}"),
|
||||
"roaster" => slug.as_deref().map_or_else(
|
||||
|| "/data?type=roasters".to_string(),
|
||||
|s| format!("/roasters/{s}"),
|
||||
),
|
||||
"cafe" => slug
|
||||
.as_deref()
|
||||
.map_or_else(|| "/data?type=cafes".to_string(), |s| format!("/cafes/{s}")),
|
||||
"roast" => match (roaster_slug.as_deref(), slug.as_deref()) {
|
||||
(Some(rs), Some(s)) => format!("/roasters/{rs}/roasts/{s}"),
|
||||
_ => "/data?type=roasts".to_string(),
|
||||
},
|
||||
_ => String::from("#"),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,8 @@
|
|||
{{ bag.roaster_name }} · {{ bag.amount }} · Opened {{ bag.created_date }}
|
||||
</p>
|
||||
</div>
|
||||
{{ detail::share_button() }}
|
||||
</header>
|
||||
|
||||
{{ detail::share_script() }}
|
||||
|
||||
{# ── Coffee + map ── #}
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
{{ detail::coffee_card(bag.roast_name, bag.roaster_name, bag.origin, bag.origin_flag, bag.region, bag.producer, bag.process, bag.tasting_notes) }}
|
||||
|
|
@ -76,47 +73,18 @@
|
|||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="rounded-lg border bg-surface p-5 md:col-span-2 flex items-center gap-2">
|
||||
{% if !bag.closed %}
|
||||
<button type="button" onclick="closeBag('{{ bag.id }}')"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-accent transition hover:text-text hover:bg-surface-alt">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-accent transition hover:text-text hover:bg-surface-alt"
|
||||
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&remaining=0')">
|
||||
{{ icons::x_mark("h-4 w-4") }} Close Bag
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" onclick="deleteBag('{{ bag.id }}')"
|
||||
class="inline-flex items-center gap-2 rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-red-700">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-surface-alt"
|
||||
data-on:click="confirm('Delete this bag? This cannot be undone.') && @delete('/api/v1/bags/{{ bag.id }}')">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const closeBag = async (bagId) => {
|
||||
if (!confirm('Close this bag? This will mark it as finished.')) return;
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const resp = await fetch(`/api/v1/bags/${bagId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ closed: true, remaining: 0.0, finished_at: today }),
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Server returned ${resp.status}`);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
alert(`Failed to close bag: ${e.message}`);
|
||||
}
|
||||
};
|
||||
const deleteBag = async (bagId) => {
|
||||
if (!confirm('Delete this bag? This cannot be undone.')) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/bags/${bagId}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Server returned ${resp.status}`);
|
||||
window.location.href = '/';
|
||||
} catch (e) {
|
||||
alert(`Failed to delete bag: ${e.message}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/detail_cards.html" as detail %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% block title %}Brewlog · {{ brew.roast_name }}{% endblock %}
|
||||
{% block description %}{{ brew.roast_name }} by {{ brew.roaster_name }} — {{ brew.coffee_weight }} coffee, {{ brew.water_volume }} water.{% endblock %}
|
||||
{% block og_title %}{{ brew.roast_name }} — Brewlog{% endblock %}
|
||||
|
|
@ -13,11 +14,8 @@
|
|||
{{ brew.roaster_name }} · Brewed {{ brew.created_date }} at {{ brew.created_time }}
|
||||
</p>
|
||||
</div>
|
||||
{{ detail::share_button() }}
|
||||
</header>
|
||||
|
||||
{{ detail::share_script() }}
|
||||
|
||||
{# ── Coffee + map ── #}
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
{{ detail::coffee_card(brew.roast_name, brew.roaster_name, brew.origin, brew.origin_flag, brew.region, brew.producer, brew.process, brew.tasting_notes) }}
|
||||
|
|
@ -85,4 +83,14 @@
|
|||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_authenticated %}
|
||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-surface-alt"
|
||||
data-on:click="confirm('Delete this brew? This cannot be undone.') && @delete('/api/v1/brews/{{ brew.id }}')">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -13,11 +13,8 @@
|
|||
{{ cafe.city }}, {{ cafe.country_flag }} {{ cafe.country }} · Added {{ cafe.created_date }}
|
||||
</p>
|
||||
</div>
|
||||
{{ detail::share_button() }}
|
||||
</header>
|
||||
|
||||
{{ detail::share_script() }}
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="rounded-lg border bg-surface p-5">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
||||
|
|
@ -48,25 +45,11 @@
|
|||
|
||||
{% if is_authenticated %}
|
||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||
<button type="button" onclick="deleteCafe('{{ cafe.id }}')"
|
||||
class="inline-flex items-center gap-2 rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-red-700">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-surface-alt"
|
||||
data-on:click="confirm('Delete this cafe? This cannot be undone.') && @delete('/api/v1/cafes/{{ cafe.id }}')">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
const deleteCafe = async (id) => {
|
||||
if (!confirm('Delete this cafe? This cannot be undone.')) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/cafes/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Server returned ${resp.status}`);
|
||||
window.location.href = '/data?type=cafes';
|
||||
} catch (e) {
|
||||
alert(`Failed to delete cafe: ${e.message}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/detail_cards.html" as detail %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% block title %}Brewlog · {{ cup.roast_name }} at {{ cup.cafe_name }}{% endblock %}
|
||||
{% block description %}{{ cup.roast_name }} by {{ cup.roaster_name }} at {{ cup.cafe_name }}, {{ cup.cafe_city }}.{% endblock %}
|
||||
{% block og_title %}{{ cup.roast_name }} at {{ cup.cafe_name }} — Brewlog{% endblock %}
|
||||
|
|
@ -13,11 +14,8 @@
|
|||
{{ cup.roaster_name }} · {{ cup.cafe_name }}, {{ cup.cafe_city }} · {{ cup.created_date }}
|
||||
</p>
|
||||
</div>
|
||||
{{ detail::share_button() }}
|
||||
</header>
|
||||
|
||||
{{ detail::share_script() }}
|
||||
|
||||
{# ── Coffee + map ── #}
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
{{ detail::coffee_card(cup.roast_name, cup.roaster_name, cup.origin, cup.origin_flag, cup.region, cup.producer, cup.process, cup.tasting_notes) }}
|
||||
|
|
@ -56,4 +54,14 @@
|
|||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_authenticated %}
|
||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-surface-alt"
|
||||
data-on:click="confirm('Delete this cup? This cannot be undone.') && @delete('/api/v1/cups/{{ cup.id }}')">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/detail_cards.html" as detail %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% block title %}Brewlog · {{ gear.make }} {{ gear.model }}{% endblock %}
|
||||
{% block og_title %}{{ gear.make }} {{ gear.model }} — Brewlog{% endblock %}
|
||||
|
|
@ -13,11 +12,8 @@
|
|||
{{ gear.category_label }} · Added {{ gear.created_date }}
|
||||
</p>
|
||||
</div>
|
||||
{{ detail::share_button() }}
|
||||
</header>
|
||||
|
||||
{{ detail::share_script() }}
|
||||
|
||||
<div class="rounded-lg border bg-surface p-5">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
||||
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||
|
|
@ -38,25 +34,11 @@
|
|||
|
||||
{% if is_authenticated %}
|
||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||
<button type="button" onclick="deleteGear('{{ gear.id }}')"
|
||||
class="inline-flex items-center gap-2 rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-red-700">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-surface-alt"
|
||||
data-on:click="confirm('Delete this gear? This cannot be undone.') && @delete('/api/v1/gear/{{ gear.id }}')">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
const deleteGear = async (id) => {
|
||||
if (!confirm('Delete this gear? This cannot be undone.')) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/gear/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Server returned ${resp.status}`);
|
||||
window.location.href = '/data?type=gear';
|
||||
} catch (e) {
|
||||
alert(`Failed to delete gear: ${e.message}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -197,22 +197,20 @@
|
|||
{% if !recent_events.is_empty() %}
|
||||
<div class="space-y-2">
|
||||
{% for event in recent_events %}
|
||||
<div class="rounded-lg border bg-surface px-4 py-3 flex items-center justify-between gap-3">
|
||||
<a href="{{ event.link }}" class="rounded-lg border bg-surface px-4 py-3 flex items-center justify-between gap-3 transition hover:border-accent/40">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<span class="text-text-muted shrink-0">
|
||||
{% if event.entity_type == "brew" %}{{ icons::beaker("h-5 w-5") }}{% elif event.entity_type == "roast" %}{{ icons::coffee_bean("h-5 w-5") }}{% elif event.entity_type == "roaster" %}{{ icons::fire("h-5 w-5") }}{% elif event.entity_type == "bag" %}{{ icons::bag("h-5 w-5") }}{% elif event.entity_type == "cup" %}{{ icons::cup("h-5 w-5") }}{% elif event.entity_type == "cafe" %}{{ icons::location("h-5 w-5") }}{% elif event.entity_type == "gear" %}{{ icons::grinder("h-5 w-5") }}{% else %}{{ icons::beaker("h-5 w-5") }}{% endif %}
|
||||
</span>
|
||||
<p class="min-w-0 truncate text-sm">
|
||||
<a href="{{ event.link }}" class="font-medium text-text hover:text-accent"
|
||||
>{{ event.title }}</a
|
||||
>{% if let Some(sub) = event.subtitle %}
|
||||
<span class="font-medium text-text">{{ event.title }}</span>{% if let Some(sub) = event.subtitle %}
|
||||
<span class="text-xs text-text-muted">· {{ sub }}</span>{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<time class="text-xs text-text-muted whitespace-nowrap shrink-0"
|
||||
>{{ event.relative_date_label }}</time
|
||||
>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
|
|
|||
36
templates/pages/roast.html
Normal file
36
templates/pages/roast.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{% extends "base.html" %}
|
||||
{% import "partials/detail_cards.html" as detail %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
{% block title %}Brewlog · {{ roast.name }}{% endblock %}
|
||||
{% block og_title %}{{ roast.name }} — Brewlog{% endblock %}
|
||||
{% block og_description %}{{ roast.name }} by {{ roast.roaster_name }} — {{ roast.origin }}{% endblock %}
|
||||
{% block head %}<meta property="og:image" content="{{ base_url }}/og-image.png" />{% endblock %}
|
||||
{% block content %}
|
||||
<header class="flex items-start justify-between gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-3xl font-semibold">{{ roast.name }}</h1>
|
||||
<p class="text-sm text-text-secondary">
|
||||
{{ roast.roaster_name }} · {{ roast.origin_flag }} {{ roast.origin }} · Added {{ roast.created_date }}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
{{ detail::coffee_card(roast.name, roast.roaster_name, roast.origin, roast.origin_flag, roast.region, roast.producer, roast.process, roast.tasting_notes) }}
|
||||
{{ detail::map_with_legend_2(roast.map_countries, roast.map_max, "Origin", "", "Roaster", "opacity-50") }}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
{{ detail::roaster_card(roast.roaster_name, roast.roaster_country, roast.roaster_country_flag, roast.roaster_city, roast.roaster_homepage) }}
|
||||
</div>
|
||||
|
||||
{% if is_authenticated %}
|
||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-surface-alt"
|
||||
data-on:click="confirm('Delete this roast? This cannot be undone.') && @delete('/api/v1/roasts/{{ roast.id }}')">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
@ -13,11 +13,8 @@
|
|||
{{ roaster.country_flag }} {{ roaster.country }}{% if let Some(c) = roaster.city %} · {{ c }}{% endif %} · Added {{ roaster.created_date }}
|
||||
</p>
|
||||
</div>
|
||||
{{ detail::share_button() }}
|
||||
</header>
|
||||
|
||||
{{ detail::share_script() }}
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2">
|
||||
<div class="rounded-lg border bg-surface p-5">
|
||||
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
||||
|
|
@ -50,25 +47,11 @@
|
|||
|
||||
{% if is_authenticated %}
|
||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||
<button type="button" onclick="deleteRoaster('{{ roaster.id }}')"
|
||||
class="inline-flex items-center gap-2 rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition hover:bg-red-700">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-2 rounded-md border px-4 py-2 text-sm font-medium text-red-600 transition hover:bg-surface-alt"
|
||||
data-on:click="confirm('Delete this roaster? This cannot be undone.') && @delete('/api/v1/roasters/{{ roaster.id }}')">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
const deleteRoaster = async (id) => {
|
||||
if (!confirm('Delete this roaster? This cannot be undone.')) return;
|
||||
try {
|
||||
const resp = await fetch(`/api/v1/roasters/${id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
if (!resp.ok) throw new Error(`Server returned ${resp.status}`);
|
||||
window.location.href = '/data?type=roasters';
|
||||
} catch (e) {
|
||||
alert(`Failed to delete roaster: ${e.message}`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -28,15 +28,13 @@
|
|||
{{ table::sortable_header("Roast", "roast", navigator, "#bag-list") }}
|
||||
{{ table::sortable_header("Status", "status", navigator, "#bag-list") }}
|
||||
{{ table::sortable_header("Finished", "finished-at", navigator, "#bag-list") }}
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
{% for bag in bags.items %}
|
||||
<tr class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="toggleRow(event)"{% endif %}
|
||||
onclick="window.location.href='/bags/{{ bag.id }}'"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||
<div>{{ bag.created_date }}</div>
|
||||
|
|
@ -76,23 +74,6 @@
|
|||
<div>{{ bag.finished_date }}</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
{% if !bag.closed %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||
{{ icons::x_circle("h-4 w-4") }} Close bag
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<span class="md:hidden">
|
||||
|
|
@ -100,36 +81,13 @@
|
|||
<span class="text-text-muted">Closed</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
||||
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="6" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
{% if !bag.closed %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
data-on:click="confirm('Close this bag? This will mark it as finished.') && @put('/api/v1/bags/{{ bag.id }}?closed=true&{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||
{{ icons::x_circle("h-4 w-4") }} Close bag
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this bag?') && @delete('/api/v1/bags/{{ bag.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#bag-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
<a href="/bags/{{ bag.id }}"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt">
|
||||
{{ icons::chevron_right("h-5 w-5") }}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,6 @@
|
|||
{% import "partials/lists/table.html" as table %}
|
||||
{% import "partials/icons.html" as icons %}
|
||||
|
||||
{% macro brew_actions(brew, navigator) %}
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="{{ brew.brew_again_url() }}"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover">
|
||||
{{ icons::plus_circle("h-4 w-4") }} Brew Again
|
||||
</a>
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this brew?') && @delete('/api/v1/brews/{{ brew.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#brew-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<div id="brew-list" class="mt-6" data-star-scope="brews">
|
||||
{% if brews.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
|
|
@ -44,15 +30,13 @@
|
|||
<th scope="col" class="px-4 py-3">Recipe</th>
|
||||
<th scope="col" class="px-4 py-3">Brewer</th>
|
||||
<th scope="col" class="px-4 py-3">Notes</th>
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
{% for brew in brews.items %}
|
||||
<tr class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="toggleRow(event)"{% endif %}
|
||||
onclick="window.location.href='/brews/{{ brew.id }}'"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||
<div>{{ brew.created_date }}</div>
|
||||
|
|
@ -96,27 +80,13 @@
|
|||
<span class="pill pill-muted">No Notes</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
{{ brew_actions(brew, navigator) }}
|
||||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
||||
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="7" class="bg-surface-alt px-4 py-2">
|
||||
{{ brew_actions(brew, navigator) }}
|
||||
<a href="/brews/{{ brew.id }}"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt">
|
||||
{{ icons::chevron_right("h-5 w-5") }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
{{ table::sortable_header("Name", "name", navigator, "#cafe-list") }}
|
||||
{{ table::sortable_header("City", "city", navigator, "#cafe-list") }}
|
||||
{{ table::sortable_header("Country", "country", navigator, "#cafe-list") }}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
data-sort-country="{{ cafe.country }}"
|
||||
data-sort-city="{{ cafe.city }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
onclick="toggleRow(event)"
|
||||
onclick="window.location.href='{{ cafe.detail_path }}'"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||
<div>{{ cafe.created_date }}</div>
|
||||
|
|
@ -52,57 +52,11 @@
|
|||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cafe.city }}</td>
|
||||
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cafe.city }}</td>
|
||||
<td data-label="Country" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cafe.country }}</td>
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">
|
||||
{{ icons::location("h-4 w-4") }} View Map
|
||||
</a>
|
||||
{% if cafe.has_website %}
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
href="{{ cafe.website_url }}" target="_blank" rel="noreferrer noopener">
|
||||
{{ icons::external_link("h-4 w-4") }} Homepage
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
||||
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
href="{{ cafe.map_url }}" target="_blank" rel="noreferrer noopener">
|
||||
{{ icons::location("h-4 w-4") }} View Map
|
||||
<a href="{{ cafe.detail_path }}"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt">
|
||||
{{ icons::chevron_right("h-5 w-5") }}
|
||||
</a>
|
||||
{% if cafe.has_website %}
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
href="{{ cafe.website_url }}" target="_blank" rel="noreferrer noopener">
|
||||
{{ icons::external_link("h-4 w-4") }} Homepage
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cafe?') && @delete('/api/v1/cafes/{{ cafe.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cafe-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
|||
|
|
@ -28,9 +28,7 @@
|
|||
{{ table::sortable_header("Roaster", "roaster", navigator, "#cup-list") }}
|
||||
{{ table::sortable_header("Cafe", "cafe", navigator, "#cup-list") }}
|
||||
{{ table::sortable_header("City", "city", navigator, "#cup-list") }}
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
|
|
@ -38,7 +36,7 @@
|
|||
<tr
|
||||
data-star-key="{{ cup.id }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="toggleRow(event)"{% endif %}
|
||||
onclick="window.location.href='/cups/{{ cup.id }}'"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||
<div>{{ cup.created_date }}</div>
|
||||
|
|
@ -55,39 +53,13 @@
|
|||
</td>
|
||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cup.cafe_city }}</td>
|
||||
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ cup.cafe_city }}</td>
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
||||
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="6" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this cup?') && @delete('/api/v1/cups/{{ cup.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#cup-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
<a href="/cups/{{ cup.id }}"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt">
|
||||
{{ icons::chevron_right("h-5 w-5") }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -28,15 +28,13 @@
|
|||
{{ table::sortable_header("Category", "category", navigator, "#gear-list") }}
|
||||
{{ table::sortable_header("Make", "make", navigator, "#gear-list") }}
|
||||
{{ table::sortable_header("Model", "model", navigator, "#gear-list") }}
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
{% for item in gear.items %}
|
||||
<tr class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="toggleRow(event)"{% endif %}
|
||||
onclick="window.location.href='/gear/{{ item.id }}'"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||
<div>{{ item.created_date }}</div>
|
||||
|
|
@ -52,39 +50,13 @@
|
|||
<td data-label="Model" class="mobile-hidden px-4 py-3 whitespace-nowrap">
|
||||
{{ item.model }}
|
||||
</td>
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
||||
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this gear?') && @delete('/api/v1/gear/{{ item.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#gear-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
<a href="/gear/{{ item.id }}"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt">
|
||||
{{ icons::chevron_right("h-5 w-5") }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -27,9 +27,7 @@
|
|||
{{ table::sortable_header("Roast", "name", navigator, "#roast-list") }}
|
||||
{{ table::sortable_header("Origin", "origin", navigator, "#roast-list") }}
|
||||
<th scope="col" class="px-4 py-3">Tasting Notes</th>
|
||||
{% if is_authenticated %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
{% endif %}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
|
|
@ -42,7 +40,7 @@
|
|||
data-sort-origin="{{ roast.origin }}"
|
||||
data-sort-producer="{{ roast.producer }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
{% if is_authenticated %}onclick="toggleRow(event)"{% endif %}
|
||||
onclick="window.location.href='{{ roast.detail_path }}'"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||
<div>{{ roast.created_date }}</div>
|
||||
|
|
@ -73,39 +71,13 @@
|
|||
<span class="pill pill-muted">No Notes</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
||||
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% if is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roast?') && @delete('/api/v1/roasts/{{ roast.full_id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roast-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
</div>
|
||||
<a href="{{ roast.detail_path }}"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt">
|
||||
{{ icons::chevron_right("h-5 w-5") }}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
{{ table::sortable_header("Name", "name", navigator, "#roaster-list") }}
|
||||
{{ table::sortable_header("Country", "country", navigator, "#roaster-list") }}
|
||||
{{ table::sortable_header("City", "city", navigator, "#roaster-list") }}
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
||||
<th scope="col" class="actions-col px-4 py-3 text-right"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y/70">
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
data-sort-country="{{ roaster.country }}"
|
||||
data-sort-city="{{ roaster.city }}"
|
||||
class="transition hover:bg-surface-alt"
|
||||
{% if roaster.has_homepage || is_authenticated %}onclick="toggleRow(event)"{% endif %}
|
||||
onclick="window.location.href='{{ roaster.detail_path }}'"
|
||||
>
|
||||
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||
<div>{{ roaster.created_date }}</div>
|
||||
|
|
@ -54,57 +54,13 @@
|
|||
{% endif %}
|
||||
<td data-label="Country" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ roaster.country }}</td>
|
||||
<td data-label="City" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ roaster.city }}</td>
|
||||
{% if roaster.has_homepage || is_authenticated %}
|
||||
<td data-label="" class="card-detail hidden">
|
||||
<div class="flex items-center gap-4">
|
||||
{% if roaster.has_homepage %}
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
href="{{ roaster.homepage_url }}" target="_blank" rel="noreferrer noopener">
|
||||
{{ icons::external_link("h-4 w-4") }} Homepage
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||
{% if roaster.has_homepage || is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt"
|
||||
title="Actions">
|
||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
||||
<span class="icon-collapse hidden">{{ icons::chevron_up("h-5 w-5") }}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if roaster.has_homepage || is_authenticated %}
|
||||
<tr class="hidden detail-row">
|
||||
<td colspan="5" class="bg-surface-alt px-4 py-2">
|
||||
<div class="flex items-center gap-4">
|
||||
{% if roaster.has_homepage %}
|
||||
<a class="inline-flex items-center gap-1 text-sm font-medium text-accent hover:text-accent-hover"
|
||||
href="{{ roaster.homepage_url }}" target="_blank" rel="noreferrer noopener">
|
||||
{{ icons::external_link("h-4 w-4") }} Homepage
|
||||
<a href="{{ roaster.detail_path }}"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-md text-text-muted transition hover:text-accent hover:bg-surface-alt">
|
||||
{{ icons::chevron_right("h-5 w-5") }}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_authenticated %}
|
||||
<button type="button"
|
||||
class="inline-flex items-center gap-1 text-sm font-medium text-text-muted hover:text-red-600"
|
||||
data-on:click="confirm('Delete this roaster?') && @delete('/api/v1/roasters/{{ roaster.id }}?{{ navigator.query() }}', {responseOverrides: {selector: '#roaster-list', mode: 'replace'}})">
|
||||
{{ icons::delete("h-4 w-4") }} Delete
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -203,6 +203,7 @@ async fn roasts_create_with_datastar_header_returns_fragment() {
|
|||
.post(app.api_url("/roasts"))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.header("referer", format!("{}/data?type=roasts", app.address))
|
||||
.json(&new_roast)
|
||||
.send()
|
||||
.await
|
||||
|
|
@ -314,6 +315,7 @@ async fn bags_update_with_datastar_header_returns_fragment() {
|
|||
.put(app.api_url(&format!("/bags/{}", bag.id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.header("referer", format!("{}/data?type=bags", app.address))
|
||||
.json(&update)
|
||||
.send()
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ macro_rules! define_datastar_entity_tests {
|
|||
.delete(app.api_url(&format!("{}/{}", $api_path, entity_id)))
|
||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||
.header("datastar-request", "true")
|
||||
.header("referer", format!("{}/data?type={}", app.address, $type_param))
|
||||
.send()
|
||||
.await
|
||||
.expect(concat!("failed to delete ", stringify!($entity)));
|
||||
|
|
|
|||
|
|
@ -124,8 +124,8 @@ async fn creating_a_roaster_surfaces_on_the_timeline() {
|
|||
"Expected roaster name to appear in timeline HTML, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("/data?type=roasters"),
|
||||
"Expected roaster link in timeline HTML, got: {body}"
|
||||
body.contains("/roasters/"),
|
||||
"Expected roaster detail link in timeline HTML, got: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -506,8 +506,8 @@ async fn creating_a_cafe_surfaces_on_the_timeline() {
|
|||
"Expected cafe name to appear in timeline HTML, got: {body}"
|
||||
);
|
||||
assert!(
|
||||
body.contains("/data?type=cafes"),
|
||||
"Expected cafe link in timeline HTML, got: {body}"
|
||||
body.contains("/cafes/"),
|
||||
"Expected cafe detail link in timeline HTML, got: {body}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue