File detail page addedd

This commit is contained in:
stephen 2026-02-12 22:55:43 +11:00
parent 222dcda775
commit 96eda53f31
4 changed files with 211 additions and 10 deletions

View file

@ -1,5 +1,5 @@
'use client'; 'use client';
// src/app/dashboard-view.tsx
import { useState } from "react"; import { useState } from "react";
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { import {
@ -21,6 +21,7 @@ import {
QuickFilter, QuickFilter,
QuickFilterControl, QuickFilterControl,
QuickFilterClear, QuickFilterClear,
GridEventListener,
} from "@mui/x-data-grid"; } from "@mui/x-data-grid";
import SyncIcon from "@mui/icons-material/Sync"; import SyncIcon from "@mui/icons-material/Sync";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
@ -156,6 +157,14 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
} }
}; };
// --- NEW: Double Click Handler ---
const handleRowDoubleClick: GridEventListener<'rowDoubleClick'> = (params) => {
// Only navigate if it's a file. If it's a folder, we could eventually navigate into it.
if (!params.row.isFolder) {
router.push(`/dashboard/files/${params.id}`);
}
};
const columns: GridColDef[] = [ const columns: GridColDef[] = [
{ {
field: "name", field: "name",
@ -163,10 +172,12 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
flex: 1.5, flex: 1.5,
minWidth: 250, minWidth: 250,
renderCell: (params) => ( renderCell: (params) => (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, height: '100%' }}> <Tooltip title={params.row.isFolder ? "" : "Double-click to view deep metadata"} arrow>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, height: '100%', cursor: 'pointer' }}>
{params.row.isFolder ? <FolderIcon sx={{ color: '#FFB020' }} /> : <InsertDriveFileIcon color="action" />} {params.row.isFolder ? <FolderIcon sx={{ color: '#FFB020' }} /> : <InsertDriveFileIcon color="action" />}
<Typography variant="body2">{params.value}</Typography> <Typography variant="body2">{params.value}</Typography>
</Box> </Box>
</Tooltip>
) )
}, },
{ {
@ -211,20 +222,20 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
<Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center" sx={{ height: '100%' }}> <Stack direction="row" spacing={0.5} justifyContent="flex-end" alignItems="center" sx={{ height: '100%' }}>
{!isFolder && ( {!isFolder && (
<> <>
<IconButton size="small" color="info" onClick={() => window.open(`/api/download?id=${params.row.id}&mode=inline`, '_blank')}> <IconButton size="small" color="info" onClick={(e) => { e.stopPropagation(); window.open(`/api/download?id=${params.row.id}&mode=inline`, '_blank'); }}>
<OpenInNewIcon fontSize="small" /> <OpenInNewIcon fontSize="small" />
</IconButton> </IconButton>
<IconButton size="small" color="success" onClick={() => window.location.href = `/api/download?id=${params.row.id}&mode=attachment`}> <IconButton size="small" color="success" onClick={(e) => { e.stopPropagation(); window.location.href = `/api/download?id=${params.row.id}&mode=attachment`; }}>
<DownloadIcon fontSize="small" /> <DownloadIcon fontSize="small" />
</IconButton> </IconButton>
</> </>
)} )}
{(isAdmin || isOwner) && ( {(isAdmin || isOwner) && (
<> <>
<IconButton size="small" color="primary" onClick={() => router.push(`/update/${params.row.id}`)}> <IconButton size="small" color="primary" onClick={(e) => { e.stopPropagation(); router.push(`/update/${params.row.id}`); }}>
<EditIcon fontSize="small" /> <EditIcon fontSize="small" />
</IconButton> </IconButton>
<IconButton size="small" color="error" onClick={() => handleDelete(params.row.id, params.row.name)}> <IconButton size="small" color="error" onClick={(e) => { e.stopPropagation(); handleDelete(params.row.id, params.row.name); }}>
<DeleteIcon fontSize="small" /> <DeleteIcon fontSize="small" />
</IconButton> </IconButton>
</> </>
@ -273,6 +284,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
showToolbar showToolbar
slots={{ toolbar: CustomToolbar }} slots={{ toolbar: CustomToolbar }}
disableRowSelectionOnClick disableRowSelectionOnClick
onRowDoubleClick={handleRowDoubleClick} // ADDED THIS HANDLER
initialState={{ initialState={{
columns: { columns: {
columnVisibilityModel: { columnVisibilityModel: {
@ -280,7 +292,12 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
}, },
}, },
}} }}
sx={{ border: 'none' }} sx={{
border: 'none',
'& .MuiDataGrid-row:hover': {
cursor: 'pointer',
},
}}
/> />
</Box> </Box>
</Box> </Box>

View file

@ -0,0 +1,132 @@
// src/app/dashboard/files/[id]/page.tsx
import { getFileNodeById } from "@/data-access/file-nodes";
import { mapMetadata } from "@/lib/transformers";
import { notFound } from "next/navigation";
import {
Box, Container, Typography, Paper, Grid, Divider, Stack, Button
} from "@mui/material";
import DescriptionIcon from '@mui/icons-material/Description';
import PhotoIcon from '@mui/icons-material/Photo';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import Link from "next/link";
export default async function FileDetailPage(props: {
params: Promise<{ id: string }>
}) {
// 1. Unwrapping params for Next.js 15/16+
const { id } = await props.params;
// 2. Fetch data via DAL
const file = await getFileNodeById(id);
// 3. Check schema-correct property 'isFolder'
if (!file || file.isFolder) {
notFound();
}
// 4. Transform data for the UI
const data = mapMetadata(file);
// 5. Derived extension logic
const extension = file.name.split('.').pop()?.toLowerCase();
const isPDF = extension === 'pdf';
// 6. Handle BigInt for Size
const sizeKB = file.size ? (Number(file.size) / 1024).toFixed(2) : "0";
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
{/* Navigation back to dashboard */}
<Link href="/dashboard" style={{ textDecoration: 'none' }}>
<Button
startIcon={<ArrowBackIcon />}
sx={{ mb: 3 }}
>
Back to Dashboard
</Button>
</Link>
<Paper elevation={3} sx={{ p: { xs: 3, md: 5 }, borderRadius: 4 }}>
<Stack direction="row" spacing={2} alignItems="center" sx={{ mb: 3 }}>
{isPDF ? (
<DescriptionIcon sx={{ color: '#f44336', fontSize: 40 }} />
) : (
<PhotoIcon sx={{ color: '#2196f3', fontSize: 40 }} />
)}
<Box>
<Typography variant="h4" fontWeight="900">{file.name}</Typography>
<Typography variant="body2" color="text.secondary">
ID: {file.id}
</Typography>
</Box>
</Stack>
<Grid container spacing={4}>
{/* Left Column: Core Info & Preview */}
<Grid item xs={12} md={7}>
<Typography variant="h6" gutterBottom fontWeight="bold">Summary</Typography>
<Divider sx={{ mb: 2 }} />
{data.textPreview && (
<Box sx={{ bgcolor: 'grey.50', p: 2, borderRadius: 2, mb: 3, border: '1px solid #ddd' }}>
<Typography variant="subtitle2" color="primary" gutterBottom sx={{ fontSize: '0.7rem', fontWeight: 'bold' }}>
DOCUMENT PREVIEW
</Typography>
<Typography variant="body2" sx={{ fontStyle: 'italic', color: 'text.secondary' }}>
"{data.textPreview}..."
</Typography>
</Box>
)}
<Stack spacing={2}>
<Box>
<Typography variant="caption" color="text.secondary">FILE TYPE</Typography>
<Typography variant="body1" fontWeight="500">{data.type}</Typography>
</Box>
<Box>
<Typography variant="caption" color="text.secondary">SIZE</Typography>
<Typography variant="body1" fontWeight="500">{sizeKB} KB</Typography>
</Box>
{data.pageCount > 0 && (
<Box>
<Typography variant="caption" color="text.secondary">PAGE COUNT</Typography>
<Typography variant="body1" fontWeight="500">{data.pageCount}</Typography>
</Box>
)}
<Box>
<Typography variant="caption" color="text.secondary">UPLOADED AT</Typography>
<Typography variant="body1" fontWeight="500">
{new Date(file.createdAt).toLocaleString()}
</Typography>
</Box>
</Stack>
</Grid>
{/* Right Column: Deep Metadata */}
<Grid item xs={12} md={5}>
<Typography variant="h6" gutterBottom fontWeight="bold">Extracted Attributes</Typography>
<Divider sx={{ mb: 2 }} />
<Stack spacing={1}>
{Object.entries(file.metadata as Record<string, any> || {}).map(([key, value]) => (
<Box key={key} sx={{ display: 'flex', justifyContent: 'space-between', py: 0.5, borderBottom: '1px solid #f0f0f0' }}>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: '500' }}>
{key}
</Typography>
<Typography variant="body2" sx={{ textAlign: 'right', pl: 2 }}>
{typeof value === 'object' ? JSON.stringify(value) : String(value)}
</Typography>
</Box>
))}
{/* FIXED: Properly closed logic for empty metadata */}
{Object.keys(file.metadata as object || {}).length === 0 && (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
No extra metadata extracted for this file.
</Typography>
)}
</Stack>
</Grid>
</Grid>
</Paper>
</Container>
);
}

Binary file not shown.

View file

@ -57,3 +57,55 @@ export const mapImageMetadata = (metadata: any) => {
} }
}; };
}; };
// src/lib/transformers.ts
/**
* Transforms the raw database FileNode into a structured object for the UI.
*/
export const mapMetadata = (node: any) => {
if (!node) return { title: "Unknown File" };
const meta = node.metadata || {};
const fileName = node.name;
const extension = fileName.split('.').pop()?.toLowerCase();
// --- PDF Logic ---
if (extension === 'pdf') {
return {
fileName,
type: 'PDF Document',
title: meta.title || fileName,
author: meta.author || 'Unknown Author',
subject: meta.subject || 'N/A',
pageCount: meta.pageCount || 0,
keywords: meta.keywords || 'None',
textPreview: node.description || meta.textPreview || '', // description field often stores the preview
creator: meta.creator || 'N/A',
producer: meta.producer || 'N/A'
};
}
// --- Image Logic (Sharp/Exif) ---
if (['jpg', 'jpeg', 'png', 'webp'].includes(extension || '')) {
// Note: your extractor lowercase keys via the sanitizer
return {
fileName,
type: 'Image',
device: `${meta.image?.make || ''} ${meta.image?.model || ''}`.trim() || 'Unknown Device',
timestamp: meta.photo?.dateTimeOriginal ? new Date(meta.photo.dateTimeOriginal) : null,
settings: {
aperture: meta.photo?.fNumber ? `f/${meta.photo.fNumber}` : 'N/A',
iso: meta.photo?.iSOSpeedRatings || 'N/A',
focalLength: meta.photo?.focalLength ? `${meta.photo.focalLength}mm` : 'N/A',
}
};
}
// --- Default Fallback ---
return {
fileName,
type: extension?.toUpperCase() || 'File',
details: meta
};
};