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

358 lines
9.5 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Button,
Card,
CardContent,
TextField,
Select,
MenuItem,
FormControl,
InputLabel,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Chip,
IconButton,
Breadcrumbs,
Link,
Alert
} from '@mui/material';
import {
Add as AddIcon,
Edit as EditIcon,
Delete as DeleteIcon,
Visibility as ViewIcon,
Home as HomeIcon,
ArticleOutlined as PagesIcon,
Public as PublicIcon,
Navigation as NavigationIcon,
Foundation as FooterIcon
} from '@mui/icons-material';
import { DataGrid, GridColDef, GridActionsCellItem } from '@mui/x-data-grid';
import { PageEditor } from '@/components/admin/pages/page-editor';
interface Page {
id: string;
title: string;
slug: string;
content: string;
contentType: 'RICH_TEXT' | 'HTML' | 'MARKDOWN';
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED';
showInNavigation: boolean;
showInFooter: boolean;
createdAt: string;
updatedAt: string;
creator: { name: string; email: string };
updater: { name: string; email: string };
}
export default function PagesManagement() {
const [pages, setPages] = useState<Page[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPage, setSelectedPage] = useState<Page | null>(null);
const [editorOpen, setEditorOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [pageToDelete, setPageToDelete] = useState<Page | null>(null);
const [filterStatus, setFilterStatus] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
fetchPages();
}, [filterStatus, searchQuery]);
const fetchPages = async () => {
try {
const params = new URLSearchParams();
if (filterStatus !== 'all') params.append('status', filterStatus);
if (searchQuery) params.append('search', searchQuery);
const response = await fetch(`/api/admin/pages?${params.toString()}`, {
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to fetch pages');
}
const data = await response.json();
setPages(data.data || []);
} catch (error) {
console.error('Error fetching pages:', error);
setError('Failed to load pages');
} finally {
setLoading(false);
}
};
const handleCreatePage = () => {
setSelectedPage(null);
setEditorOpen(true);
};
const handleEditPage = (page: Page) => {
setSelectedPage(page);
setEditorOpen(true);
};
const handleDeletePage = async (page: Page) => {
setPageToDelete(page);
setDeleteDialogOpen(true);
};
const confirmDeletePage = async () => {
if (!pageToDelete) return;
try {
const response = await fetch(`/api/admin/pages/${pageToDelete.id}`, {
method: 'DELETE',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Failed to delete page');
}
setSuccess('Page deleted successfully');
fetchPages();
setDeleteDialogOpen(false);
setPageToDelete(null);
} catch (error) {
console.error('Error deleting page:', error);
setError('Failed to delete page');
}
};
const handlePageSaved = () => {
setSuccess('Page saved successfully');
setEditorOpen(false);
fetchPages();
};
const getStatusColor = (status: string) => {
switch (status) {
case 'PUBLISHED': return 'success';
case 'DRAFT': return 'warning';
case 'ARCHIVED': return 'default';
default: return 'default';
}
};
const columns: GridColDef[] = [
{
field: 'title',
headerName: 'Title',
width: 300,
renderCell: (params) => (
<Box>
<Typography variant="body2" fontWeight="medium">
{params.value}
</Typography>
<Typography variant="caption" color="text.secondary">
/{params.row.slug}
</Typography>
</Box>
)
},
{
field: 'status',
headerName: 'Status',
width: 120,
renderCell: (params) => (
<Chip
label={params.value}
color={getStatusColor(params.value) as any}
size="small"
/>
)
},
{
field: 'showInNavigation',
headerName: 'Navigation',
width: 100,
renderCell: (params) => (
params.value ? <NavigationIcon color="primary" /> : null
)
},
{
field: 'showInFooter',
headerName: 'Footer',
width: 80,
renderCell: (params) => (
params.value ? <FooterIcon color="primary" /> : null
)
},
{
field: 'creator',
headerName: 'Created By',
width: 150,
renderCell: (params) => params.value?.name || params.value?.email
},
{
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={() => handleEditPage(params.row)}
/>,
<GridActionsCellItem
key="view"
icon={<ViewIcon />}
label="View"
onClick={() => window.open(`/pages/${params.row.slug}`, '_blank')}
/>,
<GridActionsCellItem
key="delete"
icon={<DeleteIcon />}
label="Delete"
onClick={() => handleDeletePage(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' }}>
<PagesIcon sx={{ mr: 0.5 }} fontSize="inherit" />
Pages
</Typography>
</Breadcrumbs>
{/* Header */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Box>
<Typography variant="h4" component="h1" gutterBottom>
Page Management
</Typography>
<Typography variant="body1" color="text.secondary">
Create and manage website pages for navigation and footer
</Typography>
</Box>
<Button
variant="contained"
startIcon={<AddIcon />}
onClick={handleCreatePage}
sx={{ height: 'fit-content' }}
>
New Page
</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>
)}
{/* Filters */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Box sx={{ display: 'flex', gap: 2 }}>
<TextField
label="Search pages"
variant="outlined"
size="small"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
sx={{ minWidth: 300 }}
/>
<FormControl size="small" sx={{ minWidth: 150 }}>
<InputLabel>Status</InputLabel>
<Select
value={filterStatus}
label="Status"
onChange={(e) => setFilterStatus(e.target.value)}
>
<MenuItem value="all">All Status</MenuItem>
<MenuItem value="published">Published</MenuItem>
<MenuItem value="draft">Draft</MenuItem>
<MenuItem value="archived">Archived</MenuItem>
</Select>
</FormControl>
</Box>
</CardContent>
</Card>
{/* Pages Table */}
<Card>
<DataGrid
rows={pages}
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>
{/* Page Editor Dialog */}
<PageEditor
open={editorOpen}
onClose={() => setEditorOpen(false)}
page={selectedPage}
onSave={handlePageSaved}
/>
{/* Delete Confirmation Dialog */}
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
<DialogTitle>Delete Page</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete "{pageToDelete?.title}"? This action cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
<Button onClick={confirmDeletePage} color="error" variant="contained">
Delete
</Button>
</DialogActions>
</Dialog>
</Box>
);
}