feat: with local store

This commit is contained in:
Kasper Juul Hermansen 2023-03-06 12:29:34 +01:00
parent e16127ecca
commit 762c792c05
Signed by: kjuulh
GPG Key ID: 57B6E1465221F912
12 changed files with 293 additions and 27 deletions

64
Cargo.lock generated
View File

@ -403,6 +403,15 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
[[package]]
name = "domain"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
"uuid",
]
[[package]] [[package]]
name = "drain_filter_polyfill" name = "drain_filter_polyfill"
version = "0.1.3" version = "0.1.3"
@ -441,6 +450,16 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "eyre"
version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb"
dependencies = [
"indenter",
"once_cell",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -773,6 +792,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "1.9.2" version = "1.9.2"
@ -1496,9 +1521,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.93" version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1526,6 +1551,33 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "services"
version = "0.1.0"
dependencies = [
"chrono",
"domain",
"eyre",
"serde",
"serde_json",
"serde_yaml",
"tokio",
"uuid",
]
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.6" version = "0.10.6"
@ -1603,6 +1655,7 @@ dependencies = [
"chrono", "chrono",
"console_error_panic_hook", "console_error_panic_hook",
"console_log", "console_log",
"domain",
"lazy_static", "lazy_static",
"leptos", "leptos",
"leptos_axum", "leptos_axum",
@ -1610,6 +1663,7 @@ dependencies = [
"leptos_router", "leptos_router",
"log", "log",
"serde", "serde",
"services",
"simple_logger", "simple_logger",
"thiserror", "thiserror",
"tokio", "tokio",
@ -1942,6 +1996,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c"
[[package]] [[package]]
name = "url" name = "url"
version = "2.3.1" version = "2.3.1"

View File

@ -1,3 +1,7 @@
[workspace]
members = [".", "crates/services", "crates/domain"]
[package] [package]
name = "ssr_modes_axum" name = "ssr_modes_axum"
version = "0.1.0" version = "0.1.0"
@ -27,6 +31,9 @@ wasm-bindgen = "0.2"
chrono = { version = "0.4.23", features = ["serde"] } chrono = { version = "0.4.23", features = ["serde"] }
uuid = { version = "1.3.0", features = ["v4", "wasm-bindgen", "js", "serde"] } uuid = { version = "1.3.0", features = ["v4", "wasm-bindgen", "js", "serde"] }
domain = { path = "crates/domain" }
services = { path = "crates/services", optional = true }
[features] [features]
hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"]
ssr = [ ssr = [
@ -38,6 +45,7 @@ ssr = [
"leptos_meta/ssr", "leptos_meta/ssr",
"leptos_router/ssr", "leptos_router/ssr",
"dep:leptos_axum", "dep:leptos_axum",
"dep:services",
] ]
[package.metadata.leptos] [package.metadata.leptos]

View File

@ -0,0 +1,13 @@
---
coverImage:
url: "https://cdn-rdb.arla.com/Files/arla-dk/2010638351/0606cf14-3972-4abb-b2c8-faa3249de170.jpg?crop=(0,482,0,-117)&w=1269&h=715&mode=crop&ak=6826258c&hm=f35b5bfe"
alt: billede af oksesteg
name: Gammeldags oksesteg
description: |
God gammeldags oksesteg med en intens og fyldig brun sauce. Gammeldags oksesteg
er rigtig simremad som gør de fleste glade. Så server en gammeldags oksesteg for
din gæster... både de unge og de gamle.
time: 2023-03-06
---
Some article

14
crates/domain/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[lib]
crate-type = ["cdylib", "rlib"]
[package]
name = "domain"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = { version = "0.4.23", features = ["serde"] }
serde = { version = "1.0.152", features = ["derive"] }
uuid = { version = "1.3.0", features = ["v4", "wasm-bindgen", "js", "serde"] }

View File

@ -25,7 +25,7 @@ pub struct Event {
pub cover_image: Option<Image>, pub cover_image: Option<Image>,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub time: chrono::DateTime<chrono::Utc>, pub time: chrono::NaiveDate,
pub recipe_id: Option<uuid::Uuid>, pub recipe_id: Option<uuid::Uuid>,
pub images: Vec<Image>, pub images: Vec<Image>,
pub metadata: Option<Metadata>, pub metadata: Option<Metadata>,
@ -37,7 +37,7 @@ pub struct EventOverview {
pub cover_image: Option<Image>, pub cover_image: Option<Image>,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub time: chrono::DateTime<chrono::Utc>, pub time: chrono::NaiveDate,
} }
impl From<Event> for EventOverview { impl From<Event> for EventOverview {

2
crates/services/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/Cargo.lock

View File

@ -0,0 +1,19 @@
[lib]
crate-type = ["cdylib", "rlib"]
[package]
name = "services"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = { version = "0.4.23", features = ["serde"] }
domain = { path = "../domain" }
eyre = "0.6.8"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.94"
serde_yaml = "0.9.19"
tokio = { version = "1.26.0", features = ["full"] }
uuid = { version = "1.3.0", features = ["v4", "serde"] }

143
crates/services/src/lib.rs Normal file
View File

@ -0,0 +1,143 @@
use std::path::PathBuf;
use domain::{Event, Image, Metadata};
use serde::{Deserialize, Serialize};
pub struct EventStore {
pub path: PathBuf,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawImage {
pub url: String,
pub alt: String,
pub metadata: Option<Metadata>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawEvent {
pub cover_image: Option<RawImage>,
pub name: String,
pub description: Option<String>,
#[serde(with = "short_time_stamp")]
pub time: chrono::NaiveDate,
pub recipe_id: Option<uuid::Uuid>,
//pub images: Vec<RawImage>,
pub metadata: Option<Metadata>,
#[serde(skip)]
pub content: String,
}
mod short_time_stamp {
use chrono::{DateTime, NaiveDate, TimeZone, Utc};
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &'static str = "%Y-%m-%d";
pub fn serialize<S>(date: &NaiveDate, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = format!("{}", date.format(FORMAT));
serializer.serialize_str(&s)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<NaiveDate, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
NaiveDate::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)
}
}
impl From<RawEvent> for Event {
fn from(value: RawEvent) -> Self {
Self {
id: uuid::Uuid::new_v4(),
cover_image: value.cover_image.map(|ci| ci.into()),
name: value.name,
description: value.description,
time: value.time,
recipe_id: value.recipe_id,
images: vec![],
metadata: value.metadata,
}
}
}
impl From<RawImage> for Image {
fn from(value: RawImage) -> Self {
Self {
id: uuid::Uuid::new_v4(),
url: value.url,
alt: value.alt,
metadata: value.metadata,
}
}
}
impl EventStore {
pub fn new(path: PathBuf) -> Self {
Self { path }
}
pub async fn get_upcoming_events(&self) -> eyre::Result<Vec<Event>> {
let mut event_path = self.path.clone();
event_path.push("events");
let mut dir = tokio::fs::read_dir(event_path).await?;
let mut events = vec![];
while let Ok(Some(entry)) = dir.next_entry().await {
let metadata = entry.metadata().await?;
if metadata.is_file() {
let file = tokio::fs::read(entry.path()).await?;
let content = std::str::from_utf8(&file)?;
if content.starts_with("---\n") {
let after_marker = &content[4..];
if let Some(marker_end) = after_marker.find("---\n") {
let raw_front_matter = &content[4..marker_end + 4];
let mut raw_event: RawEvent = serde_yaml::from_str(raw_front_matter)?;
raw_event.content = content[marker_end + 4..].to_string();
events.push(raw_event.into())
}
}
}
}
Ok(events)
}
}
impl Default for EventStore {
fn default() -> Self {
Self {
path: PathBuf::from("articles"),
}
}
}
#[cfg(test)]
mod test {
use domain::Event;
use crate::RawEvent;
#[test]
fn can_parse_event() {
let raw = r#"coverImage:
url: "https://cdn-rdb.arla.com/Files/arla-dk/2010638351/0606cf14-3972-4abb-b2c8-faa3249de170.jpg?crop=(0,482,0,-117)&w=1269&h=715&mode=crop&ak=6826258c&hm=f35b5bfe"
alt: billede af oksesteg
name: Gammeldags oksesteg
description: |
God gammeldags oksesteg med en intens og fyldig brun sauce. Gammeldags oksesteg
er rigtig simremad som gør de fleste glade. server en gammeldags oksesteg for
din gæster... både de unge og de gamle.
time: 2023-03-06"#;
let raw_event: RawEvent = serde_yaml::from_str(raw).unwrap();
let _: Event = raw_event.into();
}
}

View File

@ -2,7 +2,7 @@ use lazy_static::lazy_static;
use leptos::*; use leptos::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::models::{Event, EventOverview, Image}; use domain::{Event, EventOverview, Image};
lazy_static! { lazy_static! {
static ref EVENTS: Vec<Event> = vec![ static ref EVENTS: Vec<Event> = vec![
@ -18,7 +18,8 @@ lazy_static! {
description: Some("Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.".into()), description: Some("Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.".into()),
time: chrono::Utc::now() time: chrono::Utc::now()
.checked_add_days(chrono::Days::new(1)) .checked_add_days(chrono::Days::new(1))
.unwrap(), .unwrap()
.date_naive(),
recipe_id: None, recipe_id: None,
images: vec![], images: vec![],
metadata: None, metadata: None,
@ -35,7 +36,8 @@ lazy_static! {
description: Some("Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.".into()), description: Some("Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco ut ea consectetur et est culpa et culpa duis.".into()),
time: chrono::Utc::now() time: chrono::Utc::now()
.checked_add_days(chrono::Days::new(4)) .checked_add_days(chrono::Days::new(4))
.unwrap(), .unwrap()
.date_naive(),
recipe_id: None, recipe_id: None,
images: vec![], images: vec![],
metadata: None, metadata: None,
@ -52,7 +54,8 @@ lazy_static! {
description: Some("description".into()), description: Some("description".into()),
time: chrono::Utc::now() time: chrono::Utc::now()
.checked_sub_days(chrono::Days::new(2)) .checked_sub_days(chrono::Days::new(2))
.unwrap(), .unwrap()
.date_naive(),
recipe_id: None, recipe_id: None,
images: vec![], images: vec![],
metadata: None, metadata: None,
@ -71,15 +74,19 @@ pub async fn get_upcoming_events() -> Result<UpcomingEventsOverview, ServerFnErr
get_upcoming_events_fn().await get_upcoming_events_fn().await
} }
#[cfg(feature = "ssr")]
async fn get_upcoming_events_fn() -> Result<UpcomingEventsOverview, ServerFnError> { async fn get_upcoming_events_fn() -> Result<UpcomingEventsOverview, ServerFnError> {
let current_time = chrono::Utc::now(); let current_time = chrono::Utc::now();
let mut events: Vec<EventOverview> = EVENTS let es = services::EventStore::default();
let events = es
.get_upcoming_events()
.await
.map_err(|e| ServerFnError::ServerError(e.to_string()))?
.iter() .iter()
.filter(|data| data.time > current_time)
.map(|data| data.clone().into()) .map(|data| data.clone().into())
.collect(); .collect();
events.sort_by(|a, b| a.time.cmp(&b.time));
Ok(UpcomingEventsOverview { events }) Ok(UpcomingEventsOverview { events })
} }
@ -91,6 +98,7 @@ pub async fn get_full_event(event_id: uuid::Uuid) -> Result<Option<Event>, Serve
get_full_event_fn(event_id).await get_full_event_fn(event_id).await
} }
#[cfg(feature = "ssr")]
async fn get_full_event_fn(event_id: uuid::Uuid) -> Result<Option<Event>, ServerFnError> { async fn get_full_event_fn(event_id: uuid::Uuid) -> Result<Option<Event>, ServerFnError> {
let event = EVENTS let event = EVENTS
.iter() .iter()

View File

@ -10,20 +10,20 @@ pub fn App(cx: Scope) -> impl IntoView {
provide_meta_context(cx); provide_meta_context(cx);
view! { cx, view! { cx,
<Stylesheet id="leptos" href="/pkg/ssr_modes.css" /> <Stylesheet id="leptos" href="/pkg/ssr_modes.css" />
<Title text="Bitebuds" /> <Title text="Bitebuds" />
<Router> <Router>
<div class="app grid lg:grid-cols-[25%,50%,25%] sm:grid-cols-[10%,80%,10%] grid-cols-[5%,90%,5%]"> <div class="app grid lg:grid-cols-[25%,50%,25%] sm:grid-cols-[10%,80%,10%] grid-cols-[5%,90%,5%]">
<main class="main col-start-2"> <main class="main col-start-2">
<div class="pt-4"> <div class="pt-4">
<h1 class="font-semibold text-xl tracking-wide">"Bitebuds"</h1> <h1 class="font-semibold text-xl tracking-wide">"Bitebuds"</h1>
<Routes> <Routes>
<Route path="" view=|cx| view! { cx, <HomePage /> }/> <Route path="" view=|cx| view! { cx, <HomePage /> }/>
</Routes> </Routes>
</div> </div>
</main> </main>
</div> </div>
</Router> </Router>
} }
} }

View File

@ -2,7 +2,7 @@ use chrono::Datelike;
use leptos::*; use leptos::*;
use crate::api::events::*; use crate::api::events::*;
use crate::models::{EventOverview, Image}; use domain::{EventOverview, Image};
#[component] #[component]
pub fn Day( pub fn Day(

View File

@ -4,7 +4,6 @@ pub mod api;
pub mod app; pub mod app;
mod components; mod components;
pub mod fallback; pub mod fallback;
mod models;
mod pages; mod pages;
use cfg_if::cfg_if; use cfg_if::cfg_if;