Files
litoral-central-frontend/src/features/chartworkspace/components/ChartWorkspaceControls.tsx
T

539 lines
22 KiB
TypeScript

import { useState, type ReactNode } from "react";
import { BarChart3, ChevronDown, Pencil, Play, Plus, Search, Trash2, X } from "lucide-react";
import type { ChartWorkspaceResponse } from "../api/chartWorkspaceApi";
const RADIUS = "rounded-[6px]";
export function LayoutButton({
theme,
active,
icon,
title,
onClick,
}: {
theme: "dark" | "light";
active: boolean;
icon: ReactNode;
title: string;
onClick: () => void;
}) {
const isDark = theme === "dark";
return (
<button
type="button"
title={title}
aria-label={title}
onClick={onClick}
className={
active
? isDark
? `${RADIUS} grid h-10 w-10 place-items-center bg-[#4FD1C5] text-[#07101B]`
: `${RADIUS} grid h-10 w-10 place-items-center bg-[#0F766E] text-white`
: isDark
? `${RADIUS} grid h-10 w-10 place-items-center border border-[#263247] bg-[#111A2B] text-[#A8B3C7] transition hover:border-[#36506D] hover:text-white`
: `${RADIUS} grid h-10 w-10 place-items-center border border-[#D7DEE8] bg-[#F8FAFC] text-slate-600 transition hover:bg-white hover:text-[#0F172A]`
}
>
{icon}
</button>
);
}
export function ModeButton({
theme,
active,
icon,
label,
onClick,
}: {
theme: "dark" | "light";
active: boolean;
icon?: ReactNode;
label: string;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={
active
? theme === "dark"
? `${RADIUS} flex items-center justify-center gap-2 bg-[#4FD1C5] px-3 py-2 text-xs font-black text-[#07101B]`
: `${RADIUS} flex items-center justify-center gap-2 bg-[#0F766E] px-3 py-2 text-xs font-black text-white`
: theme === "dark"
? `${RADIUS} flex items-center justify-center gap-2 border border-[#263247] bg-[#111A2B] px-3 py-2 text-xs font-bold text-[#A8B3C7]`
: `${RADIUS} flex items-center justify-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-2 text-xs font-bold text-slate-600`
}
>
{icon}
{label}
</button>
);
}
export function EmptyWorkspace({
theme,
canAddMoreCharts,
title = "Nenhum gráfico aberto",
description = "Abra um gráfico guardado ou crie um novo gráfico para continuar.",
addLabel = "Novo Gráfico",
savedLabel = "Abrir Guardado",
minHeightClass = "min-h-0 flex-1",
onAddChart,
onOpenSaved,
}: {
theme: "dark" | "light";
canAddMoreCharts: boolean;
title?: string;
description?: string;
addLabel?: string;
savedLabel?: string;
minHeightClass?: string;
onAddChart: () => void;
onOpenSaved: () => void;
}) {
const isDark = theme === "dark";
return (
<section
className={
isDark
? `${RADIUS} flex ${minHeightClass} items-center justify-center border border-dashed border-[#33445F] bg-[#0E1726]/70`
: `${RADIUS} flex ${minHeightClass} items-center justify-center border border-dashed border-[#CBD5E1] bg-white`
}
>
<div className="max-w-[420px] text-center">
<div
className={
isDark
? "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#263247] bg-[#111A2B] text-[#4FD1C5]"
: "mx-auto grid h-16 w-16 place-items-center rounded-2xl border border-[#D7DEE8] bg-[#F8FAFC] text-[#0F766E]"
}
>
<BarChart3 className="h-8 w-8" />
</div>
<h2
className={
isDark
? "mt-5 text-xl font-black text-white"
: "mt-5 text-xl font-black text-[#0F172A]"
}
>
{title}
</h2>
<p
className={
isDark
? "mt-3 text-sm leading-6 text-[#A8B3C7]"
: "mt-3 text-sm leading-6 text-slate-600"
}
>
{description}
</p>
<div className="mt-6 flex items-center justify-center gap-3">
<button
type="button"
disabled={!canAddMoreCharts}
onClick={onAddChart}
className={
canAddMoreCharts
? `inline-flex h-11 items-center gap-3 ${RADIUS} bg-[#18B8A6] px-5 text-sm font-black text-white transition hover:bg-[#21C7B5]`
: `inline-flex h-11 cursor-not-allowed items-center gap-3 ${RADIUS} bg-[#18B8A6]/40 px-5 text-sm font-black text-white`
}
>
<Plus className="h-5 w-5" />
{addLabel}
</button>
<button
type="button"
onClick={onOpenSaved}
className={
isDark
? `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#263247] bg-[#111A2B] px-5 text-sm font-black text-white`
: `inline-flex h-11 items-center gap-3 ${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-5 text-sm font-black text-[#0F172A]`
}
>
<Search className="h-5 w-5" />
{savedLabel}
</button>
</div>
</div>
</section>
);
}
export function WorkspaceSelector({
theme,
workspaces,
activeWorkspaceId,
detachedWorkspaceIds,
loading,
creating,
canCreateWorkspace,
onSelectWorkspace,
onCreateWorkspace,
onRenameWorkspace,
onDeleteWorkspace,
}: {
theme: "dark" | "light";
workspaces: ChartWorkspaceResponse[];
activeWorkspaceId: number | null;
detachedWorkspaceIds: Set<number>;
loading: boolean;
creating: boolean;
canCreateWorkspace: boolean;
onSelectWorkspace: (workspaceId: number) => void;
onCreateWorkspace: (name: string) => void;
onRenameWorkspace: (workspaceId: number, name: string) => void | Promise<void>;
onDeleteWorkspace: (workspaceId: number) => void | Promise<void>;
}) {
const isDark = theme === "dark";
const [open, setOpen] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
const [renameWorkspace, setRenameWorkspace] =
useState<ChartWorkspaceResponse | null>(null);
const [search, setSearch] = useState("");
const activeWorkspace =
workspaces.find((workspace) => workspace.id === activeWorkspaceId) ??
null;
const filteredWorkspaces = workspaces.filter((workspace) =>
workspace.name.toLowerCase().includes(search.toLowerCase()),
);
const createWorkspace = (name: string) => {
onCreateWorkspace(name);
setCreateOpen(false);
setOpen(false);
};
const renameSelectedWorkspace = (name: string) => {
if (!renameWorkspace) return;
void onRenameWorkspace(renameWorkspace.id, name);
setRenameWorkspace(null);
};
return (
<div className="relative">
<button
type="button"
disabled={loading}
onClick={() => setOpen((value) => !value)}
className={
isDark
? `flex h-10 min-w-[260px] items-center justify-between gap-3 ${RADIUS} border border-[#263247] bg-[#111A2B] px-4 text-sm font-black text-white transition hover:border-[#36506D] disabled:cursor-not-allowed disabled:text-[#7F8CA3]`
: `flex h-10 min-w-[260px] items-center justify-between gap-3 ${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-4 text-sm font-black text-[#0F172A] transition hover:bg-white disabled:cursor-not-allowed disabled:text-slate-400`
}
>
<span className="min-w-0 truncate">
{loading
? "A carregar workspaces..."
: activeWorkspace?.name ?? "Workspaces"}
</span>
<ChevronDown className="h-4 w-4 shrink-0" />
</button>
{open && (
<div
className={
isDark
? `${RADIUS} absolute left-0 top-[calc(100%+8px)] z-40 w-[460px] border border-[#263247] bg-[#0E1726] p-3 shadow-2xl`
: `${RADIUS} absolute left-0 top-[calc(100%+8px)] z-40 w-[460px] border border-[#D7DEE8] bg-white p-3 shadow-2xl`
}
>
<div className="mb-3 flex items-center justify-between">
<div>
<h2 className="text-sm font-black">Workspaces</h2>
<p className={isDark ? "mt-1 text-xs text-[#7F8CA3]" : "mt-1 text-xs text-slate-500"}>
{workspaces.length}/10 workspaces neste modulo.
</p>
</div>
<div className="flex items-center gap-2">
<button
type="button"
disabled={!canCreateWorkspace || creating}
onClick={() => setCreateOpen(true)}
className={
canCreateWorkspace && !creating
? `inline-flex h-9 items-center gap-2 ${RADIUS} bg-[#18B8A6] px-3 text-xs font-black text-white transition hover:bg-[#21C7B5]`
: `inline-flex h-9 cursor-not-allowed items-center gap-2 ${RADIUS} bg-[#18B8A6]/40 px-3 text-xs font-black text-white`
}
>
<Plus className="h-4 w-4" />
Novo
</button>
<button type="button" onClick={() => setOpen(false)}>
<X className="h-4 w-4" />
</button>
</div>
</div>
<div
className={
isDark
? `${RADIUS} mb-3 flex h-10 items-center gap-2 border border-[#263247] bg-[#07101B] px-3 text-sm text-[#7F8CA3]`
: `${RADIUS} mb-3 flex h-10 items-center gap-2 border border-[#D7DEE8] bg-[#F8FAFC] px-3 text-sm text-slate-500`
}
>
<Search className="h-4 w-4" />
<input
value={search}
onChange={(event) => setSearch(event.target.value)}
placeholder="Pesquisar workspaces..."
className="w-full bg-transparent outline-none placeholder:text-inherit"
/>
</div>
<div className="max-h-[300px] space-y-2 overflow-y-auto pr-1">
{filteredWorkspaces.map((workspace) => {
const active = workspace.id === activeWorkspaceId;
const detached = detachedWorkspaceIds.has(workspace.id);
const chartCount = getWorkspaceChartCount(workspace);
return (
<div
key={workspace.id}
className={
isDark
? `${RADIUS} flex w-full items-center justify-between gap-3 border border-[#263247] bg-[#111A2B] px-3 py-3 transition hover:border-[#36506D]`
: `${RADIUS} flex w-full items-center justify-between gap-3 border border-[#D7DEE8] bg-[#F8FAFC] px-3 py-3 transition hover:bg-white`
}
>
<div className="min-w-0">
<p className="truncate text-sm font-black">
{workspace.name}
</p>
<p className={isDark ? "mt-1 truncate text-xs text-[#7F8CA3]" : "mt-1 truncate text-xs text-slate-500"}>
{chartCount}/10 graficos
{detached
? " · detached"
: workspace.defaultWorkspace
? " · ultimo aberto"
: ""}
</p>
</div>
<div className="flex shrink-0 items-center gap-3">
{active ? (
<span className="text-xs font-black text-[#4FD1C5]">
Aberto
</span>
) : detached ? (
<span className="text-xs font-black text-[#FACC15]">
Detached
</span>
) : (
<button
type="button"
onClick={() => {
onSelectWorkspace(workspace.id);
setOpen(false);
}}
title="Abrir workspace"
className="text-[#7F8CA3] transition hover:text-[#4FD1C5]"
>
<Play className="h-4 w-4" />
</button>
)}
<button
type="button"
onClick={() => setRenameWorkspace(workspace)}
title="Renomear workspace"
className="text-[#7F8CA3] transition hover:text-[#4FD1C5]"
>
<Pencil className="h-4 w-4" />
</button>
<button
type="button"
onClick={() => void onDeleteWorkspace(workspace.id)}
title="Eliminar workspace"
className="text-[#7F8CA3] transition hover:text-red-400"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</div>
);
})}
{filteredWorkspaces.length === 0 && (
<div className={isDark ? `${RADIUS} border border-dashed border-[#263247] px-4 py-6 text-center text-sm font-bold text-[#7F8CA3]` : `${RADIUS} border border-dashed border-[#D7DEE8] px-4 py-6 text-center text-sm font-bold text-slate-500`}>
Nenhum workspace encontrado.
</div>
)}
</div>
</div>
)}
{createOpen && (
<NewWorkspaceModal
theme={theme}
creating={creating}
onClose={() => setCreateOpen(false)}
onCreate={createWorkspace}
/>
)}
{renameWorkspace && (
<WorkspaceNameModal
theme={theme}
title="Renomear workspace"
actionLabel="Guardar"
initialName={renameWorkspace.name}
creating={false}
onClose={() => setRenameWorkspace(null)}
onSubmit={renameSelectedWorkspace}
/>
)}
</div>
);
}
function getWorkspaceChartCount(workspace: ChartWorkspaceResponse) {
try {
const charts = JSON.parse(workspace.chartsJson) as unknown[];
return Array.isArray(charts) ? charts.length : 0;
} catch {
return 0;
}
}
function WorkspaceNameModal({
theme,
title,
actionLabel,
initialName = "",
creating,
onClose,
onSubmit,
}: {
theme: "dark" | "light";
title: string;
actionLabel: string;
initialName?: string;
creating: boolean;
onClose: () => void;
onSubmit: (name: string) => void;
}) {
const isDark = theme === "dark";
const [name, setName] = useState(initialName);
const canCreate = name.trim().length > 0 && !creating;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/55 p-6">
<div
className={
isDark
? `${RADIUS} w-full max-w-[420px] border border-[#263247] bg-[#0E1726] shadow-2xl`
: `${RADIUS} w-full max-w-[420px] border border-[#D7DEE8] bg-white shadow-2xl`
}
>
<header
className={
isDark
? "flex items-center justify-between border-b border-[#263247] px-5 py-4"
: "flex items-center justify-between border-b border-[#D7DEE8] px-5 py-4"
}
>
<h2 className="text-sm font-black">{title}</h2>
<button type="button" onClick={onClose}>
<X className="h-4 w-4" />
</button>
</header>
<main className="p-5">
<label className="mb-2 block text-sm font-black">
Nome
</label>
<input
autoFocus
value={name}
onChange={(event) => setName(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && canCreate) {
onSubmit(name);
}
}}
placeholder="Ex: Estufa norte"
className={
isDark
? `${RADIUS} h-11 w-full border border-[#263247] bg-[#07101B] px-3 text-sm font-bold text-white outline-none focus:border-[#4FD1C5]`
: `${RADIUS} h-11 w-full border border-[#D7DEE8] bg-[#F8FAFC] px-3 text-sm font-bold text-[#0F172A] outline-none focus:border-[#0F766E]`
}
/>
</main>
<footer
className={
isDark
? "flex justify-end gap-3 border-t border-[#263247] px-5 py-4"
: "flex justify-end gap-3 border-t border-[#D7DEE8] px-5 py-4"
}
>
<button
type="button"
onClick={onClose}
className={
isDark
? `${RADIUS} border border-[#263247] bg-[#111A2B] px-4 py-2 text-sm font-bold text-white`
: `${RADIUS} border border-[#D7DEE8] bg-[#F8FAFC] px-4 py-2 text-sm font-bold text-[#0F172A]`
}
>
Cancelar
</button>
<button
type="button"
disabled={!canCreate}
onClick={() => onSubmit(name)}
className={
canCreate
? `${RADIUS} bg-[#18B8A6] px-4 py-2 text-sm font-black text-white transition hover:bg-[#21C7B5]`
: `${RADIUS} cursor-not-allowed bg-[#18B8A6]/40 px-4 py-2 text-sm font-black text-white`
}
>
{actionLabel}
</button>
</footer>
</div>
</div>
);
}
function NewWorkspaceModal({
theme,
creating,
onClose,
onCreate,
}: {
theme: "dark" | "light";
creating: boolean;
onClose: () => void;
onCreate: (name: string) => void;
}) {
return (
<WorkspaceNameModal
theme={theme}
title="Novo workspace"
actionLabel="Criar"
creating={creating}
onClose={onClose}
onSubmit={onCreate}
/>
);
}