feat: dashboard live SSE feed + screener custom search
- Dashboard 'Kabar terbaru' now subscribes to /api/stream (alerts/activity channels) and prepends live events without page reload (subscribeSSE was dead code before) - Screener: new search card (nama/sektor free-text via /api/screen q=) alongside the 4 presets
This commit is contained in:
@@ -6,6 +6,7 @@ import { Badge } from "../components/ui/badge";
|
|||||||
import { Button } from "../components/ui/button";
|
import { Button } from "../components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../components/ui/card";
|
||||||
import { Skeleton } from "../components/ui/skeleton";
|
import { Skeleton } from "../components/ui/skeleton";
|
||||||
|
import { TextField, TextFieldInput } from "../components/ui/text-field";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
|
||||||
// Preset awam -> body /api/screen. PRESET_SEMUA = default auto-jalan.
|
// Preset awam -> body /api/screen. PRESET_SEMUA = default auto-jalan.
|
||||||
@@ -24,14 +25,26 @@ function ScreenerInner() {
|
|||||||
const [busy, setBusy] = createSignal(false);
|
const [busy, setBusy] = createSignal(false);
|
||||||
const [err, setErr] = createSignal("");
|
const [err, setErr] = createSignal("");
|
||||||
const [aktif, setAktif] = createSignal("");
|
const [aktif, setAktif] = createSignal("");
|
||||||
|
const [q, setQ] = createSignal("");
|
||||||
|
const [showCustom, setShowCustom] = createSignal(false);
|
||||||
async function runPreset(p: (typeof PRESETS)[number]) {
|
async function runPreset(p: (typeof PRESETS)[number]) {
|
||||||
setErr(""); setBusy(true); setAktif(p.label);
|
setErr(""); setBusy(true); setAktif(p.label); setShowCustom(false);
|
||||||
try {
|
try {
|
||||||
const r = await api.screen(p.body);
|
const r = await api.screen(p.body);
|
||||||
setRows(r.rows); setRan(true);
|
setRows(r.rows); setRan(true);
|
||||||
} catch (e) { setErr(String(e)); }
|
} catch (e) { setErr(String(e)); }
|
||||||
finally { setBusy(false); }
|
finally { setBusy(false); }
|
||||||
}
|
}
|
||||||
|
async function runCustom() {
|
||||||
|
const query = q().trim();
|
||||||
|
if (!query) return;
|
||||||
|
setErr(""); setBusy(true); setAktif("custom");
|
||||||
|
try {
|
||||||
|
const r = await api.screen({ q: query, limit: 20 });
|
||||||
|
setRows(r.rows); setRan(true);
|
||||||
|
} catch (e) { setErr(String(e)); }
|
||||||
|
finally { setBusy(false); }
|
||||||
|
}
|
||||||
// Default = semua: auto-jalan preset "Semua" sekali saat halaman dibuka.
|
// Default = semua: auto-jalan preset "Semua" sekali saat halaman dibuka.
|
||||||
onMount(() => { if (!ran()) void runPreset(PRESET_SEMUA); });
|
onMount(() => { if (!ran()) void runPreset(PRESET_SEMUA); });
|
||||||
const hasil = () => rows().map((r) => ({ row: r, ...verdictFor(r) }));
|
const hasil = () => rows().map((r) => ({ row: r, ...verdictFor(r) }));
|
||||||
@@ -48,6 +61,13 @@ function ScreenerInner() {
|
|||||||
<CardContent><Button size="sm" onClick={() => runPreset(p)} disabled={busy()}>{busy() && aktif() === p.label ? "Menyaring…" : "Tampilkan"}</Button></CardContent>
|
<CardContent><Button size="sm" onClick={() => runPreset(p)} disabled={busy()}>{busy() && aktif() === p.label ? "Menyaring…" : "Tampilkan"}</Button></CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}</For>
|
)}</For>
|
||||||
|
<Card class={showCustom() ? "border-primary" : ""}>
|
||||||
|
<CardHeader class="pb-2"><CardTitle class="text-base">🔎 Cari nama / sektor sendiri</CardTitle><CardDescription>Ketik nama perusahaan atau sektor (mis. "bank", "energi").</CardDescription></CardHeader>
|
||||||
|
<CardContent class="flex gap-2">
|
||||||
|
<TextField class="flex-1"><TextFieldInput placeholder="mis. bank, bca, energi…" value={q()} onInput={(e) => { setQ(e.currentTarget.value); setShowCustom(true); }} onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") runCustom(); }} /></TextField>
|
||||||
|
<Button size="sm" onClick={runCustom} disabled={busy() || !q().trim()}>{busy() && aktif() === "custom" ? "Menyaring…" : "Cari"}</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Show when={err()}><p class="text-sm text-destructive">{err()}</p></Show>
|
<Show when={err()}><p class="text-sm text-destructive">{err()}</p></Show>
|
||||||
<Show when={busy()}><div class="space-y-2"><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /></div></Show>
|
<Show when={busy()}><div class="space-y-2"><Skeleton class="h-24 w-full" /><Skeleton class="h-24 w-full" /></div></Show>
|
||||||
|
|||||||
Reference in New Issue
Block a user