diff --git a/package-lock.json b/package-lock.json index 0806d80..47f0e54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "date-fns": "^3.6.0", "dompurify": "^3.0.9", "electron": "^29.1.0", + "emoji-picker-react": "^4.12.2", "gray-matter": "^4.0.3", "highlight.js": "^11.11.1", "jsdom": "^24.0.0", @@ -2827,6 +2828,21 @@ "integrity": "sha512-q7SQx6mkLy0GTJK9K9OiWeaBMV4XQtBSdf6MJUzDB/H/5tFXfIiX38Lci1Kl6SsgiEhz1SQI1ejEOU5asWEhwQ==", "license": "ISC" }, + "node_modules/emoji-picker-react": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/emoji-picker-react/-/emoji-picker-react-4.12.2.tgz", + "integrity": "sha512-6PDYZGlhidt+Kc0ay890IU4HLNfIR7/OxPvcNxw+nJ4HQhMKd8pnGnPn4n2vqC/arRFCNWQhgJP8rpsYKsz0GQ==", + "license": "MIT", + "dependencies": { + "flairup": "1.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -3799,6 +3815,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/flairup": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/flairup/-/flairup-1.0.0.tgz", + "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", + "license": "MIT" + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", diff --git a/package.json b/package.json index 4d3d066..bd66501 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "date-fns": "^3.6.0", "dompurify": "^3.0.9", "electron": "^29.1.0", + "emoji-picker-react": "^4.12.2", "gray-matter": "^4.0.3", "highlight.js": "^11.11.1", "jsdom": "^24.0.0", diff --git a/posts/pinned.json b/posts/pinned.json index a12acea..feac382 100644 --- a/posts/pinned.json +++ b/posts/pinned.json @@ -1,3 +1,11 @@ -[ - "welcome" -] \ No newline at end of file +{ + "pinned": [ + "welcome" + ], + "folderEmojis": { + "sdf": "🍉", + "sdfsdf": "🇭🇷", + "asdfasdf": "🌵", + "asdfasdfasdfasdf": "🫡" + } +} \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index d50f017..99021e1 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -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(() => { - if (typeof window !== 'undefined') { - return localStorage.getItem('lastExportChoice'); - } - return null; - }); + const [lastExportChoice, setLastExportChoice] = useState(null); + const [emojiPickerOpen, setEmojiPickerOpen] = useState(null); + const [emojiPickerAnchor, setEmojiPickerAnchor] = useState(null); + const emojiPickerRef = useRef(null); const router = useRouter(); const usernameRef = useRef(null); const passwordRef = useRef(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 (
{pinFeedback && ( @@ -965,18 +1065,75 @@ export default function AdminPage() { {/* Folders */} {currentNodes .filter((node): node is Folder => node.type === 'folder') - .map((folder) => ( -
setCurrentPath([...currentPath, folder.name])} - > -
- 📁 - {folder.name} + .map((folder) => { + const folderKey = getFolderKey(folder, currentPath); + const isPickerOpen = emojiPickerOpen === folderKey; + + return ( +
setCurrentPath([...currentPath, folder.name])} + > +
+
+ {folder.emoji || '📁'} + {folder.name} +
+ +
+ + {/* Emoji Picker - positioned outside the button to avoid conflicts */} + {isPickerOpen && ( +
e.stopPropagation()} + > +
+ Choose emoji for "{folder.name}" + +
+ { + console.log('Emoji selected:', emojiData.emoji, 'for folder:', folder.path); + handleSetFolderEmoji(folder.path, emojiData.emoji); + }} + autoFocusSearch + width={300} + height={350} + theme={getEmojiPickerTheme()} + /> +
+ )}
-
- ))} + ); + })} {/* Posts: pinned first, then unpinned */} {(() => { @@ -1052,16 +1209,73 @@ export default function AdminPage() {
{/* Folders */}
- {manageNodes.filter((n) => n.type === 'folder').map((folder: any) => ( -
setManagePath([...managePath, folder.name])} - > - 📁 - {folder.name} -
- ))} + {manageNodes.filter((n) => n.type === 'folder').map((folder: any) => { + const folderKey = getFolderKey(folder, managePath); + const isPickerOpen = emojiPickerOpen === folderKey; + + return ( +
setManagePath([...managePath, folder.name])} + > +
+ {folder.emoji || '📁'} + {folder.name} +
+ + + {/* Emoji Picker - positioned outside the button to avoid conflicts */} + {isPickerOpen && ( +
e.stopPropagation()} + > +
+ Choose emoji for "{folder.name}" + +
+ { + console.log('Emoji selected:', emojiData.emoji, 'for folder:', folder.path); + handleSetFolderEmoji(folder.path, emojiData.emoji); + }} + autoFocusSearch + width={300} + height={350} + theme={getEmojiPickerTheme()} + /> +
+ )} +
+ ); + })}
{/* Posts (pinned first) */}
diff --git a/src/app/api/admin/posts/route.ts b/src/app/api/admin/posts/route.ts index 45be787..499cdba 100644 --- a/src/app/api/admin/posts/route.ts +++ b/src/app/api/admin/posts/route.ts @@ -47,15 +47,29 @@ export async function POST(request: Request) { } } +export async function GET(request: Request) { + // Return the current pinned.json object + try { + const pinnedPath = path.join(postsDirectory, 'pinned.json'); + let pinnedData = { pinned: [], folderEmojis: {} }; + if (fs.existsSync(pinnedPath)) { + pinnedData = JSON.parse(fs.readFileSync(pinnedPath, 'utf8')); + } + return NextResponse.json(pinnedData); + } catch (error) { + return NextResponse.json({ error: 'Error reading pinned.json' }, { status: 500 }); + } +} + export async function PATCH(request: Request) { try { const body = await request.json(); - const { pinned } = body; // expects an array of slugs - if (!Array.isArray(pinned)) { - return NextResponse.json({ error: 'Invalid pinned data' }, { status: 400 }); + const { pinned, folderEmojis } = body; // expects pinned (array) and folderEmojis (object) + if (!Array.isArray(pinned) || typeof folderEmojis !== 'object') { + return NextResponse.json({ error: 'Invalid pinned or folderEmojis data' }, { status: 400 }); } const pinnedPath = path.join(postsDirectory, 'pinned.json'); - fs.writeFileSync(pinnedPath, JSON.stringify(pinned, null, 2), 'utf8'); + fs.writeFileSync(pinnedPath, JSON.stringify({ pinned, folderEmojis }, null, 2), 'utf8'); return NextResponse.json({ success: true }); } catch (error) { console.error('Error updating pinned.json:', error); diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index ca80c39..b9a328d 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -13,12 +13,15 @@ import { getPostsDirectory } from '@/lib/postsDirectory'; const postsDirectory = getPostsDirectory(); const pinnedPath = path.join(postsDirectory, 'pinned.json'); -let pinnedSlugs: string[] = []; +let pinnedData: { pinned: string[]; folderEmojis: Record } = { pinned: [], folderEmojis: {} }; if (fs.existsSync(pinnedPath)) { try { - pinnedSlugs = JSON.parse(fs.readFileSync(pinnedPath, 'utf8')); + const raw = fs.readFileSync(pinnedPath, 'utf8'); + pinnedData = JSON.parse(raw); + if (!pinnedData.pinned) pinnedData.pinned = []; + if (!pinnedData.folderEmojis) pinnedData.folderEmojis = {}; } catch { - pinnedSlugs = []; + pinnedData = { pinned: [], folderEmojis: {} }; } } @@ -102,7 +105,7 @@ async function getPostByPath(filePath: string, relPath: string) { summary: data.summary, content: processedContent, createdAt: createdAt.toISOString(), - pinned: pinnedSlugs.includes(relPath.replace(/\.md$/, '')), + pinned: pinnedData.pinned.includes(relPath.replace(/\.md$/, '')), }; } @@ -117,7 +120,8 @@ async function readPostsDir(dir: string, relDir = ''): Promise { if (entry.isDirectory()) { const children = await readPostsDir(fullPath, relPath); - folders.push({ type: 'folder', name: entry.name, path: relPath, children }); + const emoji = pinnedData.folderEmojis[relPath] || '📁'; + folders.push({ type: 'folder', name: entry.name, path: relPath, emoji, children }); } else if (entry.isFile() && entry.name.endsWith('.md')) { posts.push(await getPostByPath(fullPath, relPath)); } diff --git a/src/app/page.tsx b/src/app/page.tsx index b611b47..e30b504 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -22,6 +22,7 @@ interface Folder { name: string; path: string; children: (Folder | Post)[]; + emoji?: string; } type Node = Folder | Post; @@ -210,7 +211,7 @@ export default function Home() { className="sm:border sm:border-gray-200 sm:rounded-lg p-4 sm:p-6 bg-gray-50 cursor-pointer hover:bg-gray-100 transition-colors" onClick={() => setCurrentPath([...currentPath, folder.name])} > - 📁 {folder.name} + {folder.emoji || '📁'} {folder.name}
))}