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 {
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 self.card_type.contains("XYZ") {
f.write_str("Rank ")?;
@ -44,9 +44,7 @@ impl Display for Card {
if let Some(attr) = &self.attribute {
write!(f, "{attr}/")?;
}
write!(f, "{} {})", self.r#type, self.card_type)?;
f.write_str("<br/>")?;
f.write_str(&self.text)?;
write!(f, "{} {}", self.r#type, self.card_type)?;
if self.card_type.contains(&String::from("Monster")) {
f.write_str("<br/>")?;
match (self.atk, self.def) {
@ -57,6 +55,8 @@ impl Display for Card {
(None, None) => write!(f, "? ATK / ? DEF")?,
}
}
f.write_str("</em><br/>")?;
f.write_str(&self.text)?;
Ok(())
}
}

@ -28,7 +28,7 @@ async fn main() -> std::io::Result<()> {
// tap these so they’re initialized
let num_cards = (CARDS_BY_ID.len() + SEARCH_CARDS.len()) / 2;
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)]
@ -44,43 +44,92 @@ async fn search(q: Option<Either<web::Query<Query>, web::Form<Query>>>) -> Resul
Some(Either::Left(web::Query(Query { q }))) => Some(q),
Some(Either::Right(web::Form(Query { q }))) => Some(q),
None => None,
};
}
.filter(|s| !s.is_empty());
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!(
res,
r#"
<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="🔍">
</form>"#,
match &q {
match &query {
Some(q) => q,
None => "",
}
)
}
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 matches: Vec<&Card> = SEARCH_CARDS
.iter()
.filter(|card| query.iter().all(|(_, q)| q(card)))
.map(|c| CARDS_BY_ID.get(&c.id).unwrap())
.take(RESULT_LIMIT)
.collect();
write!(
res,
"<span class=\"meta\">Showing {} results where {} (took {:?})</span>",
matches.len(),
query.iter().map(|(f, _)| f.to_string()).join(" and "),
now.elapsed()
)?;
if let Some(q) = q {
let query = parser::parse_filters(&q)?;
let now = Instant::now();
let matches: Vec<&Card> = SEARCH_CARDS
.iter()
.filter(|card| query.iter().all(|(_, q)| q(card)))
.map(|c| CARDS_BY_ID.get(&c.id).unwrap())
.take(RESULT_LIMIT)
.collect();
if matches.is_empty() {
return Ok(());
}
res.push_str("<table>");
for card in matches {
write!(
res,
"<span class=\"meta\">Showing {} results where {} (took {:?})</span><hr/>",
matches.len(),
query.iter().map(|(f, _)| f.to_string()).join(" and "),
now.elapsed()
r#"<tr><td>{card}</td><td><a href="/{}"><img src="http://localhost:80/img/{}.jpg" class="thumb"/></a></td></tr>"#,
card.id, card.id
)?;
for card in matches {
res.push_str(&card.to_string());
res.push_str("<hr/>");
}
write!(res, "</body></html>")?;
} else {
res.write_str("Enter a query above to search")?;
}
Ok(HttpResponse::Ok().insert_header(header::ContentType::html()).body(res))
Ok(())
}
fn finish_document(res: &mut String) {
res.push_str("</body></html>")
}

@ -10,7 +10,7 @@
}
html {
padding-top: 2em;
padding: 2em 0;
background-color: var(--bg);
color: var(--fg);
font-family: 'Lato', 'Segoe UI', sans-serif;
@ -52,9 +52,63 @@ form > *:focus {
width: 10%;
}
a {
color: var(--hl);
}
hr {
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>
</head>
<body>