## 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>
349 lines
9.9 KiB
TypeScript
349 lines
9.9 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { $getRoot, $getSelection, $createParagraphNode, $createTextNode, UNDO_COMMAND, REDO_COMMAND, FORMAT_TEXT_COMMAND } from 'lexical';
|
|
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
|
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
|
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
import { $createHeadingNode } from '@lexical/rich-text';
|
|
import { $createListNode, $createListItemNode } from '@lexical/list';
|
|
import { $createLinkNode } from '@lexical/link';
|
|
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';
|
|
|
|
const theme = {
|
|
ltr: 'ltr',
|
|
rtl: 'rtl',
|
|
paragraph: 'editor-paragraph',
|
|
quote: 'editor-quote',
|
|
heading: {
|
|
h1: 'editor-heading-h1',
|
|
h2: 'editor-heading-h2',
|
|
h3: 'editor-heading-h3',
|
|
h4: 'editor-heading-h4',
|
|
h5: 'editor-heading-h5',
|
|
h6: 'editor-heading-h6',
|
|
},
|
|
list: {
|
|
nested: {
|
|
listitem: 'editor-nested-listitem',
|
|
},
|
|
ol: 'editor-list-ol',
|
|
ul: 'editor-list-ul',
|
|
listitem: 'editor-listitem',
|
|
},
|
|
image: 'editor-image',
|
|
link: 'editor-link',
|
|
text: {
|
|
bold: 'editor-text-bold',
|
|
italic: 'editor-text-italic',
|
|
overflowed: 'editor-text-overflowed',
|
|
hashtag: 'editor-text-hashtag',
|
|
underline: 'editor-text-underline',
|
|
strikethrough: 'editor-text-strikethrough',
|
|
underlineStrikethrough: 'editor-text-underlineStrikethrough',
|
|
code: 'editor-text-code',
|
|
},
|
|
code: 'editor-code',
|
|
codeHighlight: {
|
|
atrule: 'editor-tokenAttr',
|
|
attr: 'editor-tokenAttr',
|
|
boolean: 'editor-tokenProperty',
|
|
builtin: 'editor-tokenSelector',
|
|
cdata: 'editor-tokenComment',
|
|
char: 'editor-tokenSelector',
|
|
class: 'editor-tokenFunction',
|
|
'class-name': 'editor-tokenFunction',
|
|
comment: 'editor-tokenComment',
|
|
constant: 'editor-tokenProperty',
|
|
deleted: 'editor-tokenProperty',
|
|
doctype: 'editor-tokenComment',
|
|
entity: 'editor-tokenOperator',
|
|
function: 'editor-tokenFunction',
|
|
important: 'editor-tokenVariable',
|
|
inserted: 'editor-tokenSelector',
|
|
keyword: 'editor-tokenAttr',
|
|
namespace: 'editor-tokenVariable',
|
|
number: 'editor-tokenProperty',
|
|
operator: 'editor-tokenOperator',
|
|
prolog: 'editor-tokenComment',
|
|
property: 'editor-tokenProperty',
|
|
punctuation: 'editor-tokenPunctuation',
|
|
regex: 'editor-tokenVariable',
|
|
selector: 'editor-tokenSelector',
|
|
string: 'editor-tokenSelector',
|
|
symbol: 'editor-tokenProperty',
|
|
tag: 'editor-tokenProperty',
|
|
url: 'editor-tokenOperator',
|
|
variable: 'editor-tokenVariable',
|
|
},
|
|
};
|
|
|
|
interface LexicalEditorProps {
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
height?: number;
|
|
}
|
|
|
|
function ToolbarPlugin() {
|
|
const [editor] = useLexicalComposerContext();
|
|
const [headingType, setHeadingType] = useState('paragraph');
|
|
|
|
const formatText = (format: 'bold' | 'italic' | 'underline') => {
|
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
|
};
|
|
|
|
const insertHeading = (headingType: string) => {
|
|
editor.update(() => {
|
|
const selection = $getSelection();
|
|
if (selection) {
|
|
if (headingType === 'paragraph') {
|
|
const paragraph = $createParagraphNode();
|
|
selection.insertNodes([paragraph]);
|
|
} else {
|
|
const heading = $createHeadingNode(headingType as any);
|
|
selection.insertNodes([heading]);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<Toolbar
|
|
sx={{
|
|
border: '1px solid #ddd',
|
|
borderBottom: 'none',
|
|
minHeight: 48,
|
|
backgroundColor: '#f5f5f5',
|
|
flexWrap: 'wrap'
|
|
}}
|
|
>
|
|
<FormControl size="small" sx={{ minWidth: 120, mr: 1 }}>
|
|
<Select
|
|
value={headingType}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
setHeadingType(value);
|
|
insertHeading(value);
|
|
}}
|
|
displayEmpty
|
|
>
|
|
<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={() => formatText('bold')} size="small">
|
|
<FormatBold />
|
|
</IconButton>
|
|
<IconButton onClick={() => formatText('italic')} size="small">
|
|
<FormatItalic />
|
|
</IconButton>
|
|
<IconButton onClick={() => formatText('underline')} size="small">
|
|
<FormatUnderlined />
|
|
</IconButton>
|
|
|
|
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
|
|
|
<IconButton size="small">
|
|
<FormatListBulleted />
|
|
</IconButton>
|
|
<IconButton size="small">
|
|
<FormatListNumbered />
|
|
</IconButton>
|
|
|
|
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
|
|
|
<IconButton size="small">
|
|
<Link />
|
|
</IconButton>
|
|
<IconButton size="small">
|
|
<Image />
|
|
</IconButton>
|
|
|
|
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
|
|
|
<IconButton
|
|
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
|
|
size="small"
|
|
>
|
|
<Undo />
|
|
</IconButton>
|
|
<IconButton
|
|
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
|
|
size="small"
|
|
>
|
|
<Redo />
|
|
</IconButton>
|
|
</Toolbar>
|
|
);
|
|
}
|
|
|
|
function OnChangeHandler({ onChange }: { onChange: (value: string) => void }) {
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
const handleChange = () => {
|
|
editor.update(() => {
|
|
const htmlString = $generateHtmlFromNodes(editor, null);
|
|
onChange(htmlString);
|
|
});
|
|
};
|
|
|
|
return <OnChangePlugin onChange={handleChange} />;
|
|
}
|
|
|
|
function InitialContentPlugin({ content }: { content: string }) {
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
useEffect(() => {
|
|
if (content) {
|
|
editor.update(() => {
|
|
const parser = new DOMParser();
|
|
const dom = parser.parseFromString(content, 'text/html');
|
|
const nodes = $generateNodesFromDOM(editor, dom);
|
|
const root = $getRoot();
|
|
root.clear();
|
|
root.append(...nodes);
|
|
});
|
|
}
|
|
}, [editor, content]);
|
|
|
|
return null;
|
|
}
|
|
|
|
export function LexicalEditor({ value, onChange, height = 400 }: LexicalEditorProps) {
|
|
const initialConfig = {
|
|
namespace: 'MyEditor',
|
|
theme,
|
|
onError: (error: Error) => {
|
|
console.error('Lexical error:', error);
|
|
},
|
|
};
|
|
|
|
return (
|
|
<Box sx={{ width: '100%' }}>
|
|
<LexicalComposer initialConfig={initialConfig}>
|
|
<Box sx={{ border: '1px solid #ddd', borderRadius: 1 }}>
|
|
<ToolbarPlugin />
|
|
<Box sx={{ position: 'relative' }}>
|
|
<RichTextPlugin
|
|
contentEditable={
|
|
<ContentEditable
|
|
style={{
|
|
minHeight: `${height}px`,
|
|
padding: '16px',
|
|
outline: 'none',
|
|
fontSize: '14px',
|
|
lineHeight: '1.5',
|
|
fontFamily: 'inherit'
|
|
}}
|
|
/>
|
|
}
|
|
placeholder={
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '16px',
|
|
left: '16px',
|
|
color: '#999',
|
|
fontSize: '14px',
|
|
pointerEvents: 'none',
|
|
}}
|
|
>
|
|
Start writing your content...
|
|
</div>
|
|
}
|
|
ErrorBoundary={LexicalErrorBoundary}
|
|
/>
|
|
<HistoryPlugin />
|
|
<OnChangeHandler onChange={onChange} />
|
|
<InitialContentPlugin content={value} />
|
|
</Box>
|
|
</Box>
|
|
</LexicalComposer>
|
|
|
|
<style jsx global>{`
|
|
.editor-paragraph {
|
|
margin: 0 0 8px 0;
|
|
}
|
|
.editor-heading-h1 {
|
|
font-size: 32px;
|
|
font-weight: bold;
|
|
margin: 16px 0 8px 0;
|
|
}
|
|
.editor-heading-h2 {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin: 14px 0 6px 0;
|
|
}
|
|
.editor-heading-h3 {
|
|
font-size: 20px;
|
|
font-weight: bold;
|
|
margin: 12px 0 4px 0;
|
|
}
|
|
.editor-heading-h4 {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
margin: 10px 0 4px 0;
|
|
}
|
|
.editor-heading-h5 {
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
margin: 8px 0 2px 0;
|
|
}
|
|
.editor-heading-h6 {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
margin: 6px 0 2px 0;
|
|
}
|
|
.editor-text-bold {
|
|
font-weight: bold;
|
|
}
|
|
.editor-text-italic {
|
|
font-style: italic;
|
|
}
|
|
.editor-text-underline {
|
|
text-decoration: underline;
|
|
}
|
|
.editor-list-ul,
|
|
.editor-list-ol {
|
|
margin: 8px 0;
|
|
padding-left: 24px;
|
|
}
|
|
.editor-listitem {
|
|
margin: 2px 0;
|
|
}
|
|
.editor-link {
|
|
color: #1976d2;
|
|
text-decoration: underline;
|
|
}
|
|
.editor-link:hover {
|
|
text-decoration: none;
|
|
}
|
|
`}</style>
|
|
</Box>
|
|
);
|
|
} |