Files
biblical-guide.com/app/[locale]/pages/[slug]/page.tsx
Andrei 95070e5369 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>
2025-09-24 07:26:25 +00:00

309 lines
8.0 KiB
TypeScript

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>
)
}