UNPKG

bracket-vue-tool

Version:

A flexible Vue 3 component for creating interactive tournament brackets (single & double elimination) with zero runtime dependencies.

1,619 lines (1,618 loc) 48.3 kB
import { createElementBlock as p, openBlock as f, normalizeClass as H, createCommentVNode as q, withDirectives as J, createElementVNode as h, Fragment as U, renderList as W, toDisplayString as B, vModelSelect as se, ref as D, computed as A, watch as M, onMounted as R, vModelText as Z, createVNode as L, createBlock as $ } from "vue"; const j = { SINGLE_ELIMINATION: "single_elimination", DOUBLE_ELIMINATION: "double_elimination", SWISS: "swiss", ROUND_ROBIN: "round_robin" }, k = "TBD", O = { ONE: "teamOne", TWO: "teamTwo" }, w = { CAN_SELECT_TEAM: "can_select_team", CAN_EDIT_DATE: "can_edit_date", CAN_EDIT_SCOPE: "can_edit_scope", CAN_EDIT_ROUND_NAME: "can_edit_round_name", CAN_EDIT_BEST_OF: "can_edit_best_of" }, C = (i, a) => { const t = i.__vccOpts || i; for (const [e, r] of a) t[e] = r; return t; }, oe = { __name: "TeamSelect", props: { team: { type: Object, required: !0 }, teamPosition: { type: String, required: !0 }, availableTeams: { type: Array, required: !0 }, selectedTeams: { type: Array, required: !0 }, canEdit: { type: Boolean, required: !0 }, highlightedTeam: { type: Number, default: null }, permissions: { type: Object, default: () => ({ [w.CAN_SELECT_TEAM]: !0 }) }, isWinner: { type: Boolean, default: !1 }, isLoser: { type: Boolean, default: !1 }, shouldHighlight: { type: Boolean, default: !1 } }, emits: ["update:team", "highlight-team", "unhighlight-team"], setup(i, { expose: a, emit: t }) { a(); const e = i, r = t, n = D(e.team.name), s = A(() => { var d; return n.value === k ? null : ((d = e.availableTeams.find((y) => y.name === n.value)) == null ? void 0 : d.logo) || null; }); M( () => e.team, (d) => { n.value = d.name; }, { immediate: !0 } ), R(() => { console.log("TeamSelect mounted:", { team: e.team, availableTeams: e.availableTeams }); }); const o = (d) => d === k ? !1 : e.selectedTeams.includes(d) && d !== e.team.name || d === e.team.name && e.team.name !== k, c = A(() => e.availableTeams ? e.availableTeams.filter((d) => d.name === k || d.name === e.team.name ? !0 : !o(d.name)) : []), u = { props: e, emit: r, selectedTeam: n, selectedTeamLogo: s, isTeamSelected: o, availableTeamsForSelection: c, highlightTeam: () => { e.team.name !== k && r("highlight-team", e.team.name); }, unhighlightTeam: () => { r("unhighlight-team"); }, updateTeam: () => { const d = e.availableTeams.find( (y) => y.name === n.value ); console.log("Updating team:", { selectedTeam: n.value, selectedTeamData: d }), r("update:team", { position: e.teamPosition, team: { id: (d == null ? void 0 : d.id) || null, name: n.value, logo: (d == null ? void 0 : d.logo) || null, score: 0 } }); }, ref: D, computed: A, onMounted: R, watch: M, get TBD() { return k; }, get PERMISSIONS() { return w; } }; return Object.defineProperty(u, "__isScriptSetup", { enumerable: !1, value: !0 }), u; } }, le = { key: 0, class: "flex items-center gap-2" }, ce = ["src", "alt"], de = ["value", "disabled"], ue = { key: 1, class: "flex items-center gap-2" }, me = ["src", "alt"], he = { class: "text-gray-900 dark:text-white" }; function ge(i, a, t, e, r, n) { return f(), p( "div", { class: H(["flex-grow p-2.5 hover:bg-gray-200/30 dark:hover:bg-gray-950/20", { "hover:bg-green-500/20 dark:hover:bg-green-500/20": t.isWinner, "hover:bg-red-500/20 dark:hover:bg-red-500/20": t.isLoser, "bg-green-500/20 dark:bg-green-500/20": t.shouldHighlight && t.isWinner, "bg-red-500/20 dark:bg-red-500/20": t.shouldHighlight && t.isLoser }]), onMouseenter: e.highlightTeam, onMouseleave: e.unhighlightTeam }, [ t.canEdit && t.permissions[e.PERMISSIONS.CAN_SELECT_TEAM] ? (f(), p("div", le, [ e.selectedTeamLogo ? (f(), p("img", { key: 0, src: e.selectedTeamLogo, alt: e.selectedTeam, class: "w-6 h-6 rounded-full" }, null, 8, ce)) : q("v-if", !0), J(h( "select", { "onUpdate:modelValue": a[0] || (a[0] = (s) => e.selectedTeam = s), class: "form-input max-w-sm block w-full p-2 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", onChange: e.updateTeam }, [ a[1] || (a[1] = h( "option", { value: "TBD" }, "TBD", -1 /* CACHED */ )), (f(!0), p( U, null, W(e.availableTeamsForSelection, (s) => (f(), p("option", { key: s.id, value: s.name, disabled: e.isTeamSelected(s.name) }, B(s.name), 9, de))), 128 /* KEYED_FRAGMENT */ )) ], 544 /* NEED_HYDRATION, NEED_PATCH */ ), [ [se, e.selectedTeam] ]) ])) : (f(), p("div", ue, [ t.team.logo ? (f(), p("img", { key: 0, src: t.team.logo, alt: t.team.name, class: "w-6 h-6 rounded-full" }, null, 8, me)) : q("v-if", !0), h( "span", he, B(t.team.name), 1 /* TEXT */ ) ])) ], 34 /* CLASS, NEED_HYDRATION */ ); } const fe = /* @__PURE__ */ C(oe, [["render", ge], ["__file", "/app/src/components/team/TeamSelect.vue"]]), _e = { __name: "TeamScoreInput", props: { team: { type: Object, required: !0 }, teamPosition: { type: String, required: !0 }, canEditScore: { type: Boolean, required: !0 }, isFirstTeam: { type: Boolean, default: !1 } }, emits: ["update:score"], setup(i, { expose: a, emit: t }) { a(); const e = i, r = t, n = D(!1), s = D(e.team.score ?? 0), o = () => { e.canEditScore && (n.value = !0); }, c = () => { const l = parseInt(s.value) || 0; r("update:score", { position: e.teamPosition, score: l }); }; M( () => e.team, (l) => { s.value = l.score ?? 0; }, { deep: !0 } ); const _ = { props: e, emit: r, isEditing: n, score: s, selectScore: o, updateScore: c, ref: D, watch: M }; return Object.defineProperty(_, "__isScriptSetup", { enumerable: !1, value: !0 }), _; } }, Te = { key: 1, class: "text-white" }; function be(i, a, t, e, r, n) { return f(), p( "div", { class: H(["p-2.5 bg-orange-500 dark:bg-orange-600 cursor-pointer min-w-10 text-center", { "border-b border-orange-600 dark:border-orange-700": t.isFirstTeam }]), onClick: e.selectScore }, [ e.isEditing ? J((f(), p( "input", { key: 0, "onUpdate:modelValue": a[0] || (a[0] = (s) => e.score = s), type: "number", class: "form-input", required: "", min: "0", onChange: e.updateScore, onBlur: a[1] || (a[1] = (s) => e.isEditing = !1) }, null, 544 /* NEED_HYDRATION, NEED_PATCH */ )), [ [Z, e.score] ]) : (f(), p( "span", Te, B(t.team.score), 1 /* TEXT */ )) ], 2 /* CLASS */ ); } const pe = /* @__PURE__ */ C(_e, [["render", be], ["__file", "/app/src/components/team/TeamScoreInput.vue"]]), ye = { __name: "TeamRow", props: { team: { type: Object, required: !0 }, teamPosition: { type: String, required: !0 }, availableTeams: { type: Array, required: !0 }, selectedTeams: { type: Array, required: !0 }, canEdit: { type: Boolean, required: !0 }, canEditScore: { type: Boolean, required: !0 }, isWinner: { type: Boolean, default: !1 }, isLoser: { type: Boolean, default: !1 }, shouldHighlight: { type: Boolean, default: !1 }, isFirstTeam: { type: Boolean, default: !1 }, highlightedTeam: { type: Number, default: null }, permissions: { type: Object, default: () => ({ [w.CAN_SELECT_TEAM]: !0, [w.CAN_EDIT_SCOPE]: !0 }) } }, emits: [ "update:team", "update:score", "highlight-team", "unhighlight-team" ], setup(i, { expose: a }) { a(); const e = { props: i, TeamSelect: fe, TeamScoreInput: pe, get PERMISSIONS() { return w; } }; return Object.defineProperty(e, "__isScriptSetup", { enumerable: !1, value: !0 }), e; } }, ve = { class: "flex" }; function Se(i, a, t, e, r, n) { return f(), p("div", ve, [ L(e.TeamSelect, { team: t.team, "team-position": t.teamPosition, "available-teams": t.availableTeams, "selected-teams": t.selectedTeams, "highlighted-team": t.highlightedTeam, permissions: t.permissions, "can-edit": t.canEdit, "is-winner": t.isWinner, "is-loser": t.isLoser, "should-highlight": t.shouldHighlight, "is-first-team": t.isFirstTeam, "onUpdate:team": a[0] || (a[0] = (s) => i.$emit("update:team", s)), onHighlightTeam: a[1] || (a[1] = (s) => i.$emit("highlight-team", s)), onUnhighlightTeam: a[2] || (a[2] = (s) => i.$emit("unhighlight-team")) }, null, 8, ["team", "team-position", "available-teams", "selected-teams", "highlighted-team", "permissions", "can-edit", "is-winner", "is-loser", "should-highlight", "is-first-team"]), L(e.TeamScoreInput, { team: t.team, "team-position": t.teamPosition, "can-edit-score": t.canEditScore, "is-first-team": t.isFirstTeam, "onUpdate:score": a[3] || (a[3] = (s) => i.$emit("update:score", s)) }, null, 8, ["team", "team-position", "can-edit-score", "is-first-team"]) ]); } const we = /* @__PURE__ */ C(ye, [["render", Se], ["__file", "/app/src/components/team/TeamRow.vue"]]), Ee = { __name: "BracketMatch", props: { match: { type: Object, default: () => ({ teamOne: { name: k, score: 0 }, teamTwo: { name: k, score: 0 }, winner: null, date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0] // Default to today's date }) }, index: { type: Number, required: !0 }, totalMatches: { type: Number, required: !0 }, availableTeams: { type: Array, required: !0 }, selectedTeams: { type: Array, required: !0 }, roundIndex: { type: Number, required: !0 }, highlightedTeam: { type: String, default: null }, permissions: { type: Object, default: () => ({ [w.CAN_SELECT_TEAM]: !0, [w.CAN_EDIT_DATE]: !0, [w.CAN_EDIT_SCOPE]: !0 }) } }, emits: [ "update:match", "highlight-team", "unhighlight-team", "click-match" ], setup(i, { expose: a, emit: t }) { a(); const e = i, r = t, n = A(() => e.roundIndex === 0 && e.permissions[w.CAN_SELECT_TEAM]), s = A(() => e.match[O.ONE].name !== k && e.match[O.TWO].name !== k && e.permissions[w.CAN_EDIT_SCOPE]), I = { props: e, emit: r, canEdit: n, canEditScore: s, isWinner: (g) => e.match.winner === g, isLoser: (g) => e.match.winner && e.match.winner !== g, shouldHighlight: (g) => { const b = e.match[g].name; return e.highlightedTeam === b; }, highlightTeam: (g) => { r("highlight-team", g); }, unhighlightTeam: () => { r("unhighlight-team"); }, updateTeam: ({ position: g, team: b }) => { const N = { ...e.match, [g]: b }; r("update:match", N); }, updateScore: ({ position: g, score: b }) => { const N = { ...e.match, [g]: { ...e.match[g], score: b } }, S = N[O.ONE].score, E = N[O.TWO].score; S > E && (N.winner = O.ONE), E > S && (N.winner = O.TWO), S === E && (N.winner = null), r("update:match", N); }, formatDateTimeForInput: (g) => g ? new Date(g).toISOString().slice(0, 16) : "", updateDate: (g) => { const b = { ...e.match, date: g.target.value }; r("update:match", b); }, formatDate: (g) => g ? new Date(g).toLocaleDateString("uk-UA", { day: "2-digit", month: "2-digit", year: "numeric" }) : "", onMatchClick: (g) => { const b = g.target.tagName.toLowerCase(); b === "input" || b === "select" || b === "option" || b === "button" || g.target.closest("input,select,option,button") || !(!e.permissions[w.CAN_SELECT_TEAM] && !e.permissions[w.CAN_EDIT_DATE] && !e.permissions[w.CAN_EDIT_SCOPE]) || r("click-match", { match: e.match, roundIndex: e.roundIndex, matchIndex: e.index, id: e.match.id ?? null }); }, computed: A, TeamRow: we, get TBD() { return k; }, get TEAM_POSITION() { return O; }, get PERMISSIONS() { return w; } }; return Object.defineProperty(I, "__isScriptSetup", { enumerable: !1, value: !0 }), I; } }, Oe = { class: "flex flex-col w-full" }, ke = { class: "px-3 py-1 text-xs text-gray-500 dark:text-gray-400 flex items-center justify-center" }, xe = ["value", "disabled"], Ne = { key: 0, class: "absolute top-1/2 left-full w-2.5 h-[calc(100%+10px)] border-2 border-gray-300 dark:border-gray-600 border-l-0 rounded-r flex items-center z-10 -mt-[-10px] ml-[15px] mx-2 transition-colors duration-200" }; function Be(i, a, t, e, r, n) { return f(), p( "div", { class: H(["relative text-[0.8em] flex items-center", { group: t.index % 2 == 0 && t.totalMatches > 1 }]) }, [ h("div", Oe, [ h("div", ke, [ h("input", { type: "datetime-local", value: e.formatDateTimeForInput(t.match.date), class: "form-input ml-[15px] border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-900 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", disabled: !t.permissions[e.PERMISSIONS.CAN_EDIT_DATE], onInput: e.updateDate }, null, 40, xe) ]), h("div", { class: "my-1.5 ml-2.5 bg-white rounded overflow-hidden w-full min-w-[200px] shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10", onClick: e.onMatchClick }, [ L(e.TeamRow, { team: t.match.teamOne, "team-position": e.TEAM_POSITION.ONE, "available-teams": t.availableTeams, "selected-teams": t.selectedTeams, "can-edit": e.canEdit, "can-edit-score": e.canEditScore, "is-winner": e.isWinner(e.TEAM_POSITION.ONE), "is-loser": e.isLoser(e.TEAM_POSITION.ONE), "should-highlight": e.shouldHighlight(e.TEAM_POSITION.ONE), "is-first-team": !0, "can-select-team": i.canSelectTeam, "highlighted-team": t.highlightedTeam, permissions: t.permissions, "onUpdate:team": e.updateTeam, "onUpdate:score": e.updateScore, onHighlightTeam: e.highlightTeam, onUnhighlightTeam: e.unhighlightTeam }, null, 8, ["team", "team-position", "available-teams", "selected-teams", "can-edit", "can-edit-score", "is-winner", "is-loser", "should-highlight", "can-select-team", "highlighted-team", "permissions"]), L(e.TeamRow, { team: t.match.teamTwo, "team-position": e.TEAM_POSITION.TWO, "available-teams": t.availableTeams, "selected-teams": t.selectedTeams, "can-edit": e.canEdit, "can-edit-score": e.canEditScore, "is-winner": e.isWinner(e.TEAM_POSITION.TWO), "is-loser": e.isLoser(e.TEAM_POSITION.TWO), "should-highlight": e.shouldHighlight(e.TEAM_POSITION.TWO), "can-select-team": i.canSelectTeam, "highlighted-team": t.highlightedTeam, permissions: t.permissions, "onUpdate:team": e.updateTeam, "onUpdate:score": e.updateScore, onHighlightTeam: e.highlightTeam, onUnhighlightTeam: e.unhighlightTeam }, null, 8, ["team", "team-position", "available-teams", "selected-teams", "can-edit", "can-edit-score", "is-winner", "is-loser", "should-highlight", "can-select-team", "highlighted-team", "permissions"]) ]) ]), t.index % 2 == 0 && t.totalMatches > 1 ? (f(), p("div", Ne, a[0] || (a[0] = [ h( "span", { class: "w-2.5 h-0.5 bg-gray-300 dark:bg-gray-600 translate-x-full block" }, null, -1 /* CACHED */ ) ]))) : q("v-if", !0) ], 2 /* CLASS */ ); } const Ie = /* @__PURE__ */ C(Ee, [["render", Be], ["__file", "/app/src/components/bracket/BracketMatch.vue"]]), De = { __name: "BracketColumn", props: { column: { type: Object, required: !0 }, columnIndex: { type: Number, required: !0 }, availableTeams: { type: Array, required: !0 }, selectedTeams: { type: Array, required: !0 }, highlightedTeam: { type: String, default: null }, permissions: { type: Object, default: () => ({ [w.CAN_SELECT_TEAM]: !0 }) } }, emits: [ "update:match", "highlight-team", "unhighlight-team", "click-match" ], setup(i, { expose: a, emit: t }) { a(); const e = i, r = t, o = { props: e, emit: r, updateMatch: (c, _) => { r("update:match", e.columnIndex, c, _); }, onClickMatch: (c, _) => { r("click-match", { ..._, roundIndex: e.columnIndex, matchIndex: c }); }, BracketMatch: Ie, get PERMISSIONS() { return w; } }; return Object.defineProperty(o, "__isScriptSetup", { enumerable: !1, value: !0 }), o; } }, Ae = { class: "flex-1 px-5 pb-2.5 grid grid-cols-[min-content_auto]" }, Me = { class: "text-[0.7em] text-gray-900 dark:text-white flex justify-end items-center opacity-50 mt-[23px]" }; function Ce(i, a, t, e, r, n) { return f(), p("div", Ae, [ (f(!0), p( U, null, W(t.column.matches, (s, o) => (f(), p( U, { key: s.id }, [ h( "div", Me, B(s.number), 1 /* TEXT */ ), L(e.BracketMatch, { match: s, index: o, "total-matches": t.column.matches.length, "round-index": t.columnIndex, "available-teams": t.availableTeams, "selected-teams": t.selectedTeams, "highlighted-team": t.highlightedTeam, permissions: t.permissions, "onUpdate:match": (c) => e.updateMatch(o, c), onHighlightTeam: a[0] || (a[0] = (c) => i.$emit("highlight-team", c)), onUnhighlightTeam: a[1] || (a[1] = (c) => i.$emit("unhighlight-team")), onClickMatch: (c) => e.onClickMatch(o, c) }, null, 8, ["match", "index", "total-matches", "round-index", "available-teams", "selected-teams", "highlighted-team", "permissions", "onUpdate:match", "onClickMatch"]) ], 64 /* STABLE_FRAGMENT */ ))), 128 /* KEYED_FRAGMENT */ )) ]); } const ee = /* @__PURE__ */ C(De, [["render", Ce], ["__file", "/app/src/components/bracket/BracketColumn.vue"]]), Le = { __name: "BracketRoundHeaders", props: { columns: { type: Array, required: !0 }, permissions: { type: Object, required: !0 } }, emits: ["update:columns"], setup(i, { expose: a, emit: t }) { a(); const e = i, r = t, n = [1, 3, 5, 7, 9], s = D(e.columns.map((l) => l.name)); M( () => e.columns, (l) => { s.value = l.map((m) => m.name); }, { deep: !0 } ); const _ = { props: e, emit: r, bestOfValues: n, localColumnNames: s, updateColumnName: (l, m) => { s.value[l] = m; const u = [...e.columns]; u[l] = { ...u[l], name: m }, r("update:columns", u); }, updateColumnBestOf: (l, m) => { const u = [...e.columns]; u[l] = { ...u[l], bestOf: Number(m) }, r("update:columns", u); }, ref: D, watch: M, get PERMISSIONS() { return w; } }; return Object.defineProperty(_, "__isScriptSetup", { enumerable: !1, value: !0 }), _; } }, Ue = { class: "flex justify-between px-5" }, qe = { class: "flex flex-col items-center gap-2" }, We = { class: "mt-2" }, je = { class: "flex items-center rounded-md bg-white dark:bg-gray-900 border border-gray-300 dark:border-gray-600" }, Pe = { class: "join" }, Re = ["onUpdate:modelValue", "disabled", "onBlur"], He = ["value", "disabled", "onChange"], Fe = ["value"]; function ze(i, a, t, e, r, n) { return f(), p("div", Ue, [ (f(!0), p( U, null, W(t.columns, (s, o) => (f(), p("div", { key: s.name, class: "flex-1 text-center text-sm text-gray-400 py-2 rounded overflow-hidden" }, [ h("div", qe, [ h("div", We, [ h("div", je, [ h("div", Pe, [ J(h("input", { "onUpdate:modelValue": (c) => e.localColumnNames[o] = c, disabled: !t.permissions[e.PERMISSIONS.CAN_EDIT_ROUND_NAME], type: "text", class: "input rounded-md border-transparent focus:border-gray-500 focus:bg-white join-item text-sm text-gray-700 dark:bg-gray-900 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", onBlur: (c) => e.updateColumnName(o, c.target.value) }, null, 40, Re), [ [Z, e.localColumnNames[o]] ]), h("select", { value: s.bestOf, disabled: !t.permissions[e.PERMISSIONS.CAN_EDIT_BEST_OF], class: "select rounded-md border-transparent focus:border-gray-500 focus:bg-white join-item text-sm text-gray-700 dark:bg-gray-900 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500", onChange: (c) => e.updateColumnBestOf(o, c.target.value) }, [ (f(), p( U, null, W(e.bestOfValues, (c) => h("option", { key: c, value: c }, " Best of " + B(c), 9, Fe)), 64 /* STABLE_FRAGMENT */ )) ], 40, He) ]) ]) ]) ]) ]))), 128 /* KEYED_FRAGMENT */ )) ]); } const te = /* @__PURE__ */ C(Le, [["render", ze], ["__file", "/app/src/components/bracket/BracketRoundHeaders.vue"]]), Ve = { __name: "BracketSection", props: { title: { type: String, required: !0 }, columns: { type: Array, required: !0 }, availableTeams: { type: Array, required: !0 }, selectedTeams: { type: Array, required: !0 }, highlightedTeam: { type: String, default: null }, permissions: { type: Object, required: !0 }, bordered: { type: Boolean, default: !1 }, onMatchUpdate: { type: Function, required: !0 }, onColumnsUpdate: { type: Function, required: !0 }, onHighlight: { type: Function, required: !0 }, onUnhighlight: { type: Function, required: !0 }, onClickMatch: { type: Function, required: !1, default: () => { } } }, setup(i, { expose: a }) { a(); const e = { props: i, BracketColumn: ee, BracketRoundHeaders: te }; return Object.defineProperty(e, "__isScriptSetup", { enumerable: !1, value: !0 }), e; } }, Je = { class: "text-xl font-bold text-gray-800 dark:text-white mb-4" }, Ge = { class: "flex flex-col" }, Ke = { class: "overflow-x-auto" }, Ye = { class: "min-w-max" }, Qe = { class: "flex flex-1 p-5" }; function Xe(i, a, t, e, r, n) { return f(), p( "div", { class: H(["flex flex-col mt-8", { "border-t-2 border-gray-300 dark:border-gray-600 pt-8": t.bordered }]) }, [ h( "div", Je, B(t.title), 1 /* TEXT */ ), h("div", Ge, [ h("div", Ke, [ h("div", Ye, [ L(e.BracketRoundHeaders, { columns: t.columns, permissions: t.permissions, "onUpdate:columns": t.onColumnsUpdate }, null, 8, ["columns", "permissions", "onUpdate:columns"]), h("div", Qe, [ (f(!0), p( U, null, W(t.columns, (s, o) => (f(), $(e.BracketColumn, { key: s.id, column: s, "column-index": o, "available-teams": t.availableTeams, "selected-teams": t.selectedTeams, "highlighted-team": t.highlightedTeam, permissions: t.permissions, "onUpdate:match": t.onMatchUpdate, onHighlightTeam: t.onHighlight, onUnhighlightTeam: t.onUnhighlight, onClickMatch: t.onClickMatch }, null, 8, ["column", "column-index", "available-teams", "selected-teams", "highlighted-team", "permissions", "onUpdate:match", "onHighlightTeam", "onUnhighlightTeam", "onClickMatch"]))), 128 /* KEYED_FRAGMENT */ )) ]) ]) ]) ]) ], 2 /* CLASS */ ); } const Ze = /* @__PURE__ */ C(Ve, [["render", Xe], ["__file", "/app/src/components/bracket/BracketSection.vue"]]), G = () => ({ id: null, name: k, logo: null, score: 0 }), ae = (i) => ({ id: `match-${i}`, number: i, [O.ONE]: G(), [O.TWO]: G(), winner: null, date: null }); function ne(i, a) { return Array.from({ length: i }, (t, e) => a(e)); } const Et = (i, a = 3) => { const t = Math.log2(i); let e = 1; return Array.from({ length: t }, (r, n) => { const s = Math.pow(2, t - n - 1), o = ne( s, () => ae(e++) ); return { id: `upper-round-${n + 1}`, name: `Round ${n + 1}`, matches: o, bestOf: a }; }); }, z = (i, a) => { const t = i - 1; let e = 1; return Array.from({ length: t }, (r, n) => { const s = Math.pow(2, i - n - 2), o = ne( s, () => ae(e++) ); return { id: `lower-round-${n + 1}`, name: `Lower Round ${n + 1}`, matches: o, bestOf: a }; }); }, $e = { __name: "StandingsTable", props: { standings: { type: Array, required: !0 }, format: { type: String, required: !0 }, tournamentFormat: { type: Object, required: !0 } }, setup(i, { expose: a }) { a(); const t = {}; return Object.defineProperty(t, "__isScriptSetup", { enumerable: !1, value: !0 }), t; } }, et = { class: "flex flex-col mt-8 border-t-2 border-gray-300 dark:border-gray-600 pt-8" }, tt = { class: "relative overflow-x-auto shadow-md sm:rounded-lg mt-8" }, at = { class: "w-full text-sm text-left rtl:text-right text-gray-500 dark:text-gray-400" }, nt = { class: "text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400" }, it = { key: 0, scope: "col", class: "px-6 py-3" }, rt = { scope: "row", class: "px-6 py-4 font-medium text-gray-900 whitespace-nowrap dark:text-white" }, st = { class: "px-6 py-4" }, ot = { class: "px-6 py-4" }, lt = { class: "px-6 py-4" }, ct = { key: 0, class: "px-6 py-4" }, dt = { class: "px-6 py-4" }; function ut(i, a, t, e, r, n) { return f(), p("div", et, [ a[5] || (a[5] = h( "div", { class: "text-xl font-bold text-gray-800 dark:text-white mb-4" }, " Standings ", -1 /* CACHED */ )), h("div", tt, [ h("table", at, [ h("thead", nt, [ h("tr", null, [ a[0] || (a[0] = h( "th", { scope: "col", class: "px-6 py-3" }, "Place", -1 /* CACHED */ )), a[1] || (a[1] = h( "th", { scope: "col", class: "px-6 py-3" }, "Team", -1 /* CACHED */ )), a[2] || (a[2] = h( "th", { scope: "col", class: "px-6 py-3" }, "W-L-T", -1 /* CACHED */ )), a[3] || (a[3] = h( "th", { scope: "col", class: "px-6 py-3" }, "Points", -1 /* CACHED */ )), t.format === t.tournamentFormat.SWISS ? (f(), p("th", it, " Buchholz ")) : q("v-if", !0), a[4] || (a[4] = h( "th", { scope: "col", class: "px-6 py-3" }, "Pts Diff", -1 /* CACHED */ )) ]) ]), h("tbody", null, [ (f(!0), p( U, null, W(t.standings, (s) => (f(), p("tr", { key: s.id, class: "bg-white border-b dark:bg-gray-800 dark:border-gray-700 border-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600" }, [ h( "th", rt, B(s.place), 1 /* TEXT */ ), h( "td", st, B(s.name), 1 /* TEXT */ ), h( "td", ot, B(s.wins) + "-" + B(s.losses) + "-" + B(s.ties), 1 /* TEXT */ ), h( "td", lt, B(s.score), 1 /* TEXT */ ), t.format === t.tournamentFormat.SWISS ? (f(), p( "td", ct, B(s.buchholz), 1 /* TEXT */ )) : q("v-if", !0), h( "td", dt, B(s.ptsDiff), 1 /* TEXT */ ) ]))), 128 /* KEYED_FRAGMENT */ )) ]) ]) ]) ]); } const mt = /* @__PURE__ */ C($e, [["render", ut], ["__file", "/app/src/components/StandingsTable.vue"]]); function ht(i) { const a = {}, t = {}; i.forEach((r) => { r.matches.forEach((n) => { if (["teamOne", "teamTwo"].forEach((s) => { const o = n[s]; o && (a[o.id] || (a[o.id] = { ...o, wins: 0, losses: 0, ties: 0, score: 0, ptsDiff: 0, buchholz: 0 }), t[o.id] || (t[o.id] = [])); }), n.teamOne && n.teamTwo) { const s = n.teamOne, o = n.teamTwo, c = s.score || 0, _ = o.score || 0; n.winner === "teamOne" ? (a[s.id].wins++, a[s.id].score += 1, a[o.id].losses++) : n.winner === "teamTwo" ? (a[o.id].wins++, a[o.id].score += 1, a[s.id].losses++) : n.winner === null && (c > 0 || _ > 0) && (a[s.id].ties++, a[o.id].ties++, a[s.id].score += 0.5, a[o.id].score += 0.5), a[s.id].ptsDiff += c - _, a[o.id].ptsDiff += _ - c, t[s.id].push(o.id), t[o.id].push(s.id); } else n.teamOne && !n.teamTwo ? (a[n.teamOne.id].wins++, a[n.teamOne.id].score += 1) : !n.teamOne && n.teamTwo && (a[n.teamTwo.id].wins++, a[n.teamTwo.id].score += 1); }); }), Object.values(a).forEach((r) => { r.buchholz = (t[r.id] || []).reduce( (n, s) => { var o; return n + (((o = a[s]) == null ? void 0 : o.score) || 0); }, 0 ); }); const e = Object.values(a).sort( (r, n) => n.score - r.score || n.wins - r.wins || n.buchholz - r.buchholz || n.ptsDiff - r.ptsDiff ); return e.forEach((r, n) => { r.place = n + 1; }), e; } function F(i, a) { return i.filter((t) => !!t.id && t.name !== a); } function V(i, a, t, e, r, n) { const s = a.score || 0, o = t.score || 0; e === r ? (i[a.id].wins++, i[a.id].score += 1, i[t.id].losses++) : e === n ? (i[t.id].wins++, i[t.id].score += 1, i[a.id].losses++) : e === null && (s > 0 || o > 0) && (i[a.id].ties++, i[t.id].ties++, i[a.id].score += 0.5, i[t.id].score += 0.5), i[a.id].ptsDiff += s - o, i[t.id].ptsDiff += o - s; } function K({ upperColumns: i, lowerColumns: a, format: t, TOURNAMENT_FORMAT: e, TEAM_POSITION: r, TBD: n }) { switch (t) { case e.SWISS: return F(gt(i), n); case e.ROUND_ROBIN: return F( ft(i, r), n ); case e.SINGLE_ELIMINATION: case e.DOUBLE_ELIMINATION: default: return F( _t( i, a, t, e, r ), n ); } } function gt(i) { return ht(i); } function ft(i, a) { const t = {}; for (const e of i) for (const r of e.matches) { for (const o of [a.ONE, a.TWO]) { const c = r[o]; !c || !c.id || t[c.id] || (t[c.id] = { ...c, wins: 0, losses: 0, ties: 0, score: 0, ptsDiff: 0 }); } const n = r[a.ONE], s = r[a.TWO]; n && s && n.id && s.id && V( t, n, s, r.winner, a.ONE, a.TWO ); } return ie(t, ["score", "wins", "ptsDiff"]); } function _t(i, a, t, e, r) { const n = {}; for (const s of i) for (const o of s.matches) { for (const l of [r.ONE, r.TWO]) { const m = o[l]; !m || !m.id || n[m.id] || (n[m.id] = { ...m, wins: 0, losses: 0, ties: 0, score: 0, ptsDiff: 0 }); } const c = o[r.ONE], _ = o[r.TWO]; c && _ && c.id && _.id && V( n, c, _, o.winner, r.ONE, r.TWO ); } if (t === e.DOUBLE_ELIMINATION && a) for (const s of a) for (const o of s.matches) { for (const l of [r.ONE, r.TWO]) { const m = o[l]; !m || !m.id || n[m.id] || (n[m.id] = { ...m, wins: 0, losses: 0, ties: 0, score: 0, ptsDiff: 0 }); } const c = o[r.ONE], _ = o[r.TWO]; c && _ && c.id && _.id && V( n, c, _, o.winner, r.ONE, r.TWO ); } return ie(n, ["score", "wins", "ptsDiff"]); } function ie(i, a) { const t = Object.values(i).sort((e, r) => { for (const n of a) if (r[n] !== e[n]) return r[n] - e[n]; return 0; }); return t.forEach((e, r) => { e.place = r + 1; }), t; } function Tt(i, a = []) { const t = {}; i.forEach((l) => { t[l.score] || (t[l.score] = []), t[l.score].push(l); }); const e = Object.keys(t).map(Number).sort((l, m) => m - l), r = [], n = /* @__PURE__ */ new Set(), s = i.map((l) => l.id), o = new Set(a.map(([l, m]) => `${l}-${m}`)); function c(l) { const m = [], u = /* @__PURE__ */ new Set(); for (let d = 0; d < l.length; d++) { if (u.has(l[d].id)) continue; let y = !1; for (let T = d + 1; T < l.length; T++) if (!u.has(l[T].id) && !o.has(`${l[d].id}-${l[T].id}`) && !o.has(`${l[T].id}-${l[d].id}`)) { m.push([l[d], l[T]]), u.add(l[d].id), u.add(l[T].id), n.add(l[d].id), n.add(l[T].id), y = !0; break; } if (!y) { for (let T of e) if (T !== l[d].score) { for (let v of t[T] || []) if (!n.has(v.id) && !o.has(`${l[d].id}-${v.id}`) && !o.has(`${v.id}-${l[d].id}`)) return m.push([l[d], v]), u.add(l[d].id), u.add(v.id), n.add(l[d].id), n.add(v.id), m; } } } return m; } for (let l of e) { const m = t[l].filter((y) => !n.has(y.id)), u = c(m); r.push(...u); const d = m.filter((y) => !n.has(y.id)); if (d.length === 1) { let y = !1; for (let T of e) if (T !== l) { for (let v of t[T] || []) if (!n.has(v.id) && !o.has(`${d[0].id}-${v.id}`) && !o.has(`${v.id}-${d[0].id}`)) { r.push([d[0], v]), n.add(d[0].id), n.add(v.id), y = !0; break; } if (y) break; } y || (r.push([d[0], null]), n.add(d[0].id)); } } const _ = s.filter((l) => !n.has(l)); for (let l of _) { const m = i.find((u) => u.id === l); r.push([m, null]); } return r; } function Y({ upperColumns: i, lowerColumns: a, props: t, emit: e, TOURNAMENT_FORMAT: r, TEAM_POSITION: n, TBD: s }) { function o(u, d, y) { if (i.value[u] && i.value[u].matches) { if (i.value[u].matches[d] = y, y.winner && u < i.value.length - 1) { const T = u + 1, v = Math.floor(d / 2); if (i.value[T] && i.value[T].matches[v]) { const x = i.value[T].matches[v], I = d % 2 === 0 ? n.ONE : n.TWO, g = y[y.winner]; i.value[T].matches[v] = { ...x, [I]: { id: g.id, name: g.name, logo: g.logo, score: 0 } }; } } if (t.format === r.DOUBLE_ELIMINATION && y.winner) { const T = y[y.winner === n.ONE ? n.TWO : n.ONE]; if (T.name !== s) { const v = Math.floor(u / 2), x = Math.floor(d / 2); if (a.value[v] && a.value[v].matches[x]) { const I = a.value[v].matches[x], g = d % 2 === 0 ? n.ONE : n.TWO; a.value[v].matches[x] = { ...I, [g]: { id: T.id, name: T.name, logo: T.logo, score: 0 } }; } } } l(); } if (t.format === r.SWISS && i.value[u].matches.every((x) => x.winner) && u < i.value.length - 1) { const x = {}; for (let S = 0; S <= u; S++) i.value[S].matches.forEach((E) => { ["teamOne", "teamTwo"].forEach((re) => { const P = E[re]; P && (x[P.id] || (x[P.id] = { ...P, score: 0 })); }), E.winner && E.teamOne && E.teamTwo && (E.winner === "teamOne" && (x[E.teamOne.id].score += 1), E.winner === "teamTwo" && (x[E.teamTwo.id].score += 1)); }); const I = []; for (let S = 0; S <= u; S++) i.value[S].matches.forEach((E) => { E.teamOne && E.teamTwo && I.push([E.teamOne.id, E.teamTwo.id]); }); const g = Object.values(x), b = Tt(g, I), N = i.value[u + 1]; N.matches = b.map((S, E) => ({ id: `swiss-match-${u + 2}-${E + 1}`, number: E + 1, teamOne: S[0], teamTwo: S[1], winner: null, date: null })); } } function c(u) { i.value = u, l(); } function _(u) { a.value = u, l(); } function l() { e("update:state", { upper: i.value, lower: t.format === r.DOUBLE_ELIMINATION ? a.value : null }); } function m() { if (t.initialState) { if (Array.isArray(t.initialState)) { if (i.value = JSON.parse(JSON.stringify(t.initialState)), t.format === r.DOUBLE_ELIMINATION) { a.value = z( i.value.length, t.defaultBestOf ); return; } if (t.format === r.SWISS) { a.value = []; return; } return; } i.value = JSON.parse( JSON.stringify(t.initialState.upper || []) ), a.value = JSON.parse( JSON.stringify(t.initialState.lower || []) ); } } return { updateUpperMatch: o, updateUpperColumns: c, updateLowerState: _, emitTournamentState: l, initializeTournament: m }; } class bt { /** * @param {string} id * @returns {Promise<Tournament>} */ async getTournament(a) { throw new Error("Not implemented"); } /** * @param {Tournament} data * @returns {Promise<void>} */ async saveTournament(a) { throw new Error("Not implemented"); } /** * @param {string} tournamentId * @param {string} matchId * @param {Match} matchData * @returns {Promise<void>} */ async updateMatch(a, t, e) { throw new Error("Not implemented"); } /** * @returns {Promise<TournamentSummary[]>} */ async listTournaments() { throw new Error("Not implemented"); } /** * @param {string} id * @returns {Promise<void>} */ async deleteTournament(a) { throw new Error("Not implemented"); } } const Q = "tournaments"; class X extends bt { /** @returns {Object} */ _getAll() { return JSON.parse(localStorage.getItem(Q) || "{}"); } /** @param {Object} all */ _setAll(a) { localStorage.setItem(Q, JSON.stringify(a)); } /** @param {string} id */ async getTournament(a) { return this._getAll()[a] || null; } /** @param {Tournament} data */ async saveTournament(a) { const t = this._getAll(); t[a.id] = a, this._setAll(t); } /** @param {string} tournamentId, @param {string} matchId, @param {Match} matchData */ async updateMatch(a, t, e) { const r = this._getAll(), n = r[a]; if (!n) throw new Error("Tournament not found"); let s = !1; for (const o of n.rounds) for (let c = 0; c < o.matches.length; c++) o.matches[c].id === t && (o.matches[c] = { ...o.matches[c], ...e }, s = !0); if (!s) throw new Error("Match not found"); r[a] = n, this._setAll(r); } /** @returns {Promise<TournamentSummary[]>} */ async listTournaments() { const a = this._getAll(); return Object.values(a).map((t) => ({ id: t.id, name: t.name })); } /** @param {string} id */ async deleteTournament(a) { const t = this._getAll(); delete t[a], this._setAll(t); } } const pt = { __name: "TournamentBracket", props: { initialState: { type: Object, required: !0 }, availableTeams: { type: Array, required: !0 }, defaultBestOf: { type: Number, required: !0 }, format: { type: String, required: !0 }, permissions: { type: Object, default: () => ({ [w.CAN_SELECT_TEAM]: !0, [w.CAN_EDIT_ROUND_NAME]: !0, [w.CAN_EDIT_BEST_OF]: !0 }) } }, emits: ["update:state", "click-match"], setup(i, { expose: a, emit: t }) { a(); const e = t, r = i, n = D([]), s = D([]), o = D(null), c = A(() => { const b = /* @__PURE__ */ new Set(); return n.value.forEach((N) => { N.matches.forEach((S) => { S[O.ONE].name !== k && b.add(S[O.ONE].name), S[O.TWO].name !== k && b.add(S[O.TWO].name); }); }), r.format === j.DOUBLE_ELIMINATION && s.value.forEach((N) => { N.matches.forEach((S) => { S[O.ONE].name !== k && b.add(S[O.ONE].name), S[O.TWO].name !== k && b.add(S[O.TWO].name); }); }), Array.from(b); }), _ = A(() => K({ upperColumns: n.value, lowerColumns: s.value, format: r.format, TOURNAMENT_FORMAT: j, TEAM_POSITION: O, TBD: k })), l = (b) => { o.value = b; }, m = () => { o.value = null; }, u = (b) => { e("click-match", b); }, d = new X(), { updateUpperMatch: y, updateUpperColumns: T, updateLowerState: v, emitTournamentState: x, initializeTournament: I } = Y({ upperColumns: n, lowerColumns: s, props: r, emit: e, TOURNAMENT_FORMAT: j, TEAM_POSITION: O, TBD: k }); M( () => r.initialState, () => { I(); }, { deep: !0 } ), M( () => r.format, (b) => { b === j.DOUBLE_ELIMINATION && (!s.value || s.value.length === 0) && (s.value = z( n.value.length, r.defaultBestOf ), x()); } ), R(() => { I(); }); const g = { emit: e, props: r, upperColumns: n, lowerColumns: s, highlightedTeam: o, selectedTeams: c, standingsData: _, highlightTeam: l, unhighlightTeam: m, onMatchClick: u, storage: d, updateUpperMatch: y, updateUpperColumns: T, updateLowerState: v, emitTournamentState: x, initializeTournament: I, ref: D, onMounted: R, watch: M, computed: A, BracketColumn: ee, BracketRoundHeaders: te, BracketSection: Ze, get createLowerBracketStructure() { return z; }, get TOURNAMENT_FORMAT() { return j; }, get TBD() { return k; }, get TEAM_POSITION() { return O; }, get PERMISSIONS() { return w; }, StandingsTable: mt, get useStandings() { return K; }, get useBracket() { return Y; }, get LocalStorageTournament() { return X; } }; return Object.defineProperty(g, "__isScriptSetup", { enumerable: !1, value: !0 }), g; } }, yt = { class: "flex flex-col" }; function vt(i, a, t, e, r, n) { return f(), p("div", yt, [ L(e.BracketSection, { title: "Upper Bracket", columns: e.upperColumns, "available-teams": t.availableTeams, "selected-teams": e.selectedTeams, "highlighted-team": e.highlightedTeam, permissions: t.permissions, "on-match-update": e.updateUpperMatch, "on-columns-update": e.updateUpperColumns, "on-highlight": e.highlightTeam, "on-unhighlight": e.unhighlightTeam, "on-click-match": e.onMatchClick }, null, 8, ["columns", "available-teams", "selected-teams", "highlighted-team", "permissions", "on-match-update", "on-columns-update"]), t.format === e.TOURNAMENT_FORMAT.DOUBLE_ELIMINATION ? (f(), $(e.BracketSection, { key: 0, title: "Lower Bracket", columns: e.lowerColumns, "available-teams": t.availableTeams, "selected-teams": e.selectedTeams, "highlighted-team": e.highlightedTeam, permissions: t.permissions, bordered: "", "on-match-update": e.updateLowerState, "on-columns-update": e.updateLowerState, "on-highlight": e.highlightTeam, "on-unhighlight": e.unhighlightTeam }, null, 8, ["columns", "available-teams", "selected-teams", "highlighted-team", "permissions", "on-match-update", "on-columns-update"])) : q("v-if", !0), L(e.StandingsTable, { standings: e.standingsData, format: t.format, "tournament-format": e.TOURNAMENT_FORMAT }, null, 8, ["standings", "format", "tournament-format"]) ]); } const St = /* @__PURE__ */ C(pt, [["render", vt], ["__file", "/app/src/components/TournamentBracket.vue"]]), Ot = (i) => { i.component("TournamentBracket", St); }; export { w as PERMISSIONS, St as TournamentBracket, z as createLowerBracketStructure, Et as createTournamentState, Ot as install };