//! Calendar โ€” multi-event surface for cyberia.my
//! Seeded Cyber Valley events + Add Event (localStorage).

use crate::land::FLAG_SVG;
use crate::nav::CyberiaNav;
use leptos::prelude::*;
use serde::{Deserialize, Serialize};

const EVENTS_KEY: &str = "cyberia_events";

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct EventCard {
    pub id: String,
    pub title: String,
    pub city: String,
    pub when: String,
    pub where_: String,
    pub blurb: String,
    pub status: String, // live | upcoming | draft
}

fn seed_events() -> Vec<EventCard> {
    vec![
        EventCard {
            id: "cv-land-ops".into(),
            title: "Land Ops ยท Phase 0".into(),
            city: "Cyber Valley".into(),
            when: "ongoing".into(),
            where_: "Gesing ยท plots + fleets".into(),
            blurb: "Daily land work โ€” survey, clear, plant. Open the map.".into(),
            status: "live".into(),
        },
        EventCard {
            id: "cv-soft-circle".into(),
            title: "Soft circle".into(),
            city: "Cyber Valley".into(),
            when: "weekly".into(),
            where_: "Soft ยท event space".into(),
            blurb: "Community gather โ€” conferences, coworking, parties.".into(),
            status: "upcoming".into(),
        },
    ]
}

pub fn load_user_events() -> Vec<EventCard> {
    web_sys::window()
        .and_then(|w| w.local_storage().ok().flatten())
        .and_then(|ls| ls.get_item(EVENTS_KEY).ok().flatten())
        .and_then(|raw| serde_json::from_str(&raw).ok())
        .unwrap_or_default()
}

fn save_user_events(list: &[EventCard]) {
    if let Some(ls) = web_sys::window().and_then(|w| w.local_storage().ok().flatten()) {
        if let Ok(raw) = serde_json::to_string(list) {
            let _ = ls.set_item(EVENTS_KEY, &raw);
        }
    }
}

fn slugify(name: &str) -> String {
    let s: String = name
        .chars()
        .map(|c| {
            if c.is_ascii_alphanumeric() {
                c.to_ascii_lowercase()
            } else {
                '-'
            }
        })
        .collect();
    let mut out = String::new();
    for ch in s.chars() {
        if ch == '-' {
            if !out.ends_with('-') && !out.is_empty() {
                out.push(ch);
            }
        } else {
            out.push(ch);
        }
    }
    out.trim_matches('-').to_string()
}

#[component]
pub fn EventsPage() -> impl IntoView {
    let user_events = RwSignal::new(load_user_events());
    let sheet_open = RwSignal::new(false);
    let draft_title = RwSignal::new(String::new());
    let draft_city = RwSignal::new("Cyber Valley".to_string());
    let draft_when = RwSignal::new(String::new());
    let draft_where = RwSignal::new(String::new());
    let draft_blurb = RwSignal::new(String::new());
    let err = RwSignal::new(None::<String>);

    Effect::new(move |_| {
        document().set_title("Cyberia โ€” calendar");
    });

    let catalog = move || {
        let mut list = seed_events();
        list.extend(user_events.get());
        list
    };

    view! {
        <div class="page-shell cities-shell">
            <div class="site-chrome cyberia-chrome">
                <div class="chrome-inner">
                    <div class="header-row1">
                        <div class="logo-zone">
                            <h1 class="logo">
                                <a href="/cities" class="brand-flag" title="home" inner_html=FLAG_SVG></a>
                                <span style="color: var(--cyber-green);">"cyber"</span>
                                <span style="color: var(--cyber-green); margin: 0 1px;">"โ€ข"</span>
                                <span style="color: #fff;">"ia"</span>
                            </h1>
                        </div>
                        <div class="cyberia-phase-pill">
                            <span class="phase-dot"></span>
                            {move || format!("{} ON CALENDAR", catalog().len())}
                        </div>
                        <CyberiaNav active="calendar" />
                    </div>
                </div>
            </div>

            <div class="cities-stage">
                <div class="cities-hero">
                    <div>
                        <div class="cities-kicker">"CATALOG"</div>
                        <h2 class="cities-title">"Calendar"</h2>
                        <p class="cities-lead">
                            "Gatherings across Cyberia cities โ€” land ops, soft circles, and what you add."
                        </p>
                    </div>
                </div>

                <div class="cities-grid">
                    {move || catalog().into_iter().enumerate().map(|(i, e)| {
                        let live = e.status == "live";
                        let upcoming = e.status == "upcoming";
                        let st_cls = if live {
                            "city-status live"
                        } else if upcoming {
                            "city-status upcoming"
                        } else {
                            "city-status founding"
                        };
                        let title = e.title.clone();
                        let city = e.city.clone();
                        let when = e.when.clone();
                        let where_ = e.where_.clone();
                        let blurb = e.blurb.clone();
                        let status = e.status.to_uppercase();
                        let rank = i + 1;
                        let href = if e.city == "Cyber Valley" {
                            "/map".to_string()
                        } else {
                            "/calendar".to_string()
                        };
                        view! {
                            <a class=if live { "city-card live event-card" } else { "city-card founding event-card" } href=href>
                                <div class="city-card-top">
                                    <span class="city-rank">{format!("#{rank:02}")}</span>
                                    <span class=st_cls>{status}</span>
                                </div>
                                <div class="city-name">{title}</div>
                                <div class="city-region">{format!("{city} ยท {when}")}</div>
                                <p class="city-blurb">{blurb}</p>
                                <div class="city-meta">
                                    <span>{where_}</span>
                                    <span class="city-open">
                                        {if live { "OPEN MAP โ†’" } else { "LISTED โ†’" }}
                                    </span>
                                </div>
                            </a>
                        }
                    }).collect_view()}

                    <button class="city-card ghost event-card" on:click=move |_| {
                        err.set(None);
                        sheet_open.set(true);
                    }>
                        <div class="city-card-top">
                            <span class="city-rank">"+"</span>
                            <span class="city-status founding">"NEW"</span>
                        </div>
                        <div class="city-name">"Add an event"</div>
                        <div class="city-region">"any cyberia city"</div>
                        <p class="city-blurb">
                            "Name it, place it, time it. Catalog stays local until the backend ships."
                        </p>
                        <div class="city-meta">
                            <span>"when ยท where"</span>
                            <span class="city-open">"ADD EVENT โ†’"</span>
                        </div>
                    </button>
                </div>
            </div>

            {move || sheet_open.get().then(|| view! {
                <div class="cyberia-sheet-backdrop" on:click=move |_| sheet_open.set(false)>
                    <div class="cyberia-sheet" on:click=move |ev| ev.stop_propagation()>
                        <div class="sheet-h">
                            <span class="panel-kicker">"ADD EVENT"</span>
                            <button class="sheet-x" on:click=move |_| sheet_open.set(false)>"โœ•"</button>
                        </div>
                        <p class="sheet-note">
                            "Soft3 local catalog โ€” no closed backend. Event is stored in this browser."
                        </p>
                        <label class="found-label">"TITLE"</label>
                        <input
                            class="found-input"
                            type="text"
                            placeholder="e.g. Full moon trail"
                            prop:value=move || draft_title.get()
                            on:input=move |ev| draft_title.set(event_target_value(&ev))
                        />
                        <label class="found-label">"CITY"</label>
                        <input
                            class="found-input"
                            type="text"
                            placeholder="Cyber Valley"
                            prop:value=move || draft_city.get()
                            on:input=move |ev| draft_city.set(event_target_value(&ev))
                        />
                        <label class="found-label">"WHEN"</label>
                        <input
                            class="found-input"
                            type="text"
                            placeholder="e.g. 2026-08-20 ยท sunset"
                            prop:value=move || draft_when.get()
                            on:input=move |ev| draft_when.set(event_target_value(&ev))
                        />
                        <label class="found-label">"WHERE"</label>
                        <input
                            class="found-input"
                            type="text"
                            placeholder="e.g. Soft ยท edge of Sinwood"
                            prop:value=move || draft_where.get()
                            on:input=move |ev| draft_where.set(event_target_value(&ev))
                        />
                        <label class="found-label">"BLURB"</label>
                        <input
                            class="found-input"
                            type="text"
                            placeholder="one line about the event"
                            prop:value=move || draft_blurb.get()
                            on:input=move |ev| draft_blurb.set(event_target_value(&ev))
                        />
                        {move || err.get().map(|e| view! { <div class="found-err">{e}</div> })}
                        <button
                            class="intent-commit event"
                            on:click=move |_| {
                                let title = draft_title.get().trim().to_string();
                                if title.len() < 2 {
                                    err.set(Some("title too short".into()));
                                    return;
                                }
                                let mut id = slugify(&title);
                                if id.is_empty() {
                                    err.set(Some("invalid title".into()));
                                    return;
                                }
                                let mut list = user_events.get_untracked();
                                if list.iter().any(|e| e.id == id) || seed_events().iter().any(|e| e.id == id) {
                                    id = format!("{id}-{}", list.len() + 2);
                                }
                                let city = {
                                    let c = draft_city.get().trim().to_string();
                                    if c.is_empty() { "Cyber Valley".into() } else { c }
                                };
                                let when = {
                                    let w = draft_when.get().trim().to_string();
                                    if w.is_empty() { "TBD".into() } else { w }
                                };
                                let where_ = {
                                    let w = draft_where.get().trim().to_string();
                                    if w.is_empty() { "TBD".into() } else { w }
                                };
                                let blurb = {
                                    let b = draft_blurb.get().trim().to_string();
                                    if b.is_empty() {
                                        "User-listed event โ€” local catalog.".into()
                                    } else {
                                        b
                                    }
                                };
                                list.insert(0, EventCard {
                                    id,
                                    title,
                                    city,
                                    when,
                                    where_,
                                    blurb,
                                    status: "draft".into(),
                                });
                                save_user_events(&list);
                                user_events.set(list);
                                draft_title.set(String::new());
                                draft_when.set(String::new());
                                draft_where.set(String::new());
                                draft_blurb.set(String::new());
                                err.set(None);
                                sheet_open.set(false);
                            }
                        >
                            "CONFIRM ADD"
                        </button>
                    </div>
                </div>
            })}

            <div class="search-dock cyberia-dock cities-dock">
                <span class="dock-count">
                    {move || {
                        let n = catalog().len();
                        let u = user_events.get().len();
                        format!("{n} on calendar ยท {u} added here")
                    }}
                </span>
                <button class="cta-btn cta-event cta-lg cta-bold dock-found" on:click=move |_| {
                    err.set(None);
                    sheet_open.set(true);
                }>
                    <span class="cta-ico">"โœš"</span>
                    <span class="cta-copy">
                        <span class="cta-title">"ADD EVENT"</span>
                        <span class="cta-sub">"post to catalog"</span>
                    </span>
                </button>
                <a href="https://x.com/cyberiacap" target="_blank" rel="noopener" class="dock-credit">
                    "๐Ÿด a "<span style="color: var(--cyber-green);">"cyberia"</span>" project"
                </a>
            </div>
        </div>
    }
}

Homonyms

cyberia/cyberia-my/src/events.rs
cyb/evy/forks/bevy_ecs/examples/events.rs
soft3/radio/iroh-blobs/src/provider/events.rs

Graph