Add comprehensive page management system to admin dashboard
Features added: - Database schema for pages and media files with content types (Rich Text, HTML, Markdown) - Admin API routes for full page CRUD operations - Image upload functionality with file management - Rich text editor using TinyMCE with image insertion - Admin interface for creating/editing pages with SEO options - Dynamic navigation and footer integration - Public page display routes with proper SEO metadata - Support for featured images and content excerpts Admin features: - Create/edit/delete pages with rich content editor - Upload and manage images through media library - Configure pages to appear in navigation or footer - Set page status (Draft, Published, Archived) - SEO title and description management - Real-time preview of content changes 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
309
app/[locale]/pages/[slug]/page.tsx
Normal file
309
app/[locale]/pages/[slug]/page.tsx
Normal file
@@ -0,0 +1,309 @@
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import {
|
||||
Container,
|
||||
Typography,
|
||||
Box,
|
||||
Paper,
|
||||
Breadcrumbs,
|
||||
Link,
|
||||
Avatar
|
||||
} from '@mui/material'
|
||||
import {
|
||||
Home as HomeIcon,
|
||||
Article as ArticleIcon,
|
||||
CalendarToday as DateIcon
|
||||
} from '@mui/icons-material'
|
||||
|
||||
interface PageData {
|
||||
id: string
|
||||
title: string
|
||||
slug: string
|
||||
content: string
|
||||
contentType: 'RICH_TEXT' | 'HTML' | 'MARKDOWN'
|
||||
excerpt?: string
|
||||
featuredImage?: string
|
||||
seoTitle?: string
|
||||
seoDescription?: string
|
||||
publishedAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
locale: string
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
|
||||
async function getPageData(slug: string): Promise<PageData | null> {
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXTAUTH_URL || 'http://localhost:3010'}/api/pages/${slug}`, {
|
||||
next: { revalidate: 300 } // Revalidate every 5 minutes
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data.success ? data.data : null
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch page:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
|
||||
const page = await getPageData(params.slug)
|
||||
|
||||
if (!page) {
|
||||
return {
|
||||
title: 'Page Not Found',
|
||||
description: 'The requested page could not be found.'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: page.seoTitle || page.title,
|
||||
description: page.seoDescription || page.excerpt || `Read ${page.title} on Biblical Guide`,
|
||||
openGraph: {
|
||||
title: page.seoTitle || page.title,
|
||||
description: page.seoDescription || page.excerpt,
|
||||
type: 'article',
|
||||
publishedTime: page.publishedAt,
|
||||
modifiedTime: page.updatedAt,
|
||||
...(page.featuredImage && {
|
||||
images: [{
|
||||
url: page.featuredImage,
|
||||
alt: page.title
|
||||
}]
|
||||
})
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: page.seoTitle || page.title,
|
||||
description: page.seoDescription || page.excerpt,
|
||||
...(page.featuredImage && {
|
||||
images: [page.featuredImage]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent(content: string, contentType: string) {
|
||||
switch (contentType) {
|
||||
case 'HTML':
|
||||
return (
|
||||
<Box
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
sx={{
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: 1
|
||||
},
|
||||
'& p': {
|
||||
marginBottom: 2
|
||||
},
|
||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
marginTop: 3,
|
||||
marginBottom: 2
|
||||
},
|
||||
'& ul, & ol': {
|
||||
paddingLeft: 3
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'primary.main',
|
||||
paddingLeft: 2,
|
||||
marginLeft: 0,
|
||||
fontStyle: 'italic',
|
||||
backgroundColor: 'grey.50',
|
||||
padding: 2,
|
||||
borderRadius: 1
|
||||
},
|
||||
'& code': {
|
||||
backgroundColor: 'grey.100',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace'
|
||||
},
|
||||
'& pre': {
|
||||
backgroundColor: 'grey.100',
|
||||
padding: 2,
|
||||
borderRadius: 1,
|
||||
overflow: 'auto'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'MARKDOWN':
|
||||
// For now, render as plain text. In the future, you could add a markdown parser
|
||||
return (
|
||||
<Typography component="div" sx={{ whiteSpace: 'pre-wrap' }}>
|
||||
{content}
|
||||
</Typography>
|
||||
)
|
||||
|
||||
default: // RICH_TEXT
|
||||
return (
|
||||
<Box
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
sx={{
|
||||
'& img': {
|
||||
maxWidth: '100%',
|
||||
height: 'auto',
|
||||
borderRadius: 1
|
||||
},
|
||||
'& p': {
|
||||
marginBottom: 2
|
||||
},
|
||||
'& h1, & h2, & h3, & h4, & h5, & h6': {
|
||||
marginTop: 3,
|
||||
marginBottom: 2
|
||||
},
|
||||
'& ul, & ol': {
|
||||
paddingLeft: 3
|
||||
},
|
||||
'& blockquote': {
|
||||
borderLeft: '4px solid',
|
||||
borderColor: 'primary.main',
|
||||
paddingLeft: 2,
|
||||
marginLeft: 0,
|
||||
fontStyle: 'italic',
|
||||
backgroundColor: 'grey.50',
|
||||
padding: 2,
|
||||
borderRadius: 1
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default async function PageView({ params }: PageProps) {
|
||||
const page = await getPageData(params.slug)
|
||||
|
||||
if (!page) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString(params.locale, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default' }}>
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
{/* Breadcrumbs */}
|
||||
<Breadcrumbs aria-label="breadcrumb" sx={{ mb: 3 }}>
|
||||
<Link
|
||||
underline="hover"
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
color="inherit"
|
||||
href={`/${params.locale}`}
|
||||
>
|
||||
<HomeIcon sx={{ mr: 0.5 }} fontSize="inherit" />
|
||||
Home
|
||||
</Link>
|
||||
<Typography
|
||||
color="text.primary"
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<ArticleIcon sx={{ mr: 0.5 }} fontSize="inherit" />
|
||||
{page.title}
|
||||
</Typography>
|
||||
</Breadcrumbs>
|
||||
|
||||
<Paper sx={{ p: 4 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ mb: 4 }}>
|
||||
{/* Featured Image */}
|
||||
{page.featuredImage && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<img
|
||||
src={page.featuredImage}
|
||||
alt={page.title}
|
||||
style={{
|
||||
width: '100%',
|
||||
maxHeight: '400px',
|
||||
objectFit: 'cover',
|
||||
borderRadius: '8px'
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<Typography
|
||||
variant="h3"
|
||||
component="h1"
|
||||
gutterBottom
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
color: 'text.primary',
|
||||
mb: 2
|
||||
}}
|
||||
>
|
||||
{page.title}
|
||||
</Typography>
|
||||
|
||||
{/* Excerpt */}
|
||||
{page.excerpt && (
|
||||
<Typography
|
||||
variant="h6"
|
||||
color="text.secondary"
|
||||
sx={{
|
||||
fontWeight: 400,
|
||||
mb: 3,
|
||||
fontStyle: 'italic'
|
||||
}}
|
||||
>
|
||||
{page.excerpt}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Meta Information */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
mb: 4,
|
||||
pb: 2,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<DateIcon sx={{ fontSize: 16, color: 'text.secondary' }} />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Published {formatDate(page.publishedAt)}
|
||||
</Typography>
|
||||
</Box>
|
||||
{page.updatedAt !== page.publishedAt && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
• Updated {formatDate(page.updatedAt)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Content */}
|
||||
<Box sx={{
|
||||
typography: 'body1',
|
||||
lineHeight: 1.7,
|
||||
'& > *:first-of-type': { marginTop: 0 },
|
||||
'& > *:last-child': { marginBottom: 0 }
|
||||
}}>
|
||||
{renderContent(page.content, page.contentType)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user