File detail page addedd
This commit is contained in:
parent
222dcda775
commit
96eda53f31
4 changed files with 211 additions and 10 deletions
|
|
@ -1,5 +1,5 @@
|
|||
'use client';
|
||||
|
||||
// src/app/dashboard-view.tsx
|
||||
import { useState } from "react";
|
||||
import { styled } from '@mui/material/styles';
|
||||
import {
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
QuickFilter,
|
||||
QuickFilterControl,
|
||||
QuickFilterClear,
|
||||
GridEventListener,
|
||||
} from "@mui/x-data-grid";
|
||||
import SyncIcon from "@mui/icons-material/Sync";
|
||||
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[] = [
|
||||
{
|
||||
field: "name",
|
||||
|
|
@ -163,10 +172,12 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
flex: 1.5,
|
||||
minWidth: 250,
|
||||
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" />}
|
||||
<Typography variant="body2">{params.value}</Typography>
|
||||
</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%' }}>
|
||||
{!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" />
|
||||
</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" />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
{(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" />
|
||||
</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" />
|
||||
</IconButton>
|
||||
</>
|
||||
|
|
@ -273,6 +284,7 @@ export default function DashboardView({ initialFiles, user }: DashboardViewProps
|
|||
showToolbar
|
||||
slots={{ toolbar: CustomToolbar }}
|
||||
disableRowSelectionOnClick
|
||||
onRowDoubleClick={handleRowDoubleClick} // ADDED THIS HANDLER
|
||||
initialState={{
|
||||
columns: {
|
||||
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>
|
||||
|
|
|
|||
132
src/app/dashboard/files/[id]/page.tsx
Normal file
132
src/app/dashboard/files/[id]/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
src/lib/text.pdf
BIN
src/lib/text.pdf
Binary file not shown.
|
|
@ -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
|
||||
};
|
||||
};
|
||||
Loading…
Reference in a new issue