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
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else {
|
} else {
|
||||||
use axum::http::header::HeaderValue;
|
crate::application::routes::support::render_redirect_script(&detail_url)
|
||||||
let script = format!("<script>window.location.href='{detail_url}'</script>");
|
.map_err(ApiError::from)
|
||||||
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) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
Ok(Redirect::to(&detail_url).into_response())
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
|
|
@ -175,9 +167,20 @@ pub(crate) async fn update_bag(
|
||||||
state.stats_invalidator.invalidate();
|
state.stats_invalidator.invalidate();
|
||||||
|
|
||||||
if is_datastar_request(&headers) {
|
if is_datastar_request(&headers) {
|
||||||
render_bag_list_fragment(state, request, search, true)
|
let from_bag_page = headers
|
||||||
.await
|
.get("referer")
|
||||||
.map_err(ApiError::from)
|
.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 {
|
} else {
|
||||||
let enriched = state
|
let enriched = state
|
||||||
.bag_repo
|
.bag_repo
|
||||||
|
|
@ -193,7 +196,9 @@ define_delete_handler!(
|
||||||
BagId,
|
BagId,
|
||||||
BagSortKey,
|
BagSortKey,
|
||||||
bag_repo,
|
bag_repo,
|
||||||
render_bag_list_fragment
|
render_bag_list_fragment,
|
||||||
|
"type=bags",
|
||||||
|
"/data?type=bags"
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -269,16 +269,8 @@ pub(crate) async fn create_brew(
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)
|
.map_err(ApiError::from)
|
||||||
} else {
|
} else {
|
||||||
use axum::http::header::HeaderValue;
|
crate::application::routes::support::render_redirect_script(&detail_url)
|
||||||
let script = format!("<script>window.location.href='{detail_url}'</script>");
|
.map_err(ApiError::from)
|
||||||
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) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
Ok(Redirect::to(&detail_url).into_response())
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
|
|
@ -323,7 +315,9 @@ define_delete_handler!(
|
||||||
BrewId,
|
BrewId,
|
||||||
BrewSortKey,
|
BrewSortKey,
|
||||||
brew_repo,
|
brew_repo,
|
||||||
render_brew_list_fragment
|
render_brew_list_fragment,
|
||||||
|
"type=brews",
|
||||||
|
"/data?type=brews"
|
||||||
);
|
);
|
||||||
|
|
||||||
async fn render_brew_list_fragment(
|
async fn render_brew_list_fragment(
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,9 @@ define_delete_handler!(
|
||||||
CafeId,
|
CafeId,
|
||||||
CafeSortKey,
|
CafeSortKey,
|
||||||
cafe_repo,
|
cafe_repo,
|
||||||
render_cafe_list_fragment
|
render_cafe_list_fragment,
|
||||||
|
"type=cafes",
|
||||||
|
"/data?type=cafes"
|
||||||
);
|
);
|
||||||
|
|
||||||
define_list_fragment_renderer!(
|
define_list_fragment_renderer!(
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,9 @@ define_delete_handler!(
|
||||||
CupId,
|
CupId,
|
||||||
CupSortKey,
|
CupSortKey,
|
||||||
cup_repo,
|
cup_repo,
|
||||||
render_cup_list_fragment
|
render_cup_list_fragment,
|
||||||
|
"type=cups",
|
||||||
|
"/data?type=cups"
|
||||||
);
|
);
|
||||||
|
|
||||||
define_list_fragment_renderer!(
|
define_list_fragment_renderer!(
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,9 @@ define_delete_handler!(
|
||||||
GearId,
|
GearId,
|
||||||
GearSortKey,
|
GearSortKey,
|
||||||
gear_repo,
|
gear_repo,
|
||||||
render_gear_list_fragment
|
render_gear_list_fragment,
|
||||||
|
"type=gear",
|
||||||
|
"/data?type=gear"
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -59,12 +59,19 @@ macro_rules! define_enriched_get_handler {
|
||||||
|
|
||||||
/// Generates a DELETE handler with Datastar fragment re-rendering support.
|
/// 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
|
/// # Arguments
|
||||||
/// * `$fn_name` - Name of the generated handler function
|
/// * `$fn_name` - Name of the generated handler function
|
||||||
/// * `$id_type` - Type of the ID path parameter (e.g., `RoasterId`)
|
/// * `$id_type` - Type of the ID path parameter (e.g., `RoasterId`)
|
||||||
/// * `$sort_key` - Sort key type for list requests (e.g., `RoasterSortKey`)
|
/// * `$sort_key` - Sort key type for list requests (e.g., `RoasterSortKey`)
|
||||||
/// * `$repo_field` - Name of the repository field on `AppState` (e.g., `roaster_repo`)
|
/// * `$repo_field` - Name of the repository field on `AppState` (e.g., `roaster_repo`)
|
||||||
/// * `$render_fragment` - Path to the fragment render function
|
/// * `$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
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
|
|
@ -73,11 +80,13 @@ macro_rules! define_enriched_get_handler {
|
||||||
/// RoasterId,
|
/// RoasterId,
|
||||||
/// RoasterSortKey,
|
/// RoasterSortKey,
|
||||||
/// roaster_repo,
|
/// roaster_repo,
|
||||||
/// render_roaster_list_fragment
|
/// render_roaster_list_fragment,
|
||||||
|
/// "type=roasters",
|
||||||
|
/// "/data?type=roasters"
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
macro_rules! define_delete_handler {
|
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))]
|
#[tracing::instrument(skip(state, _auth_user, headers, query))]
|
||||||
pub(crate) async fn $fn_name(
|
pub(crate) async fn $fn_name(
|
||||||
axum::extract::State(state): axum::extract::State<crate::application::state::AppState>,
|
axum::extract::State(state): axum::extract::State<crate::application::state::AppState>,
|
||||||
|
|
@ -99,9 +108,19 @@ macro_rules! define_delete_handler {
|
||||||
state.stats_invalidator.invalidate();
|
state.stats_invalidator.invalidate();
|
||||||
|
|
||||||
if crate::application::routes::support::is_datastar_request(&headers) {
|
if crate::application::routes::support::is_datastar_request(&headers) {
|
||||||
$render_fragment(state, request, search, true)
|
let from_data_page = headers
|
||||||
.await
|
.get("referer")
|
||||||
.map_err(crate::application::errors::ApiError::from)
|
.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 {
|
} else {
|
||||||
Ok(axum::http::StatusCode::NO_CONTENT.into_response())
|
Ok(axum::http::StatusCode::NO_CONTENT.into_response())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,9 @@ define_delete_handler!(
|
||||||
RoasterId,
|
RoasterId,
|
||||||
RoasterSortKey,
|
RoasterSortKey,
|
||||||
roaster_repo,
|
roaster_repo,
|
||||||
render_roaster_list_fragment
|
render_roaster_list_fragment,
|
||||||
|
"type=roasters",
|
||||||
|
"/data?type=roasters"
|
||||||
);
|
);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, auth_user, headers, payload))]
|
#[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");
|
info!(roast_id = %roast.id, name = %roast.name, "roast created");
|
||||||
state.stats_invalidator.invalidate();
|
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) {
|
if is_datastar_request(&headers) {
|
||||||
render_roast_list_fragment(state, request, search, true)
|
let from_data_page = headers
|
||||||
.await
|
.get("referer")
|
||||||
.map_err(ApiError::from)
|
.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) {
|
} else if matches!(source, PayloadSource::Form) {
|
||||||
let target =
|
Ok(Redirect::to(&detail_url).into_response())
|
||||||
ListNavigator::new(ROAST_PAGE_PATH, ROAST_FRAGMENT_PATH, request, search).page_href(1);
|
|
||||||
Ok(Redirect::to(&target).into_response())
|
|
||||||
} else {
|
} else {
|
||||||
let enriched = state
|
let enriched = state
|
||||||
.roast_repo
|
.roast_repo
|
||||||
|
|
@ -146,7 +169,9 @@ define_delete_handler!(
|
||||||
RoastId,
|
RoastId,
|
||||||
RoastSortKey,
|
RoastSortKey,
|
||||||
roast_repo,
|
roast_repo,
|
||||||
render_roast_list_fragment
|
render_roast_list_fragment,
|
||||||
|
"type=roasts",
|
||||||
|
"/data?type=roasts"
|
||||||
);
|
);
|
||||||
|
|
||||||
#[tracing::instrument(skip(state, _auth_user))]
|
#[tracing::instrument(skip(state, _auth_user))]
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ mod data;
|
||||||
mod gear;
|
mod gear;
|
||||||
mod home;
|
mod home;
|
||||||
mod roasters;
|
mod roasters;
|
||||||
|
mod roasts;
|
||||||
mod stats;
|
mod stats;
|
||||||
mod timeline;
|
mod timeline;
|
||||||
mod webauthn;
|
mod webauthn;
|
||||||
|
|
@ -39,6 +40,10 @@ pub(super) fn router() -> axum::Router<AppState> {
|
||||||
.route("/cups/{id}", get(cups::cup_detail_page))
|
.route("/cups/{id}", get(cups::cup_detail_page))
|
||||||
.route("/gear/{id}", get(gear::gear_detail_page))
|
.route("/gear/{id}", get(gear::gear_detail_page))
|
||||||
.route("/roasters/{slug}", get(roasters::roaster_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("/styles.css", get(styles))
|
||||||
.route("/webauthn.js", get(webauthn_js))
|
.route("/webauthn.js", get(webauthn_js))
|
||||||
.route("/components/photo-capture.js", get(photo_capture_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)
|
(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>(
|
pub fn render_fragment<T: Template>(
|
||||||
template: T,
|
template: T,
|
||||||
selector: &'static str,
|
selector: &'static str,
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ use super::views::{
|
||||||
BagDetailView, BagOptionView, BagView, BrewDefaultsView, BrewDetailView, BrewView,
|
BagDetailView, BagOptionView, BagView, BrewDefaultsView, BrewDetailView, BrewView,
|
||||||
CafeDetailView, CafeOptionView, CafeView, CupDetailView, CupView, GearDetailView,
|
CafeDetailView, CafeOptionView, CafeView, CupDetailView, CupView, GearDetailView,
|
||||||
GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, QuickNoteView,
|
GearOptionView, GearView, ListNavigator, NearbyCafeView, Paginated, QuickNoteView,
|
||||||
RoastOptionView, RoastView, RoasterDetailView, RoasterOptionView, RoasterView, StatCard,
|
RoastDetailView, RoastOptionView, RoastView, RoasterDetailView, RoasterOptionView, RoasterView,
|
||||||
StatsView, TimelineEventView, TimelineMonthView,
|
StatCard, StatsView, TimelineEventView, TimelineMonthView,
|
||||||
};
|
};
|
||||||
use crate::domain::bags::BagSortKey;
|
use crate::domain::bags::BagSortKey;
|
||||||
use crate::domain::brews::BrewSortKey;
|
use crate::domain::brews::BrewSortKey;
|
||||||
|
|
@ -241,6 +241,16 @@ pub struct CupDetailTemplate {
|
||||||
pub cup: CupDetailView,
|
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)]
|
#[derive(Template)]
|
||||||
#[template(path = "pages/roaster.html")]
|
#[template(path = "pages/roaster.html")]
|
||||||
pub struct RoasterDetailTemplate {
|
pub struct RoasterDetailTemplate {
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ impl Default for BrewDefaultsView {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct BrewDetailView {
|
pub struct BrewDetailView {
|
||||||
|
pub id: String,
|
||||||
// Coffee info
|
// Coffee info
|
||||||
pub roast_name: String,
|
pub roast_name: String,
|
||||||
pub roaster_name: String,
|
pub roaster_name: String,
|
||||||
|
|
@ -252,6 +253,7 @@ impl BrewDetailView {
|
||||||
.join(", ");
|
.join(", ");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
id: brew.brew.id.to_string(),
|
||||||
roast_name: brew.roast_name,
|
roast_name: brew.roast_name,
|
||||||
roaster_name: brew.roaster_name,
|
roaster_name: brew.roaster_name,
|
||||||
origin: coffee.origin,
|
origin: coffee.origin,
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ impl CupView {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CupDetailView {
|
pub struct CupDetailView {
|
||||||
|
pub id: String,
|
||||||
// Coffee info
|
// Coffee info
|
||||||
pub roast_name: String,
|
pub roast_name: String,
|
||||||
pub roaster_name: String,
|
pub roaster_name: String,
|
||||||
|
|
@ -87,6 +88,7 @@ impl CupDetailView {
|
||||||
let (map_countries, map_max) = build_map_data(&map_entries);
|
let (map_countries, map_max) = build_map_data(&map_entries);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
id: cup.cup.id.to_string(),
|
||||||
roast_name: cup.roast_name,
|
roast_name: cup.roast_name,
|
||||||
roaster_name: cup.roaster_name,
|
roaster_name: cup.roaster_name,
|
||||||
origin: coffee.origin,
|
origin: coffee.origin,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub use cafes::{CafeDetailView, CafeOptionView, CafeView, NearbyCafeView};
|
||||||
pub use cups::{CupDetailView, CupView};
|
pub use cups::{CupDetailView, CupView};
|
||||||
pub use gear::{GearDetailView, GearOptionView, GearView};
|
pub use gear::{GearDetailView, GearOptionView, GearView};
|
||||||
pub use roasters::{RoasterDetailView, RoasterOptionView, RoasterView};
|
pub use roasters::{RoasterDetailView, RoasterOptionView, RoasterView};
|
||||||
pub use roasts::{RoastOptionView, RoastView};
|
pub use roasts::{RoastDetailView, RoastOptionView, RoastView};
|
||||||
pub use tasting_notes::TastingNoteView;
|
pub use tasting_notes::TastingNoteView;
|
||||||
pub use timeline::{
|
pub use timeline::{
|
||||||
TimelineBrewDataView, TimelineEventDetailView, TimelineEventView, TimelineMonthView,
|
TimelineBrewDataView, TimelineEventDetailView, TimelineEventView, TimelineMonthView,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
use crate::domain::roasters::Roaster;
|
||||||
use crate::domain::roasts::{Roast, RoastWithRoaster};
|
use crate::domain::roasts::{Roast, RoastWithRoaster};
|
||||||
|
|
||||||
use super::tasting_notes::{self, TastingNoteView};
|
use super::tasting_notes::{self, TastingNoteView};
|
||||||
|
use super::{build_coffee_info, build_map_data, build_roaster_info};
|
||||||
|
|
||||||
pub struct RoastView {
|
pub struct RoastView {
|
||||||
pub id: String,
|
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 struct RoastOptionView {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
|
|
|
||||||
|
|
@ -72,14 +72,14 @@ impl TimelineEventView {
|
||||||
let TimelineEvent {
|
let TimelineEvent {
|
||||||
id,
|
id,
|
||||||
entity_type,
|
entity_type,
|
||||||
entity_id: _,
|
entity_id,
|
||||||
action,
|
action,
|
||||||
occurred_at,
|
occurred_at,
|
||||||
title,
|
title,
|
||||||
details,
|
details,
|
||||||
tasting_notes,
|
tasting_notes,
|
||||||
slug: _,
|
slug,
|
||||||
roaster_slug: _,
|
roaster_slug,
|
||||||
brew_data,
|
brew_data,
|
||||||
} = event;
|
} = event;
|
||||||
|
|
||||||
|
|
@ -96,11 +96,21 @@ impl TimelineEventView {
|
||||||
};
|
};
|
||||||
|
|
||||||
let link = match entity_type.as_str() {
|
let link = match entity_type.as_str() {
|
||||||
"roaster" => "/data?type=roasters".to_string(),
|
"brew" => format!("/brews/{entity_id}"),
|
||||||
"roast" | "bag" | "brew" => format!("/data?type={entity_type}s"),
|
"cup" => format!("/cups/{entity_id}"),
|
||||||
"cafe" => "/data?type=cafes".to_string(),
|
"bag" => format!("/bags/{entity_id}"),
|
||||||
"cup" => "/data?type=cups".to_string(),
|
"gear" => format!("/gear/{entity_id}"),
|
||||||
"gear" => "/data?type=gear".to_string(),
|
"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("#"),
|
_ => String::from("#"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,8 @@
|
||||||
{{ bag.roaster_name }} · {{ bag.amount }} · Opened {{ bag.created_date }}
|
{{ bag.roaster_name }} · {{ bag.amount }} · Opened {{ bag.created_date }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ detail::share_button() }}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ detail::share_script() }}
|
|
||||||
|
|
||||||
{# ── Coffee + map ── #}
|
{# ── Coffee + map ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<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) }}
|
{{ 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="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">
|
<div class="rounded-lg border bg-surface p-5 md:col-span-2 flex items-center gap-2">
|
||||||
{% if !bag.closed %}
|
{% if !bag.closed %}
|
||||||
<button type="button" onclick="closeBag('{{ bag.id }}')"
|
<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">
|
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
|
{{ icons::x_mark("h-4 w-4") }} Close Bag
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="button" onclick="deleteBag('{{ bag.id }}')"
|
<button type="button"
|
||||||
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">
|
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
|
{{ icons::delete("h-4 w-4") }} Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "partials/detail_cards.html" as detail %}
|
{% import "partials/detail_cards.html" as detail %}
|
||||||
|
{% import "partials/icons.html" as icons %}
|
||||||
{% block title %}Brewlog · {{ brew.roast_name }}{% endblock %}
|
{% 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 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 %}
|
{% block og_title %}{{ brew.roast_name }} — Brewlog{% endblock %}
|
||||||
|
|
@ -13,11 +14,8 @@
|
||||||
{{ brew.roaster_name }} · Brewed {{ brew.created_date }} at {{ brew.created_time }}
|
{{ brew.roaster_name }} · Brewed {{ brew.created_date }} at {{ brew.created_time }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ detail::share_button() }}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ detail::share_script() }}
|
|
||||||
|
|
||||||
{# ── Coffee + map ── #}
|
{# ── Coffee + map ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<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) }}
|
{{ 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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,8 @@
|
||||||
{{ cafe.city }}, {{ cafe.country_flag }} {{ cafe.country }} · Added {{ cafe.created_date }}
|
{{ cafe.city }}, {{ cafe.country_flag }} {{ cafe.country }} · Added {{ cafe.created_date }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ detail::share_button() }}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ detail::share_script() }}
|
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<div class="rounded-lg border bg-surface p-5">
|
<div class="rounded-lg border bg-surface p-5">
|
||||||
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
||||||
|
|
@ -48,25 +45,11 @@
|
||||||
|
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||||
<button type="button" onclick="deleteCafe('{{ cafe.id }}')"
|
<button type="button"
|
||||||
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">
|
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
|
{{ icons::delete("h-4 w-4") }} Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "partials/detail_cards.html" as detail %}
|
{% 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 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 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 %}
|
{% 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 }}
|
{{ cup.roaster_name }} · {{ cup.cafe_name }}, {{ cup.cafe_city }} · {{ cup.created_date }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ detail::share_button() }}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ detail::share_script() }}
|
|
||||||
|
|
||||||
{# ── Coffee + map ── #}
|
{# ── Coffee + map ── #}
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<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) }}
|
{{ 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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% import "partials/detail_cards.html" as detail %}
|
|
||||||
{% import "partials/icons.html" as icons %}
|
{% import "partials/icons.html" as icons %}
|
||||||
{% block title %}Brewlog · {{ gear.make }} {{ gear.model }}{% endblock %}
|
{% block title %}Brewlog · {{ gear.make }} {{ gear.model }}{% endblock %}
|
||||||
{% block og_title %}{{ gear.make }} {{ gear.model }} — Brewlog{% endblock %}
|
{% block og_title %}{{ gear.make }} {{ gear.model }} — Brewlog{% endblock %}
|
||||||
|
|
@ -13,11 +12,8 @@
|
||||||
{{ gear.category_label }} · Added {{ gear.created_date }}
|
{{ gear.category_label }} · Added {{ gear.created_date }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ detail::share_button() }}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ detail::share_script() }}
|
|
||||||
|
|
||||||
<div class="rounded-lg border bg-surface p-5">
|
<div class="rounded-lg border bg-surface p-5">
|
||||||
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
<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">
|
<dl class="grid grid-cols-2 gap-x-4 gap-y-3 text-sm">
|
||||||
|
|
@ -38,25 +34,11 @@
|
||||||
|
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||||
<button type="button" onclick="deleteGear('{{ gear.id }}')"
|
<button type="button"
|
||||||
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">
|
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
|
{{ icons::delete("h-4 w-4") }} Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -197,22 +197,20 @@
|
||||||
{% if !recent_events.is_empty() %}
|
{% if !recent_events.is_empty() %}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{% for event in recent_events %}
|
{% 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">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<span class="text-text-muted shrink-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 %}
|
{% 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>
|
</span>
|
||||||
<p class="min-w-0 truncate text-sm">
|
<p class="min-w-0 truncate text-sm">
|
||||||
<a href="{{ event.link }}" class="font-medium text-text hover:text-accent"
|
<span class="font-medium text-text">{{ event.title }}</span>{% if let Some(sub) = event.subtitle %}
|
||||||
>{{ event.title }}</a
|
|
||||||
>{% if let Some(sub) = event.subtitle %}
|
|
||||||
<span class="text-xs text-text-muted">· {{ sub }}</span>{% endif %}
|
<span class="text-xs text-text-muted">· {{ sub }}</span>{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<time class="text-xs text-text-muted whitespace-nowrap shrink-0"
|
<time class="text-xs text-text-muted whitespace-nowrap shrink-0"
|
||||||
>{{ event.relative_date_label }}</time
|
>{{ event.relative_date_label }}</time
|
||||||
>
|
>
|
||||||
</div>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% 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 }}
|
{{ roaster.country_flag }} {{ roaster.country }}{% if let Some(c) = roaster.city %} · {{ c }}{% endif %} · Added {{ roaster.created_date }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{{ detail::share_button() }}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{{ detail::share_script() }}
|
|
||||||
|
|
||||||
<div class="grid gap-6 md:grid-cols-2">
|
<div class="grid gap-6 md:grid-cols-2">
|
||||||
<div class="rounded-lg border bg-surface p-5">
|
<div class="rounded-lg border bg-surface p-5">
|
||||||
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
<h2 class="text-lg font-semibold text-text mb-4">Details</h2>
|
||||||
|
|
@ -50,25 +47,11 @@
|
||||||
|
|
||||||
{% if is_authenticated %}
|
{% if is_authenticated %}
|
||||||
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
<div class="rounded-lg border bg-surface p-5 flex items-center gap-2">
|
||||||
<button type="button" onclick="deleteRoaster('{{ roaster.id }}')"
|
<button type="button"
|
||||||
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">
|
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
|
{{ icons::delete("h-4 w-4") }} Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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 %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,13 @@
|
||||||
{{ table::sortable_header("Roast", "roast", navigator, "#bag-list") }}
|
{{ table::sortable_header("Roast", "roast", navigator, "#bag-list") }}
|
||||||
{{ table::sortable_header("Status", "status", navigator, "#bag-list") }}
|
{{ table::sortable_header("Status", "status", navigator, "#bag-list") }}
|
||||||
{{ table::sortable_header("Finished", "finished-at", 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"></th>
|
||||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
{% for bag in bags.items %}
|
{% for bag in bags.items %}
|
||||||
<tr class="transition hover:bg-surface-alt"
|
<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">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||||
<div>{{ bag.created_date }}</div>
|
<div>{{ bag.created_date }}</div>
|
||||||
|
|
@ -76,23 +74,6 @@
|
||||||
<div>{{ bag.finished_date }}</div>
|
<div>{{ bag.finished_date }}</div>
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% 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">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<div class="inline-flex items-center gap-2">
|
<div class="inline-flex items-center gap-2">
|
||||||
<span class="md:hidden">
|
<span class="md:hidden">
|
||||||
|
|
@ -100,36 +81,13 @@
|
||||||
<span class="text-text-muted">Closed</span>
|
<span class="text-text-muted">Closed</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<button type="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"
|
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">
|
{{ icons::chevron_right("h-5 w-5") }}
|
||||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
</a>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,6 @@
|
||||||
{% import "partials/lists/table.html" as table %}
|
{% import "partials/lists/table.html" as table %}
|
||||||
{% import "partials/icons.html" as icons %}
|
{% 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">
|
<div id="brew-list" class="mt-6" data-star-scope="brews">
|
||||||
{% if brews.items.is_empty() && !navigator.has_search() %}
|
{% if brews.items.is_empty() && !navigator.has_search() %}
|
||||||
<div
|
<div
|
||||||
|
|
@ -44,15 +30,13 @@
|
||||||
<th scope="col" class="px-4 py-3">Recipe</th>
|
<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">Brewer</th>
|
||||||
<th scope="col" class="px-4 py-3">Notes</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"></th>
|
||||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
{% for brew in brews.items %}
|
{% for brew in brews.items %}
|
||||||
<tr class="transition hover:bg-surface-alt"
|
<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">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||||
<div>{{ brew.created_date }}</div>
|
<div>{{ brew.created_date }}</div>
|
||||||
|
|
@ -96,27 +80,13 @@
|
||||||
<span class="pill pill-muted">No Notes</span>
|
<span class="pill pill-muted">No Notes</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<button type="button"
|
<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"
|
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">
|
{{ icons::chevron_right("h-5 w-5") }}
|
||||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
</a>
|
||||||
<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) }}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
{{ table::sortable_header("Name", "name", navigator, "#cafe-list") }}
|
{{ table::sortable_header("Name", "name", navigator, "#cafe-list") }}
|
||||||
{{ table::sortable_header("City", "city", navigator, "#cafe-list") }}
|
{{ table::sortable_header("City", "city", navigator, "#cafe-list") }}
|
||||||
{{ table::sortable_header("Country", "country", 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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
data-sort-country="{{ cafe.country }}"
|
data-sort-country="{{ cafe.country }}"
|
||||||
data-sort-city="{{ cafe.city }}"
|
data-sort-city="{{ cafe.city }}"
|
||||||
class="transition hover:bg-surface-alt"
|
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">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||||
<div>{{ cafe.created_date }}</div>
|
<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="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="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="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">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<button type="button"
|
<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"
|
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">
|
{{ icons::chevron_right("h-5 w-5") }}
|
||||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
</a>
|
||||||
<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>
|
|
||||||
{% 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>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@
|
||||||
{{ table::sortable_header("Roaster", "roaster", navigator, "#cup-list") }}
|
{{ table::sortable_header("Roaster", "roaster", navigator, "#cup-list") }}
|
||||||
{{ table::sortable_header("Cafe", "cafe", navigator, "#cup-list") }}
|
{{ table::sortable_header("Cafe", "cafe", navigator, "#cup-list") }}
|
||||||
{{ table::sortable_header("City", "city", 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"></th>
|
||||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
|
|
@ -38,7 +36,7 @@
|
||||||
<tr
|
<tr
|
||||||
data-star-key="{{ cup.id }}"
|
data-star-key="{{ cup.id }}"
|
||||||
class="transition hover:bg-surface-alt"
|
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">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||||
<div>{{ cup.created_date }}</div>
|
<div>{{ cup.created_date }}</div>
|
||||||
|
|
@ -55,39 +53,13 @@
|
||||||
</td>
|
</td>
|
||||||
<td data-label="City" class="px-4 py-3 whitespace-nowrap md:hidden">{{ cup.cafe_city }}</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>
|
<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">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<button type="button"
|
<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"
|
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">
|
{{ icons::chevron_right("h-5 w-5") }}
|
||||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
</a>
|
||||||
<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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,13 @@
|
||||||
{{ table::sortable_header("Category", "category", navigator, "#gear-list") }}
|
{{ table::sortable_header("Category", "category", navigator, "#gear-list") }}
|
||||||
{{ table::sortable_header("Make", "make", navigator, "#gear-list") }}
|
{{ table::sortable_header("Make", "make", navigator, "#gear-list") }}
|
||||||
{{ table::sortable_header("Model", "model", 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"></th>
|
||||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
{% for item in gear.items %}
|
{% for item in gear.items %}
|
||||||
<tr class="transition hover:bg-surface-alt"
|
<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">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||||
<div>{{ item.created_date }}</div>
|
<div>{{ item.created_date }}</div>
|
||||||
|
|
@ -52,39 +50,13 @@
|
||||||
<td data-label="Model" class="mobile-hidden px-4 py-3 whitespace-nowrap">
|
<td data-label="Model" class="mobile-hidden px-4 py-3 whitespace-nowrap">
|
||||||
{{ item.model }}
|
{{ item.model }}
|
||||||
</td>
|
</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">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<button type="button"
|
<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"
|
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">
|
{{ icons::chevron_right("h-5 w-5") }}
|
||||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
</a>
|
||||||
<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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -27,9 +27,7 @@
|
||||||
{{ table::sortable_header("Roast", "name", navigator, "#roast-list") }}
|
{{ table::sortable_header("Roast", "name", navigator, "#roast-list") }}
|
||||||
{{ table::sortable_header("Origin", "origin", navigator, "#roast-list") }}
|
{{ table::sortable_header("Origin", "origin", navigator, "#roast-list") }}
|
||||||
<th scope="col" class="px-4 py-3">Tasting Notes</th>
|
<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"></th>
|
||||||
<th scope="col" class="actions-col px-4 py-3 text-right">Actions</th>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
|
|
@ -42,7 +40,7 @@
|
||||||
data-sort-origin="{{ roast.origin }}"
|
data-sort-origin="{{ roast.origin }}"
|
||||||
data-sort-producer="{{ roast.producer }}"
|
data-sort-producer="{{ roast.producer }}"
|
||||||
class="transition hover:bg-surface-alt"
|
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">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||||
<div>{{ roast.created_date }}</div>
|
<div>{{ roast.created_date }}</div>
|
||||||
|
|
@ -73,39 +71,13 @@
|
||||||
<span class="pill pill-muted">No Notes</span>
|
<span class="pill pill-muted">No Notes</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</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">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
<button type="button"
|
<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"
|
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">
|
{{ icons::chevron_right("h-5 w-5") }}
|
||||||
<span class="icon-expand">{{ icons::ellipsis_vertical("h-5 w-5") }}</span>
|
</a>
|
||||||
<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>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
{{ table::sortable_header("Name", "name", navigator, "#roaster-list") }}
|
{{ table::sortable_header("Name", "name", navigator, "#roaster-list") }}
|
||||||
{{ table::sortable_header("Country", "country", navigator, "#roaster-list") }}
|
{{ table::sortable_header("Country", "country", navigator, "#roaster-list") }}
|
||||||
{{ table::sortable_header("City", "city", 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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="divide-y/70">
|
<tbody class="divide-y/70">
|
||||||
|
|
@ -39,7 +39,7 @@
|
||||||
data-sort-country="{{ roaster.country }}"
|
data-sort-country="{{ roaster.country }}"
|
||||||
data-sort-city="{{ roaster.city }}"
|
data-sort-city="{{ roaster.city }}"
|
||||||
class="transition hover:bg-surface-alt"
|
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">
|
<td data-label="Added" class="card-date whitespace-nowrap px-4 py-3 font-medium text-text-secondary">
|
||||||
<div>{{ roaster.created_date }}</div>
|
<div>{{ roaster.created_date }}</div>
|
||||||
|
|
@ -54,57 +54,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td data-label="Country" class="mobile-hidden px-4 py-3 whitespace-nowrap">{{ roaster.country }}</td>
|
<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>
|
<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">
|
<td data-label="" class="card-actions px-4 py-3 text-right">
|
||||||
{% if roaster.has_homepage || is_authenticated %}
|
<a href="{{ roaster.detail_path }}"
|
||||||
<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">
|
||||||
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") }}
|
||||||
title="Actions">
|
</a>
|
||||||
<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>
|
</td>
|
||||||
</tr>
|
</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>
|
|
||||||
{% 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 %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ async fn roasts_create_with_datastar_header_returns_fragment() {
|
||||||
.post(app.api_url("/roasts"))
|
.post(app.api_url("/roasts"))
|
||||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
.header("datastar-request", "true")
|
.header("datastar-request", "true")
|
||||||
|
.header("referer", format!("{}/data?type=roasts", app.address))
|
||||||
.json(&new_roast)
|
.json(&new_roast)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|
@ -314,6 +315,7 @@ async fn bags_update_with_datastar_header_returns_fragment() {
|
||||||
.put(app.api_url(&format!("/bags/{}", bag.id)))
|
.put(app.api_url(&format!("/bags/{}", bag.id)))
|
||||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
.header("datastar-request", "true")
|
.header("datastar-request", "true")
|
||||||
|
.header("referer", format!("{}/data?type=bags", app.address))
|
||||||
.json(&update)
|
.json(&update)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,7 @@ macro_rules! define_datastar_entity_tests {
|
||||||
.delete(app.api_url(&format!("{}/{}", $api_path, entity_id)))
|
.delete(app.api_url(&format!("{}/{}", $api_path, entity_id)))
|
||||||
.bearer_auth(app.auth_token.as_ref().unwrap())
|
.bearer_auth(app.auth_token.as_ref().unwrap())
|
||||||
.header("datastar-request", "true")
|
.header("datastar-request", "true")
|
||||||
|
.header("referer", format!("{}/data?type={}", app.address, $type_param))
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.expect(concat!("failed to delete ", stringify!($entity)));
|
.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}"
|
"Expected roaster name to appear in timeline HTML, got: {body}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("/data?type=roasters"),
|
body.contains("/roasters/"),
|
||||||
"Expected roaster link in timeline HTML, got: {body}"
|
"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}"
|
"Expected cafe name to appear in timeline HTML, got: {body}"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("/data?type=cafes"),
|
body.contains("/cafes/"),
|
||||||
"Expected cafe link in timeline HTML, got: {body}"
|
"Expected cafe detail link in timeline HTML, got: {body}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue