Add comprehensive page management system to admin dashboard
Features added: - Database schema for pages and media files with content types (Rich Text, HTML, Markdown) - Admin API routes for full page CRUD operations - Image upload functionality with file management - Rich text editor using TinyMCE with image insertion - Admin interface for creating/editing pages with SEO options - Dynamic navigation and footer integration - Public page display routes with proper SEO metadata - Support for featured images and content excerpts Admin features: - Create/edit/delete pages with rich content editor - Upload and manage images through media library - Configure pages to appear in navigation or footer - Set page status (Draft, Published, Archived) - SEO title and description management - Real-time preview of content changes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
329
components/admin/pages/image-upload.tsx
Normal file
329
components/admin/pages/image-upload.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Box,
|
||||
Typography,
|
||||
TextField,
|
||||
Paper,
|
||||
Grid,
|
||||
Card,
|
||||
CardMedia,
|
||||
CardContent,
|
||||
CardActions,
|
||||
Alert,
|
||||
LinearProgress,
|
||||
Tabs,
|
||||
Tab
|
||||
} from '@mui/material';
|
||||
import {
|
||||
CloudUpload as UploadIcon,
|
||||
Image as ImageIcon,
|
||||
InsertPhoto as InsertIcon
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface MediaFile {
|
||||
id: string;
|
||||
filename: string;
|
||||
originalName: string;
|
||||
url: string;
|
||||
alt: string | null;
|
||||
size: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ImageUploadProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
onImageSelect: (imageUrl: string) => void;
|
||||
}
|
||||
|
||||
export function ImageUpload({ open, onClose, onImageSelect }: ImageUploadProps) {
|
||||
const [tab, setTab] = useState(0);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
|
||||
const [loadingMedia, setLoadingMedia] = useState(false);
|
||||
const [urlInput, setUrlInput] = useState('');
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const fetchMediaFiles = async () => {
|
||||
setLoadingMedia(true);
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const response = await fetch('/api/admin/media?type=image', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch media files');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setMediaFiles(data.data || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching media files:', error);
|
||||
setError('Failed to load media files');
|
||||
} finally {
|
||||
setLoadingMedia(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = async (files: FileList | null) => {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const file = files[0];
|
||||
|
||||
// Validate file type
|
||||
if (!file.type.startsWith('image/')) {
|
||||
setError('Please select an image file');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate file size (5MB limit)
|
||||
if (file.size > 5 * 1024 * 1024) {
|
||||
setError('File size must be less than 5MB');
|
||||
return;
|
||||
}
|
||||
|
||||
setUploading(true);
|
||||
setError(null);
|
||||
setUploadProgress(0);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const token = localStorage.getItem('authToken');
|
||||
const response = await fetch('/api/admin/media', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || 'Upload failed');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// Add the new file to the media files list
|
||||
setMediaFiles(prev => [result.data, ...prev]);
|
||||
|
||||
// Auto-select the uploaded image
|
||||
onImageSelect(result.data.url);
|
||||
|
||||
setUploadProgress(100);
|
||||
|
||||
// Reset form
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error);
|
||||
setError(error instanceof Error ? error.message : 'Upload failed');
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setTimeout(() => setUploadProgress(0), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
|
||||
setTab(newValue);
|
||||
if (newValue === 1 && mediaFiles.length === 0) {
|
||||
fetchMediaFiles();
|
||||
}
|
||||
};
|
||||
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const handleUrlSubmit = () => {
|
||||
if (urlInput.trim()) {
|
||||
onImageSelect(urlInput.trim());
|
||||
setUrlInput('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>Insert Image</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{error && (
|
||||
<Alert severity="error" onClose={() => setError(null)} sx={{ mb: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
||||
<Tabs value={tab} onChange={handleTabChange}>
|
||||
<Tab label="Upload New" />
|
||||
<Tab label="Media Library" />
|
||||
<Tab label="From URL" />
|
||||
</Tabs>
|
||||
</Box>
|
||||
|
||||
{/* Upload Tab */}
|
||||
{tab === 0 && (
|
||||
<Box>
|
||||
<Paper
|
||||
sx={{
|
||||
p: 4,
|
||||
textAlign: 'center',
|
||||
border: '2px dashed #ccc',
|
||||
bgcolor: 'grey.50',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
bgcolor: 'grey.100'
|
||||
}
|
||||
}}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
<UploadIcon sx={{ fontSize: 48, color: 'grey.500', mb: 2 }} />
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Click to upload or drag and drop
|
||||
</Typography>
|
||||
<Typography color="text.secondary">
|
||||
PNG, JPG, GIF up to 5MB
|
||||
</Typography>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={(e) => handleFileUpload(e.target.files)}
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
{uploading && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="body2" gutterBottom>
|
||||
Uploading...
|
||||
</Typography>
|
||||
<LinearProgress variant="determinate" value={uploadProgress} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Media Library Tab */}
|
||||
{tab === 1 && (
|
||||
<Box>
|
||||
{loadingMedia ? (
|
||||
<Typography>Loading media files...</Typography>
|
||||
) : (
|
||||
<Grid container spacing={2}>
|
||||
{mediaFiles.length === 0 ? (
|
||||
<Grid item xs={12}>
|
||||
<Typography color="text.secondary" textAlign="center">
|
||||
No images found. Upload some images first.
|
||||
</Typography>
|
||||
</Grid>
|
||||
) : (
|
||||
mediaFiles.map((file) => (
|
||||
<Grid item xs={12} sm={6} md={4} key={file.id}>
|
||||
<Card>
|
||||
<CardMedia
|
||||
component="img"
|
||||
height="140"
|
||||
image={file.url}
|
||||
alt={file.alt || file.originalName}
|
||||
sx={{ objectFit: 'cover' }}
|
||||
/>
|
||||
<CardContent sx={{ p: 1 }}>
|
||||
<Typography variant="caption" display="block" noWrap>
|
||||
{file.originalName}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{formatFileSize(file.size)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
<CardActions sx={{ pt: 0 }}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<InsertIcon />}
|
||||
onClick={() => onImageSelect(file.url)}
|
||||
>
|
||||
Insert
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Grid>
|
||||
))
|
||||
)}
|
||||
</Grid>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* URL Tab */}
|
||||
{tab === 2 && (
|
||||
<Box>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="Image URL"
|
||||
value={urlInput}
|
||||
onChange={(e) => setUrlInput(e.target.value)}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleUrlSubmit}
|
||||
disabled={!urlInput.trim()}
|
||||
>
|
||||
Insert Image
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{urlInput && (
|
||||
<Box sx={{ mt: 2, textAlign: 'center' }}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
Preview:
|
||||
</Typography>
|
||||
<img
|
||||
src={urlInput}
|
||||
alt="Preview"
|
||||
style={{
|
||||
maxWidth: '100%',
|
||||
maxHeight: '200px',
|
||||
border: '1px solid #ccc',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user