Merge pull request 'mobile' (#5) from mobile into main
Some checks failed
Deploy / build-and-deploy (push) Failing after 1s
Some checks failed
Deploy / build-and-deploy (push) Failing after 1s
Reviewed-on: http://10.0.0.13:3002/rattatwinko/markdownblog/pulls/5 emoji picker working very good very nice
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -19,6 +19,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dompurify": "^3.0.9",
|
"dompurify": "^3.0.9",
|
||||||
"electron": "^29.1.0",
|
"electron": "^29.1.0",
|
||||||
|
"emoji-picker-react": "^4.12.2",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
@@ -2827,6 +2828,21 @@
|
|||||||
"integrity": "sha512-q7SQx6mkLy0GTJK9K9OiWeaBMV4XQtBSdf6MJUzDB/H/5tFXfIiX38Lci1Kl6SsgiEhz1SQI1ejEOU5asWEhwQ==",
|
"integrity": "sha512-q7SQx6mkLy0GTJK9K9OiWeaBMV4XQtBSdf6MJUzDB/H/5tFXfIiX38Lci1Kl6SsgiEhz1SQI1ejEOU5asWEhwQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "9.2.2",
|
"version": "9.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||||
@@ -3799,6 +3815,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/flat-cache": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"dompurify": "^3.0.9",
|
"dompurify": "^3.0.9",
|
||||||
"electron": "^29.1.0",
|
"electron": "^29.1.0",
|
||||||
|
"emoji-picker-react": "^4.12.2",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"jsdom": "^24.0.0",
|
"jsdom": "^24.0.0",
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
[
|
{
|
||||||
"welcome"
|
"pinned": [
|
||||||
]
|
"welcome"
|
||||||
|
],
|
||||||
|
"folderEmojis": {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ import Link from 'next/link';
|
|||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
import hljs from 'highlight.js';
|
import hljs from 'highlight.js';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { Theme } from 'emoji-picker-react';
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -28,6 +30,7 @@ interface Folder {
|
|||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
children: (Post | Folder)[];
|
children: (Post | Folder)[];
|
||||||
|
emoji?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
@@ -43,6 +46,8 @@ interface Post {
|
|||||||
|
|
||||||
type Node = Post | Folder;
|
type Node = Post | Folder;
|
||||||
|
|
||||||
|
const EmojiPicker = dynamic(() => import('emoji-picker-react'), { ssr: false });
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
@@ -85,12 +90,10 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
const [lastExportChoice, setLastExportChoice] = useState<string | null>(() => {
|
const [lastExportChoice, setLastExportChoice] = useState<string | null>(null);
|
||||||
if (typeof window !== 'undefined') {
|
const [emojiPickerOpen, setEmojiPickerOpen] = useState<string | null>(null);
|
||||||
return localStorage.getItem('lastExportChoice');
|
const [emojiPickerAnchor, setEmojiPickerAnchor] = useState<HTMLElement | null>(null);
|
||||||
}
|
const emojiPickerRef = useRef<HTMLDivElement>(null);
|
||||||
return null;
|
|
||||||
});
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const usernameRef = useRef<HTMLInputElement>(null);
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
const passwordRef = useRef<HTMLInputElement>(null);
|
const passwordRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -101,8 +104,8 @@ export default function AdminPage() {
|
|||||||
if (auth === 'true') {
|
if (auth === 'true') {
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
loadContent();
|
loadContent();
|
||||||
const interval = setInterval(loadContent, 500);
|
} else {
|
||||||
return () => clearInterval(interval);
|
router.push('/admin');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -639,6 +642,103 @@ export default function AdminPage() {
|
|||||||
setLastExportChoice(null);
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-3 sm:p-8">
|
<div className="min-h-screen bg-gray-100 p-3 sm:p-8">
|
||||||
{pinFeedback && (
|
{pinFeedback && (
|
||||||
@@ -965,18 +1065,75 @@ export default function AdminPage() {
|
|||||||
{/* Folders */}
|
{/* Folders */}
|
||||||
{currentNodes
|
{currentNodes
|
||||||
.filter((node): node is Folder => node.type === 'folder')
|
.filter((node): node is Folder => node.type === 'folder')
|
||||||
.map((folder) => (
|
.map((folder) => {
|
||||||
<div
|
const folderKey = getFolderKey(folder, currentPath);
|
||||||
key={folder.path}
|
const isPickerOpen = emojiPickerOpen === folderKey;
|
||||||
className="border rounded-lg p-3 sm:p-4 cursor-pointer hover:bg-gray-50"
|
|
||||||
onClick={() => setCurrentPath([...currentPath, folder.name])}
|
return (
|
||||||
>
|
<div
|
||||||
<div className="flex items-center gap-2">
|
key={folder.path}
|
||||||
<span className="text-xl sm:text-2xl">📁</span>
|
className="border rounded-lg p-3 sm:p-4 cursor-pointer hover:bg-gray-50 relative"
|
||||||
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
|
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>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
|
|
||||||
{/* Posts: pinned first, then unpinned */}
|
{/* Posts: pinned first, then unpinned */}
|
||||||
{(() => {
|
{(() => {
|
||||||
@@ -1052,16 +1209,73 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Folders */}
|
{/* Folders */}
|
||||||
<div className="space-y-2 mb-4">
|
<div className="space-y-2 mb-4">
|
||||||
{manageNodes.filter((n) => n.type === 'folder').map((folder: any) => (
|
{manageNodes.filter((n) => n.type === 'folder').map((folder: any) => {
|
||||||
<div
|
const folderKey = getFolderKey(folder, managePath);
|
||||||
key={folder.path}
|
const isPickerOpen = emojiPickerOpen === folderKey;
|
||||||
className="border rounded-lg p-3 cursor-pointer hover:bg-gray-50 flex items-center gap-2 justify-center"
|
|
||||||
onClick={() => setManagePath([...managePath, folder.name])}
|
return (
|
||||||
>
|
<div
|
||||||
<span className="text-xl sm:text-2xl">📁</span>
|
key={folder.path}
|
||||||
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
|
className="border rounded-lg p-3 cursor-pointer hover:bg-gray-50 flex items-center justify-between relative"
|
||||||
</div>
|
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>
|
</div>
|
||||||
{/* Posts (pinned first) */}
|
{/* Posts (pinned first) */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -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) {
|
export async function PATCH(request: Request) {
|
||||||
try {
|
try {
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { pinned } = body; // expects an array of slugs
|
const { pinned, folderEmojis } = body; // expects pinned (array) and folderEmojis (object)
|
||||||
if (!Array.isArray(pinned)) {
|
if (!Array.isArray(pinned) || typeof folderEmojis !== 'object') {
|
||||||
return NextResponse.json({ error: 'Invalid pinned data' }, { status: 400 });
|
return NextResponse.json({ error: 'Invalid pinned or folderEmojis data' }, { status: 400 });
|
||||||
}
|
}
|
||||||
const pinnedPath = path.join(postsDirectory, 'pinned.json');
|
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 });
|
return NextResponse.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating pinned.json:', error);
|
console.error('Error updating pinned.json:', error);
|
||||||
|
|||||||
@@ -13,12 +13,15 @@ import { getPostsDirectory } from '@/lib/postsDirectory';
|
|||||||
const postsDirectory = getPostsDirectory();
|
const postsDirectory = getPostsDirectory();
|
||||||
|
|
||||||
const pinnedPath = path.join(postsDirectory, 'pinned.json');
|
const pinnedPath = path.join(postsDirectory, 'pinned.json');
|
||||||
let pinnedSlugs: string[] = [];
|
let pinnedData: { pinned: string[]; folderEmojis: Record<string, string> } = { pinned: [], folderEmojis: {} };
|
||||||
if (fs.existsSync(pinnedPath)) {
|
if (fs.existsSync(pinnedPath)) {
|
||||||
try {
|
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 {
|
} catch {
|
||||||
pinnedSlugs = [];
|
pinnedData = { pinned: [], folderEmojis: {} };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +105,7 @@ async function getPostByPath(filePath: string, relPath: string) {
|
|||||||
summary: data.summary,
|
summary: data.summary,
|
||||||
content: processedContent,
|
content: processedContent,
|
||||||
createdAt: createdAt.toISOString(),
|
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<any[]> {
|
|||||||
|
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
const children = await readPostsDir(fullPath, relPath);
|
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')) {
|
} else if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||||
posts.push(await getPostByPath(fullPath, relPath));
|
posts.push(await getPostByPath(fullPath, relPath));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface Folder {
|
|||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
children: (Folder | Post)[];
|
children: (Folder | Post)[];
|
||||||
|
emoji?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Node = Folder | Post;
|
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"
|
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])}
|
onClick={() => setCurrentPath([...currentPath, folder.name])}
|
||||||
>
|
>
|
||||||
<span className="font-semibold text-base sm:text-lg">📁 {folder.name}</span>
|
<span className="font-semibold text-base sm:text-lg">{folder.emoji || '📁'} {folder.name}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user