Changes full workspace config. Still some work to do ui + logic
This commit is contained in:
@@ -0,0 +1,538 @@
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user