## 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>
323 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
} |