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:
202
components/admin/pages/simple-rich-editor.tsx
Normal file
202
components/admin/pages/simple-rich-editor.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { Box, Toolbar, IconButton, Divider, Select, MenuItem, FormControl } from '@mui/material';
|
||||
import {
|
||||
FormatBold,
|
||||
FormatItalic,
|
||||
FormatUnderlined,
|
||||
FormatListBulleted,
|
||||
FormatListNumbered,
|
||||
Link,
|
||||
Image,
|
||||
Undo,
|
||||
Redo
|
||||
} from '@mui/icons-material';
|
||||
|
||||
interface SimpleRichEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function SimpleRichEditor({ value, onChange, height = 400 }: SimpleRichEditorProps) {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
// Update editor content when value prop changes (but not when we're updating it ourselves)
|
||||
useEffect(() => {
|
||||
if (!isUpdating && editorRef.current && editorRef.current.innerHTML !== value) {
|
||||
editorRef.current.innerHTML = value;
|
||||
}
|
||||
}, [value, isUpdating]);
|
||||
|
||||
const handleInput = () => {
|
||||
if (editorRef.current) {
|
||||
setIsUpdating(true);
|
||||
onChange(editorRef.current.innerHTML);
|
||||
setTimeout(() => setIsUpdating(false), 0);
|
||||
}
|
||||
};
|
||||
|
||||
const execCommand = (command: string, value?: string) => {
|
||||
document.execCommand(command, false, value);
|
||||
editorRef.current?.focus();
|
||||
handleInput();
|
||||
};
|
||||
|
||||
const insertHeading = (tag: string) => {
|
||||
if (tag === 'paragraph') {
|
||||
execCommand('formatBlock', 'div');
|
||||
} else {
|
||||
execCommand('formatBlock', tag);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ width: '100%' }}>
|
||||
<Box sx={{ border: '1px solid #ddd', borderRadius: 1 }}>
|
||||
<Toolbar
|
||||
sx={{
|
||||
border: 'none',
|
||||
borderBottom: '1px solid #ddd',
|
||||
minHeight: 48,
|
||||
backgroundColor: '#f5f5f5',
|
||||
flexWrap: 'wrap'
|
||||
}}
|
||||
>
|
||||
<FormControl size="small" sx={{ minWidth: 120, mr: 1 }}>
|
||||
<Select
|
||||
defaultValue="paragraph"
|
||||
displayEmpty
|
||||
onChange={(e) => insertHeading(e.target.value)}
|
||||
>
|
||||
<MenuItem value="paragraph">Paragraph</MenuItem>
|
||||
<MenuItem value="h1">Heading 1</MenuItem>
|
||||
<MenuItem value="h2">Heading 2</MenuItem>
|
||||
<MenuItem value="h3">Heading 3</MenuItem>
|
||||
<MenuItem value="h4">Heading 4</MenuItem>
|
||||
<MenuItem value="h5">Heading 5</MenuItem>
|
||||
<MenuItem value="h6">Heading 6</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
||||
|
||||
<IconButton onClick={() => execCommand('bold')} size="small">
|
||||
<FormatBold />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => execCommand('italic')} size="small">
|
||||
<FormatItalic />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => execCommand('underline')} size="small">
|
||||
<FormatUnderlined />
|
||||
</IconButton>
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
||||
|
||||
<IconButton onClick={() => execCommand('insertUnorderedList')} size="small">
|
||||
<FormatListBulleted />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => execCommand('insertOrderedList')} size="small">
|
||||
<FormatListNumbered />
|
||||
</IconButton>
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
||||
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
const url = prompt('Enter link URL:');
|
||||
if (url) execCommand('createLink', url);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Link />
|
||||
</IconButton>
|
||||
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
||||
|
||||
<IconButton onClick={() => execCommand('undo')} size="small">
|
||||
<Undo />
|
||||
</IconButton>
|
||||
<IconButton onClick={() => execCommand('redo')} size="small">
|
||||
<Redo />
|
||||
</IconButton>
|
||||
</Toolbar>
|
||||
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
onInput={handleInput}
|
||||
style={{
|
||||
minHeight: `${height}px`,
|
||||
padding: '16px',
|
||||
outline: 'none',
|
||||
fontSize: '14px',
|
||||
lineHeight: '1.5',
|
||||
fontFamily: 'inherit',
|
||||
border: 'none',
|
||||
backgroundColor: 'white'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: value }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<style jsx global>{`
|
||||
div[contenteditable] h1 {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 16px 0 8px 0;
|
||||
}
|
||||
div[contenteditable] h2 {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 14px 0 6px 0;
|
||||
}
|
||||
div[contenteditable] h3 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
margin: 12px 0 4px 0;
|
||||
}
|
||||
div[contenteditable] h4 {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0 4px 0;
|
||||
}
|
||||
div[contenteditable] h5 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin: 8px 0 2px 0;
|
||||
}
|
||||
div[contenteditable] h6 {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
margin: 6px 0 2px 0;
|
||||
}
|
||||
div[contenteditable] p {
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
div[contenteditable] ul,
|
||||
div[contenteditable] ol {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
div[contenteditable] li {
|
||||
margin: 2px 0;
|
||||
}
|
||||
div[contenteditable] a {
|
||||
color: #1976d2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
div[contenteditable] a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
div[contenteditable]:empty::before {
|
||||
content: 'Start writing your content...';
|
||||
color: #999;
|
||||
pointer-events: none;
|
||||
}
|
||||
`}</style>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user