fix: update match configuration to bestOf 5 and scoringType to score_v2
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { json } from "@sveltejs/kit";
|
||||
import { d as db } from "../../../../chunks/index2.js";
|
||||
import { sql } from "drizzle-orm";
|
||||
const GET = async () => {
|
||||
try {
|
||||
await db.execute(sql`SELECT 1`);
|
||||
return json({
|
||||
status: "ok",
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
||||
uptime: process.uptime()
|
||||
});
|
||||
} catch (e) {
|
||||
return json(
|
||||
{
|
||||
status: "error",
|
||||
error: "Database connection failed",
|
||||
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
||||
},
|
||||
{ status: 503 }
|
||||
);
|
||||
}
|
||||
};
|
||||
export {
|
||||
GET
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { d as db, m as mappool, a as mappoolSlot } from "../../../../../chunks/index2.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
const GET = async ({ params, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const pool = await db.query.mappool.findFirst({
|
||||
where: eq(mappool.id, params.id)
|
||||
});
|
||||
if (!pool) error(404, "Mappool not found");
|
||||
const slots = await db.query.mappoolSlot.findMany({
|
||||
where: eq(mappoolSlot.mappoolId, params.id),
|
||||
orderBy: (s, { asc }) => [asc(s.category), asc(s.orderInCategory)]
|
||||
});
|
||||
return json({ ...pool, slots });
|
||||
};
|
||||
const DELETE = async ({ params, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
await db.delete(mappool).where(eq(mappool.id, params.id));
|
||||
return json({ ok: true });
|
||||
};
|
||||
export {
|
||||
DELETE,
|
||||
GET
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { d as db, a as mappoolSlot } from "../../../../../../chunks/index2.js";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
const POST = async ({ params, request, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const { beatmapId, category, mods } = await request.json();
|
||||
if (!beatmapId || !category) {
|
||||
error(400, "beatmapId and category are required");
|
||||
}
|
||||
const existing = await db.query.mappoolSlot.findMany({
|
||||
where: and(
|
||||
eq(mappoolSlot.mappoolId, params.id),
|
||||
eq(mappoolSlot.category, category.toUpperCase())
|
||||
)
|
||||
});
|
||||
const [created] = await db.insert(mappoolSlot).values({
|
||||
mappoolId: params.id,
|
||||
beatmapId: String(beatmapId),
|
||||
category: category.toUpperCase(),
|
||||
orderInCategory: existing.length + 1,
|
||||
mods: mods ?? []
|
||||
}).returning();
|
||||
return json(created, { status: 201 });
|
||||
};
|
||||
export {
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { d as db, a as mappoolSlot } from "../../../../../../../chunks/index2.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
const DELETE = async ({ params, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
await db.delete(mappoolSlot).where(eq(mappoolSlot.id, params.slotId));
|
||||
return json({ ok: true });
|
||||
};
|
||||
export {
|
||||
DELETE
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { d as db, m as mappool } from "../../../../chunks/index2.js";
|
||||
const GET = async ({ locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const mappools = await db.query.mappool.findMany({
|
||||
orderBy: (m, { desc }) => [desc(m.createdAt)]
|
||||
});
|
||||
return json(mappools);
|
||||
};
|
||||
const POST = async ({ request, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const { name } = await request.json();
|
||||
if (!name?.trim()) error(400, "Name is required");
|
||||
const [created] = await db.insert(mappool).values({
|
||||
name: name.trim(),
|
||||
createdBy: locals.user.id
|
||||
}).returning();
|
||||
return json(created, { status: 201 });
|
||||
};
|
||||
export {
|
||||
GET,
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { a as cancelMatch, g as getMatchFull } from "../../../../../chunks/engine.js";
|
||||
const GET = async ({ params, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
try {
|
||||
const m = await getMatchFull(params.id);
|
||||
return json(m);
|
||||
} catch (e) {
|
||||
error(404, e.message);
|
||||
}
|
||||
};
|
||||
const DELETE = async ({ params, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
try {
|
||||
const m = await cancelMatch(params.id);
|
||||
return json(m);
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
export {
|
||||
DELETE,
|
||||
GET
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { m as moveToLobby } from "../../../../../../chunks/engine.js";
|
||||
const POST = async ({ params, request, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const body = await request.json().catch(() => ({}));
|
||||
try {
|
||||
const m = await moveToLobby(params.id, body.osuLobbyId);
|
||||
return json(m);
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
export {
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { p as pickMap } from "../../../../../../chunks/engine.js";
|
||||
const POST = async ({ params, request, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const { participantId, mappoolSlotId } = await request.json();
|
||||
if (!participantId || !mappoolSlotId) {
|
||||
error(400, "participantId and mappoolSlotId required");
|
||||
}
|
||||
try {
|
||||
const game = await pickMap(params.id, participantId, mappoolSlotId);
|
||||
return json(game);
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
export {
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { s as submitRoll } from "../../../../../../chunks/engine.js";
|
||||
const POST = async ({ params, request, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const { participantId, value } = await request.json();
|
||||
if (!participantId || value == null) {
|
||||
error(400, "participantId and value required");
|
||||
}
|
||||
try {
|
||||
const m = await submitRoll(params.id, participantId, value);
|
||||
return json(m);
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
export {
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { b as moveToRolling } from "../../../../../../chunks/engine.js";
|
||||
const POST = async ({ params, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
try {
|
||||
const m = await moveToRolling(params.id);
|
||||
return json(m);
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
export {
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { d as submitGameScores } from "../../../../../../chunks/engine.js";
|
||||
const POST = async ({ request, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const { matchGameId, scores } = await request.json();
|
||||
if (!matchGameId || !scores?.length) {
|
||||
error(400, "matchGameId and scores required");
|
||||
}
|
||||
try {
|
||||
const m = await submitGameScores(matchGameId, scores);
|
||||
return json(m);
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
export {
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { d as db } from "../../../../chunks/index2.js";
|
||||
import { c as createMatch } from "../../../../chunks/engine.js";
|
||||
const GET = async ({ locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const matches = await db.query.match.findMany({
|
||||
with: {
|
||||
participants: { with: { team: true } }
|
||||
},
|
||||
orderBy: (m, { desc }) => [desc(m.createdAt)]
|
||||
});
|
||||
return json(matches);
|
||||
};
|
||||
const POST = async ({ request, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const body = await request.json();
|
||||
const { name, config, mappoolId, teams } = body;
|
||||
if (!name || !config || !mappoolId || !teams?.length) {
|
||||
error(400, "Missing required fields");
|
||||
}
|
||||
try {
|
||||
const result = await createMatch({
|
||||
name,
|
||||
config,
|
||||
mappoolId,
|
||||
teams,
|
||||
createdBy: locals.user.id
|
||||
});
|
||||
return json(result, { status: 201 });
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
export {
|
||||
GET,
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { g as getBeatmap } from "../../../../../../chunks/api.js";
|
||||
const GET = async ({ params, locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
try {
|
||||
const beatmap = await getBeatmap(params.id);
|
||||
return json(beatmap);
|
||||
} catch {
|
||||
error(404, "Beatmap not found");
|
||||
}
|
||||
};
|
||||
export {
|
||||
GET
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { error, json } from "@sveltejs/kit";
|
||||
import { l as leaveQueue, e as getQueueStatus, j as joinQueue } from "../../../../chunks/engine.js";
|
||||
import { d as db, t as teamMember } from "../../../../chunks/index2.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
const GET = async ({ locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const status = await getQueueStatus(locals.user.id);
|
||||
return json(status);
|
||||
};
|
||||
const POST = async ({ locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
const membership = await db.query.teamMember.findFirst({
|
||||
where: eq(teamMember.userId, locals.user.id),
|
||||
with: { team: true }
|
||||
});
|
||||
const personalTeam = membership?.team;
|
||||
if (!personalTeam?.isPersonal) {
|
||||
error(400, "No personal team found");
|
||||
}
|
||||
try {
|
||||
const result = await joinQueue(locals.user.id, personalTeam.id);
|
||||
return json({ matched: !!result, match: result });
|
||||
} catch (e) {
|
||||
error(400, e.message);
|
||||
}
|
||||
};
|
||||
const DELETE = async ({ locals }) => {
|
||||
if (!locals.user) error(401, "Not logged in");
|
||||
await leaveQueue(locals.user.id);
|
||||
return json({ ok: true });
|
||||
};
|
||||
export {
|
||||
DELETE,
|
||||
GET,
|
||||
POST
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import "clsx";
|
||||
function Layout($$renderer, $$props) {
|
||||
let { children } = $$props;
|
||||
children($$renderer);
|
||||
$$renderer.push(`<!---->`);
|
||||
}
|
||||
export {
|
||||
Layout as default
|
||||
};
|
||||
22
.svelte-kit/output/server/entries/hooks.server.js
Normal file
22
.svelte-kit/output/server/entries/hooks.server.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { b as building } from "../chunks/environment.js";
|
||||
import { a as auth, s as svelteKitHandler } from "../chunks/auth.js";
|
||||
import { d as db, u as user } from "../chunks/index2.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
const handleBetterAuth = async ({ event, resolve }) => {
|
||||
const session = await auth.api.getSession({ headers: event.request.headers });
|
||||
if (session) {
|
||||
event.locals.session = session.session;
|
||||
const dbUser = await db.query.user.findFirst({
|
||||
where: eq(user.id, session.user.id)
|
||||
});
|
||||
event.locals.user = {
|
||||
...session.user,
|
||||
role: dbUser?.role ?? "player"
|
||||
};
|
||||
}
|
||||
return svelteKitHandler({ event, resolve, auth, building });
|
||||
};
|
||||
const handle = handleBetterAuth;
|
||||
export {
|
||||
handle
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
const load = async ({ locals }) => {
|
||||
return {
|
||||
user: locals.user ? {
|
||||
id: locals.user.id,
|
||||
name: locals.user.name,
|
||||
email: locals.user.email,
|
||||
image: locals.user.image,
|
||||
role: locals.user.role ?? "player"
|
||||
} : null
|
||||
};
|
||||
};
|
||||
export {
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
import { a as ensure_array_like, b as attr, c as attr_class, s as stringify, e as escape_html, d as derived } from "../../../chunks/index3.js";
|
||||
import { p as page } from "../../../chunks/index4.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../chunks/url.js";
|
||||
import "../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../chunks/root.js";
|
||||
import "../../../chunks/exports.js";
|
||||
import "../../../chunks/state.svelte.js";
|
||||
function Sidebar($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
const user = derived(() => page.data?.user);
|
||||
const nav = [
|
||||
{ href: "/", label: "Dashboard", icon: "⌂" },
|
||||
{ href: "/matches", label: "Matches", icon: "⚔" },
|
||||
{ href: "/mappools", label: "Mappools", icon: "♫" },
|
||||
{ href: "/teams", label: "Teams", icon: "⚑" }
|
||||
];
|
||||
function isActive(href) {
|
||||
if (href === "/") return page.url.pathname === "/";
|
||||
return page.url.pathname.startsWith(href);
|
||||
}
|
||||
const isAdmin = derived(() => user()?.role === "admin");
|
||||
$$renderer2.push(`<aside class="fixed top-0 left-0 z-40 flex h-full w-56 flex-col border-r border-border bg-surface-800"><a href="/" class="flex items-center gap-2.5 px-5 py-5"><div class="font-800 flex h-8 w-8 items-center justify-center rounded-md bg-accent text-sm text-surface-900">V</div> <span class="font-700 text-base tracking-tight text-text-primary">Vash Esports</span></a> <nav class="mt-2 flex flex-1 flex-col gap-0.5 px-3"><!--[-->`);
|
||||
const each_array = ensure_array_like(nav);
|
||||
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
|
||||
let item = each_array[$$index];
|
||||
$$renderer2.push(`<a${attr("href", item.href)}${attr_class(`font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors ${stringify(isActive(item.href) ? "bg-accent-dim text-accent" : "text-text-secondary hover:bg-surface-700 hover:text-text-primary")}`)}><span class="text-base">${escape_html(item.icon)}</span> ${escape_html(item.label)}</a>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (isAdmin()) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-4 mb-1 px-3"><p class="text-[10px] font-600 text-text-secondary uppercase tracking-widest">Staff</p></div> <a href="/admin"${attr_class(`font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors ${stringify(isActive("/admin") ? "bg-red-500/10 text-red-400" : "text-text-secondary hover:bg-surface-700 hover:text-text-primary")}`)}><span class="text-base">⚡</span> Admin</a>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></nav> <div class="border-t border-border p-3"><a href="/settings"${attr_class(`font-500 flex items-center gap-3 rounded-md px-3 py-2 text-sm text-text-secondary transition-colors hover:bg-surface-700 hover:text-text-primary ${stringify(isActive("/settings") ? "bg-accent-dim text-accent" : "")}`)}><span class="text-base">⚙</span> Settings</a></div></aside>`);
|
||||
});
|
||||
}
|
||||
function Header($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
const user = derived(() => page.data?.user);
|
||||
const roleBadge = {
|
||||
referee: { label: "REF", color: "bg-yellow-500/20 text-yellow-400" },
|
||||
admin: { label: "ADMIN", color: "bg-red-500/20 text-red-400" }
|
||||
};
|
||||
const badge = derived(() => user()?.role ? roleBadge[user().role] : null);
|
||||
$$renderer2.push(`<header class="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-surface-900/80 px-6 backdrop-blur-md"><div></div> <div class="flex items-center gap-4">`);
|
||||
if (user()) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="flex items-center gap-3">`);
|
||||
if (badge()) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span${attr_class(`rounded px-1.5 py-0.5 text-[10px] font-700 ${stringify(badge().color)}`)}>${escape_html(badge().label)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (user().image) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", user().image)}${attr("alt", user().name)} class="h-7 w-7 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span class="font-500 text-sm text-text-primary">${escape_html(user().name)}</span> <form method="post" action="/settings?/logout"><button type="submit" class="rounded-md border border-border px-2.5 py-1 text-xs text-text-secondary transition-colors hover:border-red-500/30 hover:text-red-400">Logout</button></form></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<a href="/login" class="font-600 rounded-md bg-accent px-3.5 py-1.5 text-sm text-surface-900 transition-colors hover:bg-accent-hover">Sign in with osu!</a>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></header>`);
|
||||
});
|
||||
}
|
||||
function _layout($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { children } = $$props;
|
||||
const user = derived(() => page.data?.user);
|
||||
if (user()) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="flex min-h-dvh">`);
|
||||
Sidebar($$renderer2);
|
||||
$$renderer2.push(`<!----> <div class="ml-56 flex flex-1 flex-col">`);
|
||||
Header($$renderer2);
|
||||
$$renderer2.push(`<!----> <main class="flex-1 p-6">`);
|
||||
children($$renderer2);
|
||||
$$renderer2.push(`<!----></main></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="min-h-dvh"><header class="flex h-14 items-center justify-between border-b border-border bg-surface-900/80 px-6 backdrop-blur-md"><a href="/" class="flex items-center gap-2.5"><div class="font-800 flex h-8 w-8 items-center justify-center rounded-md bg-accent text-sm text-surface-900">V</div> <span class="font-700 text-base tracking-tight text-text-primary">Vash Esports</span></a> <a href="/login" class="font-600 rounded-md bg-accent px-3.5 py-1.5 text-sm text-surface-900 transition-colors hover:bg-accent-hover">Sign in with osu!</a></header> <main class="p-6">`);
|
||||
children($$renderer2);
|
||||
$$renderer2.push(`<!----></main></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]-->`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_layout as default
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { d as db, b as match, c as team, m as mappool } from "../../../chunks/index2.js";
|
||||
import { desc, inArray, count, eq } from "drizzle-orm";
|
||||
const load = async ({ locals }) => {
|
||||
if (!locals.user) {
|
||||
return {
|
||||
authenticated: false,
|
||||
recentMatches: [],
|
||||
liveMatches: [],
|
||||
stats: { matches: 0, finished: 0, teams: 0, mappools: 0 }
|
||||
};
|
||||
}
|
||||
const recentMatches = await db.query.match.findMany({
|
||||
with: { participants: { with: { team: true } } },
|
||||
orderBy: desc(match.createdAt),
|
||||
limit: 5
|
||||
});
|
||||
const liveMatches = await db.query.match.findMany({
|
||||
with: { participants: { with: { team: true } } },
|
||||
where: inArray(match.state, ["LOBBY", "ROLLING", "PICKING", "PLAYING"])
|
||||
});
|
||||
const [matchCount] = await db.select({ count: count() }).from(match);
|
||||
const [teamCount] = await db.select({ count: count() }).from(team);
|
||||
const [poolCount] = await db.select({ count: count() }).from(mappool);
|
||||
const [finishedCount] = await db.select({ count: count() }).from(match).where(eq(match.state, "FINISHED"));
|
||||
return {
|
||||
authenticated: true,
|
||||
recentMatches,
|
||||
liveMatches,
|
||||
stats: {
|
||||
matches: matchCount.count,
|
||||
finished: finishedCount.count,
|
||||
teams: teamCount.count,
|
||||
mappools: poolCount.count
|
||||
}
|
||||
};
|
||||
};
|
||||
export {
|
||||
load
|
||||
};
|
||||
118
.svelte-kit/output/server/entries/pages/(app)/_page.svelte.js
Normal file
118
.svelte-kit/output/server/entries/pages/(app)/_page.svelte.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import { e as escape_html, a as ensure_array_like, b as attr, s as stringify, c as attr_class } from "../../../chunks/index3.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
const stateConfig = {
|
||||
CREATED: { label: "Created", color: "text-text-secondary" },
|
||||
LOBBY: { label: "In Lobby", color: "text-yellow-400" },
|
||||
ROLLING: {
|
||||
label: "Rolling",
|
||||
color: "text-yellow-400",
|
||||
dot: "bg-yellow-400"
|
||||
},
|
||||
PICKING: { label: "Picking", color: "text-blue-400", dot: "bg-blue-400" },
|
||||
PLAYING: { label: "Live", color: "text-green-400", dot: "bg-green-400" },
|
||||
FINISHED: { label: "Finished", color: "text-text-secondary" },
|
||||
CANCELLED: { label: "Cancelled", color: "text-red-400" }
|
||||
};
|
||||
let queueLoading = false;
|
||||
function timeAgo(date) {
|
||||
const d = new Date(date);
|
||||
const diff = Date.now() - d.getTime();
|
||||
const mins = Math.floor(diff / 6e4);
|
||||
if (mins < 1) return "just now";
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
if (!data.user) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="flex min-h-[70vh] flex-col items-center justify-center text-center"><div class="font-800 mb-6 flex h-16 w-16 items-center justify-center rounded-xl bg-accent text-2xl text-surface-900">V</div> <h1 class="text-4xl font-800 tracking-tight">Welcome to <span class="text-accent">Vash Esports</span></h1> <p class="mt-3 max-w-md text-text-secondary">Automated tournament match management for osu! — lobbies, mappools, picks, bans, and
|
||||
ELO tracking all in one place.</p> <a href="/login" class="font-600 mt-8 rounded-lg bg-accent px-6 py-3 text-surface-900 transition-colors hover:bg-accent-hover">Sign in with osu!</a> <p class="mt-4 text-xs text-text-secondary">Uses your osu! account. No extra registration needed.</p></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="mx-auto max-w-5xl"><div><h1 class="text-2xl font-700 tracking-tight">Welcome back, <span class="text-accent">${escape_html(data.user.name)}</span></h1> <p class="mt-1 text-sm text-text-secondary">Automated esports match management for osu!</p></div> <div class="mt-6 grid grid-cols-2 gap-3 sm:grid-cols-4"><div class="rounded-lg border border-border bg-surface-800 p-4"><p class="text-2xl font-800 tabular-nums text-text-primary">${escape_html(data.stats.matches)}</p> <p class="mt-1 text-xs text-text-secondary">Total Matches</p></div> <div class="rounded-lg border border-border bg-surface-800 p-4"><p class="text-2xl font-800 tabular-nums text-green-400">${escape_html(data.stats.finished)}</p> <p class="mt-1 text-xs text-text-secondary">Completed</p></div> <div class="rounded-lg border border-border bg-surface-800 p-4"><p class="text-2xl font-800 tabular-nums text-text-primary">${escape_html(data.stats.teams)}</p> <p class="mt-1 text-xs text-text-secondary">Teams</p></div> <div class="rounded-lg border border-border bg-surface-800 p-4"><p class="text-2xl font-800 tabular-nums text-text-primary">${escape_html(data.stats.mappools)}</p> <p class="mt-1 text-xs text-text-secondary">Mappools</p></div></div> `);
|
||||
if (data.liveMatches.length > 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-8"><div class="flex items-center gap-2"><div class="relative h-2 w-2"><div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div> <div class="relative h-2 w-2 rounded-full bg-green-400"></div></div> <h2 class="text-sm font-600">Live Now</h2></div> <div class="mt-3 flex flex-col gap-2"><!--[-->`);
|
||||
const each_array = ensure_array_like(data.liveMatches);
|
||||
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
|
||||
let m = each_array[$$index];
|
||||
const p1 = m.participants[0];
|
||||
const p2 = m.participants[1];
|
||||
const sc = stateConfig[m.state];
|
||||
$$renderer2.push(`<a${attr("href", `/matches/${stringify(m.id)}`)} class="flex items-center gap-4 rounded-lg border border-green-500/20 bg-surface-800 p-4 transition-all hover:border-green-500/40 hover:bg-surface-700"><div class="flex flex-1 items-center gap-3">`);
|
||||
if (p1?.team.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", p1.team.avatarUrl)} alt="" class="h-8 w-8 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span class="text-sm font-600">${escape_html(p1?.team.name ?? "?")}</span></div> <div class="flex items-center gap-3"><span class="text-xl font-800 tabular-nums">${escape_html(p1?.score ?? 0)}</span> <span class="text-xs text-text-secondary">vs</span> <span class="text-xl font-800 tabular-nums">${escape_html(p2?.score ?? 0)}</span></div> <div class="flex flex-1 items-center justify-end gap-3"><span class="text-sm font-600">${escape_html(p2?.team.name ?? "?")}</span> `);
|
||||
if (p2?.team.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", p2.team.avatarUrl)} alt="" class="h-8 w-8 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> <span${attr_class(`flex items-center gap-1.5 text-xs font-500 ${stringify(sc?.color ?? "")}`)}>`);
|
||||
if (sc?.dot) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span${attr_class(`h-1.5 w-1.5 rounded-full ${stringify(sc.dot)}`)}></span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> ${escape_html(sc?.label)}</span></a>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="mt-6 rounded-lg border border-border bg-surface-800 p-5"><div class="flex items-center justify-between"><div><h2 class="text-sm font-600">Ranked Queue</h2> <p class="mt-0.5 text-xs text-text-secondary">Find a match at your skill level</p></div> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<button${attr("disabled", queueLoading, true)} class="rounded-md bg-accent px-4 py-2 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover disabled:opacity-50">${escape_html("Find Match")}</button>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div> <div class="mt-8 grid gap-4 lg:grid-cols-3"><div class="lg:col-span-1"><h2 class="text-sm font-600">Quick Actions</h2> <div class="mt-3 flex flex-col gap-2"><a href="/matches" class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700"><span class="flex h-8 w-8 items-center justify-center rounded-md bg-accent/10 text-accent">⚔</span> <div><p class="text-sm font-600">New Match</p> <p class="text-xs text-text-secondary">Start a 1v1</p></div></a> <a href="/mappools" class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700"><span class="flex h-8 w-8 items-center justify-center rounded-md bg-purple-500/10 text-purple-400">♫</span> <div><p class="text-sm font-600">Mappools</p> <p class="text-xs text-text-secondary">Build a pool</p></div></a> <a href="/teams" class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/40 hover:bg-surface-700"><span class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-500/10 text-blue-400">⚑</span> <div><p class="text-sm font-600">Teams</p> <p class="text-xs text-text-secondary">Manage rosters</p></div></a></div></div> <div class="lg:col-span-2"><h2 class="text-sm font-600">Recent Matches</h2> <div class="mt-3 flex flex-col gap-2">`);
|
||||
const each_array_1 = ensure_array_like(data.recentMatches);
|
||||
if (each_array_1.length !== 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
for (let $$index_1 = 0, $$length = each_array_1.length; $$index_1 < $$length; $$index_1++) {
|
||||
let m = each_array_1[$$index_1];
|
||||
const p1 = m.participants[0];
|
||||
const p2 = m.participants[1];
|
||||
const config = m.config;
|
||||
const sc = stateConfig[m.state];
|
||||
$$renderer2.push(`<a${attr("href", `/matches/${stringify(m.id)}`)} class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/30 hover:bg-surface-700"><div class="min-w-0 flex-1"><div class="flex items-center gap-2"><span class="text-sm font-600">${escape_html(p1?.team.name ?? "?")}</span> `);
|
||||
if (m.state === "FINISHED") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span${attr_class(`text-xs font-700 tabular-nums ${stringify((p1?.score ?? 0) > (p2?.score ?? 0) ? "text-green-400" : "text-text-secondary")}`)}>${escape_html(p1?.score ?? 0)}</span> <span class="text-xs text-text-secondary">-</span> <span${attr_class(`text-xs font-700 tabular-nums ${stringify((p2?.score ?? 0) > (p1?.score ?? 0) ? "text-green-400" : "text-text-secondary")}`)}>${escape_html(p2?.score ?? 0)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<span class="text-xs text-text-secondary">vs</span>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span class="text-sm font-600">${escape_html(p2?.team.name ?? "?")}</span></div> <p class="mt-0.5 text-xs text-text-secondary">${escape_html(m.name ? `${m.name} · ` : "")}BO${escape_html(config.bestOf)}
|
||||
· ${escape_html(timeAgo(m.finishedAt ?? m.createdAt))}</p></div> `);
|
||||
if (m.state === "FINISHED" && m.winnerId) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
const winner = m.participants.find((p) => p.teamId === m.winnerId);
|
||||
$$renderer2.push(`<span class="text-xs text-green-400">🏆 ${escape_html(winner?.team.name)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span${attr_class(`text-xs ${stringify(sc?.color ?? "text-text-secondary")}`)}>${escape_html(sc?.label)}</span></a>`);
|
||||
}
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="rounded-lg border border-dashed border-border py-8 text-center"><p class="text-sm text-text-secondary">No matches yet. <a href="/matches" class="text-accent hover:underline">Create one</a></p></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div></div></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]-->`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { d as db, u as user, p as playerRating } from "../../../../chunks/index2.js";
|
||||
import { eq, desc } from "drizzle-orm";
|
||||
import { error } from "@sveltejs/kit";
|
||||
const ROLE_HIERARCHY = {
|
||||
player: 0,
|
||||
referee: 1,
|
||||
admin: 2
|
||||
};
|
||||
function hasRole(userRole, minRole) {
|
||||
const level = ROLE_HIERARCHY[userRole] ?? 0;
|
||||
const required = ROLE_HIERARCHY[minRole];
|
||||
return level >= required;
|
||||
}
|
||||
function requireRole(locals, minRole, message = "Insufficient permissions") {
|
||||
if (!locals.user) {
|
||||
error(401, "Not logged in");
|
||||
}
|
||||
if (!hasRole(locals.user.role, minRole)) {
|
||||
error(403, message);
|
||||
}
|
||||
}
|
||||
async function setUserRole(userId, role) {
|
||||
await db.update(user).set({ role }).where(eq(user.id, userId));
|
||||
}
|
||||
const load = async ({ locals }) => {
|
||||
requireRole(locals, "admin");
|
||||
const users = await db.query.user.findMany({
|
||||
orderBy: desc(user.createdAt)
|
||||
});
|
||||
const usersWithRatings = await Promise.all(
|
||||
users.map(async (u) => {
|
||||
const rating = await db.query.playerRating.findFirst({
|
||||
where: eq(playerRating.userId, u.id)
|
||||
});
|
||||
return {
|
||||
id: u.id,
|
||||
name: u.name,
|
||||
email: u.email,
|
||||
image: u.image,
|
||||
role: u.role ?? "player",
|
||||
createdAt: u.createdAt,
|
||||
elo: rating?.elo ?? 1e3,
|
||||
wins: rating?.wins ?? 0,
|
||||
losses: rating?.losses ?? 0
|
||||
};
|
||||
})
|
||||
);
|
||||
return { users: usersWithRatings };
|
||||
};
|
||||
const actions = {
|
||||
setRole: async ({ request, locals }) => {
|
||||
requireRole(locals, "admin");
|
||||
const form = await request.formData();
|
||||
const userId = form.get("userId")?.toString();
|
||||
const role = form.get("role")?.toString();
|
||||
if (!userId || !role) return { error: "User ID and role are required" };
|
||||
if (!["player", "referee", "admin"].includes(role)) {
|
||||
return { error: "Invalid role" };
|
||||
}
|
||||
if (userId === locals.user.id && role !== "admin") {
|
||||
return { error: "You can't demote yourself" };
|
||||
}
|
||||
await setUserRole(userId, role);
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { e as escape_html, a as ensure_array_like, b as attr, s as stringify } from "../../../../chunks/index3.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../chunks/url.js";
|
||||
import "../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../chunks/root.js";
|
||||
import "../../../../chunks/exports.js";
|
||||
import "../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
const roleBadge = {
|
||||
player: {
|
||||
label: "Player",
|
||||
color: "bg-surface-600 text-text-secondary border-border"
|
||||
},
|
||||
referee: {
|
||||
label: "Referee",
|
||||
color: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30"
|
||||
},
|
||||
admin: {
|
||||
label: "Admin",
|
||||
color: "bg-red-500/20 text-red-400 border-red-500/30"
|
||||
}
|
||||
};
|
||||
$$renderer2.push(`<div class="mx-auto max-w-4xl"><div><h1 class="text-2xl font-700 tracking-tight">Admin Panel</h1> <p class="mt-1 text-sm text-text-secondary">${escape_html(data.users.length)} registered user${escape_html(data.users.length !== 1 ? "s" : "")}</p></div> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="mt-6 rounded-lg border border-border bg-surface-800"><div class="grid grid-cols-[auto_1fr_auto_auto_auto] gap-4 border-b border-border px-4 py-3"><span class="text-xs font-600 text-text-secondary">Avatar</span> <span class="text-xs font-600 text-text-secondary">User</span> <span class="text-xs font-600 text-text-secondary">ELO</span> <span class="text-xs font-600 text-text-secondary">W/L</span> <span class="text-xs font-600 text-text-secondary">Role</span></div> <!--[-->`);
|
||||
const each_array = ensure_array_like(data.users);
|
||||
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
|
||||
let u = each_array[$$index];
|
||||
const badge = roleBadge[u.role] ?? roleBadge.player;
|
||||
$$renderer2.push(`<div class="grid grid-cols-[auto_1fr_auto_auto_auto] items-center gap-4 border-b border-border/50 px-4 py-3 last:border-b-0">`);
|
||||
if (u.image) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", u.image)} alt="" class="h-8 w-8 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="flex h-8 w-8 items-center justify-center rounded-full bg-surface-600 text-xs font-700 text-text-secondary">${escape_html(u.name.charAt(0).toUpperCase())}</div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="min-w-0"><p class="truncate text-sm font-500">${escape_html(u.name)}</p> <p class="truncate text-xs text-text-secondary">${escape_html(u.email)}</p></div> <span class="text-sm font-600 tabular-nums text-text-primary">${escape_html(u.elo)}</span> <span class="text-xs tabular-nums text-text-secondary"><span class="text-green-400">${escape_html(u.wins)}W</span> / <span class="text-red-400">${escape_html(u.losses)}L</span></span> <form method="post" action="?/setRole" class="flex items-center gap-2"><input type="hidden" name="userId"${attr("value", u.id)}/> `);
|
||||
$$renderer2.select(
|
||||
{
|
||||
name: "role",
|
||||
value: u.role,
|
||||
onchange: (e) => e.currentTarget.form?.requestSubmit(),
|
||||
class: `rounded-md border px-2 py-1 text-xs font-500 ${stringify(badge.color)} cursor-pointer bg-transparent focus:outline-none`
|
||||
},
|
||||
($$renderer3) => {
|
||||
$$renderer3.option({ value: "player", class: "bg-surface-800 text-text-primary" }, ($$renderer4) => {
|
||||
$$renderer4.push(`Player`);
|
||||
});
|
||||
$$renderer3.option({ value: "referee", class: "bg-surface-800 text-text-primary" }, ($$renderer4) => {
|
||||
$$renderer4.push(`Referee`);
|
||||
});
|
||||
$$renderer3.option({ value: "admin", class: "bg-surface-800 text-text-primary" }, ($$renderer4) => {
|
||||
$$renderer4.push(`Admin`);
|
||||
});
|
||||
}
|
||||
);
|
||||
$$renderer2.push(`</form></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import { d as db, m as mappool, a as mappoolSlot } from "../../../../../chunks/index2.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { redirect, error } from "@sveltejs/kit";
|
||||
import { g as getBeatmap } from "../../../../../chunks/api.js";
|
||||
const load = async ({ params, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const pool = await db.query.mappool.findFirst({
|
||||
where: eq(mappool.id, params.id),
|
||||
with: {
|
||||
slots: {
|
||||
orderBy: (s, { asc }) => [asc(s.category), asc(s.orderInCategory)]
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!pool) error(404, "Mappool not found");
|
||||
const slotsWithBeatmaps = await Promise.all(
|
||||
pool.slots.map(async (slot) => {
|
||||
try {
|
||||
const beatmap = await getBeatmap(slot.beatmapId);
|
||||
return {
|
||||
...slot,
|
||||
beatmap: {
|
||||
title: beatmap.beatmapset.title,
|
||||
artist: beatmap.beatmapset.artist,
|
||||
version: beatmap.version,
|
||||
starRating: beatmap.difficulty_rating,
|
||||
bpm: beatmap.bpm,
|
||||
totalLength: beatmap.total_length,
|
||||
coverUrl: beatmap.beatmapset.covers["card@2x"],
|
||||
url: beatmap.url
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
...slot,
|
||||
beatmap: null
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
return {
|
||||
pool: { ...pool, slots: slotsWithBeatmaps }
|
||||
};
|
||||
};
|
||||
const actions = {
|
||||
addSlot: async ({ params, request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
let beatmapId = form.get("beatmapId")?.toString()?.trim();
|
||||
const category = form.get("category")?.toString()?.trim()?.toUpperCase();
|
||||
if (!beatmapId || !category) {
|
||||
return { error: "Beatmap ID and category are required" };
|
||||
}
|
||||
const urlMatch = beatmapId.match(/beatmaps\/(\d+)/);
|
||||
const setMatch = beatmapId.match(/beatmapsets\/\d+#\w+\/(\d+)/);
|
||||
if (urlMatch) beatmapId = urlMatch[1];
|
||||
else if (setMatch) beatmapId = setMatch[1];
|
||||
if (!/^\d+$/.test(beatmapId)) {
|
||||
return { error: "Invalid beatmap ID" };
|
||||
}
|
||||
let beatmap;
|
||||
try {
|
||||
beatmap = await getBeatmap(beatmapId);
|
||||
} catch {
|
||||
return { error: "Beatmap not found on osu!" };
|
||||
}
|
||||
const existing = await db.query.mappoolSlot.findMany({
|
||||
where: and(
|
||||
eq(mappoolSlot.mappoolId, params.id),
|
||||
eq(mappoolSlot.category, category)
|
||||
)
|
||||
});
|
||||
await db.insert(mappoolSlot).values({
|
||||
mappoolId: params.id,
|
||||
beatmapId,
|
||||
category,
|
||||
orderInCategory: existing.length + 1,
|
||||
starRating: beatmap.difficulty_rating,
|
||||
mods: category === "NM" || category === "TB" || category === "FM" ? [] : [category]
|
||||
});
|
||||
return { success: true };
|
||||
},
|
||||
removeSlot: async ({ request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const slotId = form.get("slotId")?.toString();
|
||||
if (!slotId) return { error: "Missing slot ID" };
|
||||
await db.delete(mappoolSlot).where(eq(mappoolSlot.id, slotId));
|
||||
return { success: true };
|
||||
},
|
||||
deletePool: async ({ params, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
await db.delete(mappool).where(eq(mappool.id, params.id));
|
||||
redirect(303, "/mappools");
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import { e as escape_html, a as ensure_array_like, b as attr, c as attr_class, d as derived, s as stringify } from "../../../../../chunks/index3.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../../chunks/url.js";
|
||||
import "../../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../../chunks/root.js";
|
||||
import "../../../../../chunks/exports.js";
|
||||
import "../../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
const categories = ["NM", "HD", "HR", "DT", "FM", "TB"];
|
||||
let addingCategory = "NM";
|
||||
let beatmapInput = "";
|
||||
const grouped = derived(() => () => {
|
||||
const groups = {};
|
||||
for (const cat of categories) {
|
||||
const slots = data.pool.slots.filter((s) => s.category === cat);
|
||||
if (slots.length > 0) groups[cat] = slots;
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
function formatLength(seconds) {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
return `${m}:${s.toString().padStart(2, "0")}`;
|
||||
}
|
||||
const categoryColors = {
|
||||
NM: "bg-blue-500/20 text-blue-400 border-blue-500/30",
|
||||
HD: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
|
||||
HR: "bg-red-500/20 text-red-400 border-red-500/30",
|
||||
DT: "bg-purple-500/20 text-purple-400 border-purple-500/30",
|
||||
FM: "bg-green-500/20 text-green-400 border-green-500/30",
|
||||
TB: "bg-pink-500/20 text-pink-400 border-pink-500/30"
|
||||
};
|
||||
$$renderer2.push(`<div class="mx-auto max-w-4xl"><div class="flex items-center justify-between"><div class="flex items-center gap-3"><a href="/mappools" class="text-text-secondary transition-colors hover:text-text-primary">←</a> <div><h1 class="text-2xl font-700 tracking-tight">${escape_html(data.pool.name)}</h1> <p class="mt-1 text-sm text-text-secondary">${escape_html(data.pool.slots.length)} map${escape_html(data.pool.slots.length !== 1 ? "s" : "")}</p></div></div> <form method="post" action="?/deletePool"><button type="submit" class="rounded-md border border-red-500/30 px-3 py-1.5 text-sm text-red-400 transition-colors hover:bg-red-500/10">Delete Pool</button></form></div> <form method="post" action="?/addSlot" class="mt-6 rounded-lg border border-border bg-surface-800 p-4"><h2 class="text-sm font-600">Add Map</h2> <div class="mt-3 flex gap-3">`);
|
||||
$$renderer2.select(
|
||||
{
|
||||
name: "category",
|
||||
value: addingCategory,
|
||||
class: "rounded-md border border-border bg-surface-700 px-3 py-2 text-sm text-text-primary focus:border-accent focus:outline-none"
|
||||
},
|
||||
($$renderer3) => {
|
||||
$$renderer3.push(`<!--[-->`);
|
||||
const each_array = ensure_array_like(categories);
|
||||
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
|
||||
let cat = each_array[$$index];
|
||||
$$renderer3.option({ value: cat }, ($$renderer4) => {
|
||||
$$renderer4.push(`${escape_html(cat)}`);
|
||||
});
|
||||
}
|
||||
$$renderer3.push(`<!--]-->`);
|
||||
}
|
||||
);
|
||||
$$renderer2.push(` <input type="text" name="beatmapId"${attr("value", beatmapInput)} placeholder="Beatmap ID or URL (e.g. 75 or https://osu.ppy.sh/beatmaps/75)" class="flex-1 rounded-md border border-border bg-surface-700 px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary/50 focus:border-accent focus:outline-none"/> <button type="submit"${attr("disabled", !beatmapInput.trim(), true)} class="rounded-md bg-accent px-4 py-2 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover disabled:opacity-50">${escape_html("Add")}</button></div> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></form> <div class="mt-6 flex flex-col gap-6">`);
|
||||
const each_array_1 = ensure_array_like(Object.entries(grouped()()));
|
||||
if (each_array_1.length !== 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
for (let $$index_2 = 0, $$length = each_array_1.length; $$index_2 < $$length; $$index_2++) {
|
||||
let [category, slots] = each_array_1[$$index_2];
|
||||
$$renderer2.push(`<div><div class="mb-2 flex items-center gap-2"><span${attr_class(`rounded border px-2 py-0.5 text-xs font-600 ${stringify(categoryColors[category] ?? "bg-surface-600 text-text-secondary border-border")}`)}>${escape_html(category)}</span> <span class="text-xs text-text-secondary">${escape_html(slots.length)} map${escape_html(slots.length !== 1 ? "s" : "")}</span></div> <div class="flex flex-col gap-2"><!--[-->`);
|
||||
const each_array_2 = ensure_array_like(slots);
|
||||
for (let i = 0, $$length2 = each_array_2.length; i < $$length2; i++) {
|
||||
let slot = each_array_2[i];
|
||||
$$renderer2.push(`<div class="group flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-2 transition-colors hover:border-accent/30">`);
|
||||
if (slot.beatmap?.coverUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", slot.beatmap.coverUrl)} alt="" class="h-12 w-24 rounded object-cover"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="flex h-12 w-24 items-center justify-center rounded bg-surface-700 text-xs text-text-secondary">No cover</div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="min-w-0 flex-1">`);
|
||||
if (slot.beatmap) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<a${attr("href", slot.beatmap.url)} target="_blank" rel="noopener" class="truncate text-sm font-500 hover:text-accent">${escape_html(slot.beatmap.artist)} - ${escape_html(slot.beatmap.title)}</a> <div class="mt-0.5 flex items-center gap-3 text-xs text-text-secondary"><span>[${escape_html(slot.beatmap.version)}]</span> <span>★ ${escape_html(slot.beatmap.starRating.toFixed(2))}</span> <span>${escape_html(slot.beatmap.bpm)} BPM</span> <span>${escape_html(formatLength(slot.beatmap.totalLength))}</span></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<p class="text-sm text-text-secondary">Beatmap #${escape_html(slot.beatmapId)}</p>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> <span class="text-xs font-600 text-text-secondary">${escape_html(category)}${escape_html(i + 1)}</span> <form method="post" action="?/removeSlot"><input type="hidden" name="slotId"${attr("value", slot.id)}/> <button type="submit" class="rounded p-1 text-text-secondary opacity-0 transition-all hover:bg-red-500/10 hover:text-red-400 group-hover:opacity-100">✕</button></form></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
}
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="rounded-lg border border-dashed border-border py-12 text-center"><p class="text-sm text-text-secondary">No maps yet. Add your first map above.</p></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { d as db, m as mappool } from "../../../../chunks/index2.js";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
const load = async ({ locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const mappools = await db.query.mappool.findMany({
|
||||
with: { slots: true },
|
||||
orderBy: (m, { desc }) => [desc(m.createdAt)]
|
||||
});
|
||||
return { mappools };
|
||||
};
|
||||
const actions = {
|
||||
create: async ({ request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const name = form.get("name")?.toString()?.trim();
|
||||
if (!name) return { error: "Name is required" };
|
||||
const [created] = await db.insert(mappool).values({ name, createdBy: locals.user.id }).returning();
|
||||
redirect(303, `/mappools/${created.id}`);
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { e as escape_html, a as ensure_array_like, b as attr, s as stringify } from "../../../../chunks/index3.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../chunks/url.js";
|
||||
import "../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../chunks/root.js";
|
||||
import "../../../../chunks/exports.js";
|
||||
import "../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
$$renderer2.push(`<div class="mx-auto max-w-4xl"><div class="flex items-center justify-between"><div><h1 class="text-2xl font-700 tracking-tight">Mappools</h1> <p class="mt-1 text-sm text-text-secondary">Create and manage your beatmap pools</p></div> <button class="rounded-md bg-accent px-4 py-2 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover">${escape_html("New Pool")}</button></div> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="mt-6 flex flex-col gap-3">`);
|
||||
const each_array = ensure_array_like(data.mappools);
|
||||
if (each_array.length !== 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
|
||||
let pool = each_array[$$index];
|
||||
$$renderer2.push(`<a${attr("href", `/mappools/${stringify(pool.id)}`)} class="group flex items-center justify-between rounded-lg border border-border bg-surface-800 p-4 transition-colors hover:border-accent/40 hover:bg-surface-700"><div><h2 class="text-sm font-600">${escape_html(pool.name)}</h2> <p class="mt-1 text-xs text-text-secondary">${escape_html(pool.slots.length)} map${escape_html(pool.slots.length !== 1 ? "s" : "")}
|
||||
· Created ${escape_html(new Date(pool.createdAt).toLocaleDateString())}</p></div> <span class="text-text-secondary transition-colors group-hover:text-accent">→</span></a>`);
|
||||
}
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="rounded-lg border border-dashed border-border py-12 text-center"><p class="text-sm text-text-secondary">No mappools yet. Create one to get started.</p></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,129 @@
|
||||
import { d as db, u as user } from "../../../../../chunks/index2.js";
|
||||
import { a as cancelMatch, g as getMatchFull, p as pickMap, s as submitRoll } from "../../../../../chunks/engine.js";
|
||||
import { c as closeLobby, f as forceStartGame, p as playPickedMap, g as getLobby } from "../../../../../chunks/orchestrator.js";
|
||||
import { redirect, error } from "@sveltejs/kit";
|
||||
import { g as getBeatmap } from "../../../../../chunks/api.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
const load = async ({ params, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
let m;
|
||||
try {
|
||||
m = await getMatchFull(params.id);
|
||||
} catch {
|
||||
error(404, "Match not found");
|
||||
}
|
||||
const beatmapCache = {};
|
||||
if (m.mappool?.slots) {
|
||||
await Promise.allSettled(
|
||||
m.mappool.slots.map(async (slot) => {
|
||||
try {
|
||||
const bm = await getBeatmap(slot.beatmapId);
|
||||
beatmapCache[slot.beatmapId] = {
|
||||
title: bm.beatmapset.title,
|
||||
artist: bm.beatmapset.artist,
|
||||
version: bm.version,
|
||||
starRating: bm.difficulty_rating,
|
||||
bpm: bm.bpm,
|
||||
totalLength: bm.total_length,
|
||||
coverUrl: bm.beatmapset.covers["card@2x"],
|
||||
listCoverUrl: bm.beatmapset.covers["list@2x"]
|
||||
};
|
||||
} catch {
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
return { match: m, beatmapCache, userId: locals.user.id };
|
||||
};
|
||||
const actions = {
|
||||
reinvite: async ({ params, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const lobby = getLobby(params.id);
|
||||
if (!lobby) return { error: "No active IRC lobby for this match" };
|
||||
const m = await getMatchFull(params.id);
|
||||
const invited = [];
|
||||
for (const p of m.participants) {
|
||||
for (const pl of p.players) {
|
||||
const u = await db.query.user.findFirst({ where: eq(user.id, pl.userId) });
|
||||
if (u?.name) {
|
||||
await lobby.invite(u.name);
|
||||
invited.push(u.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { reinvited: invited };
|
||||
},
|
||||
roll: async ({ params, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const m = await getMatchFull(params.id);
|
||||
let myParticipant = m.participants.find(
|
||||
(p) => p.players.some((pl) => pl.userId === locals.user.id)
|
||||
);
|
||||
if (!myParticipant) {
|
||||
return { error: "You are not in this match" };
|
||||
}
|
||||
if (myParticipant.rollValue !== null) {
|
||||
const unrolled = m.participants.find((p) => p.rollValue === null);
|
||||
if (unrolled) {
|
||||
myParticipant = unrolled;
|
||||
} else {
|
||||
return { error: "All participants have already rolled" };
|
||||
}
|
||||
}
|
||||
const value = Math.floor(Math.random() * 100) + 1;
|
||||
await submitRoll(params.id, myParticipant.id, value);
|
||||
const lobby = getLobby(params.id);
|
||||
if (lobby) {
|
||||
lobby.chat(`${myParticipant.team?.name ?? "Player"} rolled ${value} (via web)`).catch(() => {
|
||||
});
|
||||
const updated = await getMatchFull(params.id);
|
||||
if (updated.state === "PICKING") {
|
||||
const sorted = [...updated.participants].sort(
|
||||
(a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99)
|
||||
);
|
||||
lobby.chat(
|
||||
`Rolls complete! ${sorted[0]?.team.name} picks first. Use !pick <slot> (e.g. !pick NM1) or pick in web UI.`
|
||||
).catch(() => {
|
||||
});
|
||||
}
|
||||
}
|
||||
return { rolled: value, participantId: myParticipant.id };
|
||||
},
|
||||
pick: async ({ params, request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const slotId = form.get("slotId")?.toString();
|
||||
if (!slotId) return { error: "No map selected" };
|
||||
const m = await getMatchFull(params.id);
|
||||
const sorted = [...m.participants].sort(
|
||||
(a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99)
|
||||
);
|
||||
const expectedIdx = m.games.length % sorted.length;
|
||||
const expectedPicker = sorted[expectedIdx];
|
||||
if (!expectedPicker) return { error: "Cannot determine picker" };
|
||||
try {
|
||||
const game = await pickMap(params.id, expectedPicker.id, slotId);
|
||||
playPickedMap(params.id, game.id).catch(
|
||||
(err) => console.error("[Match] IRC play failed:", err.message)
|
||||
);
|
||||
return { picked: true, gameId: game.id };
|
||||
} catch (e) {
|
||||
return { error: e.message };
|
||||
}
|
||||
},
|
||||
forceStart: async ({ params, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
await forceStartGame(params.id);
|
||||
return { started: true };
|
||||
},
|
||||
cancel: async ({ params, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
await cancelMatch(params.id);
|
||||
await closeLobby(params.id);
|
||||
redirect(303, "/matches");
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,323 @@
|
||||
import { e as escape_html, b as attr, s as stringify, c as attr_class, a as ensure_array_like, d as derived } from "../../../../../chunks/index3.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../../chunks/url.js";
|
||||
import "../../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../../chunks/root.js";
|
||||
import "../../../../../chunks/exports.js";
|
||||
import "../../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
let m = derived(() => data.match);
|
||||
let p1 = derived(() => m().participants[0]);
|
||||
let p2 = derived(() => m().participants[1]);
|
||||
const config = derived(() => m().config);
|
||||
const winsNeeded = derived(() => Math.ceil(config().bestOf / 2));
|
||||
const playedSlotIds = derived(() => new Set(m().games.map((g) => g.mappoolSlotId)));
|
||||
const sortedByPick = derived(() => [...m().participants].sort((a, b) => (a.pickOrder ?? 99) - (b.pickOrder ?? 99)));
|
||||
const expectedPickerIdx = derived(() => m().games.length % sortedByPick().length);
|
||||
const expectedPicker = derived(() => sortedByPick()[expectedPickerIdx()]);
|
||||
const canRoll = derived(() => m().state === "ROLLING" && m().participants.some((p) => p.rollValue === null));
|
||||
const catColors = {
|
||||
NM: "bg-blue-500/20 text-blue-400 border-blue-500/30",
|
||||
HD: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
|
||||
HR: "bg-red-500/20 text-red-400 border-red-500/30",
|
||||
DT: "bg-purple-500/20 text-purple-400 border-purple-500/30",
|
||||
FM: "bg-green-500/20 text-green-400 border-green-500/30",
|
||||
TB: "bg-pink-500/20 text-pink-400 border-pink-500/30"
|
||||
};
|
||||
const groupedSlots = derived(() => () => {
|
||||
if (!m().mappool?.slots) return {};
|
||||
const groups = {};
|
||||
for (const slot of m().mappool.slots) {
|
||||
if (!groups[slot.category]) groups[slot.category] = [];
|
||||
groups[slot.category].push(slot);
|
||||
}
|
||||
for (const cat of Object.keys(groups)) {
|
||||
groups[cat].sort((a, b) => a.orderInCategory - b.orderInCategory);
|
||||
}
|
||||
return groups;
|
||||
});
|
||||
let rolling = false;
|
||||
let picking = false;
|
||||
let forceStarting = false;
|
||||
function formatLength(seconds) {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${mins}:${secs.toString().padStart(2, "0")}`;
|
||||
}
|
||||
function stateLabel(s) {
|
||||
return {
|
||||
CREATED: "Waiting",
|
||||
LOBBY: "In Lobby",
|
||||
ROLLING: "Rolling",
|
||||
PICKING: "Pick Phase",
|
||||
PLAYING: "Playing",
|
||||
FINISHED: "Finished",
|
||||
CANCELLED: "Cancelled"
|
||||
}[s] ?? s;
|
||||
}
|
||||
function stateBorder(s) {
|
||||
return {
|
||||
ROLLING: "border-yellow-500/30",
|
||||
PICKING: "border-blue-500/30",
|
||||
PLAYING: "border-green-500/30",
|
||||
FINISHED: "border-accent/30"
|
||||
}[s] ?? "border-border";
|
||||
}
|
||||
$$renderer2.push(`<div class="mx-auto max-w-5xl"><div class="flex items-center gap-3"><a href="/matches" class="text-text-secondary transition-colors hover:text-text-primary">←</a> <div class="flex-1"><h1 class="text-xl font-700 tracking-tight">${escape_html(m().name || "Match")}</h1> <p class="mt-0.5 text-xs text-text-secondary">Best of ${escape_html(config().bestOf)} · First to ${escape_html(winsNeeded())} `);
|
||||
if (m().osuLobbyId) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`· <a${attr("href", `https://osu.ppy.sh/mp/${stringify(m().osuLobbyId)}`)} target="_blank" class="text-accent hover:underline">osu! mp/${escape_html(m().osuLobbyId)}</a>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></p></div> <span${attr_class(`rounded border px-2.5 py-1 text-xs font-600 ${stringify(stateBorder(m().state))}`)}>${escape_html(stateLabel(m().state))}</span> `);
|
||||
if (!["FINISHED", "CANCELLED"].includes(m().state)) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<form method="post" action="?/cancel"><button type="submit" class="rounded-md border border-red-500/30 px-3 py-1.5 text-xs text-red-400 hover:bg-red-500/10">Cancel</button></form>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> `);
|
||||
if (p1() && p2()) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6 flex items-center gap-4"><div${attr_class(`flex flex-1 items-center gap-4 rounded-lg border p-4 ${stringify(m().winnerId === p1().teamId ? "border-green-500/40 bg-green-500/5" : "border-border bg-surface-800")}`)}>`);
|
||||
if (p1().team.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", p1().team.avatarUrl)} alt="" class="h-10 w-10 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="flex-1"><p class="text-sm font-600">${escape_html(p1().team.name)}</p> `);
|
||||
if (p1().rollValue != null) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<p class="text-xs text-text-secondary">Roll: ${escape_html(p1().rollValue)}${escape_html(p1().pickOrder === 1 ? " ★" : "")}</p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> <span${attr_class(`text-3xl font-800 tabular-nums ${stringify(p1().score >= winsNeeded() ? "text-green-400" : "text-text-primary")}`)}>${escape_html(p1().score)}</span></div> <span class="text-lg font-700 text-text-secondary">vs</span> <div${attr_class(`flex flex-1 items-center gap-4 rounded-lg border p-4 ${stringify(m().winnerId === p2().teamId ? "border-green-500/40 bg-green-500/5" : "border-border bg-surface-800")}`)}><span${attr_class(`text-3xl font-800 tabular-nums ${stringify(p2().score >= winsNeeded() ? "text-green-400" : "text-text-primary")}`)}>${escape_html(p2().score)}</span> <div class="flex-1 text-right"><p class="text-sm font-600">${escape_html(p2().team.name)}</p> `);
|
||||
if (p2().rollValue != null) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<p class="text-xs text-text-secondary">${escape_html(p2().pickOrder === 1 ? "★ " : "")}Roll: ${escape_html(p2().rollValue)}</p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> `);
|
||||
if (p2().team.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", p2().team.avatarUrl)} alt="" class="h-10 w-10 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (["LOBBY", "ROLLING", "PICKING", "PLAYING"].includes(m().state)) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-3 flex justify-center"><form method="post" action="?/reinvite"><button type="submit" class="rounded-md border border-border px-3 py-1.5 text-xs text-text-secondary transition-colors hover:border-accent/40 hover:text-accent">📨 Re-invite players to lobby</button></form></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().state === "ROLLING") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"><div class="w-full max-w-md rounded-xl border border-yellow-500/30 bg-surface-800 p-8 text-center shadow-2xl"><h2 class="text-lg font-700">🎲 Roll Phase</h2> <p class="mt-2 text-sm text-text-secondary">Highest roll picks first</p> <div class="mt-6 flex justify-center gap-6"><!--[-->`);
|
||||
const each_array = ensure_array_like(m().participants);
|
||||
for (let $$index = 0, $$length = each_array.length; $$index < $$length; $$index++) {
|
||||
let p = each_array[$$index];
|
||||
$$renderer2.push(`<div class="rounded-lg border border-border bg-surface-700 px-6 py-4"><p class="text-xs text-text-secondary">${escape_html(p.team.name)}</p> `);
|
||||
if (p.rollValue != null) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<p class="mt-1 text-2xl font-800 text-yellow-400">${escape_html(p.rollValue)}</p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<p class="mt-1 text-2xl font-800 text-text-secondary/30">—</p>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> `);
|
||||
if (canRoll()) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<form method="post" action="?/roll"><button type="submit"${attr("disabled", rolling, true)} class="mt-6 rounded-md bg-accent px-8 py-3 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover disabled:opacity-50">${escape_html("🎲 Roll!")}</button></form> <p class="mt-3 text-xs text-text-secondary">or type <code class="rounded bg-surface-700 px-1.5 py-0.5 font-mono text-accent">!roll</code> in osu! chat</p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<p class="mt-6 text-sm text-text-secondary animate-pulse">Waiting for all rolls...</p>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().state === "PICKING") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"><div class="w-full max-w-3xl max-h-[80vh] overflow-y-auto rounded-xl border border-blue-500/30 bg-surface-800 p-6 shadow-2xl"><div class="text-center"><h2 class="text-lg font-700">🎯 Pick a Map</h2> <p class="mt-1 text-sm"><span class="font-600 text-accent">${escape_html(expectedPicker()?.team.name)}</span>'s turn to pick</p> <p class="mt-1 text-xs text-text-secondary">or type <code class="rounded bg-surface-700 px-1.5 py-0.5 font-mono text-accent">!pick NM1</code> in osu! chat</p></div> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="mt-6 flex flex-col gap-4"><!--[-->`);
|
||||
const each_array_1 = ensure_array_like(Object.entries(groupedSlots()()));
|
||||
for (let $$index_2 = 0, $$length = each_array_1.length; $$index_2 < $$length; $$index_2++) {
|
||||
let [category, slots] = each_array_1[$$index_2];
|
||||
$$renderer2.push(`<div><span${attr_class(`mb-2 inline-block rounded border px-2 py-0.5 text-xs font-600 ${stringify(catColors[category] ?? "border-border")}`)}>${escape_html(category)}</span> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2"><!--[-->`);
|
||||
const each_array_2 = ensure_array_like(slots);
|
||||
for (let i = 0, $$length2 = each_array_2.length; i < $$length2; i++) {
|
||||
let slot = each_array_2[i];
|
||||
const bm = data.beatmapCache[slot.beatmapId];
|
||||
const isPlayed = playedSlotIds().has(slot.id);
|
||||
$$renderer2.push(`<form method="post" action="?/pick"><input type="hidden" name="slotId"${attr("value", slot.id)}/> <button type="submit"${attr("disabled", isPlayed || picking, true)}${attr_class(`flex w-full items-center gap-3 rounded-lg border p-2 text-left transition-all ${stringify(isPlayed ? "border-border/50 bg-surface-900 opacity-30 cursor-not-allowed" : "border-border bg-surface-700 hover:border-accent hover:bg-surface-600 cursor-pointer")}`)}>`);
|
||||
if (bm?.listCoverUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", bm.listCoverUrl)} alt="" class="h-10 w-20 rounded object-cover"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="flex h-10 w-20 items-center justify-center rounded bg-surface-600 text-xs text-text-secondary">?</div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="min-w-0 flex-1">`);
|
||||
if (bm) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<p class="truncate text-xs font-500">${escape_html(bm.artist)} - ${escape_html(bm.title)}</p> <p class="text-xs text-text-secondary">[${escape_html(bm.version)}] · ★${escape_html(bm.starRating.toFixed(1))} · ${escape_html(bm.bpm)}bpm</p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<p class="text-xs text-text-secondary">#${escape_html(slot.beatmapId)}</p>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> <span class="text-xs font-600 text-text-secondary">${escape_html(category)}${escape_html(slot.orderInCategory)}</span> `);
|
||||
if (isPlayed) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span class="text-xs text-text-secondary">✓</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></button></form>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().state === "PLAYING") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
const currentGame = m().games[m().games.length - 1];
|
||||
const bm = currentGame ? data.beatmapCache[currentGame.slot?.beatmapId] : null;
|
||||
$$renderer2.push(`<div class="mt-6 rounded-lg border border-green-500/30 bg-green-500/5 p-5 text-center"><div class="flex items-center justify-center gap-2"><div class="relative h-3 w-3"><div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div> <div class="relative h-3 w-3 rounded-full bg-green-400"></div></div> <span class="text-sm font-600 text-green-400">Now Playing</span></div> `);
|
||||
if (bm) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<p class="mt-2 text-sm">${escape_html(bm.artist)} - ${escape_html(bm.title)} <span class="text-text-secondary">[${escape_html(bm.version)}]</span></p> <p class="mt-1 text-xs text-text-secondary">★${escape_html(bm.starRating.toFixed(1))} · ${escape_html(bm.bpm)}bpm · ${escape_html(formatLength(bm.totalLength))}</p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <p class="mt-3 text-xs text-text-secondary animate-pulse">Waiting for players to ready up & play in osu!...</p> <form method="post" action="?/forceStart"><button type="submit"${attr("disabled", forceStarting, true)} class="mt-4 rounded-md border border-yellow-500/30 px-4 py-2 text-xs font-600 text-yellow-400 transition-colors hover:bg-yellow-500/10 disabled:opacity-50">${escape_html("Force Start (skip ready)")}</button></form></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().state === "FINISHED") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
const winner = m().participants.find((p) => p.teamId === m().winnerId);
|
||||
$$renderer2.push(`<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"><div class="w-full max-w-md rounded-xl border border-accent/30 bg-surface-800 p-8 text-center shadow-2xl"><div class="text-4xl">🏆</div> <h2 class="mt-4 text-2xl font-800"><span class="text-accent">${escape_html(winner?.team.name)}</span> wins!</h2> <p class="mt-2 text-lg font-700 tabular-nums text-text-secondary">${escape_html(p1()?.score)} – ${escape_html(p2()?.score)}</p> <div class="mt-6 flex flex-col gap-1"><!--[-->`);
|
||||
const each_array_3 = ensure_array_like(m().games);
|
||||
for (let $$index_3 = 0, $$length = each_array_3.length; $$index_3 < $$length; $$index_3++) {
|
||||
let game = each_array_3[$$index_3];
|
||||
const bm = data.beatmapCache[game.slot?.beatmapId];
|
||||
const winnerP = m().participants.find((p) => p.id === game.winnerParticipantId);
|
||||
$$renderer2.push(`<div class="flex items-center gap-2 rounded bg-surface-700 px-3 py-1.5 text-xs"><span${attr_class(`font-600 ${stringify(catColors[game.slot?.category]?.split(" ")[1] ?? "text-text-secondary")}`)}>${escape_html(game.slot?.category)}${escape_html(game.slot?.orderInCategory)}</span> <span class="flex-1 truncate text-text-secondary">${escape_html(bm ? `${bm.artist} - ${bm.title}` : `#${game.slot?.beatmapId}`)}</span> <span${attr_class(`font-600 ${stringify(winnerP?.id === p1()?.id ? "text-blue-400" : "text-red-400")}`)}>${escape_html(winnerP?.team.name)}</span></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> <a href="/" class="mt-6 inline-block rounded-md bg-accent px-6 py-2.5 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover">Back to Dashboard</a></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().games.length > 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6"><h2 class="text-sm font-600">Games Played</h2> <div class="mt-3 flex flex-col gap-2"><!--[-->`);
|
||||
const each_array_4 = ensure_array_like(m().games);
|
||||
for (let $$index_4 = 0, $$length = each_array_4.length; $$index_4 < $$length; $$index_4++) {
|
||||
let game = each_array_4[$$index_4];
|
||||
const bm = data.beatmapCache[game.slot?.beatmapId];
|
||||
const pickerTeam = m().participants.find((p) => p.id === game.pickedByParticipantId)?.team;
|
||||
const winnerTeam = m().participants.find((p) => p.id === game.winnerParticipantId)?.team;
|
||||
$$renderer2.push(`<div class="flex items-center gap-3 rounded-lg border border-border bg-surface-800 p-3"><span class="w-6 text-center text-xs font-600 text-text-secondary">${escape_html(game.gameNumber)}</span> `);
|
||||
if (bm?.listCoverUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", bm.listCoverUrl)} alt="" class="h-8 w-16 rounded object-cover"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="min-w-0 flex-1">`);
|
||||
if (bm) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<p class="truncate text-sm font-500">${escape_html(bm.artist)} - ${escape_html(bm.title)} <span class="text-text-secondary">[${escape_html(bm.version)}]</span></p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="flex items-center gap-2 text-xs text-text-secondary"><span${attr_class(`rounded border px-1.5 py-0.5 ${stringify(catColors[game.slot?.category] ?? "border-border")}`)}>${escape_html(game.slot?.category)}${escape_html(game.slot?.orderInCategory)}</span> `);
|
||||
if (pickerTeam) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span>Picked by ${escape_html(pickerTeam.name)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div> `);
|
||||
if (game.state === "FINISHED" && p1() && p2()) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
const p1Score = game.scores.filter((s) => s.player?.participantId === p1().id).reduce((sum, s) => sum + s.score, 0);
|
||||
const p2Score = game.scores.filter((s) => s.player?.participantId === p2().id).reduce((sum, s) => sum + s.score, 0);
|
||||
$$renderer2.push(`<div class="flex items-center gap-2 font-mono text-sm tabular-nums"><span${attr_class(p1Score > p2Score ? "font-700 text-green-400" : "text-text-secondary")}>${escape_html(p1Score.toLocaleString())}</span> <span class="text-text-secondary">-</span> <span${attr_class(p2Score > p1Score ? "font-700 text-green-400" : "text-text-secondary")}>${escape_html(p2Score.toLocaleString())}</span></div>`);
|
||||
} else if (game.state === "PLAYING") {
|
||||
$$renderer2.push("<!--[1-->");
|
||||
$$renderer2.push(`<span class="text-xs text-green-400 animate-pulse">Live</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (winnerTeam) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span class="text-xs text-green-400">✓ ${escape_html(winnerTeam.name)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().state === "CREATED") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6 rounded-lg border border-border bg-surface-800 p-6 text-center"><p class="text-sm text-text-secondary animate-pulse">Creating osu! lobby...</p></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().state === "LOBBY") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6 rounded-lg border border-yellow-500/30 bg-yellow-500/5 p-6 text-center"><p class="text-sm text-text-secondary">Lobby created. Waiting for players to join...</p> `);
|
||||
if (m().osuLobbyId) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<p class="mt-2 text-xs text-text-secondary"><a${attr("href", `https://osu.ppy.sh/mp/${stringify(m().osuLobbyId)}`)} target="_blank" class="text-accent hover:underline">osu! mp/${escape_html(m().osuLobbyId)}</a></p>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (m().state === "CANCELLED") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6 rounded-lg border border-red-500/30 bg-red-500/5 p-6 text-center"><p class="text-sm text-red-400">This match was cancelled.</p></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { d as db, b as match } from "../../../../chunks/index2.js";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { desc } from "drizzle-orm";
|
||||
import { c as createMatch } from "../../../../chunks/engine.js";
|
||||
import { i as initMatchLobby } from "../../../../chunks/orchestrator.js";
|
||||
const load = async ({ locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const matches = await db.query.match.findMany({
|
||||
with: { participants: { with: { team: true } } },
|
||||
orderBy: desc(match.createdAt),
|
||||
limit: 50
|
||||
});
|
||||
const teams = await db.query.team.findMany({
|
||||
orderBy: (t, { asc }) => [asc(t.name)]
|
||||
});
|
||||
const mappools = await db.query.mappool.findMany({
|
||||
with: { slots: true },
|
||||
orderBy: (m, { desc: desc2 }) => [desc2(m.createdAt)]
|
||||
});
|
||||
return { matches, teams, mappools };
|
||||
};
|
||||
const actions = {
|
||||
createMatch: async ({ request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const name = form.get("name")?.toString()?.trim() || "Custom Match";
|
||||
const team1Id = form.get("team1")?.toString();
|
||||
const team2Id = form.get("team2")?.toString();
|
||||
const mappoolId = form.get("mappool")?.toString();
|
||||
const bestOf = parseInt(form.get("bestOf")?.toString() ?? "7");
|
||||
console.log("[Matches] CREATE:", { name, team1Id, team2Id, mappoolId, bestOf });
|
||||
if (!team1Id || !team2Id || !mappoolId) {
|
||||
return { error: "All fields are required" };
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = await createMatch({
|
||||
name,
|
||||
config: { bestOf, teamSize: 1, scoringType: "score" },
|
||||
mappoolId,
|
||||
teams: [team1Id, team2Id],
|
||||
createdBy: locals.user.id
|
||||
});
|
||||
console.log("[Matches] Created:", result.id);
|
||||
} catch (e) {
|
||||
console.error("[Matches] Error:", e);
|
||||
return { error: e.message };
|
||||
}
|
||||
initMatchLobby(result.id).catch((err) => {
|
||||
console.error("[Matches] IRC lobby failed:", err.message);
|
||||
});
|
||||
redirect(303, `/matches/${result.id}`);
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
import { e as escape_html, a as ensure_array_like, b as attr, s as stringify, c as attr_class, d as derived } from "../../../../chunks/index3.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../chunks/url.js";
|
||||
import "../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../chunks/root.js";
|
||||
import "../../../../chunks/exports.js";
|
||||
import "../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
const stateConfig = {
|
||||
CREATED: { label: "Created", color: "text-text-secondary border-border" },
|
||||
LOBBY: {
|
||||
label: "In Lobby",
|
||||
color: "text-yellow-400 border-yellow-500/30"
|
||||
},
|
||||
ROLLING: {
|
||||
label: "Rolling",
|
||||
color: "text-yellow-400 border-yellow-500/30",
|
||||
dot: "bg-yellow-400"
|
||||
},
|
||||
PICKING: {
|
||||
label: "Picking",
|
||||
color: "text-blue-400 border-blue-500/30",
|
||||
dot: "bg-blue-400"
|
||||
},
|
||||
PLAYING: {
|
||||
label: "Live",
|
||||
color: "text-green-400 border-green-500/30",
|
||||
dot: "bg-green-400"
|
||||
},
|
||||
FINISHED: {
|
||||
label: "Finished",
|
||||
color: "text-text-secondary border-border"
|
||||
},
|
||||
CANCELLED: { label: "Cancelled", color: "text-red-400 border-red-500/30" }
|
||||
};
|
||||
const liveStates = ["LOBBY", "ROLLING", "PICKING", "PLAYING"];
|
||||
const liveMatches = derived(() => data.matches.filter((m) => liveStates.includes(m.state)));
|
||||
const recentMatches = derived(() => data.matches.filter((m) => !liveStates.includes(m.state)));
|
||||
function teamDisplay(m) {
|
||||
const p1 = m.participants[0];
|
||||
const p2 = m.participants[1];
|
||||
return { p1, p2 };
|
||||
}
|
||||
function timeAgo(date) {
|
||||
const d = new Date(date);
|
||||
const diff = Date.now() - d.getTime();
|
||||
const mins = Math.floor(diff / 6e4);
|
||||
if (mins < 1) return "just now";
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
const hours = Math.floor(mins / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
$$renderer2.push(`<div class="mx-auto max-w-5xl"><div class="flex items-center justify-between"><div><h1 class="text-2xl font-700 tracking-tight">Matches</h1> <p class="mt-1 text-sm text-text-secondary">${escape_html(data.matches.length)} match${escape_html(data.matches.length !== 1 ? "es" : "")} `);
|
||||
if (liveMatches().length > 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`· <span class="text-green-400">${escape_html(liveMatches().length)} live</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></p></div> <button class="rounded-md bg-accent px-4 py-2 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover">${escape_html("New Match")}</button></div> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (liveMatches().length > 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6"><h2 class="flex items-center gap-2 text-sm font-600"><div class="relative h-2 w-2"><div class="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75"></div> <div class="relative h-2 w-2 rounded-full bg-green-400"></div></div> Live Now</h2> <div class="mt-3 flex flex-col gap-2"><!--[-->`);
|
||||
const each_array_4 = ensure_array_like(liveMatches());
|
||||
for (let $$index_4 = 0, $$length = each_array_4.length; $$index_4 < $$length; $$index_4++) {
|
||||
let m = each_array_4[$$index_4];
|
||||
const { p1, p2 } = teamDisplay(m);
|
||||
const sc = stateConfig[m.state];
|
||||
$$renderer2.push(`<a${attr("href", `/matches/${stringify(m.id)}`)} class="group flex items-center gap-4 rounded-lg border border-green-500/20 bg-surface-800 p-4 transition-all hover:border-green-500/40 hover:bg-surface-700"><div class="flex flex-1 items-center gap-3">`);
|
||||
if (p1?.team.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", p1.team.avatarUrl)} alt="" class="h-8 w-8 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span class="text-sm font-600">${escape_html(p1?.team.name ?? "?")}</span></div> <div class="flex items-center gap-3"><span class="text-xl font-800 tabular-nums">${escape_html(p1?.score ?? 0)}</span> <span class="text-xs font-600 text-text-secondary">vs</span> <span class="text-xl font-800 tabular-nums">${escape_html(p2?.score ?? 0)}</span></div> <div class="flex flex-1 items-center justify-end gap-3"><span class="text-sm font-600">${escape_html(p2?.team.name ?? "?")}</span> `);
|
||||
if (p2?.team.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", p2.team.avatarUrl)} alt="" class="h-8 w-8 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> <span${attr_class(`flex items-center gap-1.5 rounded border px-2 py-0.5 text-xs font-500 ${stringify(sc?.color ?? "border-border")}`)}>`);
|
||||
if (sc?.dot) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span${attr_class(`h-1.5 w-1.5 rounded-full ${stringify(sc.dot)}`)}></span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> ${escape_html(sc?.label ?? m.state)}</span></a>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="mt-6">`);
|
||||
if (liveMatches().length > 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<h2 class="text-sm font-600 text-text-secondary">Past Matches</h2>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="mt-3 flex flex-col gap-2">`);
|
||||
const each_array_5 = ensure_array_like(recentMatches());
|
||||
if (each_array_5.length !== 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
for (let $$index_5 = 0, $$length = each_array_5.length; $$index_5 < $$length; $$index_5++) {
|
||||
let m = each_array_5[$$index_5];
|
||||
const { p1, p2 } = teamDisplay(m);
|
||||
const config = m.config;
|
||||
const sc = stateConfig[m.state];
|
||||
$$renderer2.push(`<a${attr("href", `/matches/${stringify(m.id)}`)} class="group flex items-center gap-4 rounded-lg border border-border bg-surface-800 p-3 transition-colors hover:border-accent/30 hover:bg-surface-700"><div class="min-w-0 flex-1"><div class="flex items-center gap-2"><span class="text-sm font-600">${escape_html(p1?.team.name ?? "?")}</span> `);
|
||||
if (m.state === "FINISHED") {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span${attr_class(`text-xs font-700 tabular-nums ${stringify((p1?.score ?? 0) > (p2?.score ?? 0) ? "text-green-400" : "text-text-secondary")}`)}>${escape_html(p1?.score ?? 0)}</span> <span class="text-xs text-text-secondary">-</span> <span${attr_class(`text-xs font-700 tabular-nums ${stringify((p2?.score ?? 0) > (p1?.score ?? 0) ? "text-green-400" : "text-text-secondary")}`)}>${escape_html(p2?.score ?? 0)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<span class="text-xs text-text-secondary">vs</span>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span class="text-sm font-600">${escape_html(p2?.team.name ?? "?")}</span></div> <p class="mt-0.5 text-xs text-text-secondary">${escape_html(m.name ? `${m.name} · ` : "")}BO${escape_html(config.bestOf)} `);
|
||||
if (m.finishedAt) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`· ${escape_html(timeAgo(m.finishedAt))}`);
|
||||
} else if (m.createdAt) {
|
||||
$$renderer2.push("<!--[1-->");
|
||||
$$renderer2.push(`· ${escape_html(timeAgo(m.createdAt))}`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></p></div> `);
|
||||
if (m.state === "FINISHED" && m.winnerId) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
const winner = m.participants.find((p) => p.teamId === m.winnerId);
|
||||
$$renderer2.push(`<span class="text-xs font-500 text-green-400">🏆 ${escape_html(winner?.team.name)}</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span${attr_class(`rounded border px-2 py-0.5 text-xs font-500 ${stringify(sc?.color ?? "border-border")}`)}>${escape_html(sc?.label ?? m.state)}</span></a>`);
|
||||
}
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
if (liveMatches().length === 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="rounded-lg border border-dashed border-border py-12 text-center"><p class="text-sm text-text-secondary">No matches yet. Create one to get started.</p></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]-->`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { a as auth } from "../../../../chunks/auth.js";
|
||||
import { d as db, p as playerRating, j as account } from "../../../../chunks/index2.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
const load = async ({ locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const rating = await db.query.playerRating.findFirst({
|
||||
where: eq(playerRating.userId, locals.user.id)
|
||||
});
|
||||
const osuAccount = await db.query.account.findFirst({
|
||||
where: and(eq(account.userId, locals.user.id), eq(account.providerId, "osu"))
|
||||
});
|
||||
return {
|
||||
profile: {
|
||||
id: locals.user.id,
|
||||
name: locals.user.name,
|
||||
email: locals.user.email,
|
||||
image: locals.user.image,
|
||||
role: locals.user.role ?? "player",
|
||||
createdAt: locals.user.createdAt
|
||||
},
|
||||
rating: rating ? { elo: rating.elo, wins: rating.wins, losses: rating.losses } : { elo: 1e3, wins: 0, losses: 0 },
|
||||
hasOsuLinked: !!osuAccount
|
||||
};
|
||||
};
|
||||
const actions = {
|
||||
logout: async (event) => {
|
||||
await auth.api.signOut({
|
||||
headers: event.request.headers
|
||||
});
|
||||
redirect(302, "/");
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { b as attr, e as escape_html, c as attr_class, s as stringify, d as derived } from "../../../../chunks/index3.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../chunks/url.js";
|
||||
import "../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../chunks/root.js";
|
||||
import "../../../../chunks/exports.js";
|
||||
import "../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
const roleBadge = {
|
||||
player: { label: "Player", color: "bg-surface-600 text-text-secondary" },
|
||||
referee: { label: "Referee", color: "bg-yellow-500/20 text-yellow-400" },
|
||||
admin: { label: "Admin", color: "bg-red-500/20 text-red-400" }
|
||||
};
|
||||
const badge = derived(() => roleBadge[data.profile.role] ?? roleBadge.player);
|
||||
const winRate = derived(() => data.rating.wins + data.rating.losses > 0 ? (data.rating.wins / (data.rating.wins + data.rating.losses) * 100).toFixed(1) : "—");
|
||||
$$renderer2.push(`<div class="mx-auto max-w-2xl"><h1 class="text-2xl font-700 tracking-tight">Settings</h1> <p class="mt-1 text-sm text-text-secondary">Your profile and account settings</p> <div class="mt-6 rounded-lg border border-border bg-surface-800 p-6"><div class="flex items-center gap-4">`);
|
||||
if (data.profile.image) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", data.profile.image)}${attr("alt", data.profile.name)} class="h-16 w-16 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="flex h-16 w-16 items-center justify-center rounded-full bg-surface-600 text-xl font-700 text-text-secondary">${escape_html(data.profile.name.charAt(0).toUpperCase())}</div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="flex-1"><div class="flex items-center gap-2"><h2 class="text-lg font-700">${escape_html(data.profile.name)}</h2> <span${attr_class(`rounded px-2 py-0.5 text-xs font-600 ${stringify(badge().color)}`)}>${escape_html(badge().label)}</span></div> <p class="mt-0.5 text-sm text-text-secondary">${escape_html(data.profile.email)}</p></div></div> <div class="mt-6 border-t border-border pt-4"><h3 class="text-xs font-600 text-text-secondary uppercase tracking-wider">Connected Accounts</h3> <div class="mt-3 flex items-center gap-3 rounded-md bg-surface-700 px-4 py-3"><span class="text-lg">🎮</span> <div class="flex-1"><p class="text-sm font-500">osu!</p> <p class="text-xs text-text-secondary">`);
|
||||
if (data.hasOsuLinked) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`Connected as ${escape_html(data.profile.name)}`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`Not connected`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></p></div> `);
|
||||
if (data.hasOsuLinked) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span class="rounded bg-green-500/20 px-2 py-0.5 text-xs font-500 text-green-400">Linked</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<a href="/api/auth/osu/login" class="rounded bg-accent px-3 py-1 text-xs font-600 text-surface-900 hover:bg-accent-hover">Connect</a>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div></div> <div class="mt-4 rounded-lg border border-border bg-surface-800 p-6"><h3 class="text-xs font-600 text-text-secondary uppercase tracking-wider">Ranked Stats</h3> <div class="mt-4 grid grid-cols-4 gap-4"><div><p class="text-2xl font-800 tabular-nums text-accent">${escape_html(data.rating.elo)}</p> <p class="mt-1 text-xs text-text-secondary">ELO Rating</p></div> <div><p class="text-2xl font-800 tabular-nums text-green-400">${escape_html(data.rating.wins)}</p> <p class="mt-1 text-xs text-text-secondary">Wins</p></div> <div><p class="text-2xl font-800 tabular-nums text-red-400">${escape_html(data.rating.losses)}</p> <p class="mt-1 text-xs text-text-secondary">Losses</p></div> <div><p class="text-2xl font-800 tabular-nums text-text-primary">${escape_html(winRate())}${escape_html(winRate() !== "—" ? "%" : "")}</p> <p class="mt-1 text-xs text-text-secondary">Win Rate</p></div></div></div> <div class="mt-4 rounded-lg border border-border bg-surface-800 p-6"><h3 class="text-xs font-600 text-red-400 uppercase tracking-wider">Account</h3> <div class="mt-4"><form method="post" action="?/logout"><button type="submit" class="rounded-md border border-red-500/30 px-4 py-2 text-sm font-500 text-red-400 transition-colors hover:bg-red-500/10">Sign Out</button></form></div></div></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { d as db, c as team, t as teamMember, u as user } from "../../../../chunks/index2.js";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
const load = async ({ locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const teams = await db.query.team.findMany({
|
||||
with: {
|
||||
members: true
|
||||
},
|
||||
orderBy: (t, { desc }) => [desc(t.createdAt)]
|
||||
});
|
||||
const teamsWithUsers = await Promise.all(
|
||||
teams.map(async (t) => {
|
||||
const membersWithUsers = await Promise.all(
|
||||
t.members.map(async (m) => {
|
||||
const u = await db.query.user.findFirst({
|
||||
where: eq(user.id, m.userId)
|
||||
});
|
||||
return { ...m, user: u ? { id: u.id, name: u.name, image: u.image } : null };
|
||||
})
|
||||
);
|
||||
return { ...t, members: membersWithUsers };
|
||||
})
|
||||
);
|
||||
return { teams: teamsWithUsers, userId: locals.user.id };
|
||||
};
|
||||
const actions = {
|
||||
create: async ({ request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const name = form.get("name")?.toString()?.trim();
|
||||
if (!name) return { error: "Team name is required" };
|
||||
const [created] = await db.insert(team).values({
|
||||
name,
|
||||
ownerId: locals.user.id,
|
||||
isPersonal: false
|
||||
}).returning();
|
||||
await db.insert(teamMember).values({
|
||||
teamId: created.id,
|
||||
userId: locals.user.id,
|
||||
role: "captain"
|
||||
});
|
||||
return { success: true };
|
||||
},
|
||||
addMember: async ({ request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const teamId = form.get("teamId")?.toString();
|
||||
const username = form.get("username")?.toString()?.trim();
|
||||
if (!teamId || !username) return { error: "Team and username are required" };
|
||||
const u = await db.query.user.findFirst({
|
||||
where: eq(user.name, username)
|
||||
});
|
||||
if (!u) return { error: `User "${username}" not found. They need to log in first.` };
|
||||
const existing = await db.query.teamMember.findFirst({
|
||||
where: (m, { and, eq: eq2 }) => and(eq2(m.teamId, teamId), eq2(m.userId, u.id))
|
||||
});
|
||||
if (existing) return { error: `${username} is already on this team` };
|
||||
await db.insert(teamMember).values({
|
||||
teamId,
|
||||
userId: u.id,
|
||||
role: "member"
|
||||
});
|
||||
return { success: true };
|
||||
},
|
||||
removeMember: async ({ request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const memberId = form.get("memberId")?.toString();
|
||||
if (!memberId) return { error: "Missing member ID" };
|
||||
await db.delete(teamMember).where(eq(teamMember.id, memberId));
|
||||
return { success: true };
|
||||
},
|
||||
deleteTeam: async ({ request, locals }) => {
|
||||
if (!locals.user) redirect(302, "/");
|
||||
const form = await request.formData();
|
||||
const teamId = form.get("teamId")?.toString();
|
||||
if (!teamId) return { error: "Missing team ID" };
|
||||
const t = await db.query.team.findFirst({ where: eq(team.id, teamId) });
|
||||
if (!t || t.ownerId !== locals.user.id) return { error: "Not authorized" };
|
||||
await db.delete(team).where(eq(team.id, teamId));
|
||||
return { success: true };
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
import { e as escape_html, a as ensure_array_like, b as attr, c as attr_class, s as stringify, d as derived } from "../../../../chunks/index3.js";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../chunks/url.js";
|
||||
import "../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../chunks/root.js";
|
||||
import "../../../../chunks/exports.js";
|
||||
import "../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
let expandedTeam = null;
|
||||
let addingMemberTo = null;
|
||||
let memberUsername = "";
|
||||
const myTeams = derived(() => data.teams.filter((t) => t.members.some((m) => m.userId === data.userId)));
|
||||
const otherTeams = derived(() => data.teams.filter((t) => !t.members.some((m) => m.userId === data.userId)));
|
||||
function isOwner(t) {
|
||||
return t.ownerId === data.userId;
|
||||
}
|
||||
$$renderer2.push(`<div class="mx-auto max-w-4xl"><div class="flex items-center justify-between"><div><h1 class="text-2xl font-700 tracking-tight">Teams</h1> <p class="mt-1 text-sm text-text-secondary">${escape_html(data.teams.length)} team${escape_html(data.teams.length !== 1 ? "s" : "")}</p></div> <button class="rounded-md bg-accent px-4 py-2 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover">${escape_html("New Team")}</button></div> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
{
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (myTeams().length > 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6"><h2 class="text-sm font-600 text-text-secondary">My Teams</h2> <div class="mt-3 flex flex-col gap-3"><!--[-->`);
|
||||
const each_array = ensure_array_like(myTeams());
|
||||
for (let $$index_1 = 0, $$length = each_array.length; $$index_1 < $$length; $$index_1++) {
|
||||
let t = each_array[$$index_1];
|
||||
$$renderer2.push(`<div class="rounded-lg border border-border bg-surface-800 transition-colors"><button class="flex w-full items-center gap-4 p-4 text-left transition-colors hover:bg-surface-700">`);
|
||||
if (t.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", t.avatarUrl)} alt="" class="h-10 w-10 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="flex h-10 w-10 items-center justify-center rounded-full bg-surface-600 text-sm font-700 text-text-secondary">${escape_html(t.name.charAt(0).toUpperCase())}</div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="flex-1"><div class="flex items-center gap-2"><span class="text-sm font-600">${escape_html(t.name)}</span> `);
|
||||
if (t.isPersonal) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<span class="rounded bg-surface-600 px-1.5 py-0.5 text-[10px] font-500 text-text-secondary">solo</span>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> <p class="mt-0.5 text-xs text-text-secondary">${escape_html(t.members.length)} member${escape_html(t.members.length !== 1 ? "s" : "")}</p></div> <span${attr_class(`text-sm text-text-secondary transition-transform ${stringify(expandedTeam === t.id ? "rotate-90" : "")}`)}>›</span></button> `);
|
||||
if (expandedTeam === t.id) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="border-t border-border px-4 pb-4 pt-3"><div class="flex flex-col gap-2"><!--[-->`);
|
||||
const each_array_1 = ensure_array_like(t.members);
|
||||
for (let $$index = 0, $$length2 = each_array_1.length; $$index < $$length2; $$index++) {
|
||||
let member = each_array_1[$$index];
|
||||
$$renderer2.push(`<div class="flex items-center gap-3 rounded-md bg-surface-700 px-3 py-2">`);
|
||||
if (member.user?.image) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", member.user.image)} alt="" class="h-6 w-6 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="h-6 w-6 rounded-full bg-surface-600"></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <span class="flex-1 text-sm">${escape_html(member.user?.name ?? "Unknown")}</span> <span class="text-xs text-text-secondary">${escape_html(member.role)}</span> `);
|
||||
if (isOwner(t) && member.userId !== data.userId) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<form method="post" action="?/removeMember"><input type="hidden" name="memberId"${attr("value", member.id)}/> <button type="submit" class="text-xs text-red-400 opacity-50 transition-opacity hover:opacity-100">×</button></form>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div> `);
|
||||
if (isOwner(t) && !t.isPersonal) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
if (addingMemberTo === t.id) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<form method="post" action="?/addMember" class="mt-3 flex gap-2"><input type="hidden" name="teamId"${attr("value", t.id)}/> <input type="text" name="username"${attr("value", memberUsername)} placeholder="osu! username" required="" class="flex-1 rounded-md border border-border bg-surface-600 px-3 py-1.5 text-sm text-text-primary placeholder:text-text-secondary/50 focus:border-accent focus:outline-none"/> <button type="submit" class="rounded-md bg-accent px-3 py-1.5 text-xs font-600 text-surface-900 hover:bg-accent-hover">Add</button> <button type="button" class="rounded-md border border-border px-3 py-1.5 text-xs text-text-secondary hover:bg-surface-600">Cancel</button></form>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<button class="mt-3 w-full rounded-md border border-dashed border-border py-2 text-xs text-text-secondary transition-colors hover:border-accent/40 hover:text-accent">+ Add Member</button>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]-->`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (isOwner(t) && !t.isPersonal) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<form method="post" action="?/deleteTeam"><input type="hidden" name="teamId"${attr("value", t.id)}/> <button type="submit" class="mt-3 text-xs text-red-400 opacity-50 transition-opacity hover:opacity-100">Delete team</button></form>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (otherTeams().length > 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6"><h2 class="text-sm font-600 text-text-secondary">Other Teams</h2> <div class="mt-3 flex flex-col gap-2"><!--[-->`);
|
||||
const each_array_2 = ensure_array_like(otherTeams());
|
||||
for (let $$index_2 = 0, $$length = each_array_2.length; $$index_2 < $$length; $$index_2++) {
|
||||
let t = each_array_2[$$index_2];
|
||||
$$renderer2.push(`<div class="flex items-center gap-4 rounded-lg border border-border bg-surface-800 p-3">`);
|
||||
if (t.avatarUrl) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<img${attr("src", t.avatarUrl)} alt="" class="h-8 w-8 rounded-full"/>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`<div class="flex h-8 w-8 items-center justify-center rounded-full bg-surface-600 text-xs font-700 text-text-secondary">${escape_html(t.name.charAt(0).toUpperCase())}</div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--> <div class="flex-1"><span class="text-sm font-600">${escape_html(t.name)}</span> <p class="text-xs text-text-secondary">${escape_html(t.members.length)} member${escape_html(t.members.length !== 1 ? "s" : "")}</p></div></div>`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--> `);
|
||||
if (data.teams.length === 0) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`<div class="mt-6 rounded-lg border border-dashed border-border py-12 text-center"><p class="text-sm text-text-secondary">No teams yet. Your personal team is created when you sign in.</p></div>`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
}
|
||||
$$renderer2.push(`<!--]--></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
25
.svelte-kit/output/server/entries/pages/_error.svelte.js
Normal file
25
.svelte-kit/output/server/entries/pages/_error.svelte.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { e as escape_html } from "../../chunks/index3.js";
|
||||
import "clsx";
|
||||
import { p as page } from "../../chunks/index4.js";
|
||||
function _error($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
$$renderer2.push(`<div class="flex min-h-dvh items-center justify-center bg-surface-900"><div class="text-center"><p class="text-6xl font-800 text-accent">${escape_html(page.status)}</p> <h1 class="mt-4 text-xl font-700 text-text-primary">`);
|
||||
if (page.status === 404) {
|
||||
$$renderer2.push("<!--[-->");
|
||||
$$renderer2.push(`Page not found`);
|
||||
} else if (page.status === 403) {
|
||||
$$renderer2.push("<!--[1-->");
|
||||
$$renderer2.push(`Access denied`);
|
||||
} else if (page.status === 401) {
|
||||
$$renderer2.push("<!--[2-->");
|
||||
$$renderer2.push(`Not logged in`);
|
||||
} else {
|
||||
$$renderer2.push("<!--[!-->");
|
||||
$$renderer2.push(`Something went wrong`);
|
||||
}
|
||||
$$renderer2.push(`<!--]--></h1> <p class="mt-2 text-sm text-text-secondary">${escape_html(page.error?.message ?? "An unexpected error occurred.")}</p> <a href="/" class="mt-6 inline-block rounded-md bg-accent px-5 py-2.5 text-sm font-600 text-surface-900 transition-colors hover:bg-accent-hover">Back to Dashboard</a></div></div>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_error as default
|
||||
};
|
||||
31
.svelte-kit/output/server/entries/pages/demo/_page.svelte.js
Normal file
31
.svelte-kit/output/server/entries/pages/demo/_page.svelte.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { b as attr } from "../../../chunks/index3.js";
|
||||
import { i as initial_base, b as base } from "../../../chunks/server.js";
|
||||
import { r as resolve_route } from "../../../chunks/routing.js";
|
||||
import "../../../chunks/url.js";
|
||||
import { try_get_request_store } from "@sveltejs/kit/internal/server";
|
||||
import "../../../chunks/root.js";
|
||||
function resolve(id, params) {
|
||||
const resolved = resolve_route(
|
||||
id,
|
||||
/** @type {Record<string, string>} */
|
||||
params
|
||||
);
|
||||
{
|
||||
const store = try_get_request_store();
|
||||
if (store && !store.state.prerendering?.fallback) {
|
||||
const after_base = store.event.url.pathname.slice(initial_base.length);
|
||||
const segments = after_base.split("/").slice(2);
|
||||
const prefix = segments.map(() => "..").join("/") || ".";
|
||||
return prefix + resolved;
|
||||
}
|
||||
}
|
||||
return base + resolved;
|
||||
}
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
$$renderer2.push(`<a${attr("href", resolve("/demo/better-auth"))}>better-auth</a>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { a as auth } from "../../../../chunks/auth.js";
|
||||
const load = async (event) => {
|
||||
if (!event.locals.user) {
|
||||
return redirect(302, "/demo/better-auth/login");
|
||||
}
|
||||
return { user: event.locals.user };
|
||||
};
|
||||
const actions = {
|
||||
signOut: async (event) => {
|
||||
await auth.api.signOut({
|
||||
headers: event.request.headers
|
||||
});
|
||||
return redirect(302, "/demo/better-auth/login");
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { e as escape_html } from "../../../../chunks/index3.js";
|
||||
import "clsx";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../chunks/url.js";
|
||||
import "../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../chunks/root.js";
|
||||
import "../../../../chunks/exports.js";
|
||||
import "../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { data } = $$props;
|
||||
$$renderer2.push(`<h1>Hi, ${escape_html(data.user.name)}!</h1> <p>Your user ID is ${escape_html(data.user.id)}.</p> <form method="post" action="?/signOut"><button class="rounded-md bg-blue-600 px-4 py-2 text-white transition hover:bg-blue-700">Sign out</button></form>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import { redirect, fail } from "@sveltejs/kit";
|
||||
import { a as auth } from "../../../../../chunks/auth.js";
|
||||
import "@better-auth/core/env";
|
||||
import "@better-auth/core/utils";
|
||||
import { APIError } from "better-call";
|
||||
import "@better-auth/core/api";
|
||||
const load = async (event) => {
|
||||
if (event.locals.user) {
|
||||
return redirect(302, "/demo/better-auth");
|
||||
}
|
||||
return {};
|
||||
};
|
||||
const actions = {
|
||||
signInEmail: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const email = formData.get("email")?.toString() ?? "";
|
||||
const password = formData.get("password")?.toString() ?? "";
|
||||
try {
|
||||
await auth.api.signInEmail({
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
callbackURL: "/auth/verification-success"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof APIError) {
|
||||
return fail(400, { message: error.message || "Signin failed" });
|
||||
}
|
||||
return fail(500, { message: "Unexpected error" });
|
||||
}
|
||||
return redirect(302, "/demo/better-auth");
|
||||
},
|
||||
signUpEmail: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const email = formData.get("email")?.toString() ?? "";
|
||||
const password = formData.get("password")?.toString() ?? "";
|
||||
const name = formData.get("name")?.toString() ?? "";
|
||||
try {
|
||||
await auth.api.signUpEmail({
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
callbackURL: "/auth/verification-success"
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof APIError) {
|
||||
return fail(400, { message: error.message || "Registration failed" });
|
||||
}
|
||||
return fail(500, { message: "Unexpected error" });
|
||||
}
|
||||
return redirect(302, "/demo/better-auth");
|
||||
},
|
||||
signInSocial: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const provider = formData.get("provider")?.toString() ?? "osu";
|
||||
const callbackURL = formData.get("callbackURL")?.toString() ?? "/demo/better-auth";
|
||||
const result = await auth.api.signInSocial({
|
||||
body: {
|
||||
provider,
|
||||
callbackURL
|
||||
}
|
||||
});
|
||||
if (result.url) {
|
||||
return redirect(302, result.url);
|
||||
}
|
||||
return fail(400, { message: "Social sign-in failed" });
|
||||
}
|
||||
};
|
||||
export {
|
||||
actions,
|
||||
load
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import { e as escape_html } from "../../../../../chunks/index3.js";
|
||||
import "clsx";
|
||||
import "@sveltejs/kit/internal";
|
||||
import "../../../../../chunks/url.js";
|
||||
import "../../../../../chunks/utils.js";
|
||||
import "@sveltejs/kit/internal/server";
|
||||
import "../../../../../chunks/root.js";
|
||||
import "../../../../../chunks/exports.js";
|
||||
import "../../../../../chunks/state.svelte.js";
|
||||
function _page($$renderer, $$props) {
|
||||
$$renderer.component(($$renderer2) => {
|
||||
let { form } = $$props;
|
||||
$$renderer2.push(`<h1>Login</h1> <form method="post" action="?/signInEmail"><label>Email <input type="email" name="email" class="mt-1 rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"/></label> <label>Password <input type="password" name="password" class="mt-1 rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"/></label> <label>Name (for registration) <input name="name" class="mt-1 rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none"/></label> <button class="rounded-md bg-blue-600 px-4 py-2 text-white transition hover:bg-blue-700">Login</button> <button formaction="?/signUpEmail" class="rounded-md bg-blue-600 px-4 py-2 text-white transition hover:bg-blue-700">Register</button></form> <p class="text-red-500">${escape_html(form?.message ?? "")}</p> <hr class="my-4"/> <form method="post" action="?/signInSocial"><input type="hidden" name="provider" value="osu"/> <input type="hidden" name="callbackURL" value="/demo/better-auth"/> <button class="rounded-md bg-blue-600 px-4 py-2 text-white transition hover:bg-blue-700">Sign in with osu!</button></form>`);
|
||||
});
|
||||
}
|
||||
export {
|
||||
_page as default
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { a as auth } from "../../../chunks/auth.js";
|
||||
const load = async (event) => {
|
||||
if (event.locals.user) redirect(302, "/");
|
||||
const result = await auth.api.signInSocial({
|
||||
body: {
|
||||
provider: "osu",
|
||||
callbackURL: "/"
|
||||
}
|
||||
});
|
||||
if (result.url) redirect(302, result.url);
|
||||
return {};
|
||||
};
|
||||
export {
|
||||
load
|
||||
};
|
||||
Reference in New Issue
Block a user