at main 142 lines 5.0 kB view raw
1//! Panel showing current collaborators on a resource. 2 3use crate::auth::AuthState; 4use crate::components::button::{Button, ButtonVariant}; 5use crate::fetch::Fetcher; 6use dioxus::prelude::*; 7use jacquard::types::string::AtUri; 8 9use super::InviteDialog; 10use super::api::{SentInvite, fetch_sent_invites}; 11 12/// Props for the CollaboratorsPanel component. 13#[derive(Props, Clone, PartialEq)] 14pub struct CollaboratorsPanelProps { 15 /// The resource to show collaborators for. 16 pub resource_uri: AtUri<'static>, 17 /// CID of the resource. 18 pub resource_cid: String, 19 /// Optional title for display. 20 #[props(default)] 21 pub resource_title: Option<String>, 22 /// Callback when panel should close (for modal mode). 23 #[props(default)] 24 pub on_close: Option<EventHandler<()>>, 25} 26 27/// Panel showing collaborators and invite button. 28#[component] 29pub fn CollaboratorsPanel(props: CollaboratorsPanelProps) -> Element { 30 let auth_state = use_context::<Signal<AuthState>>(); 31 let fetcher = use_context::<Fetcher>(); 32 let mut show_invite_dialog = use_signal(|| false); 33 34 // Clone props we need in closures 35 let on_close = props.on_close.clone(); 36 let on_close_overlay = props.on_close.clone(); 37 let resource_uri = props.resource_uri.clone(); 38 let resource_uri_dialog = props.resource_uri.clone(); 39 let resource_cid = props.resource_cid.clone(); 40 let resource_title = props.resource_title.clone(); 41 42 // Fetch invites for this resource to show collaborators 43 let invites_resource = { 44 let fetcher = fetcher.clone(); 45 use_resource(move || { 46 let fetcher = fetcher.clone(); 47 let resource_uri = resource_uri.clone(); 48 let _auth = auth_state.read().did.clone(); 49 async move { 50 fetch_sent_invites(&fetcher) 51 .await 52 .ok() 53 .unwrap_or_default() 54 .into_iter() 55 .filter(|i| i.resource_uri == resource_uri) 56 .collect::<Vec<_>>() 57 } 58 }) 59 }; 60 61 let invites: Vec<SentInvite> = invites_resource().unwrap_or_default(); 62 let accepted_count = invites.iter().filter(|i| i.accepted).count(); 63 let pending_count = invites.len() - accepted_count; 64 65 let is_modal = on_close.is_some(); 66 67 let panel_content = rsx! { 68 div { class: "collaborators-panel", 69 div { class: "collaborators-header", 70 h4 { "Collaborators" } 71 div { class: "collaborators-header-actions", 72 Button { 73 variant: ButtonVariant::Ghost, 74 onclick: move |_| show_invite_dialog.set(true), 75 "Invite" 76 } 77 if let Some(ref handler) = on_close { 78 { 79 let handler = handler.clone(); 80 rsx! { 81 Button { 82 variant: ButtonVariant::Ghost, 83 onclick: move |_| handler.call(()), 84 "×" 85 } 86 } 87 } 88 } 89 } 90 } 91 92 if invites.is_empty() { 93 p { class: "empty-state", "No collaborators yet" } 94 } else { 95 div { class: "collaborators-list", 96 for invite in &invites { 97 div { 98 class: if invite.accepted { "collaborator accepted" } else { "collaborator pending" }, 99 span { class: "collaborator-did", "{invite.invitee}" } 100 span { 101 class: "collaborator-status", 102 if invite.accepted { "" } else { "..." } 103 } 104 } 105 } 106 } 107 108 div { class: "collaborators-summary", 109 "{accepted_count} active, {pending_count} pending" 110 } 111 } 112 } 113 114 InviteDialog { 115 open: show_invite_dialog(), 116 on_close: move |_| show_invite_dialog.set(false), 117 resource_uri: resource_uri_dialog.clone(), 118 resource_cid: resource_cid.clone(), 119 resource_title: resource_title.clone(), 120 } 121 }; 122 123 if is_modal { 124 rsx! { 125 div { 126 class: "collaborators-overlay", 127 onclick: move |_| { 128 if let Some(ref handler) = on_close_overlay { 129 handler.call(()); 130 } 131 }, 132 div { 133 class: "collaborators-modal", 134 onclick: move |e| e.stop_propagation(), 135 {panel_content} 136 } 137 } 138 } 139 } else { 140 panel_content 141 } 142}