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>
This commit is contained in:
@@ -216,10 +216,10 @@ export default function BibleReaderNew() {
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
// Fetch versions based on current locale
|
||||
// Fetch all bible versions
|
||||
useEffect(() => {
|
||||
setVersionsLoading(true)
|
||||
fetch(`/api/bible/versions?language=${locale}`)
|
||||
fetch(`/api/bible/versions?all=true`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success && data.versions) {
|
||||
@@ -692,10 +692,17 @@ export default function BibleReaderNew() {
|
||||
}
|
||||
}}
|
||||
disabled={versionsLoading}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 400,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{versions.map((version) => (
|
||||
<MenuItem key={version.id} value={version.id}>
|
||||
{version.abbreviation} - {version.name}
|
||||
{version.abbreviation} - {version.name} ({version.language.toUpperCase()})
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -69,15 +69,12 @@ export default function PagesManagement() {
|
||||
|
||||
const fetchPages = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
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()}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -113,12 +110,9 @@ export default function PagesManagement() {
|
||||
if (!pageToDelete) return;
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('authToken');
|
||||
const response = await fetch(`/api/admin/pages/${pageToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
457
app/admin/social-media/page.tsx
Normal file
457
app/admin/social-media/page.tsx
Normal file
@@ -0,0 +1,457 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
TextField,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
Chip,
|
||||
Alert,
|
||||
Breadcrumbs,
|
||||
Link
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Add as AddIcon,
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
Home as HomeIcon,
|
||||
Share as ShareIcon,
|
||||
Facebook,
|
||||
Twitter,
|
||||
Instagram,
|
||||
YouTube,
|
||||
LinkedIn,
|
||||
GitHub,
|
||||
MusicNote as TikTok
|
||||
} from '@mui/icons-material';
|
||||
import { DataGrid, GridColDef, GridActionsCellItem } from '@mui/x-data-grid';
|
||||
|
||||
interface SocialMediaLink {
|
||||
id: string;
|
||||
platform: string;
|
||||
name: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
isEnabled: boolean;
|
||||
order: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
creator: { name: string; email: string };
|
||||
updater: { name: string; email: string };
|
||||
}
|
||||
|
||||
const platformIcons = {
|
||||
'Facebook': Facebook,
|
||||
'Twitter': Twitter,
|
||||
'Instagram': Instagram,
|
||||
'YouTube': YouTube,
|
||||
'LinkedIn': LinkedIn,
|
||||
'GitHub': GitHub,
|
||||
'TikTok': TikTok
|
||||
};
|
||||
|
||||
const platformOptions = [
|
||||
{ value: 'facebook', label: 'Facebook', icon: 'Facebook' },
|
||||
{ value: 'twitter', label: 'Twitter', icon: 'Twitter' },
|
||||
{ value: 'instagram', label: 'Instagram', icon: 'Instagram' },
|
||||
{ value: 'youtube', label: 'YouTube', icon: 'YouTube' },
|
||||
{ value: 'linkedin', label: 'LinkedIn', icon: 'LinkedIn' },
|
||||
{ value: 'github', label: 'GitHub', icon: 'GitHub' },
|
||||
{ value: 'tiktok', label: 'TikTok', icon: 'TikTok' }
|
||||
];
|
||||
|
||||
export default function SocialMediaManagement() {
|
||||
const [socialLinks, setSocialLinks] = useState<SocialMediaLink[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedLink, setSelectedLink] = useState<SocialMediaLink | null>(null);
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [linkToDelete, setLinkToDelete] = useState<SocialMediaLink | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
platform: '',
|
||||
name: '',
|
||||
url: '',
|
||||
icon: '',
|
||||
isEnabled: true,
|
||||
order: 0
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchSocialLinks();
|
||||
}, []);
|
||||
|
||||
const fetchSocialLinks = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/admin/social-media', {
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch social media links');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setSocialLinks(data.data || []);
|
||||
} catch (error) {
|
||||
console.error('Error fetching social media links:', error);
|
||||
setError('Failed to load social media links');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateLink = () => {
|
||||
setSelectedLink(null);
|
||||
setFormData({
|
||||
platform: '',
|
||||
name: '',
|
||||
url: '',
|
||||
icon: '',
|
||||
isEnabled: true,
|
||||
order: socialLinks.length
|
||||
});
|
||||
setEditorOpen(true);
|
||||
};
|
||||
|
||||
const handleEditLink = (link: SocialMediaLink) => {
|
||||
setSelectedLink(link);
|
||||
setFormData({
|
||||
platform: link.platform,
|
||||
name: link.name,
|
||||
url: link.url,
|
||||
icon: link.icon,
|
||||
isEnabled: link.isEnabled,
|
||||
order: link.order
|
||||
});
|
||||
setEditorOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteLink = (link: SocialMediaLink) => {
|
||||
setLinkToDelete(link);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveLink = async () => {
|
||||
try {
|
||||
const url = selectedLink
|
||||
? `/api/admin/social-media/${selectedLink.id}`
|
||||
: '/api/admin/social-media';
|
||||
const method = selectedLink ? '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 social media link');
|
||||
}
|
||||
|
||||
setSuccess('Social media link saved successfully');
|
||||
setEditorOpen(false);
|
||||
fetchSocialLinks();
|
||||
} catch (error) {
|
||||
console.error('Error saving social media link:', error);
|
||||
setError(error instanceof Error ? error.message : 'Failed to save social media link');
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDeleteLink = async () => {
|
||||
if (!linkToDelete) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/social-media/${linkToDelete.id}`, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete social media link');
|
||||
}
|
||||
|
||||
setSuccess('Social media link deleted successfully');
|
||||
setDeleteDialogOpen(false);
|
||||
setLinkToDelete(null);
|
||||
fetchSocialLinks();
|
||||
} catch (error) {
|
||||
console.error('Error deleting social media link:', error);
|
||||
setError('Failed to delete social media link');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlatformChange = (platform: string) => {
|
||||
const platformOption = platformOptions.find(p => p.value === platform);
|
||||
if (platformOption) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
platform,
|
||||
name: platformOption.label,
|
||||
icon: platformOption.icon
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const renderIcon = (iconName: string) => {
|
||||
const IconComponent = platformIcons[iconName as keyof typeof platformIcons];
|
||||
return IconComponent ? <IconComponent /> : <ShareIcon />;
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'icon',
|
||||
headerName: 'Icon',
|
||||
width: 80,
|
||||
renderCell: (params) => renderIcon(params.value)
|
||||
},
|
||||
{
|
||||
field: 'name',
|
||||
headerName: 'Platform',
|
||||
width: 120,
|
||||
renderCell: (params) => (
|
||||
<Box>
|
||||
<Typography variant="body2" fontWeight="medium">
|
||||
{params.value}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{params.row.platform}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'url',
|
||||
headerName: 'URL',
|
||||
width: 300,
|
||||
renderCell: (params) => (
|
||||
<Link href={params.value} target="_blank" rel="noopener">
|
||||
{params.value}
|
||||
</Link>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'isEnabled',
|
||||
headerName: 'Status',
|
||||
width: 100,
|
||||
renderCell: (params) => (
|
||||
<Chip
|
||||
label={params.value ? 'Enabled' : 'Disabled'}
|
||||
color={params.value ? 'success' : 'default'}
|
||||
size="small"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'order',
|
||||
headerName: 'Order',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
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={() => handleEditLink(params.row)}
|
||||
/>,
|
||||
<GridActionsCellItem
|
||||
key="delete"
|
||||
icon={<DeleteIcon />}
|
||||
label="Delete"
|
||||
onClick={() => handleDeleteLink(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' }}>
|
||||
<ShareIcon sx={{ mr: 0.5 }} fontSize="inherit" />
|
||||
Social Media
|
||||
</Typography>
|
||||
</Breadcrumbs>
|
||||
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Box>
|
||||
<Typography variant="h4" component="h1" gutterBottom>
|
||||
Social Media Management
|
||||
</Typography>
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
Manage social media links displayed in the footer
|
||||
</Typography>
|
||||
</Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<AddIcon />}
|
||||
onClick={handleCreateLink}
|
||||
sx={{ height: 'fit-content' }}
|
||||
>
|
||||
Add Social Link
|
||||
</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>
|
||||
)}
|
||||
|
||||
{/* Social Links Table */}
|
||||
<Card>
|
||||
<DataGrid
|
||||
rows={socialLinks}
|
||||
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>
|
||||
|
||||
{/* Editor Dialog */}
|
||||
<Dialog open={editorOpen} onClose={() => setEditorOpen(false)} maxWidth="sm" fullWidth>
|
||||
<DialogTitle>
|
||||
{selectedLink ? 'Edit Social Media Link' : 'Add Social Media Link'}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Platform</InputLabel>
|
||||
<Select
|
||||
value={formData.platform}
|
||||
label="Platform"
|
||||
onChange={(e) => handlePlatformChange(e.target.value)}
|
||||
>
|
||||
{platformOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{renderIcon(option.icon)}
|
||||
{option.label}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
label="Display Name"
|
||||
fullWidth
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="URL"
|
||||
fullWidth
|
||||
value={formData.url}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, url: e.target.value }))}
|
||||
placeholder="https://..."
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Display Order"
|
||||
type="number"
|
||||
fullWidth
|
||||
value={formData.order}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 0 }))}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={formData.isEnabled}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, isEnabled: e.target.checked }))}
|
||||
/>
|
||||
}
|
||||
label="Enabled"
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={() => setEditorOpen(false)}>Cancel</Button>
|
||||
<Button onClick={handleSaveLink} variant="contained">
|
||||
{selectedLink ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
{/* Delete Confirmation Dialog */}
|
||||
<Dialog open={deleteDialogOpen} onClose={() => setDeleteDialogOpen(false)}>
|
||||
<DialogTitle>Delete Social Media Link</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography>
|
||||
Are you sure you want to delete the {linkToDelete?.name} link? This action cannot be undone.
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setDeleteDialogOpen(false)}>Cancel</Button>
|
||||
<Button onClick={confirmDeleteLink} color="error" variant="contained">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
143
app/api/admin/social-media/[id]/route.ts
Normal file
143
app/api/admin/social-media/[id]/route.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { verifyAdminAuth } from '@/lib/admin-auth';
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request);
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const socialMediaLink = await prisma.socialMediaLink.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
creator: { select: { name: true, email: true } },
|
||||
updater: { select: { name: true, email: true } }
|
||||
}
|
||||
});
|
||||
|
||||
if (!socialMediaLink) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Social media link not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: socialMediaLink
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching social media link:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch social media link' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request);
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const {
|
||||
platform,
|
||||
name,
|
||||
url,
|
||||
icon,
|
||||
isEnabled,
|
||||
order
|
||||
} = body;
|
||||
|
||||
if (!platform || !name || !url || !icon) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Platform, name, URL, and icon are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if another link uses the same platform
|
||||
const existingLink = await prisma.socialMediaLink.findFirst({
|
||||
where: {
|
||||
platform,
|
||||
id: { not: id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingLink) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Another social media link for this platform already exists' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const socialMediaLink = await prisma.socialMediaLink.update({
|
||||
where: { id },
|
||||
data: {
|
||||
platform,
|
||||
name,
|
||||
url,
|
||||
icon,
|
||||
isEnabled,
|
||||
order,
|
||||
updatedBy: adminUser.id
|
||||
},
|
||||
include: {
|
||||
creator: { select: { name: true, email: true } },
|
||||
updater: { select: { name: true, email: true } }
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: socialMediaLink
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating social media link:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to update social media link' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request);
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.socialMediaLink.delete({
|
||||
where: { id }
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'Social media link deleted successfully'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting social media link:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to delete social media link' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
97
app/api/admin/social-media/route.ts
Normal file
97
app/api/admin/social-media/route.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { verifyAdminAuth } from '@/lib/admin-auth';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request);
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const socialMediaLinks = await prisma.socialMediaLink.findMany({
|
||||
orderBy: { order: 'asc' },
|
||||
include: {
|
||||
creator: { select: { name: true, email: true } },
|
||||
updater: { select: { name: true, email: true } }
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: socialMediaLinks
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching social media links:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch social media links' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const adminUser = await verifyAdminAuth(request);
|
||||
if (!adminUser) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
platform,
|
||||
name,
|
||||
url,
|
||||
icon,
|
||||
isEnabled = true,
|
||||
order = 0
|
||||
} = body;
|
||||
|
||||
if (!platform || !name || !url || !icon) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Platform, name, URL, and icon are required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if platform already exists
|
||||
const existingLink = await prisma.socialMediaLink.findUnique({
|
||||
where: { platform }
|
||||
});
|
||||
|
||||
if (existingLink) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'A social media link for this platform already exists' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const socialMediaLink = await prisma.socialMediaLink.create({
|
||||
data: {
|
||||
platform,
|
||||
name,
|
||||
url,
|
||||
icon,
|
||||
isEnabled,
|
||||
order,
|
||||
createdBy: adminUser.id,
|
||||
updatedBy: adminUser.id
|
||||
},
|
||||
include: {
|
||||
creator: { select: { name: true, email: true } },
|
||||
updater: { select: { name: true, email: true } }
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: socialMediaLink
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating social media link:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to create social media link' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -16,11 +16,10 @@ export async function GET(request: Request) {
|
||||
let bibleVersion
|
||||
const langCandidates = Array.from(new Set([locale, locale.toLowerCase(), locale.toUpperCase()]))
|
||||
if (versionId) {
|
||||
// Use specific version if provided
|
||||
// Use specific version if provided (no language filter needed)
|
||||
bibleVersion = await prisma.bibleVersion.findFirst({
|
||||
where: {
|
||||
id: versionId,
|
||||
language: { in: langCandidates }
|
||||
id: versionId
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -7,12 +7,18 @@ export async function GET(request: Request) {
|
||||
try {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const locale = (searchParams.get('locale') || 'ro').toLowerCase()
|
||||
const showAll = searchParams.get('all') === 'true'
|
||||
|
||||
const langCandidates = Array.from(new Set([locale, locale.toLowerCase(), locale.toUpperCase()]))
|
||||
let whereClause = {}
|
||||
|
||||
if (!showAll) {
|
||||
const langCandidates = Array.from(new Set([locale, locale.toLowerCase(), locale.toUpperCase()]))
|
||||
whereClause = { language: { in: langCandidates } }
|
||||
}
|
||||
|
||||
const versions = await prisma.bibleVersion.findMany({
|
||||
where: { language: { in: langCandidates } },
|
||||
orderBy: [{ isDefault: 'desc' }, { name: 'asc' }]
|
||||
where: whereClause,
|
||||
orderBy: [{ isDefault: 'desc' }, { language: 'asc' }, { name: 'asc' }]
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
|
||||
30
app/api/social-media/route.ts
Normal file
30
app/api/social-media/route.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const socialMediaLinks = await prisma.socialMediaLink.findMany({
|
||||
where: { isEnabled: true },
|
||||
orderBy: { order: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
platform: true,
|
||||
name: true,
|
||||
url: true,
|
||||
icon: true,
|
||||
order: true
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: socialMediaLinks
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching social media links:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to fetch social media links' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user