fuck my butthole pls
This commit is contained in:
@@ -11,6 +11,7 @@ interface Post {
|
||||
tags: string[];
|
||||
summary: string;
|
||||
content: string;
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
interface Folder {
|
||||
@@ -28,6 +29,7 @@ interface Post {
|
||||
tags: string[];
|
||||
summary: string;
|
||||
content: string;
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
type Node = Post | Folder;
|
||||
@@ -52,6 +54,13 @@ export default function AdminPage() {
|
||||
item: null,
|
||||
});
|
||||
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();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -63,6 +72,10 @@ export default function AdminPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('pinnedPosts', JSON.stringify(pinned));
|
||||
}, [pinned]);
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
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 (
|
||||
<div className="min-h-screen bg-gray-100 p-8">
|
||||
{!isAuthenticated ? (
|
||||
@@ -483,11 +519,16 @@ export default function AdminPage() {
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Posts */}
|
||||
{currentNodes
|
||||
.filter((node): node is Post => node.type === 'post')
|
||||
.map((post) => (
|
||||
<div key={post.slug} className="border rounded-lg p-4">
|
||||
{/* Posts: pinned first, then unpinned */}
|
||||
{(() => {
|
||||
const posts = currentNodes.filter((node): node is Post => node.type === 'post');
|
||||
const pinnedPosts = posts.filter(post => post.pinned);
|
||||
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>
|
||||
<p className="text-gray-600">{post.date}</p>
|
||||
<p className="text-sm text-gray-500">{post.summary}</p>
|
||||
@@ -499,7 +540,8 @@ export default function AdminPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -516,7 +558,62 @@ export default function AdminPage() {
|
||||
<p className="text-gray-600 mb-2">
|
||||
Delete posts and folders, manage your content structure
|
||||
</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>
|
||||
|
||||
@@ -7,6 +7,14 @@ import html from 'remark-html';
|
||||
|
||||
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 getFileCreationDate(filePath: string): Date {
|
||||
const stats = fs.statSync(filePath);
|
||||
@@ -27,6 +35,7 @@ async function getPostByPath(filePath: string, relPath: string) {
|
||||
summary: data.summary,
|
||||
content: processedContent.toString(),
|
||||
createdAt: createdAt.toISOString(),
|
||||
pinned: pinnedSlugs.includes(relPath.replace(/\.md$/, '')),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,21 @@ export default function RootLayout({
|
||||
</Head>
|
||||
<body className={inter.className}>
|
||||
<header className="bg-gray-100 p-4">
|
||||
<div className="container mx-auto flex justify-between items-center">
|
||||
<div className="flex gap-2">
|
||||
<div className="container mx-auto flex flex-col md:flex-row justify-between items-center">
|
||||
<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/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/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white" alt="TypeScript" />
|
||||
</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">
|
||||
Admin Login
|
||||
</Link>
|
||||
|
||||
@@ -13,6 +13,7 @@ interface Post {
|
||||
summary: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
interface Folder {
|
||||
@@ -100,38 +101,46 @@ export default function Home() {
|
||||
</div>
|
||||
))}
|
||||
{/* 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">
|
||||
<Link href={`/posts/${post.slug}`}>
|
||||
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
|
||||
<div className="text-gray-600 mb-4">
|
||||
{post.date ? (
|
||||
<div>Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex">
|
||||
<span className="text-2xl animate-spin mr-2">⚙️</span>
|
||||
<span className="text-2xl animate-spin-reverse">⚙️</span>
|
||||
{(() => {
|
||||
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}`}>
|
||||
<h2 className="text-2xl font-semibold mb-2">{post.title}</h2>
|
||||
<div className="text-gray-600 mb-4">
|
||||
{post.date ? (
|
||||
<div>Veröffentlicht: {format(new Date(post.date), 'd. MMMM yyyy')}</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="flex">
|
||||
<span className="text-2xl animate-spin mr-2">⚙️</span>
|
||||
<span className="text-2xl animate-spin-reverse">⚙️</span>
|
||||
</div>
|
||||
<div className="text-xl font-bold mt-2">In Bearbeitung</div>
|
||||
</div>
|
||||
<div className="text-xl font-bold mt-2">In Bearbeitung</div>
|
||||
</div>
|
||||
)}
|
||||
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
||||
</div>
|
||||
<p className="text-gray-700 mb-4">{post.summary}</p>
|
||||
<div className="flex gap-2">
|
||||
{post.tags.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
)}
|
||||
<div>Erstellt: {format(new Date(post.createdAt), 'd. MMMM yyyy HH:mm')}</div>
|
||||
</div>
|
||||
<p className="text-gray-700 mb-4">{post.summary}</p>
|
||||
<div className="flex gap-2">
|
||||
{post.tags.map((tag: string) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="bg-gray-100 text-gray-800 px-3 py-1 rounded-full text-sm"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</Link>
|
||||
</article>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user