Files
biblical-guide.com/components/admin/pages/lexical-editor.tsx
Andrei 4adf1d286e 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>
2025-09-24 12:08:01 +00:00

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