emoji
This commit is contained in:
@@ -12,6 +12,8 @@ import Link from 'next/link';
|
||||
import { marked } from 'marked';
|
||||
import hljs from 'highlight.js';
|
||||
import matter from 'gray-matter';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Theme } from 'emoji-picker-react';
|
||||
|
||||
interface Post {
|
||||
slug: string;
|
||||
@@ -28,6 +30,7 @@ interface Folder {
|
||||
name: string;
|
||||
path: string;
|
||||
children: (Post | Folder)[];
|
||||
emoji?: string;
|
||||
}
|
||||
|
||||
interface Post {
|
||||
@@ -43,6 +46,8 @@ interface Post {
|
||||
|
||||
type Node = Post | Folder;
|
||||
|
||||
const EmojiPicker = dynamic(() => import('emoji-picker-react'), { ssr: false });
|
||||
|
||||
export default function AdminPage() {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const [username, setUsername] = useState('');
|
||||
@@ -85,12 +90,10 @@ export default function AdminPage() {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const [lastExportChoice, setLastExportChoice] = useState<string | null>(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('lastExportChoice');
|
||||
}
|
||||
return null;
|
||||
});
|
||||
const [lastExportChoice, setLastExportChoice] = useState<string | null>(null);
|
||||
const [emojiPickerOpen, setEmojiPickerOpen] = useState<string | null>(null);
|
||||
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
||||
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
||||
const router = useRouter();
|
||||
const usernameRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
@@ -101,8 +104,8 @@ export default function AdminPage() {
|
||||
if (auth === 'true') {
|
||||
setIsAuthenticated(true);
|
||||
loadContent();
|
||||
const interval = setInterval(loadContent, 500);
|
||||
return () => clearInterval(interval);
|
||||
} else {
|
||||
router.push('/admin');
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -639,6 +642,103 @@ export default function AdminPage() {
|
||||
setLastExportChoice(null);
|
||||
};
|
||||
|
||||
// Simple and reliable emoji update handler
|
||||
const handleSetFolderEmoji = async (folderPath: string, emoji: string) => {
|
||||
try {
|
||||
console.log('Setting emoji for folder:', folderPath, 'to:', emoji);
|
||||
|
||||
// Update local state immediately for instant feedback
|
||||
setNodes(prevNodes => {
|
||||
const updateFolderEmoji = (nodes: Node[]): Node[] => {
|
||||
return nodes.map(node => {
|
||||
if (node.type === 'folder') {
|
||||
if (node.path === folderPath) {
|
||||
return { ...node, emoji };
|
||||
} else if (node.children) {
|
||||
return { ...node, children: updateFolderEmoji(node.children) };
|
||||
}
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
return updateFolderEmoji(prevNodes);
|
||||
});
|
||||
|
||||
// Close picker immediately
|
||||
setEmojiPickerOpen(null);
|
||||
setEmojiPickerAnchor(null);
|
||||
|
||||
// Save to JSON file in background
|
||||
try {
|
||||
const pinnedRes = await fetch('/api/admin/posts', { method: 'GET' });
|
||||
const pinnedData = await pinnedRes.json();
|
||||
const folderEmojis = pinnedData.folderEmojis || {};
|
||||
folderEmojis[folderPath] = emoji;
|
||||
|
||||
await fetch('/api/admin/posts', {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folderEmojis, pinned: pinnedData.pinned || [] }),
|
||||
});
|
||||
|
||||
console.log('Emoji saved to JSON successfully');
|
||||
} catch (saveError) {
|
||||
console.error('Failed to save emoji to JSON:', saveError);
|
||||
// Don't show error to user since UI is already updated
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error updating folder emoji:', e);
|
||||
alert(`Error updating folder emoji: ${e instanceof Error ? e.message : 'Unknown error'}`);
|
||||
setEmojiPickerOpen(null);
|
||||
setEmojiPickerAnchor(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Add click outside handler to close emoji picker
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (emojiPickerOpen && emojiPickerRef.current && !emojiPickerRef.current.contains(event.target as Element)) {
|
||||
console.log('Closing emoji picker due to outside click');
|
||||
setEmojiPickerOpen(null);
|
||||
setEmojiPickerAnchor(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && emojiPickerOpen) {
|
||||
console.log('Closing emoji picker due to escape key');
|
||||
setEmojiPickerOpen(null);
|
||||
setEmojiPickerAnchor(null);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
};
|
||||
}, [emojiPickerOpen]);
|
||||
|
||||
// Add a unique key for each folder to prevent state conflicts
|
||||
const getFolderKey = (folder: Folder, currentPath: string[]) => {
|
||||
return `${currentPath.join('/')}/${folder.name}`;
|
||||
};
|
||||
|
||||
// Cleanup function to close picker
|
||||
const closeEmojiPicker = () => {
|
||||
setEmojiPickerOpen(null);
|
||||
setEmojiPickerAnchor(null);
|
||||
};
|
||||
|
||||
const getEmojiPickerTheme = (): Theme => {
|
||||
if (typeof window !== 'undefined' && window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
return Theme.DARK;
|
||||
}
|
||||
return Theme.LIGHT;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-3 sm:p-8">
|
||||
{pinFeedback && (
|
||||
@@ -965,18 +1065,75 @@ export default function AdminPage() {
|
||||
{/* Folders */}
|
||||
{currentNodes
|
||||
.filter((node): node is Folder => node.type === 'folder')
|
||||
.map((folder) => (
|
||||
<div
|
||||
key={folder.path}
|
||||
className="border rounded-lg p-3 sm:p-4 cursor-pointer hover:bg-gray-50"
|
||||
onClick={() => setCurrentPath([...currentPath, folder.name])}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl sm:text-2xl">📁</span>
|
||||
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
|
||||
.map((folder) => {
|
||||
const folderKey = getFolderKey(folder, currentPath);
|
||||
const isPickerOpen = emojiPickerOpen === folderKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={folder.path}
|
||||
className="border rounded-lg p-3 sm:p-4 cursor-pointer hover:bg-gray-50 relative"
|
||||
onClick={() => setCurrentPath([...currentPath, folder.name])}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl sm:text-2xl">{folder.emoji || '📁'}</span>
|
||||
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
|
||||
</div>
|
||||
<button
|
||||
className="px-3 sm:px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm sm:text-base font-medium shadow focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('Opening emoji picker for folder:', folderKey);
|
||||
setEmojiPickerOpen(folderKey);
|
||||
setEmojiPickerAnchor(e.currentTarget);
|
||||
}}
|
||||
aria-label="Change folder emoji"
|
||||
>
|
||||
😀 Emoji
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Emoji Picker - positioned outside the button to avoid conflicts */}
|
||||
{isPickerOpen && (
|
||||
<div
|
||||
ref={emojiPickerRef}
|
||||
className="fixed z-50 bg-white rounded shadow-lg p-2 emoji-picker-container border"
|
||||
style={{
|
||||
minWidth: 300,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium">Choose emoji for "{folder.name}"</span>
|
||||
<button
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
onClick={closeEmojiPicker}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<EmojiPicker
|
||||
onEmojiClick={(emojiData) => {
|
||||
console.log('Emoji selected:', emojiData.emoji, 'for folder:', folder.path);
|
||||
handleSetFolderEmoji(folder.path, emojiData.emoji);
|
||||
}}
|
||||
autoFocusSearch
|
||||
width={300}
|
||||
height={350}
|
||||
theme={getEmojiPickerTheme()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Posts: pinned first, then unpinned */}
|
||||
{(() => {
|
||||
@@ -1052,16 +1209,73 @@ export default function AdminPage() {
|
||||
</div>
|
||||
{/* Folders */}
|
||||
<div className="space-y-2 mb-4">
|
||||
{manageNodes.filter((n) => n.type === 'folder').map((folder: any) => (
|
||||
<div
|
||||
key={folder.path}
|
||||
className="border rounded-lg p-3 cursor-pointer hover:bg-gray-50 flex items-center gap-2 justify-center"
|
||||
onClick={() => setManagePath([...managePath, folder.name])}
|
||||
>
|
||||
<span className="text-xl sm:text-2xl">📁</span>
|
||||
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
|
||||
</div>
|
||||
))}
|
||||
{manageNodes.filter((n) => n.type === 'folder').map((folder: any) => {
|
||||
const folderKey = getFolderKey(folder, managePath);
|
||||
const isPickerOpen = emojiPickerOpen === folderKey;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={folder.path}
|
||||
className="border rounded-lg p-3 cursor-pointer hover:bg-gray-50 flex items-center justify-between relative"
|
||||
onClick={() => setManagePath([...managePath, folder.name])}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl sm:text-2xl">{folder.emoji || '📁'}</span>
|
||||
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
|
||||
</div>
|
||||
<button
|
||||
className="px-3 sm:px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm sm:text-base font-medium shadow focus:outline-none focus:ring-2 focus:ring-blue-400"
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
console.log('Opening emoji picker for folder:', folderKey);
|
||||
setEmojiPickerOpen(folderKey);
|
||||
setEmojiPickerAnchor(e.currentTarget);
|
||||
}}
|
||||
aria-label="Change folder emoji"
|
||||
>
|
||||
😀 Emoji
|
||||
</button>
|
||||
|
||||
{/* Emoji Picker - positioned outside the button to avoid conflicts */}
|
||||
{isPickerOpen && (
|
||||
<div
|
||||
ref={emojiPickerRef}
|
||||
className="fixed z-50 bg-white rounded shadow-lg p-2 emoji-picker-container border"
|
||||
style={{
|
||||
minWidth: 300,
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-sm font-medium">Choose emoji for "{folder.name}"</span>
|
||||
<button
|
||||
className="text-gray-500 hover:text-gray-700"
|
||||
onClick={closeEmojiPicker}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<EmojiPicker
|
||||
onEmojiClick={(emojiData) => {
|
||||
console.log('Emoji selected:', emojiData.emoji, 'for folder:', folder.path);
|
||||
handleSetFolderEmoji(folder.path, emojiData.emoji);
|
||||
}}
|
||||
autoFocusSearch
|
||||
width={300}
|
||||
height={350}
|
||||
theme={getEmojiPickerTheme()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* Posts (pinned first) */}
|
||||
<div className="space-y-2">
|
||||
|
||||
Reference in New Issue
Block a user