use crate::api::types::*;
use crate::api::ApiClient;
use crate::hooks::{use_auth, use_time_ago, use_websocket};
use dioxus::prelude::*;
use std::collections::HashSet;
#[component]
pub fn Home() -> Element {
let auth = use_auth();
let mut stats = use_signal(|| None::<DatabaseStats>);
let mut timeline_events = use_signal(|| Vec::<TimelineEvent>::new());
let mut latest_packages = use_signal(|| Vec::<Package>::new());
let mut loading = use_signal(|| true);
let mut timeline_offset = use_signal(|| 0);
let mut timeline_total = use_signal(|| 0);
let mut timeline_loading = use_signal(|| false);
let mut displayed_event_ids = use_signal(|| HashSet::<u64>::new());
let token = auth.token();
let is_authenticated = auth.is_authenticated();
let token_for_effect = token.clone();
use_effect(move || {
let token_clone = token_for_effect.clone();
spawn(async move {
let client = ApiClient::new().with_token(token_clone.clone());
if let Ok(db_stats) = client.get_stats().await {
stats.set(Some(db_stats));
}
if is_authenticated {
if let Ok(timeline) = client.get_timeline(0, 20).await {
let mut ids = HashSet::new();
for event in &timeline.events {
ids.insert(event.id);
}
displayed_event_ids.set(ids);
timeline_events.set(timeline.events);
timeline_total.set(timeline.total);
timeline_offset.set(20);
}
} else {
if let Ok(response) = client.get_packages(None, 1, 6).await {
latest_packages.set(response.packages);
}
}
loading.set(false);
});
});
{
let ws_url = if cfg!(debug_assertions) {
"ws://localhost:3000/ws/timeline".to_string()
} else {
format!(
"ws://{}/ws/timeline",
web_sys::window()
.and_then(|w| w.location().host().ok())
.unwrap_or_else(|| "localhost:3000".to_string())
)
};
use_websocket::<WebSocketMessage, _>(ws_url, move |msg: WebSocketMessage| {
match msg {
WebSocketMessage::TimelineEvent { event } => {
let event_id = event.id;
if !displayed_event_ids.read().contains(&event_id) {
displayed_event_ids.write().insert(event_id);
timeline_events.write().insert(0, event);
}
}
_ => {
}
}
});
}
let load_more_timeline = move |_| {
if timeline_loading() {
return;
}
let offset = timeline_offset();
let token_clone = token.clone();
spawn(async move {
timeline_loading.set(true);
let client = ApiClient::new().with_token(token_clone);
if let Ok(timeline) = client.get_timeline(offset, 20).await {
let mut events = timeline_events.write();
let mut ids = displayed_event_ids.write();
for event in timeline.events {
if !ids.contains(&event.id) {
ids.insert(event.id);
events.push(event);
}
}
timeline_offset.set(offset + 20);
}
timeline_loading.set(false);
});
};
rsx! {
main { class: "relative",
section { class: "hero-gradient relative overflow-hidden",
div { class: "container mx-auto px-6 py-20 relative z-10",
div { class: "text-white max-w-4xl",
h1 { class: "text-4xl md:text-6xl font-bold mb-4 leading-tight",
"FossDB"
}
p { class: "text-lg md:text-xl mb-8 text-gray-300 leading-relaxed",
"A self-hosted database for tracking open source software packages. "
"Scrapes package metadata from registries like crates.io and provides "
"a queryable REST API for dependency analysis."
}
if let Some(db_stats) = stats() {
div { class: "grid grid-cols-2 md:grid-cols-4 gap-4 mb-8",
div { class: "bg-gray-800/50 rounded-lg p-4 border border-gray-700",
div { class: "text-gray-400 text-sm", "Packages" }
div { class: "text-2xl font-bold text-blue-400", "{db_stats.total_packages}" }
}
div { class: "bg-gray-800/50 rounded-lg p-4 border border-gray-700",
div { class: "text-gray-400 text-sm", "Versions" }
div { class: "text-2xl font-bold text-purple-400", "{db_stats.total_versions}" }
}
div { class: "bg-gray-800/50 rounded-lg p-4 border border-gray-700",
div { class: "text-gray-400 text-sm", "Users" }
div { class: "text-2xl font-bold text-green-400", "{db_stats.total_users}" }
}
div { class: "bg-gray-800/50 rounded-lg p-4 border border-gray-700",
div { class: "text-gray-400 text-sm", "CVEs" }
div { class: "text-2xl font-bold text-red-400", "{db_stats.total_vulnerabilities}" }
}
}
}
}
}
}
section { class: "py-24 bg-gray-900",
div { class: "container mx-auto px-6",
div { class: "text-center mb-16",
if is_authenticated {
h2 { class: "text-4xl font-bold text-gray-100 mb-6", "Your Timeline" }
p { class: "text-xl text-gray-300", "Updates from packages you follow" }
} else {
h2 { class: "text-4xl font-bold text-gray-100 mb-6", "Global Timeline" }
p { class: "text-xl text-gray-300", "Real-time updates from the open source ecosystem" }
}
}
div { class: "max-w-4xl mx-auto space-y-4",
if loading() {
div { class: "flex justify-center py-12",
div { class: "animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" }
}
} else {
for event in timeline_events().iter() {
TimelineEventCard { event: event.clone() }
}
if timeline_events().len() < timeline_total() as usize {
div { class: "flex justify-center mt-8",
button {
class: "px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed",
disabled: timeline_loading(),
onclick: load_more_timeline,
if timeline_loading() {
"Loading..."
} else {
"Load More"
}
}
}
}
if timeline_events().is_empty() {
div { class: "text-center py-12",
if is_authenticated {
p { class: "text-gray-400 text-lg", "No timeline events yet. Subscribe to packages to see updates here!" }
} else {
p { class: "text-gray-400 text-lg", "Waiting for new releases..." }
}
}
}
}
}
}
}
}
}
}
#[component]
fn TimelineEventCard(event: TimelineEvent) -> Element {
let time_ago = use_time_ago(event.created_at);
let (icon_class, icon_color) = match event.event_type {
TimelineEventType::PackageAdded => ("M12 4v16m8-8H4", "text-green-400"),
TimelineEventType::NewRelease => ("M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z", "text-blue-400"),
TimelineEventType::SecurityAlert => (
"M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z",
"text-red-400",
),
TimelineEventType::PackageUpdated => ("M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z", "text-gray-400"),
};
rsx! {
div { class: "bg-gray-800 rounded-lg p-6 border border-gray-700",
div { class: "flex items-start space-x-4",
div { class: "flex-shrink-0",
svg {
class: "w-6 h-6 {icon_color}",
fill: "none",
stroke: "currentColor",
view_box: "0 0 24 24",
path {
stroke_linecap: "round",
stroke_linejoin: "round",
stroke_width: "2",
d: "{icon_class}"
}
}
}
div { class: "flex-1",
h3 { class: "text-lg font-semibold text-gray-100 mb-2",
"{event.package_name}"
}
p { class: "text-gray-400", "{event.message}" }
if let Some(v) = &event.version {
p { class: "text-sm text-blue-400 mt-1", "Version: {v}" }
}
}
div { class: "text-sm text-gray-500",
"{time_ago()}"
}
}
}
}
}