add card images and detail view

This commit is contained in:
kageru 2023-01-30 15:57:08 +01:00
parent 78975b00af
commit 27adb0f196
3 changed files with 133 additions and 30 deletions

@ -30,7 +30,7 @@ pub struct Card {
impl Display for Card { impl Display for Card {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} (", &self.name)?; write!(f, r#"<h2><a href="/{}">{}</a></h2><br/><em>"#, &self.id, &self.name)?;
if let Some(level) = self.level { if let Some(level) = self.level {
if self.card_type.contains("XYZ") { if self.card_type.contains("XYZ") {
f.write_str("Rank ")?; f.write_str("Rank ")?;
@ -44,9 +44,7 @@ impl Display for Card {
if let Some(attr) = &self.attribute { if let Some(attr) = &self.attribute {
write!(f, "{attr}/")?; write!(f, "{attr}/")?;
} }
write!(f, "{} {})", self.r#type, self.card_type)?; write!(f, "{} {}", self.r#type, self.card_type)?;
f.write_str("<br/>")?;
f.write_str(&self.text)?;
if self.card_type.contains(&String::from("Monster")) { if self.card_type.contains(&String::from("Monster")) {
f.write_str("<br/>")?; f.write_str("<br/>")?;
match (self.atk, self.def) { match (self.atk, self.def) {
@ -57,6 +55,8 @@ impl Display for Card {
(None, None) => write!(f, "? ATK / ? DEF")?, (None, None) => write!(f, "? ATK / ? DEF")?,
} }
} }
f.write_str("</em><br/>")?;
f.write_str(&self.text)?;
Ok(()) Ok(())
} }
} }

@ -28,7 +28,7 @@ async fn main() -> std::io::Result<()> {
// tap these so they’re initialized // tap these so they’re initialized
let num_cards = (CARDS_BY_ID.len() + SEARCH_CARDS.len()) / 2; let num_cards = (CARDS_BY_ID.len() + SEARCH_CARDS.len()) / 2;
println!("Read {num_cards} cards in {:?}", now.elapsed()); println!("Read {num_cards} cards in {:?}", now.elapsed());
HttpServer::new(|| App::new().service(search)).bind((Ipv4Addr::from([127, 0, 0, 1]), 8080))?.run().await HttpServer::new(|| App::new().service(search).service(card_info)).bind((Ipv4Addr::from([127, 0, 0, 1]), 8080))?.run().await
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -44,22 +44,64 @@ async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> Resul
Some(Either::Left(web::Query(Query { q }))) => Some(q), Some(Either::Left(web::Query(Query { q }))) => Some(q),
Some(Either::Right(web::Form(Query { q }))) => Some(q), Some(Either::Right(web::Form(Query { q }))) => Some(q),
None => None, None => None,
}; }
.filter(|s| !s.is_empty());
let mut res = String::with_capacity(10_000); let mut res = String::with_capacity(10_000);
res.write_str(HEADER)?; res.push_str(HEADER);
render_searchbox(&mut res, &q)?;
match q {
Some(q) => render_results(&mut res, &q)?,
None => res.push_str("Enter a query above to search"),
}
finish_document(&mut res);
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
}
#[get("/{id}")]
async fn card_info(card_id: web::Path<usize>) -> Result<HttpResponse, Box<dyn std::error::Error>> {
let mut res = String::with_capacity(2_000);
res.push_str(HEADER);
render_searchbox(&mut res, &None)?;
match CARDS_BY_ID.get(&card_id) {
Some(card) => {
res.push_str(r#""#);
write!(
res,
r#"
<div class="row">
<div class="column left">{card}</div>
<div class="column right"><img style="width: 100%;" src="http://localhost:80/img/{}.jpg"/></div>
</div>"#,
card.id,
)?;
}
None => res.push_str("Card not found"),
}
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
}
fn render_searchbox(res: &mut String, query: &Option<String>) -> std::fmt::Result {
write!( write!(
res, res,
r#" r#"
<form action="/"> <form action="/">
<input type="text" name="q" id="searchbox" placeholder="Enter query (e.g. l:5 c:synchro atk>2000)" value="{}"><input type="submit" id="submit" value="🔍"> <input type="text" name="q" id="searchbox" placeholder="Enter query (e.g. l:5 c:synchro atk>2000)" value="{}"><input type="submit" id="submit" value="🔍">
</form>"#, </form>"#,
match &q { match &query {
Some(q) => q, Some(q) => q,
None => "", None => "",
} }
)?; )
if let Some(q) = q { }
let query = parser::parse_filters(&q)?;
fn render_results(res: &mut String, query: &str) -> Result<(), Box<dyn std::error::Error>> {
let query = match parser::parse_filters(query) {
Ok(q) => q,
Err(e) => {
write!(res, "Could not parse query: {e:?}")?;
return Ok(());
}
};
let now = Instant::now(); let now = Instant::now();
let matches: Vec<&Card> = SEARCH_CARDS let matches: Vec<&Card> = SEARCH_CARDS
.iter() .iter()
@ -69,18 +111,25 @@ async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> Resul
.collect(); .collect();
write!( write!(
res, res,
"<span class=\"meta\">Showing {} results where {} (took {:?})</span><hr/>", "<span class=\"meta\">Showing {} results where {} (took {:?})</span>",
matches.len(), matches.len(),
query.iter().map(|(f, _)| f.to_string()).join(" and "), query.iter().map(|(f, _)| f.to_string()).join(" and "),
now.elapsed() now.elapsed()
)?; )?;
if matches.is_empty() {
return Ok(());
}
res.push_str("<table>");
for card in matches { for card in matches {
res.push_str(&card.to_string()); write!(
res.push_str("<hr/>"); res,
r#"<tr><td>{card}</td><td><a href="/{}"><img src="http://localhost:80/img/{}.jpg" class="thumb"/></a></td></tr>"#,
card.id, card.id
)?;
} }
write!(res, "</body></html>")?; Ok(())
} else {
res.write_str("Enter a query above to search")?;
} }
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
fn finish_document(res: &mut String) {
res.push_str("</body></html>")
} }

@ -10,7 +10,7 @@
} }
html { html {
padding-top: 2em; padding: 2em 0;
background-color: var(--bg); background-color: var(--bg);
color: var(--fg); color: var(--fg);
font-family: 'Lato', 'Segoe UI', sans-serif; font-family: 'Lato', 'Segoe UI', sans-serif;
@ -52,9 +52,63 @@ form > *:focus {
width: 10%; width: 10%;
} }
a {
color: var(--hl);
}
hr { hr {
border-color: var(--hl); border-color: var(--hl);
} }
table {
width: 100%;
}
td {
border-bottom: 2px solid var(--hl);
padding: 0.5em 0;
text-align: justify;
}
td:first-child {
width: 80%;
padding-right: 1em;
}
td:nth-child(2) {
width: 20%;
}
h1, h2 {
margin-block-end: 0;
font-weight: normal;
}
h2 {
font-size: 120%;
}
.thumb {
max-height: 250px;
}
.column {
float: left;
margin-bottom: 2em;
}
.left {
width: 60%;
padding-right: 1em;
}
.right {
width: 35%;
}
@media screen and (max-width: 680px) {
.column {
width: 100%;
}
}
.row:after {
content: "";
display: table;
clear: both;
}
</style> </style>
</head> </head>
<body> <body>