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:
2025-09-24 12:08:01 +00:00
parent 3b34d7518b
commit 4adf1d286e
17 changed files with 1817 additions and 122 deletions

View File

@@ -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' },

View File

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

View File

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

View File

@@ -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 (
<Box sx={{ mt: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 1 }}>
<Typography variant="subtitle2">Content</Typography>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
<Typography variant="subtitle2">Rich Text Content</Typography>
<Button size="small" onClick={() => setImageUploadOpen(true)}>
Insert Image
</Button>
</Box>
<Editor
onInit={(evt, editor) => editorRef.current = { getEditor: () => editor }}
<SimpleRichEditor
value={formData.content}
onEditorChange={(content) => 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}
/>
</Box>
);
@@ -237,7 +198,7 @@ export function PageEditor({ open, onClose, page, onSave }: PageEditorProps) {
</Box>
<TextField
multiline
rows={20}
rows={60}
fullWidth
variant="outlined"
value={formData.content}
@@ -253,7 +214,7 @@ export function PageEditor({ open, onClose, page, onSave }: PageEditorProps) {
<Typography variant="subtitle2" gutterBottom>Markdown Content</Typography>
<TextField
multiline
rows={20}
rows={60}
fullWidth
variant="outlined"
value={formData.content}

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

View File

@@ -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<DynamicPage[]>([])
const [socialLinks, setSocialLinks] = useState<SocialMediaLink[]>([])
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 <IconComponent />
}
return (
<Paper component="footer" sx={{ bgcolor: 'grey.900', color: 'white', py: 6 }}>
<Container maxWidth="lg">
@@ -74,12 +115,7 @@ export function Footer() {
{t('footer.quickLinks.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.quickLinks.about')}
</Button>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.quickLinks.blog')}
</Button>
{/* Static important links */}
<Button
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
@@ -87,22 +123,21 @@ export function Footer() {
>
{t('footer.quickLinks.contact')}
</Button>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.quickLinks.support')}
</Button>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.quickLinks.api')}
</Button>
{dynamicPages.map((page) => (
<Button
key={page.id}
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
onClick={() => router.push(`/${locale}/pages/${page.slug}`)}
>
{page.title}
</Button>
))}
{/* 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) => (
<Button
key={page.id}
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
onClick={() => router.push(`/${locale}/pages/${page.slug}`)}
>
{page.title}
</Button>
))}
</Box>
</Box>
@@ -112,18 +147,20 @@ export function Footer() {
{t('footer.legal.title')}
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.legal.terms')}
</Button>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.legal.privacy')}
</Button>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.legal.cookies')}
</Button>
<Button color="inherit" sx={{ justifyContent: 'flex-start', p: 0 }}>
{t('footer.legal.gdpr')}
</Button>
{/* Dynamic legal pages */}
{dynamicPages
.filter(page => ['terms', 'privacy', 'cookies', 'gdpr'].includes(page.slug))
.sort((a, b) => (a.footerOrder || 999) - (b.footerOrder || 999))
.map((page) => (
<Button
key={page.id}
color="inherit"
sx={{ justifyContent: 'flex-start', p: 0 }}
onClick={() => router.push(`/${locale}/pages/${page.slug}`)}
>
{page.title}
</Button>
))}
</Box>
</Box>
@@ -132,19 +169,25 @@ export function Footer() {
<Typography variant="h6" sx={{ mb: 2 }}>
{t('footer.social.title')}
</Typography>
<Box sx={{ display: 'flex', gap: 1 }}>
<IconButton color="inherit" size="small">
<Facebook />
</IconButton>
<IconButton color="inherit" size="small">
<Twitter />
</IconButton>
<IconButton color="inherit" size="small">
<Instagram />
</IconButton>
<IconButton color="inherit" size="small">
<YouTube />
</IconButton>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
{socialLinks.map((link) => (
<IconButton
key={link.id}
color="inherit"
size="small"
href={link.url}
target="_blank"
rel="noopener noreferrer"
title={link.name}
>
{renderSocialIcon(link.icon)}
</IconButton>
))}
{socialLinks.length === 0 && (
<Typography variant="caption" color="grey.500">
No social media links configured
</Typography>
)}
</Box>
</Box>
</Box>