Files
biblical-guide.com/components/admin/pages/page-editor.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

432 lines
13 KiB
TypeScript

'use client';
import { useState, useEffect, useRef } from 'react';
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
FormControl,
InputLabel,
Select,
MenuItem,
FormControlLabel,
Switch,
Box,
Typography,
Tab,
Tabs,
Alert,
Grid,
Paper
} from '@mui/material';
import { ImageUpload } from './image-upload';
import { SimpleRichEditor } from './simple-rich-editor';
interface Page {
id?: string;
title: string;
slug: string;
content: string;
contentType: 'RICH_TEXT' | 'HTML' | 'MARKDOWN';
excerpt?: string;
featuredImage?: string;
seoTitle?: string;
seoDescription?: string;
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
showInNavigation: boolean;
showInFooter: boolean;
navigationOrder?: number;
footerOrder?: number;
}
interface PageEditorProps {
open: boolean;
onClose: () => void;
page?: Page | null;
onSave: () => void;
}
export function PageEditor({ open, onClose, page, onSave }: PageEditorProps) {
const [formData, setFormData] = useState<Page>({
title: '',
slug: '',
content: '',
contentType: 'RICH_TEXT',
excerpt: '',
featuredImage: '',
seoTitle: '',
seoDescription: '',
status: 'DRAFT',
showInNavigation: false,
showInFooter: false,
navigationOrder: undefined,
footerOrder: undefined
});
const [contentTab, setContentTab] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [imageUploadOpen, setImageUploadOpen] = useState(false);
const editorRef = useRef<any>(null);
useEffect(() => {
if (page) {
setFormData({
...page,
excerpt: page.excerpt || '',
featuredImage: page.featuredImage || '',
seoTitle: page.seoTitle || '',
seoDescription: page.seoDescription || ''
});
} else {
setFormData({
title: '',
slug: '',
content: '',
contentType: 'RICH_TEXT',
excerpt: '',
featuredImage: '',
seoTitle: '',
seoDescription: '',
status: 'DRAFT',
showInNavigation: false,
showInFooter: false,
navigationOrder: undefined,
footerOrder: undefined
});
}
setError(null);
}, [page, open]);
const generateSlug = (title: string) => {
return title
.toLowerCase()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
};
const handleTitleChange = (title: string) => {
setFormData(prev => ({
...prev,
title,
slug: prev.slug || generateSlug(title)
}));
};
const handleSubmit = async () => {
if (!formData.title || !formData.slug || !formData.content) {
setError('Title, slug, and content are required');
return;
}
setLoading(true);
setError(null);
try {
const url = page ? `/api/admin/pages/${page.id}` : '/api/admin/pages';
const method = page ? '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 page');
}
onSave();
} catch (error) {
console.error('Error saving page:', error);
setError(error instanceof Error ? error.message : 'Failed to save page');
} finally {
setLoading(false);
}
};
const handleImageInsert = (imageUrl: string) => {
if (formData.contentType === 'RICH_TEXT' && editorRef.current) {
const editor = editorRef.current.getEditor();
editor.insertContent(`<img src="${imageUrl}" alt="" style="max-width: 100%; height: auto;" />`);
} else if (formData.contentType === 'HTML') {
const imageTag = `<img src="${imageUrl}" alt="" style="max-width: 100%; height: auto;" />`;
setFormData(prev => ({
...prev,
content: prev.content + imageTag
}));
}
setImageUploadOpen(false);
};
const renderContentEditor = () => {
switch (formData.contentType) {
case 'RICH_TEXT':
return (
<Box sx={{ mt: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle2">Rich Text Content</Typography>
<Button size="small" onClick={() => setImageUploadOpen(true)}>
Insert Image
</Button>
</Box>
<SimpleRichEditor
value={formData.content}
onChange={(content) => setFormData(prev => ({ ...prev, content }))}
height={800}
/>
</Box>
);
case 'HTML':
return (
<Box sx={{ mt: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle2">HTML Content</Typography>
<Button size="small" onClick={() => setImageUploadOpen(true)}>
Insert Image
</Button>
</Box>
<TextField
multiline
rows={60}
fullWidth
variant="outlined"
value={formData.content}
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
sx={{ fontFamily: 'monospace' }}
/>
</Box>
);
case 'MARKDOWN':
return (
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle2" gutterBottom>Markdown Content</Typography>
<TextField
multiline
rows={60}
fullWidth
variant="outlined"
value={formData.content}
onChange={(e) => setFormData(prev => ({ ...prev, content: e.target.value }))}
sx={{ fontFamily: 'monospace' }}
/>
</Box>
);
default:
return null;
}
};
return (
<>
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<DialogTitle>
{page ? 'Edit Page' : 'Create New Page'}
</DialogTitle>
<DialogContent>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 3 }}>
<Tabs value={contentTab} onChange={(_, newValue) => setContentTab(newValue)}>
<Tab label="Content" />
<Tab label="Settings" />
<Tab label="SEO" />
</Tabs>
</Box>
{/* Content Tab */}
{contentTab === 0 && (
<Box>
<Box sx={{ display: 'flex', gap: 2, flexDirection: { xs: 'column', sm: 'row' } }}>
<Box sx={{ flex: { xs: '1', sm: '2' } }}>
<TextField
label="Page Title"
fullWidth
required
value={formData.title}
onChange={(e) => handleTitleChange(e.target.value)}
/>
</Box>
<Box sx={{ flex: '1' }}>
<FormControl fullWidth>
<InputLabel>Content Type</InputLabel>
<Select
value={formData.contentType}
label="Content Type"
onChange={(e) => setFormData(prev => ({ ...prev, contentType: e.target.value as any }))}
>
<MenuItem value="RICH_TEXT">Rich Text</MenuItem>
<MenuItem value="HTML">HTML</MenuItem>
<MenuItem value="MARKDOWN">Markdown</MenuItem>
</Select>
</FormControl>
</Box>
</Box>
<TextField
label="URL Slug"
fullWidth
required
value={formData.slug}
onChange={(e) => setFormData(prev => ({ ...prev, slug: e.target.value }))}
helperText="This will be the URL path for your page"
sx={{ mt: 2 }}
/>
<TextField
label="Excerpt"
fullWidth
multiline
rows={2}
value={formData.excerpt}
onChange={(e) => setFormData(prev => ({ ...prev, excerpt: e.target.value }))}
helperText="Brief description (optional)"
sx={{ mt: 2 }}
/>
{renderContentEditor()}
</Box>
)}
{/* Settings Tab */}
{contentTab === 1 && (
<Box>
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', sm: 'row' } }}>
<Box sx={{ flex: '1' }}>
<Paper sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>Publication</Typography>
<FormControl fullWidth sx={{ mb: 2 }}>
<InputLabel>Status</InputLabel>
<Select
value={formData.status}
label="Status"
onChange={(e) => setFormData(prev => ({ ...prev, status: e.target.value as any }))}
>
<MenuItem value="DRAFT">Draft</MenuItem>
<MenuItem value="PUBLISHED">Published</MenuItem>
<MenuItem value="ARCHIVED">Archived</MenuItem>
</Select>
</FormControl>
<TextField
label="Featured Image URL"
fullWidth
value={formData.featuredImage}
onChange={(e) => setFormData(prev => ({ ...prev, featuredImage: e.target.value }))}
/>
</Paper>
</Box>
<Box sx={{ flex: '1' }}>
<Paper sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom>Display Options</Typography>
<FormControlLabel
control={
<Switch
checked={formData.showInNavigation}
onChange={(e) => setFormData(prev => ({ ...prev, showInNavigation: e.target.checked }))}
/>
}
label="Show in Navigation"
/>
{formData.showInNavigation && (
<TextField
label="Navigation Order"
type="number"
fullWidth
size="small"
value={formData.navigationOrder || ''}
onChange={(e) => setFormData(prev => ({ ...prev, navigationOrder: parseInt(e.target.value) || undefined }))}
sx={{ mt: 1, mb: 2 }}
/>
)}
<FormControlLabel
control={
<Switch
checked={formData.showInFooter}
onChange={(e) => setFormData(prev => ({ ...prev, showInFooter: e.target.checked }))}
/>
}
label="Show in Footer"
/>
{formData.showInFooter && (
<TextField
label="Footer Order"
type="number"
fullWidth
size="small"
value={formData.footerOrder || ''}
onChange={(e) => setFormData(prev => ({ ...prev, footerOrder: parseInt(e.target.value) || undefined }))}
sx={{ mt: 1 }}
/>
)}
</Paper>
</Box>
</Box>
</Box>
)}
{/* SEO Tab */}
{contentTab === 2 && (
<Box>
<TextField
label="SEO Title"
fullWidth
value={formData.seoTitle}
onChange={(e) => setFormData(prev => ({ ...prev, seoTitle: e.target.value }))}
helperText="Optimize for search engines (leave empty to use page title)"
sx={{ mb: 2 }}
/>
<TextField
label="SEO Description"
fullWidth
multiline
rows={3}
value={formData.seoDescription}
onChange={(e) => setFormData(prev => ({ ...prev, seoDescription: e.target.value }))}
helperText="Meta description for search engines (150-160 characters recommended)"
/>
</Box>
)}
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>
Cancel
</Button>
<Button onClick={handleSubmit} variant="contained" disabled={loading}>
{loading ? 'Saving...' : page ? 'Update Page' : 'Create Page'}
</Button>
</DialogActions>
</Dialog>
<ImageUpload
open={imageUploadOpen}
onClose={() => setImageUploadOpen(false)}
onImageSelect={handleImageInsert}
/>
</>
);
}