diff --git a/app/[locale]/bible/reader.tsx b/app/[locale]/bible/reader.tsx index 651c145..b3c86a3 100644 --- a/app/[locale]/bible/reader.tsx +++ b/app/[locale]/bible/reader.tsx @@ -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) => ( - {version.abbreviation} - {version.name} + {version.abbreviation} - {version.name} ({version.language.toUpperCase()}) ))} diff --git a/app/admin/pages/page.tsx b/app/admin/pages/page.tsx index 618ccf7..7ef714c 100644 --- a/app/admin/pages/page.tsx +++ b/app/admin/pages/page.tsx @@ -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) { diff --git a/app/admin/social-media/page.tsx b/app/admin/social-media/page.tsx new file mode 100644 index 0000000..51c394a --- /dev/null +++ b/app/admin/social-media/page.tsx @@ -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([]); + const [loading, setLoading] = useState(true); + const [selectedLink, setSelectedLink] = useState(null); + const [editorOpen, setEditorOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [linkToDelete, setLinkToDelete] = useState(null); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(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 ? : ; + }; + + const columns: GridColDef[] = [ + { + field: 'icon', + headerName: 'Icon', + width: 80, + renderCell: (params) => renderIcon(params.value) + }, + { + field: 'name', + headerName: 'Platform', + width: 120, + renderCell: (params) => ( + + + {params.value} + + + {params.row.platform} + + + ) + }, + { + field: 'url', + headerName: 'URL', + width: 300, + renderCell: (params) => ( + + {params.value} + + ) + }, + { + field: 'isEnabled', + headerName: 'Status', + width: 100, + renderCell: (params) => ( + + ) + }, + { + 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) => [ + } + label="Edit" + onClick={() => handleEditLink(params.row)} + />, + } + label="Delete" + onClick={() => handleDeleteLink(params.row)} + /> + ] + } + ]; + + return ( + + {/* Breadcrumbs */} + + + + Admin + + + + Social Media + + + + {/* Header */} + + + + Social Media Management + + + Manage social media links displayed in the footer + + + } + onClick={handleCreateLink} + sx={{ height: 'fit-content' }} + > + Add Social Link + + + + {/* Alerts */} + {error && ( + setError(null)} sx={{ mb: 2 }}> + {error} + + )} + {success && ( + setSuccess(null)} sx={{ mb: 2 }}> + {success} + + )} + + {/* Social Links Table */} + + + + + {/* Editor Dialog */} + setEditorOpen(false)} maxWidth="sm" fullWidth> + + {selectedLink ? 'Edit Social Media Link' : 'Add Social Media Link'} + + + + + + Platform + handlePlatformChange(e.target.value)} + > + {platformOptions.map((option) => ( + + + {renderIcon(option.icon)} + {option.label} + + + ))} + + + + setFormData(prev => ({ ...prev, name: e.target.value }))} + /> + + setFormData(prev => ({ ...prev, url: e.target.value }))} + placeholder="https://..." + /> + + setFormData(prev => ({ ...prev, order: parseInt(e.target.value) || 0 }))} + /> + + setFormData(prev => ({ ...prev, isEnabled: e.target.checked }))} + /> + } + label="Enabled" + /> + + + + + setEditorOpen(false)}>Cancel + + {selectedLink ? 'Update' : 'Create'} + + + + + {/* Delete Confirmation Dialog */} + setDeleteDialogOpen(false)}> + Delete Social Media Link + + + Are you sure you want to delete the {linkToDelete?.name} link? This action cannot be undone. + + + + setDeleteDialogOpen(false)}>Cancel + + Delete + + + + + ); +} \ No newline at end of file diff --git a/app/api/admin/social-media/[id]/route.ts b/app/api/admin/social-media/[id]/route.ts new file mode 100644 index 0000000..a5d1761 --- /dev/null +++ b/app/api/admin/social-media/[id]/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/app/api/admin/social-media/route.ts b/app/api/admin/social-media/route.ts new file mode 100644 index 0000000..33f3c4d --- /dev/null +++ b/app/api/admin/social-media/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/app/api/bible/books/route.ts b/app/api/bible/books/route.ts index 8234707..02c5ee2 100644 --- a/app/api/bible/books/route.ts +++ b/app/api/bible/books/route.ts @@ -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 { diff --git a/app/api/bible/versions/route.ts b/app/api/bible/versions/route.ts index cb21e84..d3967f3 100644 --- a/app/api/bible/versions/route.ts +++ b/app/api/bible/versions/route.ts @@ -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({ diff --git a/app/api/social-media/route.ts b/app/api/social-media/route.ts new file mode 100644 index 0000000..7e0a8b7 --- /dev/null +++ b/app/api/social-media/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/components/admin/layout/admin-layout.tsx b/components/admin/layout/admin-layout.tsx index 92c7c40..a7a775c 100644 --- a/components/admin/layout/admin-layout.tsx +++ b/components/admin/layout/admin-layout.tsx @@ -32,7 +32,8 @@ import { AccountCircle, AdminPanelSettings, Launch as LaunchIcon, - Article as PageIcon + Article as PageIcon, + Share } from '@mui/icons-material'; interface AdminLayoutProps { @@ -51,6 +52,7 @@ const menuItems = [ { text: 'Dashboard', icon: Dashboard, href: '/admin' }, { text: 'Users', icon: People, href: '/admin/users' }, { text: 'Pages', icon: PageIcon, href: '/admin/pages' }, + { text: 'Social Media', icon: Share, href: '/admin/social-media' }, { text: 'Content Moderation', icon: Gavel, href: '/admin/content' }, { text: 'Analytics', icon: Analytics, href: '/admin/analytics' }, { text: 'Chat Monitoring', icon: Chat, href: '/admin/chat' }, diff --git a/components/admin/pages/image-upload.tsx b/components/admin/pages/image-upload.tsx index ce07c8a..aa567e4 100644 --- a/components/admin/pages/image-upload.tsx +++ b/components/admin/pages/image-upload.tsx @@ -56,11 +56,8 @@ export function ImageUpload({ open, onClose, onImageSelect }: ImageUploadProps) const fetchMediaFiles = async () => { setLoadingMedia(true); try { - const token = localStorage.getItem('authToken'); const response = await fetch('/api/admin/media?type=image', { - headers: { - 'Authorization': `Bearer ${token}` - } + credentials: 'include' }); if (!response.ok) { @@ -102,12 +99,9 @@ export function ImageUpload({ open, onClose, onImageSelect }: ImageUploadProps) const formData = new FormData(); formData.append('file', file); - const token = localStorage.getItem('authToken'); const response = await fetch('/api/admin/media', { method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - }, + credentials: 'include', body: formData }); diff --git a/components/admin/pages/lexical-editor.tsx b/components/admin/pages/lexical-editor.tsx new file mode 100644 index 0000000..358419c --- /dev/null +++ b/components/admin/pages/lexical-editor.tsx @@ -0,0 +1,349 @@ +'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 ( + + + { + const value = e.target.value; + setHeadingType(value); + insertHeading(value); + }} + displayEmpty + > + Paragraph + Heading 1 + Heading 2 + Heading 3 + Heading 4 + Heading 5 + Heading 6 + + + + + + formatText('bold')} size="small"> + + + formatText('italic')} size="small"> + + + formatText('underline')} size="small"> + + + + + + + + + + + + + + + + + + + + + + + + editor.dispatchCommand(UNDO_COMMAND, undefined)} + size="small" + > + + + editor.dispatchCommand(REDO_COMMAND, undefined)} + size="small" + > + + + + ); +} + +function OnChangeHandler({ onChange }: { onChange: (value: string) => void }) { + const [editor] = useLexicalComposerContext(); + + const handleChange = () => { + editor.update(() => { + const htmlString = $generateHtmlFromNodes(editor, null); + onChange(htmlString); + }); + }; + + return ; +} + +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 ( + + + + + + + } + placeholder={ + + Start writing your content... + + } + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + + + + + ); +} \ No newline at end of file diff --git a/components/admin/pages/page-editor.tsx b/components/admin/pages/page-editor.tsx index ebab011..114c26f 100644 --- a/components/admin/pages/page-editor.tsx +++ b/components/admin/pages/page-editor.tsx @@ -22,8 +22,8 @@ import { Grid, Paper } from '@mui/material'; -import { Editor } from '@tinymce/tinymce-react'; import { ImageUpload } from './image-upload'; +import { SimpleRichEditor } from './simple-rich-editor'; interface Page { id?: string; @@ -128,16 +128,15 @@ export function PageEditor({ open, onClose, page, onSave }: PageEditorProps) { setError(null); try { - const token = localStorage.getItem('authToken'); const url = page ? `/api/admin/pages/${page.id}` : '/api/admin/pages'; const method = page ? 'PUT' : 'POST'; const response = await fetch(url, { method, headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` + 'Content-Type': 'application/json' }, + credentials: 'include', body: JSON.stringify(formData) }); @@ -174,54 +173,16 @@ export function PageEditor({ open, onClose, page, onSave }: PageEditorProps) { case 'RICH_TEXT': return ( - - Content + + Rich Text Content setImageUploadOpen(true)}> Insert Image - editorRef.current = { getEditor: () => editor }} + setFormData(prev => ({ ...prev, content }))} - init={{ - height: 400, - menubar: true, - plugins: [ - 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', - 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', - 'insertdatetime', 'media', 'table', 'help', 'wordcount' - ], - toolbar: 'undo redo | blocks | ' + - 'bold italic forecolor | alignleft aligncenter ' + - 'alignright alignjustify | bullist numlist outdent indent | ' + - 'removeformat | link image | code | help', - content_style: 'body { font-family: Arial, Helvetica, sans-serif; font-size: 14px }', - images_upload_handler: (blobInfo: any) => { - return new Promise((resolve, reject) => { - const formData = new FormData(); - formData.append('file', blobInfo.blob(), blobInfo.filename()); - - const token = localStorage.getItem('authToken'); - fetch('/api/admin/media', { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}` - }, - body: formData - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - resolve(result.data.url); - } else { - reject(result.error || 'Upload failed'); - } - }) - .catch(error => reject(error)); - }); - } - }} + onChange={(content) => setFormData(prev => ({ ...prev, content }))} + height={800} /> ); @@ -237,7 +198,7 @@ export function PageEditor({ open, onClose, page, onSave }: PageEditorProps) { Markdown Content void; + height?: number; +} + +export function SimpleRichEditor({ value, onChange, height = 400 }: SimpleRichEditorProps) { + const editorRef = useRef(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 ( + + + + + insertHeading(e.target.value)} + > + Paragraph + Heading 1 + Heading 2 + Heading 3 + Heading 4 + Heading 5 + Heading 6 + + + + + + execCommand('bold')} size="small"> + + + execCommand('italic')} size="small"> + + + execCommand('underline')} size="small"> + + + + + + execCommand('insertUnorderedList')} size="small"> + + + execCommand('insertOrderedList')} size="small"> + + + + + + { + const url = prompt('Enter link URL:'); + if (url) execCommand('createLink', url); + }} + size="small" + > + + + + + + execCommand('undo')} size="small"> + + + execCommand('redo')} size="small"> + + + + + + + + + + ); +} \ No newline at end of file diff --git a/components/layout/footer.tsx b/components/layout/footer.tsx index 486b756..8ab4ec2 100644 --- a/components/layout/footer.tsx +++ b/components/layout/footer.tsx @@ -15,6 +15,10 @@ import { Twitter, Instagram, YouTube, + LinkedIn, + GitHub, + MusicNote as TikTok, + Share as DefaultIcon } from '@mui/icons-material' import { useRouter } from 'next/navigation' import { useTranslations, useLocale } from 'next-intl' @@ -27,8 +31,18 @@ interface DynamicPage { footerOrder?: number } +interface SocialMediaLink { + id: string + platform: string + name: string + url: string + icon: string + order: number +} + export function Footer() { const [dynamicPages, setDynamicPages] = useState([]) + const [socialLinks, setSocialLinks] = useState([]) const router = useRouter() const t = useTranslations('home') const tSeo = useTranslations('seo') @@ -36,6 +50,7 @@ export function Footer() { useEffect(() => { fetchDynamicPages() + fetchSocialLinks() }, []) const fetchDynamicPages = async () => { @@ -50,10 +65,36 @@ export function Footer() { } } + const fetchSocialLinks = async () => { + try { + const response = await fetch('/api/social-media') + if (response.ok) { + const data = await response.json() + setSocialLinks(data.data || []) + } + } catch (error) { + console.error('Failed to fetch social media links:', error) + } + } + const getCurrentYear = () => { return new Date().getFullYear() } + const renderSocialIcon = (iconName: string) => { + const iconMap = { + 'Facebook': Facebook, + 'Twitter': Twitter, + 'Instagram': Instagram, + 'YouTube': YouTube, + 'LinkedIn': LinkedIn, + 'GitHub': GitHub, + 'TikTok': TikTok + } + const IconComponent = iconMap[iconName as keyof typeof iconMap] || DefaultIcon + return + } + return ( @@ -74,12 +115,7 @@ export function Footer() { {t('footer.quickLinks.title')} - - {t('footer.quickLinks.about')} - - - {t('footer.quickLinks.blog')} - + {/* Static important links */} {t('footer.quickLinks.contact')} - - {t('footer.quickLinks.support')} - - - {t('footer.quickLinks.api')} - - {dynamicPages.map((page) => ( - router.push(`/${locale}/pages/${page.slug}`)} - > - {page.title} - - ))} + + {/* Dynamic pages - filtered for non-legal pages */} + {dynamicPages + .filter(page => !['terms', 'privacy', 'cookies', 'gdpr'].includes(page.slug)) + .sort((a, b) => (a.footerOrder || 999) - (b.footerOrder || 999)) + .map((page) => ( + router.push(`/${locale}/pages/${page.slug}`)} + > + {page.title} + + ))} @@ -112,18 +147,20 @@ export function Footer() { {t('footer.legal.title')} - - {t('footer.legal.terms')} - - - {t('footer.legal.privacy')} - - - {t('footer.legal.cookies')} - - - {t('footer.legal.gdpr')} - + {/* Dynamic legal pages */} + {dynamicPages + .filter(page => ['terms', 'privacy', 'cookies', 'gdpr'].includes(page.slug)) + .sort((a, b) => (a.footerOrder || 999) - (b.footerOrder || 999)) + .map((page) => ( + router.push(`/${locale}/pages/${page.slug}`)} + > + {page.title} + + ))} @@ -132,19 +169,25 @@ export function Footer() { {t('footer.social.title')} - - - - - - - - - - - - - + + {socialLinks.map((link) => ( + + {renderSocialIcon(link.icon)} + + ))} + {socialLinks.length === 0 && ( + + No social media links configured + + )} diff --git a/package-lock.json b/package-lock.json index 3afb04e..a9b1531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,13 @@ "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.2.8", "@formatjs/intl-localematcher": "^0.6.1", + "@lexical/html": "^0.35.0", + "@lexical/link": "^0.35.0", + "@lexical/list": "^0.35.0", + "@lexical/plain-text": "^0.35.0", + "@lexical/react": "^0.35.0", + "@lexical/rich-text": "^0.35.0", + "@lexical/utils": "^0.35.0", "@mui/icons-material": "^7.3.2", "@mui/lab": "^7.0.0-beta.17", "@mui/material": "^7.3.2", @@ -40,6 +47,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "jsonwebtoken": "^9.0.2", + "lexical": "^0.35.0", "lucide-react": "^0.544.0", "negotiator": "^1.0.0", "next": "^15.5.3", @@ -61,6 +69,7 @@ "tailwindcss": "^4.1.13", "tinymce": "^8.1.2", "typescript": "^5.9.2", + "uuid": "^13.0.0", "zod": "^3.25.76", "zustand": "^5.0.8" }, @@ -887,6 +896,21 @@ "@floating-ui/utils": "^0.2.10" } }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, "node_modules/@floating-ui/react-dom": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", @@ -1451,6 +1475,261 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lexical/clipboard": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.35.0.tgz", + "integrity": "sha512-ko7xSIIiayvDiqjNDX6fgH9RlcM6r9vrrvJYTcfGVBor5httx16lhIi0QJZ4+RNPvGtTjyFv4bwRmsixRRwImg==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.35.0", + "@lexical/list": "0.35.0", + "@lexical/selection": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/code": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/code/-/code-0.35.0.tgz", + "integrity": "sha512-ox4DZwETQ9IA7+DS6PN8RJNwSAF7RMjL7YTVODIqFZ5tUFIf+5xoCHbz7Fll0Bvixlp12hVH90xnLwTLRGpkKw==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.35.0", + "lexical": "0.35.0", + "prismjs": "^1.30.0" + } + }, + "node_modules/@lexical/devtools-core": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/devtools-core/-/devtools-core-0.35.0.tgz", + "integrity": "sha512-C2wwtsMCR6ZTfO0TqpSM17RLJWyfHmifAfCTjFtOJu15p3M6NO/nHYK5Mt7YMQteuS89mOjB4ng8iwoLEZ6QpQ==", + "license": "MIT", + "dependencies": { + "@lexical/html": "0.35.0", + "@lexical/link": "0.35.0", + "@lexical/mark": "0.35.0", + "@lexical/table": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/dragon": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.35.0.tgz", + "integrity": "sha512-SL6mT5pcqrt6hEbJ16vWxip5+r3uvMd0bQV5UUxuk+cxIeuP86iTgRh0HFR7SM2dRTYovL6/tM/O+8QLAUGTIg==", + "license": "MIT", + "dependencies": { + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/hashtag": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.35.0.tgz", + "integrity": "sha512-LYJWzXuO2ZjKsvQwrLkNZiS2TsjwYkKjlDgtugzejquTBQ/o/nfSn/MmVx6EkYLOYizaJemmZbz3IBh+u732FA==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/history": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/history/-/history-0.35.0.tgz", + "integrity": "sha512-onjDRLLxGbCfHexSxxrQaDaieIHyV28zCDrbxR5dxTfW8F8PxjuNyuaG0z6o468AXYECmclxkP+P4aT6poHEpQ==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/html": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/html/-/html-0.35.0.tgz", + "integrity": "sha512-rXGFE5S5rKsg3tVnr1s4iEgOfCApNXGpIFI3T2jGEShaCZ5HLaBY9NVBXnE9Nb49e9bkDkpZ8FZd1qokCbQXbw==", + "license": "MIT", + "dependencies": { + "@lexical/selection": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/link": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/link/-/link-0.35.0.tgz", + "integrity": "sha512-+0Wx6cBwO8TfdMzpkYFacsmgFh8X1rkiYbq3xoLvk3qV8upYxaMzK1s8Q1cpKmWyI0aZrU6z7fiK4vUqB7+69w==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/list": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/list/-/list-0.35.0.tgz", + "integrity": "sha512-owsmc8iwgExBX8sFe8fKTiwJVhYULt9hD1RZ/HwfaiEtRZZkINijqReOBnW2mJfRxBzhFSWc4NG3ISB+fHYzqw==", + "license": "MIT", + "dependencies": { + "@lexical/selection": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/mark": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/mark/-/mark-0.35.0.tgz", + "integrity": "sha512-W0hwMTAVeexvpk9/+J6n1G/sNkpI/Meq1yeDazahFLLAwXLHtvhIAq2P/klgFknDy1hr8X7rcsQuN/bqKcKHYg==", + "license": "MIT", + "dependencies": { + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/markdown": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.35.0.tgz", + "integrity": "sha512-BlNyXZAt4gWidMw0SRWrhBETY1BpPglFBZI7yzfqukFqgXRh7HUQA28OYeI/nsx9pgNob8TiUduUwShqqvOdEA==", + "license": "MIT", + "dependencies": { + "@lexical/code": "0.35.0", + "@lexical/link": "0.35.0", + "@lexical/list": "0.35.0", + "@lexical/rich-text": "0.35.0", + "@lexical/text": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/offset": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/offset/-/offset-0.35.0.tgz", + "integrity": "sha512-DRE4Df6qYf2XiV6foh6KpGNmGAv2ANqt3oVXpyS6W8hTx3+cUuAA1APhCZmLNuU107um4zmHym7taCu6uXW5Yg==", + "license": "MIT", + "dependencies": { + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/overflow": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.35.0.tgz", + "integrity": "sha512-B25YvnJQTGlZcrNv7b0PJBLWq3tl8sql497OHfYYLem7EOMPKKDGJScJAKM/91D4H/mMAsx5gnA/XgKobriuTg==", + "license": "MIT", + "dependencies": { + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/plain-text": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.35.0.tgz", + "integrity": "sha512-lwBCUNMJf7Gujp2syVWMpKRahfbTv5Wq+H3HK1Q1gKH1P2IytPRxssCHvexw9iGwprSyghkKBlbF3fGpEdIJvQ==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.35.0", + "@lexical/selection": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/react": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/react/-/react-0.35.0.tgz", + "integrity": "sha512-uYAZSqumH8tRymMef+A0f2hQvMwplKK9DXamcefnk3vSNDHHqRWQXpiUo6kD+rKWuQmMbVa5RW4xRQebXEW+1A==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.27.8", + "@lexical/devtools-core": "0.35.0", + "@lexical/dragon": "0.35.0", + "@lexical/hashtag": "0.35.0", + "@lexical/history": "0.35.0", + "@lexical/link": "0.35.0", + "@lexical/list": "0.35.0", + "@lexical/mark": "0.35.0", + "@lexical/markdown": "0.35.0", + "@lexical/overflow": "0.35.0", + "@lexical/plain-text": "0.35.0", + "@lexical/rich-text": "0.35.0", + "@lexical/table": "0.35.0", + "@lexical/text": "0.35.0", + "@lexical/utils": "0.35.0", + "@lexical/yjs": "0.35.0", + "lexical": "0.35.0", + "react-error-boundary": "^3.1.4" + }, + "peerDependencies": { + "react": ">=17.x", + "react-dom": ">=17.x" + } + }, + "node_modules/@lexical/rich-text": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.35.0.tgz", + "integrity": "sha512-qEHu8g7vOEzz9GUz1VIUxZBndZRJPh9iJUFI+qTDHj+tQqnd5LCs+G9yz6jgNfiuWWpezTp0i1Vz/udNEuDPKQ==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.35.0", + "@lexical/selection": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/selection": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/selection/-/selection-0.35.0.tgz", + "integrity": "sha512-mMtDE7Q0nycXdFTTH/+ta6EBrBwxBB4Tg8QwsGntzQ1Cq//d838dpXpFjJOqHEeVHUqXpiuj+cBG8+bvz/rPRw==", + "license": "MIT", + "dependencies": { + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/table": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/table/-/table-0.35.0.tgz", + "integrity": "sha512-9jlTlkVideBKwsEnEkqkdg7A3mije1SvmfiqoYnkl1kKJCLA5iH90ywx327PU0p+bdnURAytWUeZPXaEuEl2OA==", + "license": "MIT", + "dependencies": { + "@lexical/clipboard": "0.35.0", + "@lexical/utils": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/text": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/text/-/text-0.35.0.tgz", + "integrity": "sha512-uaMh46BkysV8hK8wQwp5g/ByZW+2hPDt8ahAErxtf8NuzQem1FHG/f5RTchmFqqUDVHO3qLNTv4AehEGmXv8MA==", + "license": "MIT", + "dependencies": { + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/utils": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/utils/-/utils-0.35.0.tgz", + "integrity": "sha512-2H393EYDnFznYCDFOW3MHiRzwEO5M/UBhtUjvTT+9kc+qhX4U3zc8ixQalo5UmZ5B2nh7L/inXdTFzvSRXtsRA==", + "license": "MIT", + "dependencies": { + "@lexical/list": "0.35.0", + "@lexical/selection": "0.35.0", + "@lexical/table": "0.35.0", + "lexical": "0.35.0" + } + }, + "node_modules/@lexical/yjs": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.35.0.tgz", + "integrity": "sha512-3DSP7QpmTGYU9bN/yljP0PIao4tNIQtsR4ycauWNSawxs/GQCZtSmAPcLRnCm6qpqsDDjUtKjO/1Ej8FRp0m0w==", + "license": "MIT", + "dependencies": { + "@lexical/offset": "0.35.0", + "@lexical/selection": "0.35.0", + "lexical": "0.35.0" + }, + "peerDependencies": { + "yjs": ">=13.5.22" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.2", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.2.tgz", @@ -4781,6 +5060,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "peer": true, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -4857,6 +5147,34 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lexical": { + "version": "0.35.0", + "resolved": "https://registry.npmjs.org/lexical/-/lexical-0.35.0.tgz", + "integrity": "sha512-3VuV8xXhh5xJA6tzvfDvE0YBCMkIZUmxtRilJQDDdCgJCc+eut6qAv2qbN+pbqvarqcQqPN1UF+8YvsjmyOZpw==", + "license": "MIT" + }, + "node_modules/lib0": { + "version": "0.2.114", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", + "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/lightningcss": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", @@ -6621,6 +6939,15 @@ } } }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -6695,6 +7022,22 @@ "react": "^19.1.1" } }, + "node_modules/react-error-boundary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.4.tgz", + "integrity": "sha512-uM9uPzZJTF6wRQORmSrvOIgt4lJ9MC1sNgEOj2XGsDTRE4kmpWxg7ENK9EWNKJRMAOY9z0MuF4yIfl6gp4sotA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-is": { "version": "19.1.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.1.tgz", @@ -7326,6 +7669,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/tailwind-merge": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", @@ -7647,6 +7996,19 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -7767,6 +8129,24 @@ "node": ">= 6" } }, + "node_modules/yjs": { + "version": "13.6.27", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz", + "integrity": "sha512-OIDwaflOaq4wC6YlPBy2L6ceKeKuF7DeTxx+jPzv1FHn9tCZ0ZwSRnUBxD05E3yed46fv/FWJbvR+Ud7x0L7zw==", + "license": "MIT", + "peer": true, + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/zod": { "version": "3.25.76", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", diff --git a/package.json b/package.json index 9fdd303..0d06f93 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,13 @@ "@emotion/styled": "^11.14.1", "@fontsource/roboto": "^5.2.8", "@formatjs/intl-localematcher": "^0.6.1", + "@lexical/html": "^0.35.0", + "@lexical/link": "^0.35.0", + "@lexical/list": "^0.35.0", + "@lexical/plain-text": "^0.35.0", + "@lexical/react": "^0.35.0", + "@lexical/rich-text": "^0.35.0", + "@lexical/utils": "^0.35.0", "@mui/icons-material": "^7.3.2", "@mui/lab": "^7.0.0-beta.17", "@mui/material": "^7.3.2", @@ -53,6 +60,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "jsonwebtoken": "^9.0.2", + "lexical": "^0.35.0", "lucide-react": "^0.544.0", "negotiator": "^1.0.0", "next": "^15.5.3", @@ -74,6 +82,7 @@ "tailwindcss": "^4.1.13", "tinymce": "^8.1.2", "typescript": "^5.9.2", + "uuid": "^13.0.0", "zod": "^3.25.76", "zustand": "^5.0.8" }, diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8ed1ef8..1cf7c13 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -32,6 +32,8 @@ model User { createdPages Page[] @relation("PageCreator") updatedPages Page[] @relation("PageUpdater") uploadedFiles MediaFile[] + createdSocialMedia SocialMediaLink[] @relation("SocialMediaCreator") + updatedSocialMedia SocialMediaLink[] @relation("SocialMediaUpdater") @@index([role]) } @@ -347,4 +349,24 @@ enum PageStatus { DRAFT PUBLISHED ARCHIVED +} + +model SocialMediaLink { + id String @id @default(uuid()) + platform String // facebook, twitter, instagram, youtube, linkedin, tiktok, etc. + name String // Display name (e.g., "Facebook", "Instagram") + url String // Full URL to the social media profile + icon String // Icon identifier (material-ui icon name) + isEnabled Boolean @default(true) + order Int @default(0) // Display order in footer + createdBy String + updatedBy String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + creator User @relation("SocialMediaCreator", fields: [createdBy], references: [id]) + updater User @relation("SocialMediaUpdater", fields: [updatedBy], references: [id]) + + @@unique([platform]) + @@index([isEnabled, order]) } \ No newline at end of file