mobile ; heading scroll broken

This commit is contained in:
2025-06-21 20:39:22 +02:00
parent 7b556b2d09
commit 1cc864e4f0
12 changed files with 1117 additions and 325 deletions

View File

@@ -640,15 +640,15 @@ export default function AdminPage() {
};
return (
<div className="min-h-screen bg-gray-100 p-8">
<div className="min-h-screen bg-gray-100 p-3 sm:p-8">
{pinFeedback && (
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-6 py-2 rounded shadow-lg z-50">
<div className="fixed top-4 left-1/2 transform -translate-x-1/2 bg-blue-600 text-white px-4 sm:px-6 py-2 rounded shadow-lg z-50 text-sm sm:text-base">
{pinFeedback}
</div>
)}
{!isAuthenticated ? (
<div className="max-w-md mx-auto bg-white p-8 rounded-lg shadow-md">
<h1 className="text-2xl font-bold mb-6">Admin Login</h1>
<div className="max-w-md mx-auto bg-white p-4 sm:p-8 rounded-lg shadow-md">
<h1 className="text-xl sm:text-2xl font-bold mb-4 sm:mb-6">Admin Login</h1>
<form onSubmit={handleLogin} className="space-y-4" autoComplete="on">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700">
@@ -661,7 +661,7 @@ export default function AdminPage() {
ref={usernameRef}
value={username}
onChange={(e) => setUsername(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 text-base"
required
autoComplete="username"
/>
@@ -677,14 +677,14 @@ export default function AdminPage() {
ref={passwordRef}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 px-3 py-2 text-base"
required
autoComplete="current-password"
/>
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
className="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 text-base font-medium"
>
Login
</button>
@@ -692,31 +692,32 @@ export default function AdminPage() {
</div>
) : (
<div className="max-w-6xl mx-auto">
<div className="flex justify-between items-center mb-8">
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
<div className="flex gap-2">
{/* Mobile-friendly header */}
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6 sm:mb-8 space-y-4 sm:space-y-0">
<h1 className="text-2xl sm:text-3xl font-bold">Admin Dashboard</h1>
<div className="flex flex-col sm:flex-row gap-2 sm:gap-2">
<button
onClick={handleLogout}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
className="px-4 py-3 sm:py-2 bg-red-600 text-white rounded hover:bg-red-700 text-base font-medium"
>
Logout
</button>
<button
onClick={() => setShowChangePassword(true)}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
className="px-4 py-3 sm:py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-base font-medium"
>
Passwort ändern
</button>
{/* Docker warning above export button */}
{isDocker && (
<div className="mb-2 px-4 py-2 bg-yellow-200 text-yellow-900 rounded border border-yellow-400 font-semibold text-sm text-center">
<div className="px-4 py-2 bg-yellow-200 text-yellow-900 rounded border border-yellow-400 font-semibold text-xs sm:text-sm text-center">
<span className="font-bold">Warning:</span> Docker is in use. Exporting will export the entire <span className="font-mono">/app</span> root directory (including all files and folders in the container's root).
</div>
)}
<div className="flex items-center gap-2">
<button
onClick={handleExportTarball}
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
className="px-4 py-3 sm:py-2 bg-green-600 text-white rounded hover:bg-green-700 text-base font-medium"
title="Export Docker Posts"
>
Export Posts
@@ -726,7 +727,7 @@ export default function AdminPage() {
<span>💾 {lastExportChoice === 'docker' ? 'Docker' : 'Local'}</span>
<button
onClick={clearExportChoice}
className="text-red-500 hover:text-red-700"
className="text-red-500 hover:text-red-700 p-1"
title="Clear remembered choice"
>
×
@@ -739,16 +740,16 @@ export default function AdminPage() {
{/* Password Change Modal */}
{showChangePassword && (
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50">
<div className="bg-white p-8 rounded-lg shadow-lg max-w-md w-full relative">
<div className="fixed inset-0 bg-black bg-opacity-40 flex items-center justify-center z-50 p-4">
<div className="bg-white p-4 sm:p-8 rounded-lg shadow-lg max-w-md w-full relative">
<button
className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 text-2xl"
className="absolute top-2 right-2 text-gray-400 hover:text-gray-700 text-2xl p-2"
onClick={() => setShowChangePassword(false)}
title="Schließen"
>
×
</button>
<h2 className="text-xl font-bold mb-4">Passwort ändern</h2>
<h2 className="text-lg sm:text-xl font-bold mb-4">Passwort ändern</h2>
<form onSubmit={handleChangePassword} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Altes Passwort</label>
@@ -756,7 +757,7 @@ export default function AdminPage() {
type="password"
value={changePwOld}
onChange={e => setChangePwOld(e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-base"
required
autoComplete="current-password"
/>
@@ -767,7 +768,7 @@ export default function AdminPage() {
type="password"
value={changePwNew}
onChange={e => setChangePwNew(e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-base"
required
autoComplete="new-password"
/>
@@ -778,7 +779,7 @@ export default function AdminPage() {
type="password"
value={changePwConfirm}
onChange={e => setChangePwConfirm(e.target.value)}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-base"
required
autoComplete="new-password"
/>
@@ -788,7 +789,7 @@ export default function AdminPage() {
)}
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700"
className="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 text-base font-medium"
>
Passwort speichern
</button>
@@ -797,17 +798,17 @@ export default function AdminPage() {
</div>
)}
{/* Breadcrumbs with back button */}
<div className="flex items-center gap-4 mb-6">
{/* Mobile-friendly breadcrumbs */}
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4 mb-4 sm:mb-6">
{currentPath.length > 0 && (
<button
onClick={() => setCurrentPath(currentPath.slice(0, -1))}
className="flex items-center gap-2 px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors"
className="flex items-center gap-2 px-3 sm:px-4 py-2 bg-gray-200 rounded hover:bg-gray-300 transition-colors text-sm sm:text-base"
title="Go back one level"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
className="h-4 w-4 sm:h-5 sm:w-5"
viewBox="0 0 20 20"
fill="currentColor"
>
@@ -820,13 +821,13 @@ export default function AdminPage() {
Back
</button>
)}
<div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-1 sm:gap-2">
{breadcrumbs.map((crumb, index) => (
<div key={crumb.path.join('/')} className="flex items-center">
{index > 0 && <span className="mx-2 text-gray-500">/</span>}
{index > 0 && <span className="mx-1 sm:mx-2 text-gray-500">/</span>}
<button
onClick={() => setCurrentPath(crumb.path)}
className={`px-2 py-1 rounded ${
className={`px-2 py-1 rounded text-sm sm:text-base ${
index === breadcrumbs.length - 1
? 'bg-blue-100 text-blue-800'
: 'hover:bg-gray-200'
@@ -840,25 +841,25 @@ export default function AdminPage() {
</div>
{/* Show current folder path above post creation form */}
<div className="mb-2 text-gray-500 text-sm">
<div className="mb-2 text-gray-500 text-xs sm:text-sm">
Current folder: <span className="font-mono">{currentPath.join('/') || 'root'}</span>
</div>
{/* Create Folder Form */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h2 className="text-2xl font-bold mb-4">Create New Folder</h2>
<form onSubmit={handleCreateFolder} className="flex gap-4">
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-6 sm:mb-8">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Create New Folder</h2>
<form onSubmit={handleCreateFolder} className="flex flex-col sm:flex-row gap-3 sm:gap-4">
<input
type="text"
value={newFolderName}
onChange={(e) => setNewFolderName(e.target.value)}
placeholder="Folder name"
className="flex-1 rounded-md border border-gray-300 px-3 py-2"
className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-base"
required
/>
<button
type="submit"
className="px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700"
className="px-4 py-3 sm:py-2 bg-green-600 text-white rounded hover:bg-green-700 text-base font-medium"
>
Create Folder
</button>
@@ -867,7 +868,7 @@ export default function AdminPage() {
{/* Drag and Drop Zone */}
<div
className={`mb-8 p-8 border-2 border-dashed rounded-lg text-center ${
className={`mb-6 sm:mb-8 p-4 sm:p-8 border-2 border-dashed rounded-lg text-center ${
isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
}`}
onDragOver={handleDragOver}
@@ -875,14 +876,14 @@ export default function AdminPage() {
onDrop={handleDrop}
>
<div className="text-gray-600">
<p className="text-lg font-medium">Drag and drop Markdown files here</p>
<p className="text-sm">Files will be uploaded to: {currentPath.join('/') || 'root'}</p>
<p className="text-base sm:text-lg font-medium">Drag and drop Markdown files here</p>
<p className="text-xs sm:text-sm">Files will be uploaded to: {currentPath.join('/') || 'root'}</p>
</div>
</div>
{/* Create Post Form */}
<div className="bg-white rounded-lg shadow p-6 mb-8">
<h2 className="text-2xl font-bold mb-4">Create New Post</h2>
<div className="bg-white rounded-lg shadow p-4 sm:p-6 mb-6 sm:mb-8">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Create New Post</h2>
<form onSubmit={editingPost ? handleEditPost : handleCreatePost} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700">Title</label>
@@ -890,7 +891,7 @@ export default function AdminPage() {
type="text"
value={newPost.title}
onChange={(e) => setNewPost({ ...newPost, title: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-base"
required
/>
</div>
@@ -900,7 +901,7 @@ export default function AdminPage() {
type="date"
value={newPost.date}
onChange={(e) => setNewPost({ ...newPost, date: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-base"
required
/>
</div>
@@ -910,7 +911,7 @@ export default function AdminPage() {
type="text"
value={newPost.tags}
onChange={(e) => setNewPost({ ...newPost, tags: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-base"
placeholder="tag1, tag2, tag3"
/>
</div>
@@ -919,41 +920,36 @@ export default function AdminPage() {
<textarea
value={newPost.summary}
onChange={(e) => setNewPost({ ...newPost, summary: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 text-base"
rows={2}
required
/>
</div>
{/* Labels Row */}
<div className="flex flex-row gap-4">
<div className="w-full md:w-1/2 flex items-end h-10">
<label className="block text-sm font-medium text-gray-700">Content (Markdown)</label>
</div>
<div className="w-full md:w-1/2 flex items-end h-10">
<label className="block text-sm font-medium text-gray-700">Live Preview</label>
</div>
</div>
<div className="flex flex-col md:flex-row gap-4 mt-1">
{/* Markdown Editor */}
<div className="w-full md:w-1/2 flex flex-col">
<textarea
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
className="w-full rounded-md border border-gray-300 px-3 py-2 font-mono" style={{ height: '320px' }}
rows={10}
required
/>
</div>
{/* Live Markdown Preview */}
<div className="w-full md:w-1/2 flex flex-col">
<div className="p-4 border rounded bg-gray-50 overflow-auto" style={{ height: '320px' }}>
<div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: previewHtml }} />
{/* Mobile-friendly content editor */}
<div className="space-y-4">
<div className="flex flex-col sm:flex-row gap-4">
<div className="w-full sm:w-1/2">
<label className="block text-sm font-medium text-gray-700 mb-2">Content (Markdown)</label>
<textarea
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
className="w-full rounded-md border border-gray-300 px-3 py-2 font-mono text-sm sm:text-base"
style={{ height: '240px' }}
rows={10}
required
/>
</div>
<div className="w-full sm:w-1/2">
<label className="block text-sm font-medium text-gray-700 mb-2">Live Preview</label>
<div className="p-3 sm:p-4 border rounded bg-gray-50 overflow-auto" style={{ height: '240px' }}>
<div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: previewHtml }} />
</div>
</div>
</div>
</div>
<button
type="submit"
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
className="w-full flex justify-center py-3 px-4 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-blue-600 hover:bg-blue-700"
>
{editingPost ? 'Save Changes' : 'Create Post'}
</button>
@@ -961,8 +957,8 @@ export default function AdminPage() {
</div>
{/* Content List */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-2xl font-bold mb-4">Content</h2>
<div className="bg-white rounded-lg shadow p-4 sm:p-6">
<h2 className="text-xl sm:text-2xl font-bold mb-4">Content</h2>
<div className="space-y-4">
{/* Folders */}
{currentNodes
@@ -970,12 +966,12 @@ export default function AdminPage() {
.map((folder) => (
<div
key={folder.path}
className="border rounded-lg p-4 cursor-pointer hover:bg-gray-50"
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-2xl">📁</span>
<span className="font-semibold text-lg">{folder.name}</span>
<span className="text-xl sm:text-2xl">📁</span>
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
</div>
</div>
))}
@@ -986,9 +982,9 @@ export default function AdminPage() {
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 flex flex-col gap-2">
<div className="flex items-center gap-4">
<h3 className="text-xl font-semibold flex-1">{post.title}</h3>
<div key={post.slug} className="border rounded-lg p-3 sm:p-4 relative flex flex-col gap-2">
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4">
<h3 className="text-lg sm:text-xl font-semibold flex-1">{post.title}</h3>
<button
onClick={() => {
// Split post.slug into folder and filename
@@ -997,19 +993,19 @@ export default function AdminPage() {
const folder = slugParts.length > 1 ? slugParts.slice(0, -1).join('/') : currentPath.join('/');
loadPostRaw(filename, folder);
}}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-lg font-bold shadow focus:outline-none focus:ring-2 focus:ring-blue-400"
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"
>
✏️ Edit
</button>
{post.pinned && (
<span title="Angeheftet" className="text-2xl ml-2">📌</span>
<span title="Angeheftet" className="text-xl sm:text-2xl">📌</span>
)}
</div>
<p className="text-gray-600">{post.date}</p>
<p className="text-sm text-gray-500">{post.summary}</p>
<div className="mt-2 flex gap-2">
<p className="text-sm sm:text-base text-gray-600">{post.date}</p>
<p className="text-xs sm:text-sm text-gray-500">{post.summary}</p>
<div className="mt-2 flex flex-wrap gap-1 sm:gap-2">
{(post.tags || []).map((tag) => (
<span key={tag} className="bg-gray-100 px-2 py-1 rounded text-sm">
<span key={tag} className="bg-gray-100 px-2 py-1 rounded text-xs sm:text-sm">
{tag}
</span>
))}
@@ -1020,24 +1016,25 @@ export default function AdminPage() {
</div>
</div>
<div className="w-full mt-12">
{/* Mobile-friendly manage content section */}
<div className="w-full mt-8 sm:mt-12">
<button
className="w-full flex justify-center items-center bg-white p-4 rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer text-3xl focus:outline-none"
className="w-full flex justify-center items-center bg-white p-4 rounded-lg shadow hover:shadow-md transition-shadow cursor-pointer text-2xl sm:text-3xl focus:outline-none"
onClick={() => setShowManageContent((v) => !v)}
aria-label={showManageContent ? 'Hide Manage Content' : 'Show Manage Content'}
>
<span>{showManageContent ? '' : ''}</span>
</button>
{showManageContent && (
<div className="mt-4 bg-white p-6 rounded-lg shadow text-center">
<p className="text-gray-600 mb-2">
<div className="mt-4 bg-white p-4 sm:p-6 rounded-lg shadow text-center">
<p className="text-gray-600 mb-2 text-sm sm:text-base">
Delete posts and folders, manage your content structure
</p>
{/* Folder navigation breadcrumbs */}
<div className="flex flex-wrap justify-center gap-2 mb-4">
<div className="flex flex-wrap justify-center gap-1 sm: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'}`}
className={`px-2 py-1 rounded text-sm sm:text-base ${managePath.length === 0 ? 'bg-blue-100 text-blue-800' : 'hover:bg-gray-200'}`}
>
Root
</button>
@@ -1045,7 +1042,7 @@ export default function AdminPage() {
<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'}`}
className={`px-2 py-1 rounded text-sm sm:text-base ${idx === managePath.length - 1 ? 'bg-blue-100 text-blue-800' : 'hover:bg-gray-200'}`}
>
{name}
</button>
@@ -1059,8 +1056,8 @@ export default function AdminPage() {
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>
<span className="text-xl sm:text-2xl">📁</span>
<span className="font-semibold text-base sm:text-lg">{folder.name}</span>
</div>
))}
</div>
@@ -1075,17 +1072,17 @@ export default function AdminPage() {
>
<div className="flex-1 text-left flex items-center gap-2">
{pinned.includes(post.slug) && (
<span title="Angeheftet" className="text-xl">📌</span>
<span title="Angeheftet" className="text-lg sm:text-xl">📌</span>
)}
<div>
<div className="font-semibold">{post.title}</div>
<div className="font-semibold text-sm sm:text-base">{post.title}</div>
<div className="text-xs text-gray-500">{post.date}</div>
<div className="text-xs text-gray-400">{post.summary}</div>
</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'}`}
className={`text-xl sm:text-2xl focus:outline-none p-1 ${pinned.includes(post.slug) ? 'text-yellow-500' : 'text-gray-400 hover:text-yellow-500'}`}
title={pinned.includes(post.slug) ? 'Lösen' : 'Anheften'}
>
{pinned.includes(post.slug) ? '' : ''}
@@ -1093,7 +1090,7 @@ export default function AdminPage() {
</div>
))}
</div>
<a href="/admin/manage" className="block mt-6 text-blue-600 hover:underline">Go to Content Manager</a>
<a href="/admin/manage" className="block mt-6 text-blue-600 hover:underline text-sm sm:text-base">Go to Content Manager</a>
</div>
)}
</div>