Add account based multiplayer hosting and bug report retention
This commit is contained in:
@@ -36,7 +36,13 @@ type Draft = {
|
||||
banned_at: string | null;
|
||||
ban_reason: string | null;
|
||||
};
|
||||
export function DraftEditor({ id }: { id: string }) {
|
||||
export function DraftEditor({
|
||||
id,
|
||||
hostName,
|
||||
}: {
|
||||
id: string;
|
||||
hostName: string;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const dirty = useRef(false);
|
||||
const [draft, setDraft] = useState<Draft | null>(null),
|
||||
@@ -180,7 +186,9 @@ export function DraftEditor({ id }: { id: string }) {
|
||||
"This shared map is private and playable without moderator review. Only the original owner can submit it for public review.",
|
||||
)}
|
||||
</p>
|
||||
{draft.state === "draft" && <StartGame datasetId={id} />}
|
||||
{draft.state === "draft" && (
|
||||
<StartGame datasetId={id} hostName={hostName} />
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
{!draft.parent_dataset_id &&
|
||||
|
||||
@@ -2,16 +2,22 @@
|
||||
import { useI18n } from "@/components/I18nProvider";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Timer, ZoomIn, Users } from "lucide-react";
|
||||
import { difficulties, type Difficulty } from "@/shared/protocol";
|
||||
import { api } from "@/lib/client-api";
|
||||
export { api } from "@/lib/client-api";
|
||||
export function StartGame({ datasetId }: { datasetId: string }) {
|
||||
export function StartGame({
|
||||
datasetId,
|
||||
hostName,
|
||||
}: {
|
||||
datasetId: string;
|
||||
hostName: string | null;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>("normal"),
|
||||
[mode, setMode] = useState<"solo" | "multiplayer">("solo"),
|
||||
[displayName, setDisplayName] = useState(""),
|
||||
[playerLimit, setPlayerLimit] = useState(4),
|
||||
[loading, setLoading] = useState(false),
|
||||
[error, setError] = useState("");
|
||||
@@ -25,7 +31,7 @@ export function StartGame({ datasetId }: { datasetId: string }) {
|
||||
{
|
||||
datasetId,
|
||||
difficulty,
|
||||
...(mode === "multiplayer" ? { displayName, playerLimit } : {}),
|
||||
...(mode === "multiplayer" ? { playerLimit } : {}),
|
||||
},
|
||||
);
|
||||
router.push((mode === "solo" ? "/play/" : "/multiplayer/") + session.id);
|
||||
@@ -82,17 +88,16 @@ export function StartGame({ datasetId }: { datasetId: string }) {
|
||||
</p>
|
||||
{mode === "multiplayer" && (
|
||||
<div className="multiplayer-settings">
|
||||
<label htmlFor="room-display-name">{t("Your player name")}</label>
|
||||
<input
|
||||
id="room-display-name"
|
||||
value={displayName}
|
||||
onChange={(event) => setDisplayName(event.target.value)}
|
||||
minLength={2}
|
||||
maxLength={32}
|
||||
placeholder={t("Enter a player name")}
|
||||
autoComplete="nickname"
|
||||
disabled={loading}
|
||||
/>
|
||||
{hostName ? (
|
||||
<p>
|
||||
{t("Host display name")}: <strong>{hostName}</strong>
|
||||
</p>
|
||||
) : (
|
||||
<p className="muted">
|
||||
{t("Log in to create a multiplayer room")}{" "}
|
||||
<Link href="/log-in?next=/servers">{t("Log in")}</Link>
|
||||
</p>
|
||||
)}
|
||||
<label htmlFor="room-player-limit">
|
||||
{t("Player limit (including you)")}
|
||||
</label>
|
||||
@@ -118,9 +123,7 @@ export function StartGame({ datasetId }: { datasetId: string }) {
|
||||
<button
|
||||
className="button primary full"
|
||||
onClick={start}
|
||||
disabled={
|
||||
loading || (mode === "multiplayer" && displayName.trim().length < 2)
|
||||
}
|
||||
disabled={loading || (mode === "multiplayer" && !hostName)}
|
||||
>
|
||||
{loading
|
||||
? t("Preparing your world…")
|
||||
|
||||
@@ -218,7 +218,10 @@ export function SupportInbox({ pagePath }: { pagePath: string }) {
|
||||
{inbox.reports.map((report) => (
|
||||
<li key={report.id}>
|
||||
<Link href={`/support/${report.id}`}>
|
||||
<strong>{report.title}</strong>
|
||||
<strong>
|
||||
{report.title}
|
||||
{report.state === "done" && <> · {t("Done")}</>}
|
||||
</strong>
|
||||
<small>
|
||||
{formatDate(report.created_at)} ·{" "}
|
||||
{report.linear_identifier ??
|
||||
@@ -331,6 +334,47 @@ export function SupportConversation({ id }: { id: string }) {
|
||||
{report.linear_identifier ?? t("Waiting for support")}
|
||||
</span>
|
||||
<h1>{report.title}</h1>
|
||||
{report.state === "done" && (
|
||||
<div role="status">
|
||||
<p>
|
||||
<strong>{t("Done")}</strong> ·{" "}
|
||||
{t(
|
||||
"This report is closed and will be deleted from MineGeo and Linear after three days.",
|
||||
)}
|
||||
</p>
|
||||
{report.delete_after && (
|
||||
<p>
|
||||
{t("Scheduled deletion")}: {formatDate(report.delete_after)}
|
||||
</p>
|
||||
)}
|
||||
{!report.linear_done_synced && (
|
||||
<p>{t("Syncing Done status with Linear…")}</p>
|
||||
)}
|
||||
{report.cleanup_error && (
|
||||
<p className="error-message">
|
||||
{t(
|
||||
"Support cleanup paused. The support team may need to update Linear permissions.",
|
||||
)}{" "}
|
||||
<button
|
||||
className="text-link"
|
||||
disabled={busy}
|
||||
onClick={() => void action("retry-cleanup", {})}
|
||||
>
|
||||
{t("Retry")}
|
||||
</button>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{report.state === "open" && (
|
||||
<button
|
||||
className="button"
|
||||
disabled={busy}
|
||||
onClick={() => void action("close", {})}
|
||||
>
|
||||
{t("Close report")}
|
||||
</button>
|
||||
)}
|
||||
{report.state === "pending" && (
|
||||
<p role="status">
|
||||
{t("Your report is saved. We’re connecting it to support.")}
|
||||
@@ -432,7 +476,7 @@ export function SupportConversation({ id }: { id: string }) {
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
{report.state !== "draft" && (
|
||||
{report.state !== "draft" && report.state !== "done" && (
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user