Skip to content

Commit

Permalink
Merge mera
Browse files Browse the repository at this point in the history
  • Loading branch information
42pupusas committed Dec 13, 2024
2 parents ecc4968 + 4014b55 commit f4a9c63
Show file tree
Hide file tree
Showing 64 changed files with 3,739 additions and 72 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
**/target/
**/dist/
**/node_modules/
**/.DS_Store
/test

# Ignore output.css files
Expand Down
213 changes: 193 additions & 20 deletions business/src/pages/history.rs
Original file line number Diff line number Diff line change
@@ -1,47 +1,220 @@
use crate::contexts::OrderDataStore;

use fuente::mass::HistoryIcon;
use fuente::{
mass::HistoryIcon,
models::{OrderStatus, OrderInvoiceState},
};
use yew::prelude::*;

#[derive(Clone, PartialEq)]
enum HistoryFilter {
Completed,
Canceled,
}

#[function_component(HistoryPage)]
pub fn history_page() -> Html {
let order_ctx = use_context::<OrderDataStore>().expect("No order context found");
let orders = order_ctx.order_history();
let filter_state = use_state(|| HistoryFilter::Completed);
let selected_order = use_state(|| None::<String>);

let filtered_orders = orders
.iter()
.filter(|order| match *filter_state {
HistoryFilter::Completed => order.get_order_status() == OrderStatus::Completed,
HistoryFilter::Canceled => order.get_order_status() == OrderStatus::Canceled,
})
.collect::<Vec<_>>();

if let Some(order_id) = (*selected_order).clone() {
if let Some(order) = orders.iter().find(|o| o.id() == order_id) {
return html! {
<OrderDetails
order={order.clone()}
on_back={Callback::from({
let selected = selected_order.clone();
move |_| selected.set(None)
})}
/>
};
}
}

html! {
<div class="flex flex-col flex-1">
<h2 class="text-4xl">{"Order History"}</h2>
{if orders.is_empty() {
html! {
<BlankHistory />
}
<div class="flex flex-row justify-between items-center mb-4">
<h2 class="text-4xl">{"Order History"}</h2>
<div class="flex gap-2">
<button
onclick={Callback::from({
let filter = filter_state.clone();
move |_| filter.set(HistoryFilter::Completed)
})}
class={classes!(
if *filter_state == HistoryFilter::Completed {
"bg-fuente text-white"
} else {
"bg-gray-200"
},
"px-4",
"py-2",
"rounded-lg"
)}
>
{"Completed"}
</button>
<button
onclick={Callback::from({
let filter = filter_state.clone();
move |_| filter.set(HistoryFilter::Canceled)
})}
class={classes!(
if *filter_state == HistoryFilter::Canceled {
"bg-fuente text-white"
} else {
"bg-gray-200"
},
"px-4",
"py-2",
"rounded-lg"
)}
>
{"Canceled"}
</button>
</div>
</div>

{if filtered_orders.is_empty() {
html! { <BlankHistory /> }
} else {
html! {
<div class="flex flex-col w-full h-full overflow-y-scroll">
{for orders.iter().map(|order| {
<div class="flex flex-col w-full h-full gap-4 overflow-y-auto">
{filtered_orders.iter().map(|order| {
let order_req = order.get_order_request();
let address = order_req.address;
let profile = order_req.profile;

let order_id = order.id();
let selected = selected_order.clone();

html! {
<div class="flex flex-col w-full p-4 border-b border-neutral-200">
<div
onclick={Callback::from(move |_| selected.set(Some(order_id.clone())))}
class="flex flex-col p-4 border rounded-lg cursor-pointer hover:bg-gray-50"
>
<div class="flex justify-between items-center">
<h4 class="text-lg font-semibold">{order.id()}</h4>
<p class="text-sm text-neutral-400">{profile.nickname()}</p>
</div>
<div class="flex flex-col mt-2">
<p class="text-sm text-neutral-400">
{address.lookup().display_name()}
</p>
<div>
<h4 class="font-semibold">{profile.nickname()}</h4>
<p class="text-sm text-gray-500">
{format!("Order #{}", &order.id()[..8])}
</p>
</div>
<div class="text-right">
<p class="text-sm font-medium">
{format!("{:.2} SRD", order_req.products.total())}
</p>
<p class={classes!(
"text-sm",
if order.get_order_status() == OrderStatus::Completed {
"text-green-600"
} else {
"text-red-600"
}
)}>
{order.get_order_status().display()}
</p>
</div>
</div>
</div>
}
})}
}).collect::<Html>()}
</div>
}
}}
</div>
}
}

#[derive(Properties, Clone, PartialEq)]
struct OrderDetailsProps {
order: OrderInvoiceState,
on_back: Callback<MouseEvent>,
}

#[function_component(OrderDetails)]
fn order_details(props: &OrderDetailsProps) -> Html {
let order_req = props.order.get_order_request();
let products = order_req.products.counted_products();

html! {
<div class="flex flex-col w-full h-full">
<div class="flex items-center gap-4 mb-6">
<button
onclick={props.on_back.clone()}
class="p-2 rounded-lg hover:bg-gray-100"
>
{"← Back"}
</button>
<h2 class="text-2xl font-semibold">
{format!("Order Details #{}", &props.order.id()[..8])}
</h2>
</div>

<div class="grid grid-cols-2 gap-8">
<div class="space-y-6">
<div class="space-y-2">
<h3 class="font-medium text-gray-500">{"Customer Information"}</h3>
<p>{format!("Name: {}", order_req.profile.nickname())}</p>
<p>{format!("Phone: {}", order_req.profile.telephone())}</p>
<p>{format!("Email: {}", order_req.profile.email())}</p>
</div>

<div class="space-y-2">
<h3 class="font-medium text-gray-500">{"Delivery Address"}</h3>
<p class="text-sm">{order_req.address.lookup().display_name()}</p>
</div>

<div class="space-y-2">
<h3 class="font-medium text-gray-500">{"Order Status"}</h3>
<p class={classes!(
"font-medium",
if props.order.get_order_status() == OrderStatus::Completed {
"text-green-600"
} else {
"text-red-600"
}
)}>
{props.order.get_order_status().display()}
</p>
</div>
</div>

<div class="space-y-4">
<h3 class="font-medium text-gray-500">{"Order Items"}</h3>
<div class="space-y-2">
{products.iter().map(|(product, count)| {
let subtotal = product.price().parse::<f64>().unwrap() * *count as f64;
html! {
<div class="flex justify-between py-2 border-b">
<div>
<p class="font-medium">{product.name()}</p>
<p class="text-sm text-gray-500">
{format!("{} x {} SRD", count, product.price())}
</p>
</div>
<p class="font-medium">{format!("{:.2} SRD", subtotal)}</p>
</div>
}
}).collect::<Html>()}

<div class="flex justify-between pt-4 font-medium">
<p>{"Total"}</p>
<p>{format!("{:.2} SRD", order_req.products.total())}</p>
</div>
</div>
</div>
</div>
</div>
}
}
#[function_component(BlankHistory)]
pub fn history_page() -> Html {
html! {
Expand Down
6 changes: 4 additions & 2 deletions consumer/src/consumer.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use consumer::{
contexts::{
CartProvider, CommerceDataProvider, ConsumerDataAction, ConsumerDataProvider,
ConsumerDataStore, LiveOrderProvider,
ConsumerDataStore, LiveOrderProvider, FavoritesProvider,
},
pages::NewProfilePage,
router::ConsumerPages,
Expand Down Expand Up @@ -89,7 +89,9 @@ fn app_context(props: &ChildrenProps) -> Html {
<CommerceDataProvider>
<CartProvider>
<LiveOrderProvider>
{props.children.clone()}
<FavoritesProvider>
{props.children.clone()}
</FavoritesProvider>
</LiveOrderProvider>
</CartProvider>
</CommerceDataProvider>
Expand Down
126 changes: 126 additions & 0 deletions consumer/src/contexts/favorites.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::rc::Rc;
use fuente::models::FavoriteStore;
use nostr_minions::{browser_api::IdbStoreManager, key_manager::NostrIdStore};
use yew::{platform::spawn_local, prelude::*};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FavoritesData {
has_loaded: bool,
favorites: Vec<FavoriteStore>,
}

impl FavoritesData {
pub fn is_loaded(&self) -> bool {
self.has_loaded
}

pub fn get_favorites(&self) -> Vec<FavoriteStore> {
self.favorites.clone()
}

pub fn is_favorite(&self, commerce_id: &str) -> bool {
self.favorites.iter().any(|f| f.commerce_id == commerce_id)
}
}

pub enum FavoritesAction {
LoadFavorites(Vec<FavoriteStore>),
AddFavorite(FavoriteStore),
RemoveFavorite(String),
SetLoaded,
}

impl Reducible for FavoritesData {
type Action = FavoritesAction;

fn reduce(self: Rc<Self>, action: Self::Action) -> Rc<Self> {
match action {
FavoritesAction::LoadFavorites(favorites) => Rc::new(FavoritesData {
has_loaded: self.has_loaded,
favorites,
}),
FavoritesAction::AddFavorite(favorite) => {
let db_entry = favorite.clone();
spawn_local(async move {
if let Err(e) = db_entry.save_to_store().await {
gloo::console::error!("Failed to save favorite:", e);
}
});

let mut favorites = self.favorites.clone();
favorites.push(favorite);

Rc::new(FavoritesData {
has_loaded: self.has_loaded,
favorites,
})
}
FavoritesAction::RemoveFavorite(commerce_id) => {
let mut favorites = self.favorites.clone();
if let Some(favorite) = favorites.iter().find(|f| f.commerce_id == commerce_id).cloned() {
spawn_local(async move {
if let Err(e) = favorite.delete_from_store().await {
gloo::console::error!("Failed to delete favorite:", e);
}
});
}

favorites.retain(|f| f.commerce_id != commerce_id);

Rc::new(FavoritesData {
has_loaded: self.has_loaded,
favorites,
})
}
FavoritesAction::SetLoaded => Rc::new(FavoritesData {
has_loaded: true,
favorites: self.favorites.clone(),
}),
}
}
}

pub type FavoritesStore = UseReducerHandle<FavoritesData>;

#[derive(Clone, Debug, Properties, PartialEq)]
pub struct FavoritesChildren {
pub children: Children,
}

#[function_component(FavoritesProvider)]
pub fn favorites_provider(props: &FavoritesChildren) -> Html {
let ctx = use_reducer(|| FavoritesData {
has_loaded: false,
favorites: vec![],
});

let ctx_clone = ctx.clone();
let key_ctx = use_context::<NostrIdStore>().expect("NostrIdStore not found");

use_effect_with(key_ctx, move |key_ctx| {
if let Some(keys) = key_ctx.get_nostr_key() {
let ctx = ctx_clone.clone();
spawn_local(async move {
match FavoriteStore::retrieve_all_from_store().await {
Ok(favorites) => {
// Filter favorites for current user
let user_favorites = favorites
.into_iter()
.filter(|f| f.user_id == keys.public_key())
.collect();
ctx.dispatch(FavoritesAction::LoadFavorites(user_favorites));
}
Err(e) => gloo::console::error!("Failed to load favorites:", e),
}
ctx.dispatch(FavoritesAction::SetLoaded);
});
}
|| {}
});

html! {
<ContextProvider<FavoritesStore> context={ctx}>
{props.children.clone()}
</ContextProvider<FavoritesStore>>
}
}
Loading

0 comments on commit f4a9c63

Please sign in to comment.