add key()

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-13 12:47:04 +08:00
parent e9b99f8243
commit bfc4b9fc67
15 changed files with 221 additions and 76 deletions
@@ -110,23 +110,14 @@ export default function UsersTable() {
fetchUsers({ page: 1, limit: pagination.limit });
};
// ─── Attach filterId + filterValue to each stat so TableDashboard
// knows which column/value to apply when clicked ──────────────────────
const dashboardStats = (usersDashboard?.stats ?? []).map((s) => ({ // ← was dashboard?.users?.stats
// ─── Attach filterId/filterValue (or onClick) to each stat ────────────────
const statsWithFilter = (usersDashboard?.stats ?? []).map((s) => ({
...s,
filterId: s.key === "archived" || s.key === "total" ? null : "is_active",
filterValue: s.key === "active" ? ["true"]
: s.key === "inactive" ? ["false"]
: null,
filterId: s.key === "active" ? "is_active" : null,
filterValue: s.key === "active" ? ["true"] : null,
onClick: s.key === "inactive" ? () => navigate("/admin/users/archived") : undefined,
}));
// Override verified → correct column
const statsWithFilter = dashboardStats.map((s) =>
s.key === "verified"
? { ...s, filterId: "is_verified", filterValue: ["true"] }
: s
);
// ─── Attach filterId to each breakdown so clicking a slice filters ────────
const dashboardBreakdowns = (usersDashboard?.breakdowns ?? []).map((b) => ({
...b,
@@ -35,9 +35,15 @@ const cellOverrides = {
export function buildDataColumns(attributes, rowActions) {
const visibleAttributes = attributes.filter((a) => !a.hidden);
const dataColumns = buildColumns(visibleAttributes, { cellOverrides }).map((col) =>
// "groups" is a joined association, not a real orderable column on the
// Users table — sorting by it would error server-side.
col.id === "groups" ? { ...col, enableSorting: false } : col
);
return [
buildSelectionColumn(),
...buildColumns(visibleAttributes, { cellOverrides }),
...dataColumns,
buildRowActionsColumn(rowActions, { dropdownLabel: "User Actions" }),
];
}
@@ -15,6 +15,7 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Spinner } from "@/components/ui/spinner";
import { Progress } from "@/components/ui/progress";
import {
Select,
SelectContent,
@@ -122,6 +123,7 @@ export default function AddAsset() {
const fileRef = useRef(null);
const thumbnailRef = useRef(null);
const [thumbKey, setThumbKey] = useState(0);
const [progress, setProgress] = useState(null); // { phase: 'uploading'|'storing'|'done'|'error', pct } | null
const {
register,
@@ -182,6 +184,7 @@ export default function AddAsset() {
if (hasFileError) return;
setProgress({ phase: "uploading", pct: 0 });
const result = await uploadAsset({
file: fileRef.current,
thumbnail: thumbnailRef.current ?? undefined,
@@ -191,11 +194,20 @@ export default function AddAsset() {
is_public: data.is_public === "true",
storage_provider: data.storage_provider,
createdBy: user?.user_id,
onProgress: setProgress,
});
setProgress(null);
if (result) { bypassOnce(); navigate(-1); }
};
const progressLabel = {
uploading: "Uploading to server…",
storing: "Storing to server...",
done: "Done.",
error: "Upload failed.",
}[progress?.phase];
return (
<div className="max-w-2xl mx-auto px-4 py-6 space-y-6">
@@ -352,6 +364,17 @@ export default function AddAsset() {
</div>
{/* ── Upload progress (real — see AdminAssetsContext.uploadAsset) ── */}
{progress && (
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>{progressLabel}</span>
<span>{progress.pct ?? 0}%</span>
</div>
<Progress value={progress.pct ?? 0} />
</div>
)}
{/* ── Actions ── */}
<div className="flex justify-end gap-3">
<Button
@@ -1,11 +1,10 @@
// modules/admin/pages/assets/ViewAudioAsset.jsx
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Lock, Globe, Music2, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Badge } from "@/components/ui/badge";
@@ -34,18 +33,14 @@ export default function ViewAudioAsset() {
const navigate = useNavigate();
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const { asset: selectedAsset, loading, notFound } = useAssetFetchState(assetId);
const { src: streamUrl, thumbnailUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
if (loading) {
return <AssetPageLoader />;
}
if (!selectedAsset) {
if (notFound) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-3 text-muted-foreground">
<p>Asset not found.</p>
@@ -1,11 +1,10 @@
// modules/admin/pages/assets/ViewDocumentAsset.jsx
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Lock, Globe, FileText, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Spinner } from "@/components/ui/spinner";
@@ -32,18 +31,14 @@ export default function ViewDocumentAsset() {
const navigate = useNavigate();
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const { asset: selectedAsset, loading, notFound } = useAssetFetchState(assetId);
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
if (loading) {
return <AssetPageLoader />;
}
if (!selectedAsset) {
if (notFound) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-3 text-muted-foreground">
<p>Asset not found.</p>
@@ -1,11 +1,10 @@
// modules/admin/pages/assets/ViewImageAsset.jsx
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Lock, Globe, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Badge } from "@/components/ui/badge";
@@ -30,18 +29,14 @@ export default function ViewImageAsset() {
const navigate = useNavigate();
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const { asset: selectedAsset, loading, notFound } = useAssetFetchState(assetId);
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
if (loading) {
return <AssetPageLoader />;
}
if (!selectedAsset) {
if (notFound) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-3 text-muted-foreground">
<p>Asset not found.</p>
@@ -1,11 +1,10 @@
// modules/admin/pages/assets/ViewVideoAsset.jsx
import { useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { ArrowLeft, Lock, Globe, Download } from "lucide-react";
import { useDateFormat } from "@/hooks/useDateFormat";
import { useAssets } from "@/contexts/AdminAssetsContext";
import { useAssetFetchState } from "@/hooks/useAssetFetchState";
import { useAssetPreviewSrc } from "@/hooks/useAssetPreviewSrc";
import { downloadAsset } from "@/utils/media.util";
import { Badge } from "@/components/ui/badge";
@@ -39,18 +38,14 @@ export default function ViewVideoAsset() {
const navigate = useNavigate();
const { fmtDateTime } = useDateFormat();
const { selectedAsset, loading, fetchAsset } = useAssets();
const { asset: selectedAsset, loading, notFound } = useAssetFetchState(assetId);
const { src: streamUrl } = useAssetPreviewSrc(selectedAsset, { scope: "admin" });
useEffect(() => {
if (assetId) fetchAsset(assetId);
}, [assetId]);
if (loading) {
return <AssetPageLoader />;
}
if (!selectedAsset) {
if (notFound) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-3 text-muted-foreground">
<p>Asset not found.</p>
@@ -391,7 +391,7 @@ export default function AddStaffUserPage() {
};
const res = await addStaffUser(payload);
if (res) { bypassOnce(); navigate("/admin/users/all"); }
if (res) { bypassOnce(); navigate("/admin/users"); }
});
return (
+8 -1
View File
@@ -9,7 +9,7 @@ import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
import { Spinner } from "@/components/ui/spinner";
import { House, Trophy, Award, BadgeCheck, Activity, ChevronLeft, ChevronRight, ShieldBan, ShieldCheck } from "lucide-react";
import { House, Trophy, Award, BadgeCheck, Activity, ChevronLeft, ChevronRight, ShieldBan, ShieldCheck, Pencil } from "lucide-react";
import { ROLE_CONFIG } from "@/data/profile.data";
import { BADGE_STYLES } from "@/utils/table.util";
import { getActionBadge } from "@/data/activity.data";
@@ -107,6 +107,13 @@ export default function ViewUser() {
</div>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => navigate(`/admin/users/edit/${userId}`)}
>
<Pencil className="size-4 mr-1.5" /> Edit Info
</Button>
{user.is_banned ? (
<Button
size="sm"
+2
View File
@@ -12,6 +12,7 @@ import ProfilePage from '@/components/generic/Profile'
import UserList from '../pages/users/UserList'
import AddUser from '../pages/users/AddStaffUser'
import ViewUser from '../pages/users/ViewUser'
import EditUser from '../pages/users/EditUser'
// User Groups
import GroupList from '../pages/user_groups/GroupList'
@@ -149,6 +150,7 @@ export const AdminRoutes = {
{ index: true, element: <UserList /> },
{ path: 'add/staff', element: <AddUser /> },
{ path: 'view/:userId', element: <ViewUser /> },
{ path: 'edit/:id', element: <EditUser /> },
{ path: 'archived', element: <ArchivedUserList /> },
{ path: ':userId/activity', element: <UserActivityPage /> },
]