ux: completely replace auth modal with a sleek inline auth dropdown in header
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m34s
All checks were successful
Build & Push Docker Image to Gitea Registry / build-and-push (push) Successful in 2m34s
This commit is contained in:
@@ -4,7 +4,7 @@ import type { ReactNode } from "react";
|
|||||||
|
|
||||||
import Footer from "@/components/Footer";
|
import Footer from "@/components/Footer";
|
||||||
import Header from "@/components/Header";
|
import Header from "@/components/Header";
|
||||||
import AuthModal from "@/components/auth/AuthModal";
|
|
||||||
|
|
||||||
import ContextProvider from "@/context/ContextProvider";
|
import ContextProvider from "@/context/ContextProvider";
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
|
|||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<AuthModal />
|
|
||||||
</ContextProvider>
|
</ContextProvider>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ThemeToggle from "./ThemeToggle";
|
|||||||
import { useSession } from "@/context/session";
|
import { useSession } from "@/context/session";
|
||||||
import { logout } from "@/lib/auth";
|
import { logout } from "@/lib/auth";
|
||||||
import JoinGame from "./home/JoinGame";
|
import JoinGame from "./home/JoinGame";
|
||||||
|
import AuthDropdownContent from "./auth/AuthDropdownContent";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { user, setUser } = useSession();
|
const { user, setUser } = useSession();
|
||||||
@@ -89,11 +90,16 @@ export default function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<label tabIndex={0} htmlFor="auth-modal" className="btn btn-ghost btn-circle avatar">
|
<div className="dropdown dropdown-end">
|
||||||
<div className="w-10 rounded-full">
|
<label tabIndex={0} className="btn btn-ghost btn-circle avatar">
|
||||||
<IconUser className="m-auto block h-full" />
|
<div className="w-10 rounded-full">
|
||||||
|
<IconUser className="m-auto block h-full" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div tabIndex={0} className="dropdown-content z-50 mt-3 w-80 rounded-box bg-base-100 p-4 shadow-lg border border-base-300 dark:border-neutral">
|
||||||
|
<AuthDropdownContent />
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
182
client/src/components/auth/AuthDropdownContent.tsx
Normal file
182
client/src/components/auth/AuthDropdownContent.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SessionContext } from "@/context/session";
|
||||||
|
import { login, register, setGuestSession } from "@/lib/auth";
|
||||||
|
import type { FormEvent } from "react";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { API_URL } from "@/config";
|
||||||
|
|
||||||
|
import Guest from "./Guest";
|
||||||
|
import Login from "./Login";
|
||||||
|
import Register from "./Register";
|
||||||
|
|
||||||
|
export default function AuthDropdownContent() {
|
||||||
|
const session = useContext(SessionContext);
|
||||||
|
const [activeTab, setActiveTab] = useState<"guest" | "login" | "register">("guest");
|
||||||
|
const [serverMessage, setServerMessage] = useState<string | null>(null);
|
||||||
|
const [buttonLoading, setButtonLoading] = useState(false);
|
||||||
|
const [ssoEnabled, setSsoEnabled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch(`${API_URL}/v1/auth/sso/config`, { credentials: "include" })
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
if (data && data.enabled) {
|
||||||
|
setSsoEnabled(true);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => console.error("Error loading SSO config:", err));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function closeDropdown() {
|
||||||
|
if (typeof document !== "undefined" && document.activeElement) {
|
||||||
|
(document.activeElement as HTMLElement).blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitAuth(e: FormEvent<HTMLFormElement>) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const target = e.target as HTMLFormElement;
|
||||||
|
setServerMessage(null);
|
||||||
|
setButtonLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (activeTab === "guest") {
|
||||||
|
const guestName = target.elements.namedItem("guestName") as HTMLInputElement;
|
||||||
|
if (!guestName || !guestName.value) return;
|
||||||
|
|
||||||
|
const user = await setGuestSession(guestName.value);
|
||||||
|
if (typeof user === "string") {
|
||||||
|
setServerMessage(user);
|
||||||
|
} else if (user?.id) {
|
||||||
|
session?.setUser(user);
|
||||||
|
closeDropdown();
|
||||||
|
guestName.value = "";
|
||||||
|
}
|
||||||
|
} else if (activeTab === "login") {
|
||||||
|
const loginName = target.elements.namedItem("loginName") as HTMLInputElement;
|
||||||
|
const loginPassword = target.elements.namedItem("loginPassword") as HTMLInputElement;
|
||||||
|
if (!loginName || !loginName.value || !loginPassword || !loginPassword.value) return;
|
||||||
|
|
||||||
|
const user = await login(loginName.value, loginPassword.value);
|
||||||
|
if (typeof user === "string") {
|
||||||
|
setServerMessage(user);
|
||||||
|
} else if (user?.id) {
|
||||||
|
session?.setUser(user);
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
} else if (activeTab === "register") {
|
||||||
|
const registerName = target.elements.namedItem("registerName") as HTMLInputElement;
|
||||||
|
const registerEmail = target.elements.namedItem("registerEmail") as HTMLInputElement;
|
||||||
|
const registerPassword = target.elements.namedItem("registerPassword") as HTMLInputElement;
|
||||||
|
if (!registerName || !registerName.value || !registerPassword || !registerPassword.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await register(
|
||||||
|
registerName.value,
|
||||||
|
registerPassword.value,
|
||||||
|
registerEmail.value || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (typeof user === "string") {
|
||||||
|
setServerMessage(user);
|
||||||
|
} else if (user?.id) {
|
||||||
|
session?.setUser(user);
|
||||||
|
closeDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setButtonLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setServerMessage(null);
|
||||||
|
}, [activeTab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="tabs flex-nowrap w-full">
|
||||||
|
<span
|
||||||
|
onClick={() => {
|
||||||
|
setActiveTab("guest");
|
||||||
|
}}
|
||||||
|
className={
|
||||||
|
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg cursor-pointer" +
|
||||||
|
(activeTab === "guest"
|
||||||
|
? " tab-active text-base-content"
|
||||||
|
: " text-base-content border-opacity-10 hover:border-opacity-30")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Gast
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => setActiveTab("login")}
|
||||||
|
className={
|
||||||
|
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg cursor-pointer" +
|
||||||
|
(activeTab === "login"
|
||||||
|
? " tab-active"
|
||||||
|
: " text-base-content border-opacity-10 hover:border-opacity-30")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Anmelden
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
onClick={() => setActiveTab("register")}
|
||||||
|
className={
|
||||||
|
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg cursor-pointer" +
|
||||||
|
(activeTab === "register"
|
||||||
|
? " tab-active"
|
||||||
|
: " text-base-content border-opacity-10 hover:border-opacity-30")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Registrieren
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="flex flex-col px-1" onSubmit={submitAuth}>
|
||||||
|
{activeTab === "guest" && (
|
||||||
|
<Guest currentName={session?.user?.name || "Gast"} />
|
||||||
|
)}
|
||||||
|
{activeTab === "login" && <Login />}
|
||||||
|
{activeTab === "register" && <Register />}
|
||||||
|
|
||||||
|
{serverMessage && <div className="text-error mt-2 text-sm">{serverMessage}</div>}
|
||||||
|
|
||||||
|
<div className="mt-4 flex justify-end gap-2">
|
||||||
|
<button className={"btn btn-primary btn-sm flex-grow" + (buttonLoading ? " loading" : "")} type="submit">
|
||||||
|
{activeTab === "guest" && "Als Gast spielen"}
|
||||||
|
{activeTab === "login" && "Anmelden"}
|
||||||
|
{activeTab === "register" && "Registrieren"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ssoEnabled && (
|
||||||
|
<>
|
||||||
|
<div className="divider opacity-70 my-2 text-xs">oder</div>
|
||||||
|
<a
|
||||||
|
href={`${API_URL}/v1/auth/sso/login`}
|
||||||
|
className="btn btn-accent btn-outline btn-sm w-full gap-2"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="h-4 w-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Anmelden mit Keycloak
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,194 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { SessionContext } from "@/context/session";
|
|
||||||
import { login, register, setGuestSession } from "@/lib/auth";
|
|
||||||
import type { FormEvent } from "react";
|
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
|
||||||
import { API_URL } from "@/config";
|
|
||||||
|
|
||||||
import Guest from "./Guest";
|
|
||||||
import Login from "./Login";
|
|
||||||
import Register from "./Register";
|
|
||||||
|
|
||||||
export default function AuthModal() {
|
|
||||||
const session = useContext(SessionContext);
|
|
||||||
const [activeTab, setActiveTab] = useState<"guest" | "login" | "register">("guest");
|
|
||||||
const [serverMessage, setServerMessage] = useState<string | null>(null);
|
|
||||||
const [buttonLoading, setButtonLoading] = useState(false);
|
|
||||||
const [ssoEnabled, setSsoEnabled] = useState(false);
|
|
||||||
const modalToggleRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetch(`${API_URL}/v1/auth/sso/config`, { credentials: "include" })
|
|
||||||
.then((res) => res.json())
|
|
||||||
.then((data) => {
|
|
||||||
if (data && data.enabled) {
|
|
||||||
setSsoEnabled(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => console.error("Error loading SSO config:", err));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
function closeModal() {
|
|
||||||
if (modalToggleRef.current?.checked) {
|
|
||||||
modalToggleRef.current.checked = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitAuth(e: FormEvent<HTMLFormElement>) {
|
|
||||||
e.preventDefault();
|
|
||||||
|
|
||||||
const target = e.target as HTMLFormElement;
|
|
||||||
setServerMessage(null);
|
|
||||||
setButtonLoading(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (activeTab === "guest") {
|
|
||||||
const guestName = target.elements.namedItem("guestName") as HTMLInputElement;
|
|
||||||
if (!guestName || !guestName.value) return;
|
|
||||||
|
|
||||||
const user = await setGuestSession(guestName.value);
|
|
||||||
if (typeof user === "string") {
|
|
||||||
setServerMessage(user);
|
|
||||||
} else if (user?.id) {
|
|
||||||
session?.setUser(user);
|
|
||||||
closeModal();
|
|
||||||
guestName.value = "";
|
|
||||||
}
|
|
||||||
} else if (activeTab === "login") {
|
|
||||||
const loginName = target.elements.namedItem("loginName") as HTMLInputElement;
|
|
||||||
const loginPassword = target.elements.namedItem("loginPassword") as HTMLInputElement;
|
|
||||||
if (!loginName || !loginName.value || !loginPassword || !loginPassword.value) return;
|
|
||||||
|
|
||||||
const user = await login(loginName.value, loginPassword.value);
|
|
||||||
if (typeof user === "string") {
|
|
||||||
setServerMessage(user);
|
|
||||||
} else if (user?.id) {
|
|
||||||
session?.setUser(user);
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
} else if (activeTab === "register") {
|
|
||||||
const registerName = target.elements.namedItem("registerName") as HTMLInputElement;
|
|
||||||
const registerEmail = target.elements.namedItem("registerEmail") as HTMLInputElement;
|
|
||||||
const registerPassword = target.elements.namedItem("registerPassword") as HTMLInputElement;
|
|
||||||
if (!registerName || !registerName.value || !registerPassword || !registerPassword.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await register(
|
|
||||||
registerName.value,
|
|
||||||
registerPassword.value,
|
|
||||||
registerEmail.value || undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof user === "string") {
|
|
||||||
setServerMessage(user);
|
|
||||||
} else if (user?.id) {
|
|
||||||
session?.setUser(user);
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setButtonLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setServerMessage(null);
|
|
||||||
}, [activeTab]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input type="checkbox" id="auth-modal" className="modal-toggle" ref={modalToggleRef} />
|
|
||||||
|
|
||||||
<label
|
|
||||||
htmlFor="auth-modal"
|
|
||||||
className="modal cursor-pointer"
|
|
||||||
>
|
|
||||||
<div className="modal-box flex max-w-sm flex-col gap-4 pt-2 relative">
|
|
||||||
<div className="tabs flex-nowrap w-full">
|
|
||||||
<span
|
|
||||||
onClick={() => {
|
|
||||||
setActiveTab("guest");
|
|
||||||
}}
|
|
||||||
className={
|
|
||||||
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg" +
|
|
||||||
(activeTab === "guest"
|
|
||||||
? " tab-active text-base-content"
|
|
||||||
: " text-base-content border-opacity-10 hover:border-opacity-30")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Gast
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
onClick={() => setActiveTab("login")}
|
|
||||||
className={
|
|
||||||
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg" +
|
|
||||||
(activeTab === "login"
|
|
||||||
? " tab-active"
|
|
||||||
: " text-base-content border-opacity-10 hover:border-opacity-30")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Anmelden
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
onClick={() => setActiveTab("register")}
|
|
||||||
className={
|
|
||||||
"tab tab-bordered tab-border-2 flex-grow rounded-tl-lg" +
|
|
||||||
(activeTab === "register"
|
|
||||||
? " tab-active"
|
|
||||||
: " text-base-content border-opacity-10 hover:border-opacity-30")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Registrieren
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form className="flex flex-col px-2" onSubmit={submitAuth}>
|
|
||||||
{activeTab === "guest" && (
|
|
||||||
<Guest currentName={session?.user?.name || "Gast"} />
|
|
||||||
)}
|
|
||||||
{activeTab === "login" && <Login />}
|
|
||||||
{activeTab === "register" && <Register />}
|
|
||||||
|
|
||||||
{serverMessage && <div className="text-error mt-2">{serverMessage}</div>}
|
|
||||||
<div className="modal-action items-center">
|
|
||||||
<label htmlFor="auth-modal" className="btn btn-ghost">
|
|
||||||
Abbrechen
|
|
||||||
</label>
|
|
||||||
<button className={"btn btn-primary" + (buttonLoading ? " loading" : "")} type="submit">
|
|
||||||
{activeTab === "guest" && "Als Gast spielen"}
|
|
||||||
{activeTab === "login" && "Anmelden"}
|
|
||||||
{activeTab === "register" && "Registrieren"}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ssoEnabled && (
|
|
||||||
<>
|
|
||||||
<div className="divider opacity-70">oder</div>
|
|
||||||
<a
|
|
||||||
href={`${API_URL}/v1/auth/sso/login`}
|
|
||||||
className="btn btn-accent btn-outline w-full gap-2"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
className="h-5 w-5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M12 1.5a5.25 5.25 0 0 0-5.25 5.25v3a3 3 0 0 0-3 3v6.75a3 3 0 0 0 3 3h10.5a3 3 0 0 0 3-3v-6.75a3 3 0 0 0-3-3v-3c0-2.9-2.35-5.25-5.25-5.25Zm3.75 8.25v-3a3.75 3.75 0 1 0-7.5 0v3h7.5Z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Anmelden mit Keycloak
|
|
||||||
</a>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user