First Commit
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
IconButton,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@mui/material";
|
||||
|
||||
import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder";
|
||||
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
||||
|
||||
export type SavedViewParams = {
|
||||
q?: string;
|
||||
status?: string;
|
||||
companyId?: number;
|
||||
location?: string;
|
||||
needsFollowUp?: boolean;
|
||||
};
|
||||
|
||||
type SavedView = {
|
||||
id: string;
|
||||
name: string;
|
||||
params: SavedViewParams;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const KEY = "jt_saved_views_v1";
|
||||
|
||||
function loadViews(): SavedView[] {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(KEY);
|
||||
if (!raw) return [];
|
||||
const v = JSON.parse(raw);
|
||||
if (!Array.isArray(v)) return [];
|
||||
return v as SavedView[];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveViews(views: SavedView[]) {
|
||||
window.localStorage.setItem(KEY, JSON.stringify(views));
|
||||
}
|
||||
|
||||
export default function SavedViewsMenu({
|
||||
current,
|
||||
onApply,
|
||||
}: {
|
||||
current: SavedViewParams;
|
||||
onApply: (p: SavedViewParams) => void;
|
||||
}) {
|
||||
const [anchor, setAnchor] = useState<HTMLElement | null>(null);
|
||||
const [name, setName] = useState("");
|
||||
const [views, setViews] = useState<SavedView[]>(() => loadViews());
|
||||
|
||||
const hasAny = views.length > 0;
|
||||
|
||||
const canSave = useMemo(() => name.trim().length > 0, [name]);
|
||||
|
||||
const open = Boolean(anchor);
|
||||
|
||||
const add = () => {
|
||||
if (!canSave) return;
|
||||
const next: SavedView = {
|
||||
id: `v_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
name: name.trim(),
|
||||
params: { ...current },
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
const updated = [next, ...views].slice(0, 50);
|
||||
setViews(updated);
|
||||
saveViews(updated);
|
||||
setName("");
|
||||
};
|
||||
|
||||
const remove = (id: string) => {
|
||||
const updated = views.filter((v) => v.id !== id);
|
||||
setViews(updated);
|
||||
saveViews(updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Saved views">
|
||||
<IconButton size="small" onClick={(e) => setAnchor(e.currentTarget)}>
|
||||
<BookmarkBorderIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Menu
|
||||
anchorEl={anchor}
|
||||
open={open}
|
||||
onClose={() => setAnchor(null)}
|
||||
PaperProps={{ sx: { minWidth: 320, p: 0.5 } }}
|
||||
>
|
||||
<MenuItem disableRipple sx={{ cursor: "default", alignItems: "flex-start" }}>
|
||||
<ListItemText
|
||||
primary="Saved views"
|
||||
secondary="Save the current filters as a 1-click view."
|
||||
/>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem disableRipple sx={{ cursor: "default" }}>
|
||||
<TextField
|
||||
size="small"
|
||||
label="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem disableRipple sx={{ cursor: "default", justifyContent: "flex-end" }}>
|
||||
<Button variant="contained" size="small" onClick={add} disabled={!canSave}>
|
||||
Save current
|
||||
</Button>
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
{!hasAny && (
|
||||
<MenuItem disabled>No saved views yet.</MenuItem>
|
||||
)}
|
||||
|
||||
{views.map((v) => (
|
||||
<MenuItem
|
||||
key={v.id}
|
||||
onClick={() => {
|
||||
onApply(v.params);
|
||||
setAnchor(null);
|
||||
}}
|
||||
sx={{ gap: 1 }}
|
||||
>
|
||||
<ListItemText
|
||||
primary={v.name}
|
||||
secondary={
|
||||
[
|
||||
v.params.status ? `Status: ${v.params.status}` : null,
|
||||
v.params.location ? `Loc: ${v.params.location}` : null,
|
||||
v.params.needsFollowUp ? "Needs follow-up" : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" · ") || "No filters"
|
||||
}
|
||||
/>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
remove(v.id);
|
||||
}}
|
||||
>
|
||||
<DeleteOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user