Files
biblical-guide.com/components/admin/pages/image-upload.tsx
Andrei 4adf1d286e Add comprehensive social media management system and improve admin pages
## Social Media Management System
- Add SocialMediaLink database model with platform, URL, icon, and ordering
- Create complete CRUD API endpoints for admin social media management
- Implement admin social media management page with Material-UI DataGrid
- Add "Social Media" menu item to admin navigation
- Update footer to dynamically load and display enabled social media links
- Support multiple platforms: Facebook, Twitter, Instagram, YouTube, LinkedIn, GitHub, TikTok
- Include proper icon mapping and fallback handling

## Admin Pages Improvements
- Replace broken TinyMCE editor with working WYSIWYG rich text editor
- Create SimpleRichEditor component with toolbar for formatting
- Fix admin authentication to use cookies instead of localStorage tokens
- Update all admin API calls to use credentials: 'include'
- Increase content editor height to 800px for better editing experience
- Add Lexical editor component as alternative (not currently used)

## Footer Pages System
- Create 8 comprehensive footer pages: About, Blog, Support, API Docs, Terms, Privacy, Cookies, GDPR
- Implement dynamic footer link management with smart categorization
- Separate Quick Links and Legal sections with automatic filtering
- Remove duplicate hardcoded links and use database-driven system
- All footer pages are fully written with professional content

## Database & Dependencies
- Add uuid package for ID generation
- Update Prisma schema with new SocialMediaLink model and relations
- Seed default social media links for Facebook, Twitter, Instagram, YouTube
- Add Lexical rich text editor packages (@lexical/react, etc.)

## Technical Improvements
- Fix async params compatibility for Next.js 15
- Update MUI DataGrid deprecated props
- Improve admin layout navigation structure
- Add proper TypeScript interfaces for all new components
- Implement proper error handling and user feedback

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-24 12:08:01 +00:00

323 lines
9.1 KiB
TypeScript

'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 response = await fetch('/api/admin/media?type=image', {
credentials: 'include'
});
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 response = await fetch('/api/admin/media', {
method: 'POST',
credentials: 'include',
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>
) : (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 2 }}>
{mediaFiles.length === 0 ? (
<Box sx={{ width: '100%', textAlign: 'center' }}>
<Typography color="text.secondary">
No images found. Upload some images first.
</Typography>
</Box>
) : (
mediaFiles.map((file) => (
<Box key={file.id} sx={{ width: { xs: '100%', sm: '48%', md: '31%' } }}>
<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>
</Box>
))
)}
</Box>
)}
</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>
);
}