refactor(fe): replace utility classes with semantic CSS across components
- add ~400 lines of clean semantic CSS classes (msg-*, panel-*, search-*, filter-*, feed-*, auth-*, sidebar-*, etc.) - convert message_card.rs: .message-row -> .msg-row, .message-card -> .msg-card, remove all tailwind utility classes - convert messages/mod.rs: .card -> .panel-card, .search-row -> .search-bar, .filter-chip -> .filter-chip-v2, .message-stats -> .stats-bar - convert message_feed.rs: .space-y-4 -> .feed-wrap, .empty-state -> .feed-empty - convert auth.rs: inline styles -> .auth-* semantic classes - convert sidebar: .btn.btn-ghost -> .sidebar-btn.is-active - keep utility classes in CSS for backward compatibility with non-refactored components (dashboard, live)
This commit is contained in:
+1200
-1008
File diff suppressed because it is too large
Load Diff
@@ -84,6 +84,10 @@ pub fn App() -> impl IntoView {
|
|||||||
async move {
|
async move {
|
||||||
match config_api::get_config().await {
|
match config_api::get_config().await {
|
||||||
Ok(cfg) => {
|
Ok(cfg) => {
|
||||||
|
web_sys::console::log_2(
|
||||||
|
&"[config] fetched OK".into(),
|
||||||
|
&format!("monitorGuildId={:?}", cfg.monitor_guild_id).into(),
|
||||||
|
);
|
||||||
config.monitor_guild_id.set(cfg.monitor_guild_id);
|
config.monitor_guild_id.set(cfg.monitor_guild_id);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -166,14 +170,9 @@ fn TabButton(tab: Tab, ui: UiContext, label: &'static str) -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<button
|
<button
|
||||||
class:btn=true
|
class="sidebar-btn"
|
||||||
class:btn-ghost=true
|
class:is-active=move || active_tab.get() == tab1
|
||||||
class:btn-active=move || active_tab.get() == tab1
|
|
||||||
on:click=move |_| active_tab.set(tab4.clone())
|
on:click=move |_| active_tab.set(tab4.clone())
|
||||||
style:background=move || if active_tab.get() == tab2 { "var(--surface-overlay)" } else { "" }
|
|
||||||
style:color=move || if active_tab.get() == tab3 { "var(--color-primary)" } else { "" }
|
|
||||||
style:width="100%"
|
|
||||||
style:justify-content="flex-start"
|
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -52,21 +52,17 @@ pub fn AuthOverlay() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="modal-overlay">
|
<div class="modal-overlay">
|
||||||
<div class="modal-content" style="width: 380px;">
|
<div class="modal-content auth-box">
|
||||||
<div class="modal-body" style="text-align: center;">
|
<div class="modal-body">
|
||||||
<div style="font-size: 3rem; margin-bottom: 1rem;">
|
<div class="auth-lock">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
|
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--color-primary)" stroke-width="2">
|
||||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h2 style="font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem;">
|
<h2 class="auth-title">"Akses Dashboard"</h2>
|
||||||
"Akses Dashboard"
|
<p class="auth-desc">"Masukkan password admin untuk melanjutkan"</p>
|
||||||
</h2>
|
<form class="auth-form" on:submit=handle_submit>
|
||||||
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 1.5rem;">
|
|
||||||
"Masukkan password admin untuk melanjutkan"
|
|
||||||
</p>
|
|
||||||
<form on:submit=handle_submit style="display: flex; flex-direction: column; gap: 0.75rem;">
|
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
class="input"
|
class="input"
|
||||||
@@ -75,11 +71,11 @@ pub fn AuthOverlay() -> impl IntoView {
|
|||||||
on:input=move |ev| set_password.set(event_target_value(&ev))
|
on:input=move |ev| set_password.set(event_target_value(&ev))
|
||||||
/>
|
/>
|
||||||
{move || error.get().map(|e| view! {
|
{move || error.get().map(|e| view! {
|
||||||
<p style="color: var(--color-error); font-size: 0.75rem;">{e}</p>
|
<p class="auth-error">{e}</p>
|
||||||
})}
|
})}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="btn btn-primary w-full btn-lg"
|
class="btn btn-primary btn-lg"
|
||||||
disabled=move || loading.get()
|
disabled=move || loading.get()
|
||||||
>
|
>
|
||||||
{move || if loading.get() { "Memproses..." } else { "Masuk" }}
|
{move || if loading.get() { "Memproses..." } else { "Masuk" }}
|
||||||
|
|||||||
@@ -189,23 +189,23 @@ pub fn MessageRow(
|
|||||||
let on_click_re = move |_| on_reanalyze(reanalyze_id.clone());
|
let on_click_re = move |_| on_reanalyze(reanalyze_id.clone());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="message-row">
|
<div class="msg-row">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
|
<div class="msg-row-bar">
|
||||||
<span class="message-timestamp" title=time_ago(message.created_at)>
|
<span class="msg-row-time" title=time_ago(message.created_at)>
|
||||||
{fmt_time(message.created_at)}
|
{fmt_time(message.created_at)}
|
||||||
</span>
|
</span>
|
||||||
{message.edited_at.is_some().then(|| view! {
|
{message.edited_at.is_some().then(|| view! {
|
||||||
<span class="flex items-center gap-0.5 text-xs text-secondary">
|
<span class="msg-row-badge edited">
|
||||||
"✎ edited"
|
"✎ edited"
|
||||||
</span>
|
</span>
|
||||||
})}
|
})}
|
||||||
{message.deleted_at.is_some().then(|| view! {
|
{message.deleted_at.is_some().then(|| view! {
|
||||||
<span class="flex items-center gap-0.5 text-xs text-destructive">
|
<span class="msg-row-badge deleted">
|
||||||
"🗑 deleted"
|
"🗑 deleted"
|
||||||
</span>
|
</span>
|
||||||
})}
|
})}
|
||||||
<div class="ml-auto flex items-center gap-1">
|
<div class="msg-row-status">
|
||||||
<StatusBadgeInline status=ai_st.clone() />
|
<StatusBadgeInline status=ai_st.clone() />
|
||||||
{message.ai_severity.as_ref().filter(|s| **s != AiSeverity::None).map(|sev| view! {
|
{message.ai_severity.as_ref().filter(|s| **s != AiSeverity::None).map(|sev| view! {
|
||||||
<span class=format!("badge text-xs {}", severity_class(sev))>{format!("{:?}", sev)}</span>
|
<span class=format!("badge text-xs {}", severity_class(sev))>{format!("{:?}", sev)}</span>
|
||||||
@@ -213,19 +213,11 @@ pub fn MessageRow(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reply - field removed from MessageRecord */}
|
|
||||||
|
|
||||||
{/* Forward - field removed from MessageRecord */}
|
|
||||||
|
|
||||||
{/* Crosspost - field removed from MessageRecord */}
|
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
{show.then(|| {
|
{show.then(|| {
|
||||||
let rendered = render_emojis(display);
|
let rendered = render_emojis(display);
|
||||||
let cls_str = if message.deleted_at.is_some() { "text-secondary/60" } else { "" };
|
|
||||||
let class_str = format!("whitespace-pre-wrap break-words text-sm leading-6 {}", cls_str);
|
|
||||||
view! {
|
view! {
|
||||||
<p class=class_str>
|
<p class="msg-row-body" class:is-deleted=message.deleted_at.is_some()>
|
||||||
{rendered.into_iter().collect::<Vec<_>>()}
|
{rendered.into_iter().collect::<Vec<_>>()}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -233,7 +225,7 @@ pub fn MessageRow(
|
|||||||
|
|
||||||
{/* Stickers */}
|
{/* Stickers */}
|
||||||
{(!stickers.is_empty()).then(|| view! {
|
{(!stickers.is_empty()).then(|| view! {
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="msg-media-row">
|
||||||
{stickers.iter().map(|s| {
|
{stickers.iter().map(|s| {
|
||||||
let url_owned = s.url.clone().unwrap_or_default();
|
let url_owned = s.url.clone().unwrap_or_default();
|
||||||
let name_owned = s.name.clone().unwrap_or_default();
|
let name_owned = s.name.clone().unwrap_or_default();
|
||||||
@@ -242,11 +234,11 @@ pub fn MessageRow(
|
|||||||
<div>
|
<div>
|
||||||
{if has_url {
|
{if has_url {
|
||||||
view! {
|
view! {
|
||||||
<img src=url_owned alt=name_owned class="h-12 w-12 rounded-lg border border-border object-contain bg-surface/50" loading="lazy" />
|
<img src=url_owned alt=name_owned class="msg-sticker" loading="lazy" />
|
||||||
}.into_any()
|
}.into_any()
|
||||||
} else {
|
} else {
|
||||||
view! {
|
view! {
|
||||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg border border-border bg-surface/50">
|
<div class="msg-sticker-placeholder">
|
||||||
"😊"
|
"😊"
|
||||||
</div>
|
</div>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
@@ -265,15 +257,15 @@ pub fn MessageRow(
|
|||||||
let url2 = a.url.clone();
|
let url2 = a.url.clone();
|
||||||
let name1 = a.name.clone();
|
let name1 = a.name.clone();
|
||||||
view! {
|
view! {
|
||||||
<a href=url1 target="_blank" class="shrink-0 overflow-hidden rounded-lg border border-border">
|
<a href=url1 target="_blank" class="msg-thumb-link">
|
||||||
<img src=url2 alt=name1 class="h-16 w-16 object-cover hover:scale-105 transition-transform" loading="lazy" />
|
<img src=url2 alt=name1 class="msg-thumb" loading="lazy" />
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>();
|
}).collect::<Vec<_>>();
|
||||||
let overflow = if imgs.len() > 4 {
|
let overflow = if imgs.len() > 4 {
|
||||||
let extra = imgs.len() - 4;
|
let extra = imgs.len() - 4;
|
||||||
view! {
|
view! {
|
||||||
<div class="flex h-16 w-16 items-center justify-center rounded-lg border border-border bg-surface text-xs text-secondary">
|
<div class="msg-media-overflow">
|
||||||
<span>{"+"} {extra}</span> <span class="ml-0.5">"🖼"</span>
|
<span>{"+"} {extra}</span> <span class="ml-0.5">"🖼"</span>
|
||||||
</div>
|
</div>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
@@ -281,7 +273,7 @@ pub fn MessageRow(
|
|||||||
().into_any()
|
().into_any()
|
||||||
};
|
};
|
||||||
view! {
|
view! {
|
||||||
<div class="flex gap-2 overflow-x-auto">
|
<div class="msg-media-row is-scroll">
|
||||||
{images_view}
|
{images_view}
|
||||||
{overflow}
|
{overflow}
|
||||||
</div>
|
</div>
|
||||||
@@ -296,13 +288,13 @@ pub fn MessageRow(
|
|||||||
let videos_view = vids_local.iter().take(4).map(|a| {
|
let videos_view = vids_local.iter().take(4).map(|a| {
|
||||||
let url = a.url.clone();
|
let url = a.url.clone();
|
||||||
view! {
|
view! {
|
||||||
<video src=url controls class="h-28 w-48 shrink-0 rounded-lg border border-border object-cover bg-black" preload="metadata"></video>
|
<video src=url controls class="msg-video" preload="metadata"></video>
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>();
|
}).collect::<Vec<_>>();
|
||||||
let overflow = if vids.len() > 4 {
|
let overflow = if vids.len() > 4 {
|
||||||
let extra = vids.len() - 4;
|
let extra = vids.len() - 4;
|
||||||
view! {
|
view! {
|
||||||
<div class="flex h-28 w-16 items-center justify-center rounded-lg border border-border bg-surface text-xs text-secondary">
|
<div class="msg-media-overflow tall">
|
||||||
<span>{"+"} {extra}</span> <span class="ml-0.5">"▶"</span>
|
<span>{"+"} {extra}</span> <span class="ml-0.5">"▶"</span>
|
||||||
</div>
|
</div>
|
||||||
}.into_any()
|
}.into_any()
|
||||||
@@ -310,7 +302,7 @@ pub fn MessageRow(
|
|||||||
().into_any()
|
().into_any()
|
||||||
};
|
};
|
||||||
view! {
|
view! {
|
||||||
<div class="flex gap-2 overflow-x-auto">
|
<div class="msg-media-row is-scroll">
|
||||||
{videos_view}
|
{videos_view}
|
||||||
{overflow}
|
{overflow}
|
||||||
</div>
|
</div>
|
||||||
@@ -323,7 +315,7 @@ pub fn MessageRow(
|
|||||||
{if !cats.is_empty() {
|
{if !cats.is_empty() {
|
||||||
let cats_local = cats.clone();
|
let cats_local = cats.clone();
|
||||||
view! {
|
view! {
|
||||||
<div class="flex flex-wrap gap-1">
|
<div class="msg-cats">
|
||||||
{cats_local.iter().map(|c| view! {
|
{cats_local.iter().map(|c| view! {
|
||||||
<span class="badge badge-secondary text-xs">{c.clone()}</span>
|
<span class="badge badge-secondary text-xs">{c.clone()}</span>
|
||||||
}).collect::<Vec<_>>()}
|
}).collect::<Vec<_>>()}
|
||||||
@@ -335,17 +327,17 @@ pub fn MessageRow(
|
|||||||
|
|
||||||
{/* AI Analysis */}
|
{/* AI Analysis */}
|
||||||
{message.ai_analysis.as_ref().map(|analysis| {
|
{message.ai_analysis.as_ref().map(|analysis| {
|
||||||
let border_str = if ai_st == AiStatus::Flagged { "border-l-3 bg-warning/5" } else { "border-l-3 bg-success/5" };
|
let f_cls = if ai_st == AiStatus::Flagged { "flagged" } else { "clean" };
|
||||||
let icon = if ai_st == AiStatus::Flagged { "🚨" } else { "ℹ️" };
|
let icon = if ai_st == AiStatus::Flagged { "🚨" } else { "ℹ️" };
|
||||||
let analysis_summary_str = analysis_summary.clone();
|
let analysis_summary_str = analysis_summary.clone();
|
||||||
let analysis_str = analysis.clone();
|
let analysis_str = analysis.clone();
|
||||||
view! {
|
view! {
|
||||||
<div class=format!("rounded-lg px-3 py-2 {}", border_str)>
|
<div class=format!("msg-analysis {}", f_cls)>
|
||||||
<div class="flex items-start gap-2 text-xs">
|
<div class="msg-analysis-row">
|
||||||
<span class="mt-0.5 shrink-0">{icon}</span>
|
<span class="msg-analysis-icon">{icon}</span>
|
||||||
<div class="min-w-0 flex-1">
|
<div class="msg-analysis-body">
|
||||||
<span class="block font-medium mb-1">{analysis_summary_str}</span>
|
<span class="msg-analysis-summary">{analysis_summary_str}</span>
|
||||||
<div class="text-xs leading-relaxed whitespace-pre-wrap">{analysis_str}</div>
|
<div class="msg-analysis-text">{analysis_str}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -356,14 +348,14 @@ pub fn MessageRow(
|
|||||||
{message.ai_error.as_ref().map(|e| {
|
{message.ai_error.as_ref().map(|e| {
|
||||||
let error_str = e.clone();
|
let error_str = e.clone();
|
||||||
view! {
|
view! {
|
||||||
<div class="rounded-lg bg-warning/5 px-3 py-2 text-xs text-warning">
|
<div class="msg-error">
|
||||||
<span>"AI error: "{error_str}</span>
|
<span>"AI error: "{error_str}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Re-analyze */}
|
{/* Re-analyze */}
|
||||||
<div class="flex items-center gap-2">
|
<div class="msg-actions">
|
||||||
<button
|
<button
|
||||||
class=format!("btn btn-sm {}", if ai_st == AiStatus::Error { "btn-destructive" } else { "btn-outline" })
|
class=format!("btn btn-sm {}", if ai_st == AiStatus::Error { "btn-destructive" } else { "btn-outline" })
|
||||||
on:click=on_click_re
|
on:click=on_click_re
|
||||||
@@ -373,7 +365,7 @@ pub fn MessageRow(
|
|||||||
" Re-analyze"
|
" Re-analyze"
|
||||||
</button>
|
</button>
|
||||||
{(ai_st == AiStatus::Error).then(|| view! {
|
{(ai_st == AiStatus::Error).then(|| view! {
|
||||||
<span class="text-xs text-secondary/70">"Click to retry"</span>
|
<span class="msg-retry-hint">"Click to retry"</span>
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -404,39 +396,30 @@ pub fn MessageCard(
|
|||||||
format!("# {}", c.channel_name.as_deref().unwrap_or("?"))
|
format!("# {}", c.channel_name.as_deref().unwrap_or("?"))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let card_cls = if deleted {
|
let card_cls = if deleted { " is-deleted" } else { "" };
|
||||||
"border-destructive/20 opacity-60"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<article class=format!("message-card shadow-sm transition-all {}", card_cls)>
|
<article class=format!("msg-card{}", card_cls)>
|
||||||
<div class="flex gap-3 p-4">
|
<div class="msg-card-inner">
|
||||||
<img src=avatar alt="" class="h-10 w-10 shrink-0 rounded-full object-cover ring-2 ring-primary/30" />
|
<img src=avatar alt="" class="msg-card-avatar" />
|
||||||
<div class="min-w-0 flex-1">
|
<div class="msg-card-body">
|
||||||
<div class="flex items-baseline gap-2 mb-2">
|
<div class="msg-card-head">
|
||||||
<span class="font-semibold text-sm">{first.username.clone()}</span>
|
<span class="msg-card-name">{first.username.clone()}</span>
|
||||||
{loc_label.as_ref().map(|l| {
|
{loc_label.as_ref().map(|l| {
|
||||||
let label_str = l.clone();
|
let label_str = l.clone();
|
||||||
view! {
|
view! {
|
||||||
<span class="flex items-center gap-1 text-xs text-secondary/50 bg-surface/50 px-1.5 py-0.5 rounded-full">
|
<span class="msg-card-channel">{label_str}</span>
|
||||||
"#" " " {label_str}
|
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
<span class="text-xs text-secondary/60">
|
<span class="msg-card-time">
|
||||||
{time_ago(first.created_at)}
|
{time_ago(first.created_at)}
|
||||||
{has_multi.then(|| format!(" · {} msgs", messages.len()))}
|
{has_multi.then(|| format!(" · {} msgs", messages.len()))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class=if has_multi { "space-y-2.5" } else { "" }>
|
<div class="msg-card-messages" class:separated=has_multi>
|
||||||
{messages.into_iter().enumerate().map(|(i, msg)| {
|
{messages.into_iter().enumerate().map(|(i, msg)| {
|
||||||
let sep = has_multi && i > 0;
|
|
||||||
view! {
|
view! {
|
||||||
<div class=if sep { "pt-2.5 border-t border-border/30" } else { "" }>
|
|
||||||
<MessageRow message=msg on_reanalyze=on_reanalyze.clone() />
|
<MessageRow message=msg on_reanalyze=on_reanalyze.clone() />
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()}
|
}).collect::<Vec<_>>()}
|
||||||
</div>
|
</div>
|
||||||
@@ -450,16 +433,16 @@ pub fn MessageCard(
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn MessageCardSkeleton() -> impl IntoView {
|
pub fn MessageCardSkeleton() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<article class="message-card">
|
<article class="msg-card">
|
||||||
<div class="flex gap-3 p-4">
|
<div class="msg-skel">
|
||||||
<div class="skeleton skeleton-circular" style="width:40px;height:40px"></div>
|
<div class="msg-skel-avatar"></div>
|
||||||
<div class="min-w-0 flex-1 space-y-3">
|
<div class="msg-skel-lines">
|
||||||
<div class="skeleton" style="height:20px;width:192px"></div>
|
<div class="msg-skel-line" style="width:192px"></div>
|
||||||
<div class="skeleton" style="height:16px;width:100%"></div>
|
<div class="msg-skel-line" style="width:100%"></div>
|
||||||
<div class="skeleton" style="height:16px;width:75%"></div>
|
<div class="msg-skel-line" style="width:75%"></div>
|
||||||
<div class="flex gap-2">
|
<div class="msg-skel-badges">
|
||||||
<div class="skeleton" style="height:24px;width:64px;border-radius:9999px"></div>
|
<div class="msg-skel-badge" style="width:64px"></div>
|
||||||
<div class="skeleton" style="height:24px;width:80px;border-radius:9999px"></div>
|
<div class="msg-skel-badge" style="width:80px"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ pub fn MessageFeed(
|
|||||||
// Loading state
|
// Loading state
|
||||||
if loading {
|
if loading {
|
||||||
return view! {
|
return view! {
|
||||||
<div class="space-y-4">
|
<div class="flex flex-col gap-4">
|
||||||
{std::iter::repeat_with(|| {
|
{std::iter::repeat_with(|| {
|
||||||
use super::message_card::MessageCardSkeleton;
|
use super::message_card::MessageCardSkeleton;
|
||||||
view! { <MessageCardSkeleton /> }
|
view! { <MessageCardSkeleton /> }
|
||||||
@@ -105,8 +105,8 @@ pub fn MessageFeed(
|
|||||||
|
|
||||||
if messages.is_empty() {
|
if messages.is_empty() {
|
||||||
return view! {
|
return view! {
|
||||||
<div class="empty-state">
|
<div class="feed-empty">
|
||||||
<div class="empty-state-title">
|
<div class="feed-empty-title">
|
||||||
{if empty_text.is_empty() { "No messages" } else { empty_text }}
|
{if empty_text.is_empty() { "No messages" } else { empty_text }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,7 +119,7 @@ pub fn MessageFeed(
|
|||||||
let loading_more_val = loading_more;
|
let loading_more_val = loading_more;
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="space-y-4">
|
<div class="feed-wrap">
|
||||||
{groups.into_iter().map(|group| {
|
{groups.into_iter().map(|group| {
|
||||||
let cb = on_reanalyze.clone();
|
let cb = on_reanalyze.clone();
|
||||||
view! {
|
view! {
|
||||||
@@ -131,7 +131,7 @@ pub fn MessageFeed(
|
|||||||
{has_more_val.then(|| {
|
{has_more_val.then(|| {
|
||||||
view! {
|
view! {
|
||||||
<>
|
<>
|
||||||
<div node_ref=sentinel_ref class="h-4">
|
<div node_ref=sentinel_ref class="feed-sentinel">
|
||||||
{loading_more_val.then(|| {
|
{loading_more_val.then(|| {
|
||||||
use super::message_card::MessageCardSkeleton;
|
use super::message_card::MessageCardSkeleton;
|
||||||
view! { <MessageCardSkeleton /> }
|
view! { <MessageCardSkeleton /> }
|
||||||
@@ -141,7 +141,7 @@ pub fn MessageFeed(
|
|||||||
{(!loading_more_val).then(|| {
|
{(!loading_more_val).then(|| {
|
||||||
let load_more_cb = on_load_more.clone();
|
let load_more_cb = on_load_more.clone();
|
||||||
view! {
|
view! {
|
||||||
<div class="mt-4 text-center">
|
<div class="feed-loader">
|
||||||
<button
|
<button
|
||||||
class="btn btn-outline btn-sm"
|
class="btn btn-outline btn-sm"
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
|
|||||||
@@ -80,12 +80,21 @@ pub fn use_messages() -> MessagesState {
|
|||||||
|
|
||||||
match get_messages(&guild_id, Some(30), None, None).await {
|
match get_messages(&guild_id, Some(30), None, None).await {
|
||||||
Ok(PageResult { data, next_cursor }) => {
|
Ok(PageResult { data, next_cursor }) => {
|
||||||
|
web_sys::console::log_3(
|
||||||
|
&"[messages] fetch OK".into(),
|
||||||
|
&format!("count={}", data.len()).into(),
|
||||||
|
&format!("cursor={:?}", next_cursor).into(),
|
||||||
|
);
|
||||||
messages_signal.set(data);
|
messages_signal.set(data);
|
||||||
cursor_signal.set(next_cursor);
|
cursor_signal.set(next_cursor);
|
||||||
current_guild_signal.set(Some(guild_id));
|
current_guild_signal.set(Some(guild_id));
|
||||||
set_loading.set(false);
|
set_loading.set(false);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
web_sys::console::log_2(
|
||||||
|
&"[messages] fetch ERROR".into(),
|
||||||
|
&format!("{}", e).into(),
|
||||||
|
);
|
||||||
error_signal.set(Some(format!("Failed to fetch messages: {}", e)));
|
error_signal.set(Some(format!("Failed to fetch messages: {}", e)));
|
||||||
set_loading.set(false);
|
set_loading.set(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,10 +186,10 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
view! {
|
view! {
|
||||||
<div class="messages-panel">
|
<div class="messages-panel">
|
||||||
{/* Header card */}
|
{/* Header card */}
|
||||||
<div class="card">
|
<div class="panel-card">
|
||||||
<div class="card-header">
|
<div class="panel-card-head">
|
||||||
<div class="card-title">"Messages"</div>
|
<div class="panel-card-title">"Messages"</div>
|
||||||
<p class="card-description">
|
<p class="panel-card-desc">
|
||||||
"Messages are automatically captured from all text channels. Real-time updates arrive via WebSocket."
|
"Messages are automatically captured from all text channels. Real-time updates arrive via WebSocket."
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -197,7 +197,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
|
|
||||||
{/* Stats badges */}
|
{/* Stats badges */}
|
||||||
{move || (total() > 0).then(|| view! {
|
{move || (total() > 0).then(|| view! {
|
||||||
<div class="message-stats">
|
<div class="stats-bar">
|
||||||
<span class="badge badge-outline text-xs">{total()} " total" {state.has_more.get().then_some("+")}</span>
|
<span class="badge badge-outline text-xs">{total()} " total" {state.has_more.get().then_some("+")}</span>
|
||||||
<span class="badge badge-success text-xs">{clean()} " clean"</span>
|
<span class="badge badge-success text-xs">{clean()} " clean"</span>
|
||||||
<span class="badge badge-primary text-xs">{flagged()} " flagged"</span>
|
<span class="badge badge-primary text-xs">{flagged()} " flagged"</span>
|
||||||
@@ -213,13 +213,11 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Search + filters row */}
|
{/* Search + filters row */}
|
||||||
<div class="search-row">
|
<div class="search-bar">
|
||||||
<div class="relative flex-1" style="min-width:200px">
|
<div class="search-wrap">
|
||||||
{/* Search icon as SVG */}
|
<svg width="16" height="16" class="search-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
|
||||||
<svg width="16" height="16" style="position:absolute;left:0.75rem;top:50%;transform:translateY(-50%);color:var(--color-primary)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><path d="m21 21-4.35-4.35"></path></svg>
|
|
||||||
<input
|
<input
|
||||||
class="input"
|
class="search-input"
|
||||||
style="padding-left:2.25rem;border-radius:9999px"
|
|
||||||
placeholder="Search message content..."
|
placeholder="Search message content..."
|
||||||
prop:value=search_query
|
prop:value=search_query
|
||||||
on:input=move |ev| set_search_query.set(event_target_value(&ev))
|
on:input=move |ev| set_search_query.set(event_target_value(&ev))
|
||||||
@@ -258,20 +256,18 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
disabled=move || retrying_all.get()
|
disabled=move || retrying_all.get()
|
||||||
>
|
>
|
||||||
{/* Rotate CCW icon as SVN */}
|
<svg class=format!("mr-1.5 h-3.5 w-3.5{}", if retrying_all.get() { " icon-spin" } else { "" }) xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"></path><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path></svg>
|
||||||
<svg class=format!("mr-1.5 h-3.5 w-3.5{}", if retrying_all.get() { " animate-spin" } else { "" }) xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7v6h6"></path><path d="M21 17a9 9 0 00-9-9 9 9 0 00-6 2.3L3 13"></path></svg>
|
|
||||||
{move || if retrying_all.get() { "Retrying...".to_string() } else { format!("Retry All Errors ({})", err_count) }}
|
{move || if retrying_all.get() { "Retrying...".to_string() } else { format!("Retry All Errors ({})", err_count) }}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
<div class="ml-auto flex items-center" style="gap:0.375rem">
|
<div class="filter-group">
|
||||||
{/* Filter icon as SVG since lucide-leptos Filter unavailable */}
|
|
||||||
<svg width="16" height="16" style="color:var(--color-primary)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>
|
<svg width="16" height="16" style="color:var(--color-primary)" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon></svg>
|
||||||
{FILTERS.iter().map(|f| {
|
{FILTERS.iter().map(|f| {
|
||||||
let f_ptr: &'static str = f;
|
let f_ptr: &'static str = f;
|
||||||
view! {
|
view! {
|
||||||
<button class="filter-chip" class:active=move || ai_filter.get() == f_ptr on:click=move |_| set_filter(f_ptr) >{*f}</button>
|
<button class="filter-chip-v2" class:is-active=move || ai_filter.get() == f_ptr on:click=move |_| set_filter(f_ptr) >{*f}</button>
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()}
|
}).collect::<Vec<_>>()}
|
||||||
</div>
|
</div>
|
||||||
@@ -281,7 +277,7 @@ pub fn MessagesPanel() -> impl IntoView {
|
|||||||
{move || show_search.get().then(|| {
|
{move || show_search.get().then(|| {
|
||||||
let n = search_results.get().len();
|
let n = search_results.get().len();
|
||||||
view! {
|
view! {
|
||||||
<div class="text-sm text-secondary">
|
<div class="search-count">
|
||||||
"Found " {n} " result" {if n != 1 { "s" } else { "" }}
|
"Found " {n} " result" {if n != 1 { "s" } else { "" }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user