fuck my butthole pls
This commit is contained in:
@@ -11,6 +11,7 @@ interface Post {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
summary: string;
|
summary: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
pinned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Folder {
|
interface Folder {
|
||||||
@@ -28,6 +29,7 @@ interface Post {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
summary: string;
|
summary: string;
|
||||||
content: string;
|
content: string;
|
||||||
|
pinned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Node = Post | Folder;
|
type Node = Post | Folder;
|
||||||
@@ -52,6 +54,13 @@ export default function AdminPage() {
|
|||||||
item: null,
|
item: null,
|
||||||
});
|
});
|
||||||
const [showManageContent, setShowManageContent] = useState(false);
|
const [showManageContent, setShowManageContent] = useState(false);
|
||||||
|
const [managePath, setManagePath] = useState<string[]>([]);
|
||||||
|
const [pinned, setPinned] = useState<string[]>(() => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
return JSON.parse(localStorage.getItem('pinnedPosts') || '[]');
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -63,6 +72,10 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('pinnedPosts', JSON.stringify(pinned));
|
||||||
|
}, [pinned]);
|
||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/posts');
|
const response = await fetch('/api/posts');
|
||||||
@@ -180,6 +193,23 @@ export default function AdminPage() {
|
|||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Get nodes for manage content
|
||||||
|
const getManageNodes = (): Node[] => {
|
||||||
|
let currentNodes: Node[] = nodes;
|
||||||
|
for (const segment of managePath) {
|
||||||
|
const folder = currentNodes.find(
|
||||||
|
(n) => n.type === 'folder' && n.name === segment
|
||||||
|
) as Folder | undefined;
|
||||||
|
if (folder) {
|
||||||
|
currentNodes = folder.children;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentNodes;
|
||||||
|
};
|
||||||
|
const manageNodes = getManageNodes();
|
||||||
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
const handleDragOver = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDragging(true);
|
setIsDragging(true);
|
||||||
@@ -270,6 +300,12 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePin = (slug: string) => {
|
||||||
|
setPinned((prev) =>
|
||||||
|
prev.includes(slug) ? prev.filter((s) => s !== slug) : [slug, ...prev]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-100 p-8">
|
<div className="min-h-screen bg-gray-100 p-8">
|
||||||
{!isAuthenticated ? (
|
{!isAuthenticated ? (
|
||||||
@@ -483,11 +519,16 @@ export default function AdminPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Posts */}
|
{/* Posts: pinned first, then unpinned */}
|
||||||
{currentNodes
|
{(() => {
|
||||||
.filter((node): node is Post => node.type === 'post')
|
const posts = currentNodes.filter((node): node is Post => node.type === 'post');
|
||||||
.map((post) => (
|
const pinnedPosts = posts.filter(post => post.pinned);
|
||||||
<div key={post.slug} className="border rounded-lg p-4">
|
const unpinnedPosts = posts.filter(post => !post.pinned);
|
||||||
|
return [...pinnedPosts, ...unpinnedPosts].map((post) => (
|
||||||
|
<div key={post.slug} className="border rounded-lg p-4 relative">
|
||||||
|
{post.pinned && (
|
||||||
|
<span title="Pinned" className="absolute top-2 right-2 text-2xl">📌</span>
|
||||||
|
)}
|
||||||
<h3 className="text-xl font-semibold">{post.title}</h3>
|
<h3 className="text-xl font-semibold">{post.title}</h3>
|
||||||
<p className="text-gray-600">{post.date}</p>
|
<p className="text-gray-600">{post.date}</p>
|
||||||
<p className="text-sm text-gray-500">{post.summary}</p>
|
<p className="text-sm text-gray-500">{post.summary}</p>
|
||||||
@@ -499,7 +540,8 @@ export default function AdminPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -516,7 +558,62 @@ export default function AdminPage() {
|
|||||||
<p className="text-gray-600 mb-2">
|
<p className="text-gray-600 mb-2">
|
||||||
Delete posts and folders, manage your content structure
|
Delete posts and folders, manage your content structure
|
||||||
</p>
|
</p>
|
||||||
<a href="/admin/manage" className="text-blue-600 hover:underline">Go to Content Manager</a>
|
{/* Folder navigation breadcrumbs */}
|
||||||
|
<div className="flex flex-wrap justify-center gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
onClick={() => setManagePath([])}
|
||||||
|
className={`px-2 py-1 rounded ${managePath.length === 0 ? 'bg-blue-100 text-blue-800' : 'hover:bg-gray-200'}`}
|
||||||
|
>
|
||||||
|
Root
|
||||||
|
</button>
|
||||||
|
{managePath.map((name, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setManagePath(managePath.slice(0, idx + 1))}
|
||||||
|
className={`px-2 py-1 rounded ${idx === managePath.length - 1 ? 'bg-blue-100 text-blue-800' : 'hover:bg-gray-200'}`}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</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-2xl">📁</span>
|
||||||
|
<span className="font-semibold text-lg">{folder.name}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{/* Posts (pinned first) */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{[...manageNodes.filter((n) => n.type === 'post' && pinned.includes(n.slug)),
|
||||||
|
...manageNodes.filter((n) => n.type === 'post' && !pinned.includes(n.slug))
|
||||||
|
].map((post: any) => (
|
||||||
|
<div
|
||||||
|
key={post.slug}
|
||||||
|
className={`border rounded-lg p-3 flex items-center gap-3 justify-between ${pinned.includes(post.slug) ? 'bg-yellow-100 border-yellow-400' : ''}`}
|
||||||
|
>
|
||||||
|
<div className="flex-1 text-left">
|
||||||
|
<div className="font-semibold">{post.title}</div>
|
||||||
|
<div className="text-xs text-gray-500">{post.date}</div>
|
||||||
|
<div className="text-xs text-gray-400">{post.summary}</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => handlePin(post.slug)}
|
||||||
|
className={`text-2xl focus:outline-none ${pinned.includes(post.slug) ? 'text-yellow-500' : 'text-gray-400 hover:text-yellow-500'}`}
|
||||||
|
title={pinned.includes(post.slug) ? 'Unpin' : 'Pin'}
|
||||||
|
>
|
||||||
|
{pinned.includes(post.slug) ? '★' : '☆'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<a href="/admin/manage" className="block mt-6 text-blue-600 hover:underline">Go to Content Manager</a>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ import html from 'remark-html';
|
|||||||
|
|
||||||
const postsDirectory = path.join(process.cwd(), 'posts');
|
const postsDirectory = path.join(process.cwd(), 'posts');
|
||||||
|
|
||||||
|
const pinnedPath = path.join(postsDirectory, 'pinned.json');
|
||||||
|
let pinnedSlugs: string[] = [];
|
||||||
|
if (fs.existsSync(pinnedPath)) {
|
||||||
|
try {
|
||||||
|
pinnedSlugs = JSON.parse(fs.readFileSync(pinnedPath, 'utf8'));
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to get file creation date
|
// Function to get file creation date
|
||||||
function getFileCreationDate(filePath: string): Date {
|
function getFileCreationDate(filePath: string): Date {
|
||||||
const stats = fs.statSync(filePath);
|
const stats = fs.statSync(filePath);
|
||||||
@@ -27,6 +35,7 @@ async function getPostByPath(filePath: string, relPath: string) {
|
|||||||
summary: data.summary,
|
summary: data.summary,
|
||||||
content: processedContent.toString(),
|
content: processedContent.toString(),
|
||||||
createdAt: createdAt.toISOString(),
|
createdAt: createdAt.toISOString(),
|
||||||
|
pinned: pinnedSlugs.includes(relPath.replace(/\.md$/, '')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,21 @@ export default function RootLayout({
|
|||||||
</Head>
|
</Head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<header className="bg-gray-100 p-4">
|
<header className="bg-gray-100 p-4">
|
||||||
<div className="container mx-auto flex justify-between items-center">
|
<div className="container mx-auto flex flex-col md:flex-row justify-between items-center">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 justify-center md:justify-start w-full md:w-auto mb-2 md:mb-0">
|
||||||
<img src="https://img.shields.io/badge/markdown-%23000000.svg?style=for-the-badge&logo=markdown&logoColor=white" alt="Markdown" />
|
<img src="https://img.shields.io/badge/markdown-%23000000.svg?style=for-the-badge&logo=markdown&logoColor=white" alt="Markdown" />
|
||||||
<img src="https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white" alt="Next.js" />
|
<img src="https://img.shields.io/badge/Next-black?style=for-the-badge&logo=next.js&logoColor=white" alt="Next.js" />
|
||||||
<img src="https://img.shields.io/badge/tailwindcss-%2338BDF8.svg?style=for-the-badge&logo=tailwind-css&logoColor=white" alt="Tailwind CSS" />
|
<img src="https://img.shields.io/badge/tailwindcss-%2338BDF8.svg?style=for-the-badge&logo=tailwind-css&logoColor=white" alt="Tailwind CSS" />
|
||||||
<img src="https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
|
<img src="https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex gap-2 justify-center w-full md:w-auto mt-2 md:mt-0">
|
||||||
|
<a href="https://instagram.com/rattatwinko" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img src="https://img.shields.io/badge/Instagram-%23E4405F.svg?style=for-the-badge&logo=Instagram&logoColor=white" alt="Instagram" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/ZockerKatze" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img src="https://img.shields.io/badge/GitHub-%23121011.svg?style=for-the-badge&logo=GitHub&logoColor=white" alt="GitHub" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<Link href="/admin" className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full">
|
<Link href="/admin" className="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full">
|
||||||
Admin Login
|
Admin Login
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ interface Post {
|
|||||||
summary: string;
|
summary: string;
|
||||||
content: string;
|
content: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
pinned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Folder {
|
interface Folder {
|
||||||
@@ -100,8 +101,15 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* Posts */}
|
{/* Posts */}
|
||||||
{nodes.filter((n) => n.type === 'post').map((post: any) => (
|
{(() => {
|
||||||
<article key={post.slug} className="border rounded-lg p-6 hover:shadow-lg transition-shadow">
|
const posts = nodes.filter((n) => n.type === 'post');
|
||||||
|
const pinnedPosts = posts.filter((post: any) => post.pinned);
|
||||||
|
const unpinnedPosts = posts.filter((post: any) => !post.pinned);
|
||||||
|
return [...pinnedPosts, ...unpinnedPosts].map((post: any) => (
|
||||||
|
<article key={post.slug} className="border rounded-lg p-6 hover:shadow-lg transition-shadow relative">
|
||||||
|
{post.pinned && (
|
||||||
|
<span className="absolute top-4 right-4 text-2xl" title="Pinned">📌</span>
|
||||||
|
)}
|
||||||
<Link href={`/posts/${post.slug}`}>
|
<Link href={`/posts/${post.slug}`}>
|
||||||
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
|
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
|
||||||
<div className="text-gray-600 mb-4">
|
<div className="text-gray-600 mb-4">
|
||||||
@@ -131,7 +139,8 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</article>
|
</article>
|
||||||
))}
|
));
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user