feat: implement admin login form localization, add import functionality, and enhance file handling

This commit is contained in:
2025-08-13 11:57:50 +02:00
parent 9dd7b6641a
commit 42f46db2c9
6 changed files with 279 additions and 40 deletions

View File

@@ -0,0 +1,40 @@
a7B9-kD3f-92Lm-Q1z
Z1x9-3kLp-a8D2-mN7
pQ4d-T7v2-bK9X-f5G
m3N8-wQ2z-V6bP-tR4
gH7k-Lp2Q-xD5f-sE9
R8tV-c2Bq-J7mZ-yK1
nK5w-D9sP-T3vL-rU6
Y4dE-q7Wm-hP2k-Js9
c2Fh-X8nR-L5vq-tG7
bM6x-pQ3Z-s7Kf-U1h
V9cQ-h3Tn-2KxF-z6P
k5Lr-M8pQ-w2Tg-Y9d
H2sN-v7Qb-c4XK-p8J
q8Wk-R5nD-1zLp-G6y
T7bH-y2Kq-M9vF-c3R
j4Dq-P6sL-t8XG-r1V
F6kP-q1Xv-3TzB-n7H
x9Nf-L2dQ-5vHk-S8p
G3rH-t9Kc-Q4sV-m1Z
s1Vx-B7mQ-8kDt-P5g
P5kZ-w4Sg-2HnV-y9M
L8cJ-q3Vt-7pXh-D2f
d7Xv-T1kL-6mQp-R8s
K2nP-z6Hq-9vBd-J3t
u9Hc-R4sV-5kLm-Q2x
N3vG-y8Kp-1TqD-s6F
h5Qm-C9tB-4vXn-Z7k
W6zF-p2Jc-8KqH-d1L
r2Kp-G5sN-7tVh-M4q
J4mT-x9Hc-2QvP-k8S
f8Pq-D3kV-6zLn-R1y
Q7zD-h1Sg-9KpM-t4V
z6Kx-P5vH-1cQn-Y8d
C9tB-u4Hq-7mKs-p2J
y1Vd-G7nP-3kLq-S6h
M2cL-t5Xh-9vQp-r8K
b4Sg-N6kP-2HqV-J9t
X5nV-q8Tz-1pKd-L3h
e3Kq-R7vH-5nXc-P2m
H1pM-w9Dk-4sQv-T7g
1 a7B9-kD3f-92Lm-Q1z
2 Z1x9-3kLp-a8D2-mN7
3 pQ4d-T7v2-bK9X-f5G
4 m3N8-wQ2z-V6bP-tR4
5 gH7k-Lp2Q-xD5f-sE9
6 R8tV-c2Bq-J7mZ-yK1
7 nK5w-D9sP-T3vL-rU6
8 Y4dE-q7Wm-hP2k-Js9
9 c2Fh-X8nR-L5vq-tG7
10 bM6x-pQ3Z-s7Kf-U1h
11 V9cQ-h3Tn-2KxF-z6P
12 k5Lr-M8pQ-w2Tg-Y9d
13 H2sN-v7Qb-c4XK-p8J
14 q8Wk-R5nD-1zLp-G6y
15 T7bH-y2Kq-M9vF-c3R
16 j4Dq-P6sL-t8XG-r1V
17 F6kP-q1Xv-3TzB-n7H
18 x9Nf-L2dQ-5vHk-S8p
19 G3rH-t9Kc-Q4sV-m1Z
20 s1Vx-B7mQ-8kDt-P5g
21 P5kZ-w4Sg-2HnV-y9M
22 L8cJ-q3Vt-7pXh-D2f
23 d7Xv-T1kL-6mQp-R8s
24 K2nP-z6Hq-9vBd-J3t
25 u9Hc-R4sV-5kLm-Q2x
26 N3vG-y8Kp-1TqD-s6F
27 h5Qm-C9tB-4vXn-Z7k
28 W6zF-p2Jc-8KqH-d1L
29 r2Kp-G5sN-7tVh-M4q
30 J4mT-x9Hc-2QvP-k8S
31 f8Pq-D3kV-6zLn-R1y
32 Q7zD-h1Sg-9KpM-t4V
33 z6Kx-P5vH-1cQn-Y8d
34 C9tB-u4Hq-7mKs-p2J
35 y1Vd-G7nP-3kLq-S6h
36 M2cL-t5Xh-9vQp-r8K
37 b4Sg-N6kP-2HqV-J9t
38 X5nV-q8Tz-1pKd-L3h
39 e3Kq-R7vH-5nXc-P2m
40 H1pM-w9Dk-4sQv-T7g

View File

@@ -0,0 +1,97 @@
import React, { useRef, useState, useCallback } from "react";
type CircleUploadProps = {
onFiles?: (files: File[]) => void;
accept?: string;
multiple?: boolean;
};
const CircleUpload: React.FC<CircleUploadProps> = ({
onFiles,
accept = "",
multiple = true,
}) => {
const [isDragging, setIsDragging] = useState(false);
const [files, setFiles] = useState<File[]>([]);
const inputRef = useRef<HTMLInputElement | null>(null);
const processFiles = useCallback(
(fileList: FileList | null) => {
if (!fileList) return;
const arr = Array.from(fileList);
setFiles(arr);
onFiles?.(arr);
},
[onFiles]
);
const openFileDialog = () => inputRef.current?.click();
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsDragging(false);
processFiles(e.dataTransfer.files);
};
const handleDrag = (e: React.DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") setIsDragging(true);
else if (e.type === "dragleave") setIsDragging(false);
};
return (
<div className="flex flex-col items-center space-y-3">
<div
role="button"
tabIndex={0}
onClick={openFileDialog}
onKeyDown={(e) =>
(e.key === "Enter" || e.key === " ") && openFileDialog()
}
onDrop={handleDrop}
onDragEnter={handleDrag}
onDragOver={handleDrag}
onDragLeave={handleDrag}
className={[
"w-48 h-48 rounded-full border-2 border-dashed flex flex-col items-center justify-center text-center px-4 transition-colors cursor-pointer select-none",
"border-blue-400 text-blue-500 bg-blue-50/40 hover:bg-blue-50",
isDragging && "bg-blue-100 border-blue-500",
].join(" ")}
>
<span className="text-sm font-medium">
Dateien hierher ziehen
<br />
<span className="text-xs text-gray-500">oder klicken</span>
</span>
<span className="mt-2 inline-block rounded bg-blue-600 text-white text-xs px-2 py-1">
Upload
</span>
<input
ref={inputRef}
type="file"
className="hidden"
accept={accept}
multiple={multiple}
onChange={(e) => processFiles(e.target.files)}
/>
</div>
{files.length > 0 && (
<ul className="w-56 max-h-32 overflow-auto text-xs bg-gray-50 border rounded p-2 space-y-1">
{files.map((f) => (
<li key={f.name} className="truncate">
{f.name}{" "}
<span className="text-gray-400">
({Math.round(f.size / 1024)} KB)
</span>
</li>
))}
</ul>
)}
</div>
);
};
export default CircleUpload;

View File

@@ -0,0 +1,83 @@
import React from "react";
import CircleUpload from "./CircleUpload";
import { CircleX } from "lucide-react";
type ImportGUIProps = {
onClose: () => void;
};
const ImportGUI: React.FC<ImportGUIProps> = ({ onClose }) => {
return (
<div className="fixed inset-0 z-50 flex items-start justify-center pt-24">
{/* Backdrop */}
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" />
{/* Dialog Panel */}
<div className="relative z-10 w-11/12 max-w-lg rounded-2xl border border-black/10 bg-white/95 p-6 shadow-xl ring-1 ring-black/5">
<div className="flex items-start justify-between gap-4">
<div>
<h2 className="text-xl font-extrabold tracking-tight text-zinc-900">
Losnummern importieren
</h2>
<p className="mt-1 text-sm text-zinc-600 leading-relaxed">
Importieren Sie Losnummern als strukturierte Datei. Unterstützte
Formate:
</p>
<div className="mt-2 flex flex-wrap gap-2">
{[".csv"].map((fileFormat) => (
<span
key={fileFormat}
className="inline-flex items-center rounded-full border border-black/10 bg-zinc-100 px-3 py-0.5 text-xs font-medium text-zinc-700 shadow-inner"
>
{fileFormat}
</span>
))}
</div>
<a href="/Beispiel.csv" download>
<button className="inline-flex justify-center rounded bg-zinc-300 px-1 py-1 text-xs text-zinc-800 shadow transition hover:bg-zinc-400 active:bg-zinc-500">
Beispiel (Download)
</button>
</a>
</div>
<button
type="button"
onClick={onClose}
className="group rounded-full p-1.5 text-zinc-500 transition hover:bg-zinc-200/70 hover:text-zinc-700 active:scale-95"
aria-label="Schließen"
>
<CircleX className="h-6 w-6" strokeWidth={2.25} />
</button>
</div>
<div className="mt-6">
<CircleUpload accept=".csv" />
</div>
{/* Placeholder for optional file summary / mapping UI */}
<div className="mt-6 hidden rounded-xl border border-dashed border-black/15 bg-zinc-50/60 p-4 text-center text-xs text-zinc-500">
Ausgewählte Dateien werden hier aufgelistet.
</div>
{/* Actions */}
<div className="mt-8 flex flex-col-reverse gap-3 sm:flex-row sm:justify-end">
<button
type="button"
onClick={onClose}
className="inline-flex justify-center rounded-xl bg-zinc-300 px-5 py-3 text-sm font-bold text-zinc-800 shadow transition hover:bg-zinc-400 active:bg-zinc-500"
>
Abbrechen
</button>
<button
type="button"
className="inline-flex justify-center rounded-xl bg-blue-600 px-6 py-3 text-sm font-bold text-white shadow transition hover:bg-blue-700 active:bg-blue-800 disabled:opacity-60 disabled:cursor-not-allowed"
disabled
>
Importieren
</button>
</div>
</div>
</div>
);
};
export default ImportGUI;

View File

@@ -40,13 +40,13 @@ const LoginForm: React.FC<LoginFormProps> = ({ onClose, onLoginSuccess }) => {
className="block text-sm font-medium text-zinc-800" className="block text-sm font-medium text-zinc-800"
htmlFor="username" htmlFor="username"
> >
Username Admin Benutzername
</label> </label>
<input <input
id="username" id="username"
name="username" name="username"
type="text" type="text"
placeholder="yourname" placeholder="username"
className="w-full rounded-xl border border-black/25 bg-white/80 px-4 py-2.5 text-sm text-zinc-800 placeholder-zinc-400 shadow-inner outline-none focus:border-black/40 focus:ring-2 focus:ring-black/10" className="w-full rounded-xl border border-black/25 bg-white/80 px-4 py-2.5 text-sm text-zinc-800 placeholder-zinc-400 shadow-inner outline-none focus:border-black/40 focus:ring-2 focus:ring-black/10"
required required
/> />
@@ -55,13 +55,13 @@ const LoginForm: React.FC<LoginFormProps> = ({ onClose, onLoginSuccess }) => {
className="mt-3 block text-sm font-medium text-zinc-800" className="mt-3 block text-sm font-medium text-zinc-800"
htmlFor="password" htmlFor="password"
> >
Password Admin Passwort
</label> </label>
<input <input
id="password" id="password"
name="password" name="password"
type="password" type="password"
placeholder="••••••••" placeholder="password"
className="w-full rounded-xl border border-black/25 bg-white/80 px-4 py-2.5 text-sm text-zinc-800 placeholder-zinc-400 shadow-inner outline-none focus:border-black/40 focus:ring-2 focus:ring-black/10" className="w-full rounded-xl border border-black/25 bg-white/80 px-4 py-2.5 text-sm text-zinc-800 placeholder-zinc-400 shadow-inner outline-none focus:border-black/40 focus:ring-2 focus:ring-black/10"
required required
/> />

View File

@@ -1,9 +1,14 @@
import React from "react"; import React from "react";
import { Sheet, WholeWord } from "lucide-react"; import { Sheet, WholeWord } from "lucide-react";
import { useState } from "react";
import ImportGUI from "./ImportGUI";
// Sub navigation bar for admin views: provides import + clear selection actions // Sub navigation bar for admin views: provides import + clear selection actions
const SubHeaderAdmin: React.FC = () => { const SubHeaderAdmin: React.FC = () => {
const [showImport, setShowImport] = useState(false);
return ( return (
<>
<header className="sticky top-0 z-30 w-full border-b border-gray-200/70 bg-white/90 backdrop-blur supports-[backdrop-filter]:bg-white/60"> <header className="sticky top-0 z-30 w-full border-b border-gray-200/70 bg-white/90 backdrop-blur supports-[backdrop-filter]:bg-white/60">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-2.5 md:px-6"> <div className="mx-auto flex max-w-7xl items-center justify-between gap-4 px-4 py-2.5 md:px-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@@ -20,6 +25,7 @@ const SubHeaderAdmin: React.FC = () => {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => setShowImport(true)}
type="button" type="button"
className="group inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3.5 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" className="group inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white px-3.5 py-2 text-sm font-medium text-gray-700 shadow-sm transition hover:bg-gray-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
> >
@@ -42,6 +48,8 @@ const SubHeaderAdmin: React.FC = () => {
</div> </div>
</div> </div>
</header> </header>
{showImport && <ImportGUI onClose={() => setShowImport(false)} />}
</>
); );
}; };

View File

@@ -0,0 +1,11 @@
export function downloadHandler(filename: string, content: string) {
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}