## 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>
358 lines
9.5 KiB
TypeScript
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>
|
|
);
|
|
} |