mirror of
https://github.com/kjuulh/bitebuds.git
synced 2025-08-12 02:59:46 +02:00
feat: with base app
This commit is contained in:
101
src/api/events.rs
Normal file
101
src/api/events.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use lazy_static::lazy_static;
|
||||
use leptos::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::models::{Event, EventOverview, Image};
|
||||
|
||||
lazy_static! {
|
||||
static ref EVENTS: Vec<Event> = vec![
|
||||
Event {
|
||||
cover_image: Some(Image {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
url: "https://images.unsplash.com/photo-1513104890138-7c749659a591?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=400&q=80".into(),
|
||||
alt: "some-alt".into(),
|
||||
metadata: None,
|
||||
}),
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: "Pizza".into(),
|
||||
description: Some("Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat.".into()),
|
||||
time: chrono::Utc::now()
|
||||
.checked_add_days(chrono::Days::new(1))
|
||||
.unwrap(),
|
||||
recipe_id: None,
|
||||
images: vec![],
|
||||
metadata: None,
|
||||
},
|
||||
Event {
|
||||
cover_image: Some(Image {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
url: "https://images.unsplash.com/photo-1513104890138-7c749659a591?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=400&q=80".into(),
|
||||
alt: "some-alt".into(),
|
||||
metadata: None,
|
||||
}),
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: "Kød boller".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()
|
||||
.checked_add_days(chrono::Days::new(4))
|
||||
.unwrap(),
|
||||
recipe_id: None,
|
||||
images: vec![],
|
||||
metadata: None,
|
||||
},
|
||||
Event {
|
||||
cover_image: Some(Image {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
url: "https://images.unsplash.com/photo-1513104890138-7c749659a591?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=400&q=80".into(),
|
||||
alt: "some-alt".into(),
|
||||
metadata: None,
|
||||
}),
|
||||
id: uuid::Uuid::new_v4(),
|
||||
name: "Pizza".into(),
|
||||
description: Some("description".into()),
|
||||
time: chrono::Utc::now()
|
||||
.checked_sub_days(chrono::Days::new(2))
|
||||
.unwrap(),
|
||||
recipe_id: None,
|
||||
images: vec![],
|
||||
metadata: None,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct UpcomingEventsOverview {
|
||||
pub events: Vec<EventOverview>,
|
||||
}
|
||||
|
||||
#[server(GetUpcomingEvents, "/api")]
|
||||
pub async fn get_upcoming_events() -> Result<UpcomingEventsOverview, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
get_upcoming_events_fn().await
|
||||
}
|
||||
|
||||
async fn get_upcoming_events_fn() -> Result<UpcomingEventsOverview, ServerFnError> {
|
||||
let current_time = chrono::Utc::now();
|
||||
|
||||
let mut events: Vec<EventOverview> = EVENTS
|
||||
.iter()
|
||||
.filter(|data| data.time > current_time)
|
||||
.map(|data| data.clone().into())
|
||||
.collect();
|
||||
events.sort_by(|a, b| a.time.cmp(&b.time));
|
||||
|
||||
Ok(UpcomingEventsOverview { events })
|
||||
}
|
||||
|
||||
#[server(GetFullEvent, "/api")]
|
||||
pub async fn get_full_event(event_id: uuid::Uuid) -> Result<Option<Event>, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
|
||||
get_full_event_fn(event_id).await
|
||||
}
|
||||
|
||||
async fn get_full_event_fn(event_id: uuid::Uuid) -> Result<Option<Event>, ServerFnError> {
|
||||
let event = EVENTS
|
||||
.iter()
|
||||
.find(|data| data.id == event_id)
|
||||
.map(|d| d.clone());
|
||||
|
||||
Ok(event)
|
||||
}
|
9
src/api/mod.rs
Normal file
9
src/api/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
pub mod events;
|
||||
|
||||
use leptos::*;
|
||||
|
||||
#[cfg(feature = "ssr")]
|
||||
pub fn register() {
|
||||
events::GetUpcomingEvents::register();
|
||||
events::GetFullEvent::register();
|
||||
}
|
190
src/app.rs
190
src/app.rs
@@ -1,9 +1,8 @@
|
||||
use lazy_static::lazy_static;
|
||||
use leptos::*;
|
||||
use leptos_meta::*;
|
||||
use leptos_router::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::pages::home::*;
|
||||
|
||||
#[component]
|
||||
pub fn App(cx: Scope) -> impl IntoView {
|
||||
@@ -11,179 +10,20 @@ pub fn App(cx: Scope) -> impl IntoView {
|
||||
provide_meta_context(cx);
|
||||
|
||||
view! { cx,
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css"/>
|
||||
<Title text="Welcome to Leptos"/>
|
||||
<Stylesheet id="leptos" href="/pkg/ssr_modes.css" />
|
||||
<Title text="Bitebuds" />
|
||||
|
||||
<Router>
|
||||
<main>
|
||||
<Routes>
|
||||
// We’ll load the home page with out-of-order streaming and <Suspense/>
|
||||
<Route path="" view=|cx| view! { cx, <HomePage/> }/>
|
||||
|
||||
// We'll load the posts with async rendering, so they can set
|
||||
// the title and metadata *after* loading the data
|
||||
<Route
|
||||
path="/post/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
ssr=SsrMode::Async
|
||||
/>
|
||||
<Route
|
||||
path="/post_in_order/:id"
|
||||
view=|cx| view! { cx, <Post/> }
|
||||
ssr=SsrMode::InOrder
|
||||
/>
|
||||
</Routes>
|
||||
<Router>
|
||||
<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">
|
||||
<div class="pt-4">
|
||||
<h1 class="font-semibold text-xl tracking-wide">"Bitebuds"</h1>
|
||||
<Routes>
|
||||
<Route path="" view=|cx| view! { cx, <HomePage /> }/>
|
||||
</Routes>
|
||||
</div>
|
||||
</main>
|
||||
</Router>
|
||||
</div>
|
||||
</Router>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn HomePage(cx: Scope) -> impl IntoView {
|
||||
// load the posts
|
||||
let posts =
|
||||
create_resource(cx, || (), |_| async { list_post_metadata().await });
|
||||
let posts_view = move || {
|
||||
posts.with(cx, |posts| posts
|
||||
.clone()
|
||||
.map(|posts| {
|
||||
posts.iter()
|
||||
.map(|post| view! { cx, <li><a href=format!("/post/{}", post.id)>{&post.title}</a> "|" <a href=format!("/post_in_order/{}", post.id)>{&post.title}"(in order)"</a></li>})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
)
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<h1>"My Great Blog"</h1>
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading posts..."</p> }>
|
||||
<ul>{posts_view}</ul>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PostParams {
|
||||
id: usize,
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Post(cx: Scope) -> impl IntoView {
|
||||
let query = use_params::<PostParams>(cx);
|
||||
let id = move || {
|
||||
query.with(|q| {
|
||||
q.as_ref().map(|q| q.id).map_err(|_| PostError::InvalidId)
|
||||
})
|
||||
};
|
||||
let post = create_resource(cx, id, |id| async move {
|
||||
match id {
|
||||
Err(e) => Err(e),
|
||||
Ok(id) => get_post(id)
|
||||
.await
|
||||
.map(|data| data.ok_or(PostError::PostNotFound))
|
||||
.map_err(|_| PostError::ServerError)
|
||||
.flatten(),
|
||||
}
|
||||
});
|
||||
|
||||
let post_view = move || {
|
||||
post.with(cx, |post| {
|
||||
post.clone().map(|post| {
|
||||
view! { cx,
|
||||
// render content
|
||||
<h1>{&post.title}</h1>
|
||||
<p>{&post.content}</p>
|
||||
|
||||
// since we're using async rendering for this page,
|
||||
// this metadata should be included in the actual HTML <head>
|
||||
// when it's first served
|
||||
<Title text=post.title/>
|
||||
<Meta name="description" content=post.content/>
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
view! { cx,
|
||||
<Suspense fallback=move || view! { cx, <p>"Loading post..."</p> }>
|
||||
<ErrorBoundary fallback=|cx, errors| {
|
||||
view! { cx,
|
||||
<div class="error">
|
||||
<h1>"Something went wrong."</h1>
|
||||
<ul>
|
||||
{move || errors.get()
|
||||
.into_iter()
|
||||
.map(|(_, error)| view! { cx, <li>{error.to_string()} </li> })
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
}>
|
||||
{post_view}
|
||||
</ErrorBoundary>
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
// Dummy API
|
||||
lazy_static! {
|
||||
static ref POSTS: Vec<Post> = vec![
|
||||
Post {
|
||||
id: 0,
|
||||
title: "My first post".to_string(),
|
||||
content: "This is my first post".to_string(),
|
||||
},
|
||||
Post {
|
||||
id: 1,
|
||||
title: "My second post".to_string(),
|
||||
content: "This is my second post".to_string(),
|
||||
},
|
||||
Post {
|
||||
id: 2,
|
||||
title: "My third post".to_string(),
|
||||
content: "This is my third post".to_string(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PostError {
|
||||
#[error("Invalid post ID.")]
|
||||
InvalidId,
|
||||
#[error("Post not found.")]
|
||||
PostNotFound,
|
||||
#[error("Server error.")]
|
||||
ServerError,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Post {
|
||||
id: usize,
|
||||
title: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct PostMetadata {
|
||||
id: usize,
|
||||
title: String,
|
||||
}
|
||||
|
||||
#[server(ListPostMetadata, "/api")]
|
||||
pub async fn list_post_metadata() -> Result<Vec<PostMetadata>, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Ok(POSTS
|
||||
.iter()
|
||||
.map(|data| PostMetadata {
|
||||
id: data.id,
|
||||
title: data.title.clone(),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[server(GetPost, "/api")]
|
||||
pub async fn get_post(id: usize) -> Result<Option<Post>, ServerFnError> {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
Ok(POSTS.iter().find(|post| post.id == id).cloned())
|
||||
}
|
||||
|
197
src/components/day.rs
Normal file
197
src/components/day.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
use chrono::Datelike;
|
||||
use leptos::*;
|
||||
|
||||
use crate::api::events::*;
|
||||
use crate::models::{EventOverview, Image};
|
||||
|
||||
#[component]
|
||||
pub fn Day(
|
||||
cx: Scope,
|
||||
event: EventOverview,
|
||||
next: Option<bool>,
|
||||
last: Option<bool>,
|
||||
) -> impl IntoView {
|
||||
let (expanded, set_expanded) = create_signal(cx, false);
|
||||
let day = event.time.weekday().to_string();
|
||||
let timestamp = event.time.format("%Y-%m-%d").to_string();
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div class="sm:grid grid-cols-[1fr,4fr] gap-4 space-y-4 sm:space-y-0">
|
||||
<div class="relative">
|
||||
{if !last.unwrap_or(false) {
|
||||
view! {
|
||||
cx,
|
||||
<div class="bg-gray-300 absolute top-3 left-[3px] h-full w-0.5 hidden sm:block z-0"/>
|
||||
}.into_view(cx)
|
||||
} else {
|
||||
view! {cx, <div></div>}.into_view(cx)
|
||||
}}
|
||||
<div class="col-start-1 flex space-x-2">
|
||||
<div class={format!("hidden sm:block w-2 h-2 rounded-full mt-2.5 z-10 {}", if next.unwrap_or(false) {"bg-orange-600"} else { "bg-gray-300"})} />
|
||||
<div class="inline-block">
|
||||
<span class={format!("text-md font-medium {}", if next.unwrap_or(false) {"text-orange-600"} else {"text-gray-700"})}>
|
||||
{day}
|
||||
</span>
|
||||
<p class="text-xs font-normal text-gray-500">
|
||||
{timestamp}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-start-2 transition-all sm:pb-6">
|
||||
{move || if expanded() == true {
|
||||
view! {
|
||||
cx,
|
||||
<DayContentExpanded event_id=event.id.clone()/>
|
||||
}.into_view(cx)
|
||||
} else {
|
||||
view! {
|
||||
cx,
|
||||
<DayContentCollapsed event=event.clone() setter=set_expanded />
|
||||
}.into_view(cx)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider block sm:hidden h-0.5 w-full bg-gray-300 my-6 rounded-full" />
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn DayContentExpanded(cx: Scope, event_id: uuid::Uuid) -> impl IntoView {
|
||||
let full_event = create_resource(cx, move || (), move |_| get_full_event(event_id));
|
||||
|
||||
let image = |cx: Scope, image: Option<Image>| {
|
||||
if let Some(image) = image {
|
||||
view! {
|
||||
cx,
|
||||
<img src={image.url} alt=image.alt class="object-cover max-h-[250px] " />
|
||||
}.into_view(cx)
|
||||
} else {
|
||||
view! {cx, <div></div>}.into_view(cx)
|
||||
}
|
||||
};
|
||||
|
||||
let event_view = move || full_event.with(cx, |event| {
|
||||
event.clone().map(|event| {event.map(|event| view! {
|
||||
cx,
|
||||
<article class="day-content space-x-3 min-h-[150px] flex flex-col">
|
||||
{image(cx, event.cover_image)}
|
||||
<div class="day-content__body space-y-2 pt-6">
|
||||
<h2 class="font-semibold text-xl text-orange-600">{event.name}</h2>
|
||||
|
||||
{
|
||||
event.description.map(|d| view! {cx,
|
||||
<p class="font-normal sm:px-6 text">
|
||||
{d}
|
||||
</p>
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
event.recipe_id.map(|_r| {view! {cx,
|
||||
<h3 class="font-medium text-lg pt-2 text-orange-600">"Recipe"</h3>
|
||||
|
||||
<ol class="px-10">
|
||||
<li class="list-item list-decimal">
|
||||
"Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat."
|
||||
</li>
|
||||
<li class="list-item list-decimal">
|
||||
"Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur
|
||||
cupidatat."
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h3 class="font-medium text-lg pt-2 text-orange-600">"References"</h3>
|
||||
<ul class="px-10">
|
||||
<li class="list-item list-decimal">
|
||||
<a href={r"https://google.com"}>
|
||||
"Lorem ipsum dolor sit amet, qui minim labore adipisicing minim sint cillum sint consectetur cupidatat"</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h3 class="font-medium text-lg pt-2 text-orange-600">"Images"</h3>
|
||||
<div class="day-content__images grid grid-cols-3 gap-4 mx-4 pt-2">
|
||||
<img
|
||||
src={r"https://images.unsplash.com/photo-1677856217391-838a585c8290?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"}
|
||||
alt="no alt text in sight"
|
||||
class="object-cover"
|
||||
/>
|
||||
<img
|
||||
src={r"https://images.unsplash.com/photo-1677856217391-838a585c8290?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"}
|
||||
alt="no alt text in sight"
|
||||
class="object-cover"
|
||||
/>
|
||||
<img
|
||||
src={r"https://images.unsplash.com/photo-1677856217391-838a585c8290?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2070&q=80"}
|
||||
alt="no alt text in sight"
|
||||
class="object-cover"
|
||||
/>
|
||||
</div>
|
||||
}})
|
||||
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="pb-10" />
|
||||
})})
|
||||
});
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<Suspense fallback=move || view! {cx, <p>"Loading events..."</p>}>
|
||||
{event_view}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn DayContentCollapsed(
|
||||
cx: Scope,
|
||||
setter: WriteSignal<bool>,
|
||||
event: EventOverview,
|
||||
) -> impl IntoView {
|
||||
let image = event.cover_image.clone();
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<article class="day-content sm:grid grid-cols-[30%,70%] sm:space-x-6 flex flex-col">
|
||||
{if let Some(image) = image {
|
||||
view! {
|
||||
cx,
|
||||
<div class="content-start justify-start">
|
||||
<img src={image.url} alt=image.alt class="object-cover place-self-center w-full max-w-full max-h-[150px] sm:max-h-full " />
|
||||
</div>
|
||||
}.into_view(cx)
|
||||
} else {
|
||||
view!{cx, <div></div>}.into_view(cx)
|
||||
}}
|
||||
<div class="day-content__body flex flex-col">
|
||||
<h2 class="font-semibold text-lg text-orange-600">{event.name}</h2>
|
||||
{if let Some(mut description) = event.description.clone() {
|
||||
description.truncate(120);
|
||||
view! {cx,
|
||||
<p class="font-normal text-sm">
|
||||
{
|
||||
if description.len() == 120 {
|
||||
format!("{description}...")
|
||||
} else {
|
||||
description
|
||||
}
|
||||
}
|
||||
</p>
|
||||
}.into_view(cx)
|
||||
} else {
|
||||
view! {cx, <div></div>}.into_view(cx)
|
||||
}}
|
||||
<div class="flex-grow" />
|
||||
<button
|
||||
on:click=move |_| setter.update(|value| *value = !*value)
|
||||
class="transition-all h-3 w-20 bg-gray-200 hover:bg-gray-300 self-center rounded-b-[4rem] rounded-t-[1rem] mt-3"
|
||||
/>
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
}
|
1
src/components/mod.rs
Normal file
1
src/components/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod day;
|
@@ -1,7 +1,11 @@
|
||||
#![feature(result_flattening)]
|
||||
|
||||
pub mod api;
|
||||
pub mod app;
|
||||
mod components;
|
||||
pub mod fallback;
|
||||
mod models;
|
||||
mod pages;
|
||||
use cfg_if::cfg_if;
|
||||
|
||||
cfg_if! {
|
||||
|
15
src/main.rs
15
src/main.rs
@@ -1,12 +1,16 @@
|
||||
#[cfg(feature = "ssr")]
|
||||
#[tokio::main]
|
||||
async fn main(){
|
||||
async fn main() {
|
||||
use axum::{
|
||||
extract::{Extension, Path},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use leptos::*;
|
||||
use leptos_axum::{generate_route_list, LeptosRoutes};
|
||||
use axum::{extract::{Extension, Path}, Router, routing::{get, post}};
|
||||
use std::sync::Arc;
|
||||
use ssr_modes_axum::fallback::file_and_error_handler;
|
||||
use ssr_modes_axum::app::*;
|
||||
use ssr_modes_axum::fallback::file_and_error_handler;
|
||||
use std::sync::Arc;
|
||||
|
||||
let conf = get_configuration(None).await.unwrap();
|
||||
let addr = conf.leptos_options.site_addr;
|
||||
@@ -14,8 +18,7 @@ async fn main(){
|
||||
// Generate the list of routes in your Leptos App
|
||||
let routes = generate_route_list(|cx| view! { cx, <App/> }).await;
|
||||
|
||||
GetPost::register();
|
||||
ListPostMetadata::register();
|
||||
ssr_modes_axum::api::register();
|
||||
|
||||
let app = Router::new()
|
||||
.route("/api/*fn_name", post(leptos_axum::handle_server_fns))
|
||||
|
53
src/models.rs
Normal file
53
src/models.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Metadata(HashMap<String, String>);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Recipe {
|
||||
pub id: uuid::Uuid,
|
||||
pub metadata: Option<Metadata>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Image {
|
||||
pub id: uuid::Uuid,
|
||||
pub url: String,
|
||||
pub alt: String,
|
||||
pub metadata: Option<Metadata>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Event {
|
||||
pub id: uuid::Uuid,
|
||||
pub cover_image: Option<Image>,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub time: chrono::DateTime<chrono::Utc>,
|
||||
pub recipe_id: Option<uuid::Uuid>,
|
||||
pub images: Vec<Image>,
|
||||
pub metadata: Option<Metadata>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct EventOverview {
|
||||
pub id: uuid::Uuid,
|
||||
pub cover_image: Option<Image>,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub time: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<Event> for EventOverview {
|
||||
fn from(value: Event) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
cover_image: value.cover_image,
|
||||
name: value.name,
|
||||
description: value.description,
|
||||
time: value.time,
|
||||
}
|
||||
}
|
||||
}
|
52
src/pages/home.rs
Normal file
52
src/pages/home.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
use leptos::*;
|
||||
|
||||
use crate::api;
|
||||
use crate::components::day::{Day, DayProps};
|
||||
|
||||
#[component]
|
||||
pub fn HomePage(cx: Scope) -> impl IntoView {
|
||||
let events = create_resource(
|
||||
cx,
|
||||
|| (),
|
||||
|_| async { api::events::get_upcoming_events().await },
|
||||
);
|
||||
|
||||
let events_view = move || {
|
||||
events.with(cx, |events| {
|
||||
events.clone().map(|event_overview| {
|
||||
event_overview
|
||||
.events
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, event)| {
|
||||
view! {
|
||||
cx,
|
||||
<Day
|
||||
event=event.clone()
|
||||
next={Some(index == 0)}
|
||||
last={
|
||||
if event_overview.events.len() - 1 == index {
|
||||
Some(true)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
cx,
|
||||
<div class="space-y-4 pt-8">
|
||||
<Suspense fallback=move || view! {cx, <p>"Loading events..."</p>}>
|
||||
<ul class="days flex flex-col">
|
||||
{events_view}
|
||||
</ul>
|
||||
</Suspense>
|
||||
</div>
|
||||
}
|
||||
}
|
1
src/pages/mod.rs
Normal file
1
src/pages/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod home;
|
Reference in New Issue
Block a user