tinkr 0.0.43

Tinkr is a web framework for quickly building full-stack web applications with Leptos.
Documentation
use super::admin_table::{DashboardOrderListItem, OrdersTable};
use crate::RecordId;
use crate::components::{Page, heading::Heading};
use crate::date_utils::{FormatDatetime, format_datetime};
use leptos::prelude::*;
use leptos_router::hooks::{use_navigate, use_params_map};
use tw_merge::tw_merge;

#[server]
async fn fetch_user_orders() -> Result<Vec<DashboardOrderListItem>, ServerFnError> {
    let user = crate::session::get_user().await?;
    let db = crate::db::db_init().await?;

    let mut orders_req = db
        .query(
            /* surrealql */
            r#"
            SELECT 
                id, 
                created_at, 
                user.id as user_id, 
                user.name as user_name, 
                user.email as user_email, 
                paid, 
                done, 
                total 
                FROM order 
                WHERE user = $user_id AND (paid == true OR (paid == false AND created_at > time::now() - 3d ))
                ORDER BY created_at DESC 
                LIMIT 50;
            "#,
        )
        .bind(("user_id", user.id))
        .await?;
    let orders = orders_req.take::<Vec<DashboardOrderListItem>>(0)?;

    Ok(orders)
}

#[component]
pub fn OrderSideBarWrapper(children: Children) -> impl IntoView {
    view! {
        <div class="container mx-auto grid gap-5 grid-cols-2 lg:grid-cols-6">
            <div class="col-span-2 hidden lg:block">
                <UserOrderListNarrow />
            </div>
            <div class="col-span-4">{children()}</div>
            <div class="col-span-2 block lg:hidden flex w-full flex-col">
                <UserOrderListNarrow />
            </div>
        </div>
    }
}

#[component]
pub fn UserOrderListNarrow() -> impl IntoView {
    let orders_resource = OnceResource::new(fetch_user_orders());

    view! {
        <Page>
            <Heading>"Order History"</Heading>

            <Suspense fallback=move || {
                view! {
                    <div class="px-6 py-4 text-center text-neutral-500 dark:text-neutral-400">
                        "Loading your orders..."
                    </div>
                }
            }>
                {move || {
                    match orders_resource.get() {
                        Some(Ok(orders)) => {

                            view! {
                                <OrdersTableNarrowUserView
                                    orders=orders
                                    nav_pattern="/order/{}".to_string()
                                />
                            }
                                .into_any()
                        }
                        Some(Err(err)) => {
                            view! {
                                <div class="px-6 py-4 text-center text-red-500 dark:text-red-400">
                                    "Error loading orders: " {err.to_string()}
                                </div>
                            }
                                .into_any()
                        }
                        None => {
                            view! {
                                <div class="px-6 py-4 text-center text-red-500 dark:text-red-400">
                                    "Error loading orders: " "No data"
                                </div>
                            }
                                .into_any()
                        }
                    }
                }}
            </Suspense>
        </Page>
    }
}

// Reusable OrdersTable component that can be used by both admin and user views
#[component]
pub fn OrdersTableNarrowUserView(
    orders: Vec<DashboardOrderListItem>,
    #[prop(default = "/admin/{}/edit".to_string())] nav_pattern: String,
) -> impl IntoView {
    let navigate = use_navigate();
    let params = use_params_map();

    view! {
        <div class="w-full">
            <div class="flex flex-row gap-5 items-center px-4 py-2">
                <div class="text-left text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider flex-1">
                    "Order Date"
                </div>

                <div class="text-right text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider">
                    "Total"
                </div>

                <div class="text-right text-xs font-medium text-neutral-500 dark:text-neutral-300 uppercase tracking-wider w-[100px]">
                    "Status"
                </div>
            </div>

            <div class="bg-white dark:bg-neutral-900 divide-y divide-neutral-200 dark:divide-neutral-700 ">
                {move || {
                    if orders.is_empty() {
                        view! {
                            <div class="px-6 py-4 text-center text-neutral-500 dark:text-neutral-400">
                                "No orders found"
                            </div>
                        }
                            .into_any()
                    } else {
                        let current_order_id_value = params
                            .read()
                            .get("order_id")
                            .unwrap_or_default();
                        let current_viewed_order: Option<RecordId> = if current_order_id_value
                            .is_empty()
                        {
                            None
                        } else {
                            Some(RecordId::from_table_key("order", &current_order_id_value))
                        };
                        orders
                            .iter()
                            .map(|order| {
                                let order_ref = order.id.key().to_string();
                                let navigate = navigate.clone();
                                let nav_pattern = nav_pattern.clone();
                                let active = current_viewed_order
                                    .as_ref()
                                    .map_or(false, |oid| oid == &order.id);

                                view! {
                                    <a
                                        class=tw_merge!(
                                            "hover:bg-neutral-50 dark:hover:bg-neutral-700/50 cursor-pointer flex flex-row px-4 py-2 gap-5 items-center",
                                             if active {
                                                 "bg-neutral-100 dark:bg-neutral-800 font-semibold"
                                             } else {
                                                 ""
                                             }
                                        )
                                        on:click=move |_| {
                                            let nav_url = nav_pattern.replace("{}", &order_ref);
                                            navigate(&nav_url, Default::default());
                                        }
                                    >

                                        <div class="whitespace-nowrap text-sm text-neutral-500 dark:text-neutral-400 font-mono tracking-0 text-xs">
                                            {order.created_at.format_custom("%a %d %b")}<br />
                                            <span class="text-neutral-800 dark:text-neutral-200 text-xs">
                                                {order.created_at.ago()}
                                            </span>
                                        </div>

                                        <div class="flex-1" />

                                        <div class="whitespace-nowrap text-sm text-neutral-900 dark:text-neutral-100 text-right">
                                            "R " {order.total.clone()}
                                        </div>

                                        <div class="w-[100px] flex flex-row gap-2 justify-end items-center">
                                            {order.status_badge()}
                                        </div>
                                    </a>
                                }
                            })
                            .collect::<Vec<_>>()
                            .into_any()
                    }
                }}
            </div>
        </div>
    }
}

#[component]
pub fn UserOrderListFull() -> impl IntoView {
    let orders_resource = OnceResource::new(fetch_user_orders());

    view! {
        <Page>
            <Heading>"Order History"</Heading>

            <Suspense fallback=move || {
                view! {
                    <div class="px-6 py-4 text-center text-neutral-500 dark:text-neutral-400">
                        "Loading your orders..."
                    </div>
                }
            }>
                {move || {
                    match orders_resource.get() {
                        Some(Ok(orders)) => {
                            view! {
                                <OrdersTable
                                    orders=orders
                                    show_user_columns=false
                                    nav_pattern="/order/{}".to_string()
                                />
                            }
                                .into_any()
                        }
                        Some(Err(err)) => {
                            view! {
                                <div class="px-6 py-4 text-center text-red-500 dark:text-red-400">
                                    "Error loading orders: " {err.to_string()}
                                </div>
                            }
                                .into_any()
                        }
                        None => {
                            view! {
                                <div class="px-6 py-4 text-center text-red-500 dark:text-red-400">
                                    "Error loading orders: " "No data"
                                </div>
                            }
                                .into_any()
                        }
                    }
                }}
            </Suspense>
        </Page>
    }
}

#[component]
pub fn OrderListUserPage() -> impl IntoView {
    view! {
        <div class="container mx-auto">
            <UserOrderListFull />
        </div>
    }
}