feat: redesign homepage layout and fix invite links
- Homepage: Grid-Layout mit 3 Karten (Spiel erstellen, KI, Freunde/Beitreten) statt vertikalem Stack mit Dividers - CreateGame: kompakteres Styling, auf Deutsch, in Primary-Card integriert - Einladungslink: APP_URL statt hardcoded ches.su in GamePage und CopyLink - JoinGame: URL-Parsing für beliebige Domains (nicht nur ches.su) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,51 +6,74 @@ import PublicGames from "@/components/home/PublicGames/PublicGames";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useSession } from "@/context/session";
|
import { useSession } from "@/context/session";
|
||||||
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { user } = useSession();
|
const { user } = useSession();
|
||||||
|
const isLoggedIn = user?.id && typeof user.id === "number";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-wrap items-start justify-center gap-8 px-4 py-10 lg:gap-16">
|
<div className="flex flex-col gap-8 w-full max-w-5xl px-4 py-8">
|
||||||
<PublicGames />
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
{/* Hauptaktionen */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
|
||||||
{/* Against AI */}
|
<div className="card bg-base-200 border-2 border-primary shadow-lg">
|
||||||
<div className="flex flex-col items-center">
|
<div className="card-body gap-3">
|
||||||
<h2 className="mb-4 text-xl font-bold leading-tight">Gegen KI spielen</h2>
|
<h2 className="card-title text-primary">♟ Spiel erstellen</h2>
|
||||||
<Link href="/ai" className="btn btn-primary w-full max-w-xs">
|
<p className="text-sm opacity-70">Erstelle eine Partie und teile den Einladungslink mit Freunden</p>
|
||||||
♟ Gegen Stockfish spielen
|
<div className="mt-2">
|
||||||
</Link>
|
<CreateGame />
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divider divider-vertical">oder</div>
|
|
||||||
|
|
||||||
{/* Friends */}
|
|
||||||
{user?.id && typeof user.id === "number" && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<h2 className="mb-4 text-xl font-bold leading-tight">Freunde</h2>
|
|
||||||
<Link href="/friends" className="btn btn-outline w-full max-w-xs">
|
|
||||||
👥 Freunde verwalten
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="divider divider-vertical">oder</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex flex-col items-center">
|
|
||||||
<h2 className="mb-4 text-xl font-bold leading-tight">Per Einladung beitreten</h2>
|
|
||||||
<JoinGame />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider divider-vertical">oder</div>
|
<div className="card bg-base-200 shadow-lg">
|
||||||
|
<div className="card-body gap-3">
|
||||||
<div className="flex flex-col items-center">
|
<h2 className="card-title">🤖 Gegen KI spielen</h2>
|
||||||
<h2 className="mb-4 text-xl font-bold leading-tight">Spiel erstellen</h2>
|
<p className="text-sm opacity-70">Fordere Stockfish auf verschiedenen Stufen heraus — von Anfänger bis Meister</p>
|
||||||
<CreateGame />
|
<div className="mt-auto pt-4">
|
||||||
|
<Link href="/ai" className="btn btn-secondary w-full">Jetzt spielen</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="card bg-base-200 shadow-lg">
|
||||||
|
<div className="card-body gap-3">
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<>
|
||||||
|
<h2 className="card-title">👥 Freunde</h2>
|
||||||
|
<p className="text-sm opacity-70">Freunde hinzufügen, verwalten und zu einer Partie einladen</p>
|
||||||
|
<div className="mt-auto pt-4">
|
||||||
|
<Link href="/friends" className="btn btn-outline w-full">Freunde verwalten</Link>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="card-title">🔗 Einladung annehmen</h2>
|
||||||
|
<p className="text-sm opacity-70 mb-2">Einladungslink oder Code eingeben um beizutreten</p>
|
||||||
|
<JoinGame />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Öffentliche Spiele + Beitreten */}
|
||||||
|
<div className={`grid gap-6 grid-cols-1 ${isLoggedIn ? "lg:grid-cols-3" : ""}`}>
|
||||||
|
<div className={isLoggedIn ? "lg:col-span-2" : ""}>
|
||||||
|
<PublicGames />
|
||||||
|
</div>
|
||||||
|
{isLoggedIn && (
|
||||||
|
<div className="card bg-base-200 shadow h-fit">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">🔗 Einladung annehmen</h2>
|
||||||
|
<p className="text-sm opacity-70 mb-2">Link oder Code eingeben</p>
|
||||||
|
<JoinGame />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { Chess } from "chess.js";
|
|||||||
import type { ClearPremoves } from "react-chessboard";
|
import type { ClearPremoves } from "react-chessboard";
|
||||||
import { Chessboard } from "react-chessboard";
|
import { Chessboard } from "react-chessboard";
|
||||||
|
|
||||||
import { API_URL } from "@/config";
|
import { API_URL, APP_URL } from "@/config";
|
||||||
import { io } from "socket.io-client";
|
import { io } from "socket.io-client";
|
||||||
|
|
||||||
import { lobbyReducer, squareReducer } from "./reducers";
|
import { lobbyReducer, squareReducer } from "./reducers";
|
||||||
@@ -408,12 +408,8 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function copyInvite() {
|
function copyInvite() {
|
||||||
const text = `https://ches.su/${lobby.endReason ? `archive/${lobby.id}` : initialLobby.code}`;
|
const text = `${APP_URL}/${lobby.endReason ? `archive/${lobby.id}` : initialLobby.code}`;
|
||||||
if ("clipboard" in navigator) {
|
navigator.clipboard.writeText(text).catch(() => {});
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
} else {
|
|
||||||
document.execCommand("copy", true, text);
|
|
||||||
}
|
|
||||||
setCopiedLink(true);
|
setCopiedLink(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCopiedLink(false);
|
setCopiedLink(false);
|
||||||
@@ -586,7 +582,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
onClick={copyInvite}
|
onClick={copyInvite}
|
||||||
>
|
>
|
||||||
<IconCopy size={16} />
|
<IconCopy size={16} />
|
||||||
ches.su/{lobby.endReason ? `archive/${lobby.id}` : initialLobby.code}
|
{APP_URL.replace(/^https?:\/\//, "")}/{lobby.endReason ? `archive/${lobby.id}` : initialLobby.code}
|
||||||
</label>
|
</label>
|
||||||
<div tabIndex={0} className="dropdown-content badge badge-neutral text-xs shadow">
|
<div tabIndex={0} className="dropdown-content badge badge-neutral text-xs shadow">
|
||||||
copied to clipboard
|
copied to clipboard
|
||||||
|
|||||||
@@ -33,35 +33,35 @@ export default function CreateGame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form className="form-control" onSubmit={submitCreateGame}>
|
<form className="flex flex-col gap-3" onSubmit={submitCreateGame}>
|
||||||
<label className="label cursor-pointer">
|
<label className="flex items-center justify-between cursor-pointer">
|
||||||
<span className="label-text">Unlisted/invite-only</span>
|
<span className="label-text text-sm">Nur per Einladung</span>
|
||||||
<input type="checkbox" className="checkbox" name="createUnlisted" id="createUnlisted" />
|
<input type="checkbox" className="checkbox checkbox-primary checkbox-sm" name="createUnlisted" id="createUnlisted" />
|
||||||
</label>
|
</label>
|
||||||
<label className="label" htmlFor="createStartingSide">
|
<div className="flex gap-2">
|
||||||
<span className="label-text">Select your side</span>
|
|
||||||
</label>
|
|
||||||
<div className="input-group">
|
|
||||||
<select
|
<select
|
||||||
className="select select-bordered"
|
className="select select-bordered select-sm flex-1"
|
||||||
name="createStartingSide"
|
name="createStartingSide"
|
||||||
id="createStartingSide"
|
id="createStartingSide"
|
||||||
>
|
>
|
||||||
<option value="random">Random</option>
|
<option value="random">Zufällig</option>
|
||||||
<option value="white">White</option>
|
<option value="white">Weiß</option>
|
||||||
<option value="black">Black</option>
|
<option value="black">Schwarz</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
className={
|
className={
|
||||||
"btn" +
|
"btn btn-primary btn-sm" +
|
||||||
(buttonLoading ? " loading" : "") +
|
(buttonLoading ? " loading" : "") +
|
||||||
(!session?.user?.id ? " btn-disabled text-base-content" : "")
|
(!session?.user?.id ? " btn-disabled" : "")
|
||||||
}
|
}
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
Create
|
Erstellen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{!session?.user?.id && (
|
||||||
|
<p className="text-xs opacity-60">Bitte einloggen um ein Spiel zu erstellen</p>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export default function JoinGame() {
|
|||||||
|
|
||||||
setButtonLoading(true);
|
setButtonLoading(true);
|
||||||
|
|
||||||
if (code.startsWith("ches.su")) {
|
if (!code.startsWith("http") && code.includes("/")) {
|
||||||
code = "http://" + code;
|
code = "https://" + code;
|
||||||
}
|
}
|
||||||
if (code.startsWith("http")) {
|
if (code.startsWith("http")) {
|
||||||
code = new URL(code).pathname.split("/")[1];
|
code = new URL(code).pathname.split("/")[1];
|
||||||
|
|||||||
@@ -2,18 +2,14 @@
|
|||||||
|
|
||||||
import { IconCopy } from "@tabler/icons-react";
|
import { IconCopy } from "@tabler/icons-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { APP_URL } from "@/config";
|
||||||
|
|
||||||
export default function CopyLink({ name }: { name: string }) {
|
export default function CopyLink({ name }: { name: string }) {
|
||||||
const [copiedLink, setCopiedLink] = useState(false);
|
const [copiedLink, setCopiedLink] = useState(false);
|
||||||
|
const host = APP_URL.replace(/^https?:\/\//, "");
|
||||||
|
|
||||||
function copyLink() {
|
function copyLink() {
|
||||||
const text = `https://ches.su/user/${name}`;
|
navigator.clipboard.writeText(`${APP_URL}/user/${name}`).catch(() => {});
|
||||||
|
|
||||||
if ("clipboard" in navigator) {
|
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
} else {
|
|
||||||
document.execCommand("copy", true, text);
|
|
||||||
}
|
|
||||||
setCopiedLink(true);
|
setCopiedLink(true);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setCopiedLink(false);
|
setCopiedLink(false);
|
||||||
@@ -27,7 +23,7 @@ export default function CopyLink({ name }: { name: string }) {
|
|||||||
onClick={copyLink}
|
onClick={copyLink}
|
||||||
>
|
>
|
||||||
<IconCopy size={16} />
|
<IconCopy size={16} />
|
||||||
ches.su/user/{name}
|
{host}/user/{name}
|
||||||
</label>
|
</label>
|
||||||
<div tabIndex={0} className="dropdown-content badge badge-neutral text-xs shadow">
|
<div tabIndex={0} className="dropdown-content badge badge-neutral text-xs shadow">
|
||||||
copied to clipboard
|
copied to clipboard
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// back-end server url
|
|
||||||
export const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
|
export const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001";
|
||||||
|
export const APP_URL = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||||
|
|||||||
Reference in New Issue
Block a user