Files
biblical-guide.com/app/admin/social-media/page.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

457 lines
12 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
Card,
CardContent,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Switch,
FormControlLabel,
IconButton,
Chip,
Alert,
Breadcrumbs,
Link
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
Home as HomeIcon,
Share as ShareIcon,
Facebook,
Twitter,
Instagram,
YouTube,
LinkedIn,
GitHub,
MusicNote as TikTok
} from '@mui/icons-material';
import { DataGrid, GridColDef, GridActionsCellItem } from '@mui/x-data-grid';
interface SocialMediaLink {
id: string;
platform: string;
name: string;
url: string;
icon: string;
isEnabled: boolean;
order: number;
createdAt: string;
updatedAt: string;
creator: { name: string; email: string };
updater: { name: string; email: string };
}
const platformIcons = {
'Facebook': Facebook,
'Twitter': Twitter,
'Instagram': Instagram,
'YouTube': YouTube,
'LinkedIn': LinkedIn,
'GitHub': GitHub,
'TikTok': TikTok
};
const platformOptions = [
{ value: 'facebook', label: 'Facebook', icon: 'Facebook' },
{ value: 'twitter', label: 'Twitter', icon: 'Twitter' },
{ value: 'instagram', label: 'Instagram', icon: 'Instagram' },
{ value: 'youtube', label: 'YouTube', icon: 'YouTube' },
{ value: 'linkedin', label: 'LinkedIn', icon: 'LinkedIn' },
{ value: 'github', label: 'GitHub', icon: 'GitHub' },
{ value: 'tiktok', label: 'TikTok', icon: 'TikTok' }
];
export default function SocialMediaManagement() {
const [socialLinks, setSocialLinks] = useState<SocialMediaLink[]>([]);
const [loading, setLoading] = useState(true);
const [selectedLink, setSelectedLink] = useState<SocialMediaLink | null>(null);
const [editorOpen, setEditorOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [linkToDelete, setLinkToDelete] = useState<SocialMediaLink | null>(null);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [formData, setFormData] = useState({
platform: '',
name: '',
url: '',
icon: '',
isEnabled: true,
order: 0
});
useEffect(() => {
fetchSocialLinks();
}, []);
const fetchSocialLinks = async () => {
try {
const response = await fetch('/api/admin/social-media', {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch social media links');
}
const data = await response.json();
setSocialLinks(data.data || []);
} catch (error) {
console.error('Error fetching social media links:', error);
setError('Failed to load social media links');
} finally {
setLoading(false);
}
};
const handleCreateLink = () => {
setSelectedLink(null);
setFormData({
platform: '',
name: '',
url: '',
icon: '',
isEnabled: true,
order: socialLinks.length
});
setEditorOpen(true);
};
const handleEditLink = (link: SocialMediaLink) => {
setSelectedLink(link);
setFormData({
platform: link.platform,
name: link.name,
url: link.url,
icon: link.icon,
isEnabled: link.isEnabled,
order: link.order
});
setEditorOpen(true);
};
const handleDeleteLink = (link: SocialMediaLink) => {
setLinkToDelete(link);
setDeleteDialogOpen(true);
};
const handleSaveLink = async () => {
try {
const url = selectedLink
? `/api/admin/social-media/${selectedLink.id}`
: '/api/admin/social-media';
const method = selectedLink ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify(formData)
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to save social media link');
}
setSuccess('Social media link saved successfully');
setEditorOpen(false);
fetchSocialLinks();
} catch (error) {
console.error('Error saving social media link:', error);
setError(error instanceof Error ? error.message : 'Failed to save social media link');
}
};
const confirmDeleteLink = async () => {
if (!linkToDelete) return;
try {
const response = await fetch(`/api/admin/social-media/${linkToDelete.id}`, {
method: 'DELETE',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to delete social media link');
}
setSuccess('Social media link deleted successfully');
setDeleteDialogOpen(false);
setLinkToDelete(null);
fetchSocialLinks();
} catch (error) {
console.error('Error deleting social media link:', error);
setError('Failed to delete social media link');
}
};
const handlePlatformChange = (platform: string) => {
const platformOption = platformOptions.find(p => p.value === platform);
if (platformOption) {
setFormData(prev => ({
...prev,
platform,
name: platformOption.label,
icon: platformOption.icon
}));
}
};
const renderIcon = (iconName: string) => {
const IconComponent = platformIcons[iconName as keyof typeof platformIcons];
return IconComponent ? <IconComponent /> : <ShareIcon />;
};
const columns: GridColDef[] = [
{
field: 'icon',
headerName: 'Icon',
width: 80,
renderCell: (params) => renderIcon(params.value)
},
{
field: 'name',
headerName: 'Platform',
width: 120,
renderCell: (params) => (
<Box>
<Typography variant="body2" fontWeight="medium">
{params.value}
</Typography>
<Typography variant="caption" color="text.secondary">
{params.row.platform}
</Typography>
</Box>
)
},
{
field: 'url',
headerName: 'URL',
width: 300,
renderCell: (params) => (
<Link href={params.value} target="_blank" rel="noopener">
{params.value}
</Link>
)
},
{
field: 'isEnabled',
headerName: 'Status',
width: 100,
renderCell: (params) => (
<Chip
label={params.value ? 'Enabled' : 'Disabled'}
color={params.value ? 'success' : 'default'}
size="small"
/>
)
},
{
field: 'order',
headerName: 'Order',
width: 80
},
{
field: 'updatedAt',
headerName: 'Last Updated',
width: 150,
renderCell: (params) => new Date(params.value).toLocaleDateString()
},
{
field: 'actions',
type: 'actions',
headerName: 'Actions',
width: 120,
getActions: (params) => [
<GridActionsCellItem
key="edit"
icon={<EditIcon />}
label="Edit"
onClick={() => handleEditLink(params.row)}
/>,
<GridActionsCellItem
key="delete"
icon={<DeleteIcon />}
label="Delete"
onClick={() => handleDeleteLink(params.row)}
/>
]
}
];
return (
<Box>
{/* Breadcrumbs */}
<Breadcrumbs aria-label="breadcrumb" sx={{ mb: 3 }}>
<Link
underline="hover"
sx={{ display: 'flex', alignItems: 'center' }}
color="inherit"
href="/admin"
>
<HomeIcon sx={{ mr: 0.5 }} fontSize="inherit" />
Admin
</Link>
<Typography color="text.primary" sx={{ display: 'flex', alignItems: 'center' }}>
<ShareIcon sx={{ mr: 0.5 }} fontSize="inherit" />
Social Media
</Typography>
</Breadcrumbs>
{/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h4" component="h1" gutterBottom>
Social Media Management
</Typography>
<Typography variant="body1" color="text.secondary">
Manage social media links displayed in the footer
</Typography>
</Box>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={handleCreateLink}
sx={{ height: 'fit-content' }}
>
Add Social Link
</Button>
</Box>
{/* Alerts */}
{error && (
<Alert severity="error" onClose={() => setError(null)} sx={{ mb: 2 }}>
{error}
</Alert>
)}
{success && (
<Alert severity="success" onClose={() => setSuccess(null)} sx={{ mb: 2 }}>
{success}
</Alert>
)}
{/* Social Links Table */}
<Card>
<DataGrid
rows={socialLinks}
columns={columns}
loading={loading}
autoHeight
initialState={{
pagination: {
paginationModel: { pageSize: 25, page: 0 }
}
}}
pageSizeOptions={[25, 50, 100]}
disableRowSelectionOnClick
sx={{
'& .MuiDataGrid-cell': {
borderBottom: '1px solid #f0f0f0'
}
}}
/>
</Card>
{/* Editor Dialog */}
<Dialog open={editorOpen} onClose={() => setEditorOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle>
{selectedLink ? 'Edit Social Media Link' : 'Add Social Media Link'}
</DialogTitle>
<DialogContent>
<Box sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
<FormControl fullWidth>
<InputLabel>Platform</InputLabel>
<Select
value={formData.platform}
label="Platform"
onChange={(e) => handlePlatformChange(e.target.value)}
>
{platformOptions.map((option) => (
<MenuItem key={option.value} value={option.value}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{renderIcon(option.icon)}
{option.label}
</Box>
</MenuItem>
))}
</Select>
</FormControl>
<TextField
label="Display Name"
fullWidth
value={formData.name}
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
/>
<TextField
label="URL"
fullWidth
value={formData.url}
onChange={(e) => setFormData(prev => ({ ...prev, url: e.target.value }))}
placeholder="https://..."
/>
<TextField
label="Display Order"
type="number"
fullWidth
value={formData.order}
onChange={(e) => setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 0 }))}
/>
<FormControlLabel
control={
<Switch
checked={formData.isEnabled}
onChange={(e) => setFormData(prev => ({ ...prev, isEnabled: e.target.checked }))}
/>
}
label="Enabled"
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setEditorOpen(false)}>Cancel</Button>
<Button onClick={handleSaveLink} variant="contained">
{selectedLink ? 'Update' : 'Create'}
</Button>
</DialogActions>
</Dialog>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete Social Media Link</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete the {linkToDelete?.name} link? This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button onClick={confirmDeleteLink} color="error" variant="contained">
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
);
}