## 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>
202 lines
5.7 KiB
TypeScript
202 lines
5.7 KiB
TypeScript
'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>
|
|
);
|
|
} |