Add frontend stuff

master
John Montagu, the 4th Earl of Sandvich 2024-11-06 20:55:58 -08:00
parent 210b590544
commit 67567046b8
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
48 changed files with 2342 additions and 123 deletions

View File

@ -13,7 +13,8 @@
"pinia": "^2.2.4",
"v-tooltip": "^2.1.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
"vue-router": "^4.4.5",
"vue-select": "^4.0.0-beta.6"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
@ -31,6 +32,7 @@
"eslint-plugin-vue": "^9.29.0",
"jsdom": "^25.0.1",
"npm-run-all2": "^6.2.3",
"openapi-typescript-codegen": "^0.29.0",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.14",
@ -53,6 +55,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "11.7.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
"integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.15",
"js-yaml": "^4.1.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/philsturgeon"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
@ -868,6 +888,13 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"dev": true,
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2376,6 +2403,19 @@
"node": ">=6"
}
},
"node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/camelcase-css": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@ -4059,6 +4099,28 @@
"dev": true,
"license": "MIT"
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
@ -5127,6 +5189,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@ -5328,6 +5397,48 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/openapi-typescript-codegen": {
"version": "0.29.0",
"resolved": "https://registry.npmjs.org/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz",
"integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^11.5.4",
"camelcase": "^6.3.0",
"commander": "^12.0.0",
"fs-extra": "^11.2.0",
"handlebars": "^4.7.8"
},
"bin": {
"openapi": "bin/index.js"
}
},
"node_modules/openapi-typescript-codegen/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/openapi-typescript-codegen/node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -6308,7 +6419,6 @@
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -6881,6 +6991,20 @@
}
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
@ -7316,6 +7440,15 @@
"vue": "^3.2.0"
}
},
"node_modules/vue-select": {
"version": "4.0.0-beta.6",
"resolved": "https://registry.npmjs.org/vue-select/-/vue-select-4.0.0-beta.6.tgz",
"integrity": "sha512-K+zrNBSpwMPhAxYLTCl56gaMrWZGgayoWCLqe5rWwkB8aUbAUh7u6sXjIR7v4ckp2WKC7zEEUY27g6h1MRsIHw==",
"license": "MIT",
"peerDependencies": {
"vue": "3.x"
}
},
"node_modules/vue-tsc": {
"version": "2.1.6",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz",
@ -7447,6 +7580,13 @@
"node": ">=0.10.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",

View File

@ -11,7 +11,8 @@
"build-only": "vite build",
"type-check": "vue-tsc --build --force",
"lint": "eslint . --fix",
"format": "prettier --write src/"
"format": "prettier --write src/",
"openapi-generate": "openapi --input 'http://localhost:5000/apidoc/openapi.json' --output src/client --name AvailabilitfClient"
},
"dependencies": {
"axios": "^1.7.7",
@ -19,7 +20,8 @@
"pinia": "^2.2.4",
"v-tooltip": "^2.1.3",
"vue": "^3.5.12",
"vue-router": "^4.4.5"
"vue-router": "^4.4.5",
"vue-select": "^4.0.0-beta.6"
},
"devDependencies": {
"@tsconfig/node20": "^20.1.4",
@ -37,6 +39,7 @@
"eslint-plugin-vue": "^9.29.0",
"jsdom": "^25.0.1",
"npm-run-all2": "^6.2.3",
"openapi-typescript-codegen": "^0.29.0",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.14",

View File

@ -8,6 +8,7 @@ const baseUrl = window.location.origin;
<header>
<div class="wrapper">
<nav>
<h1>availabili.tf</h1>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/schedule">Schedule</RouterLink>
<RouterLink to="/schedule/roster">Roster Builder</RouterLink>
@ -25,7 +26,9 @@ const baseUrl = window.location.origin;
</div>
</header>
<RouterView />
<div class="content">
<RouterView />
</div>
</template>
<style scoped>
@ -45,7 +48,8 @@ nav {
width: 100%;
font-size: 12px;
text-align: center;
margin-top: 2rem;
margin: 0;
align-items: center;
}
nav a.router-link-exact-active {
@ -64,6 +68,11 @@ nav a:hover {
background-color: var(--accent-transparent);
}
nav > h1 {
line-height: unset;
margin-right: 1rem;
}
@media (min-width: 1024px) {
header {
display: flex;
@ -88,7 +97,14 @@ nav a:hover {
font-size: 1rem;
padding: 1rem 0;
margin-top: 1rem;
}
}
#app > div.content {
display: flex;
}
#app > div.content > main {
width: 100%;
}
</style>

View File

@ -37,7 +37,8 @@
--flamingo: #f0c6c6;
--flamingo-transparent: #f0c6c655;
--green: #a6e3a1;
--green: #40a02b;
--yellow: #df8e1d;
--lavender: #7287fd;
--accent: var(--lavender);
--accent-transparent-80: color-mix(in srgb, var(--accent), transparent 80%);
@ -45,6 +46,10 @@
--accent-transparent: var(--accent-transparent-80);
--shadow: color-mix(in srgb, var(--text), transparent 50%);
--vs-selected-color: var(--text)!important;
--vs-dropdown-option--active-bg: var(--accent)!important;
--vs-dropdown-option--active-color: var(--base)!important;
--vs-border-color: var(--overlay-0)!important;
}
/* semantic color variables for this project */

View File

@ -4,7 +4,6 @@
width: 100%;
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
font-weight: normal;
}
@ -30,7 +29,7 @@ button {
background-color: var(--crust);
border: none;
padding: 10px 20px;
border-radius: 8px;
border-radius: 4px;
font-family:
Inter,
-apple-system,
@ -63,7 +62,7 @@ button:hover {
button.accent {
background-color: var(--accent);
color: var(--base);
text-transform: uppercase;
/*text-transform: uppercase;*/
}
button.accent.dark {
@ -88,6 +87,10 @@ button.transparent:hover {
background-color: var(--surface-0);
}
button.small {
padding: 8px 16px;
}
button[disabled] {
color: var(--overlay-0);
cursor: initial;
@ -119,3 +122,31 @@ select {
.subtext {
color: var(--overlay-0);
}
main {
padding: 2rem;
}
input {
padding: 6px 9px;
border: none;
/*outline: 1px solid var(--overlay-0);*/
border: 1px solid var(--overlay-0);
border-radius: 4px;
background-color: transparent;
/*font-size: 11pt;*/
font-size: 15px;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
}

View File

@ -0,0 +1,315 @@
[
"Africa/Abidjan",
"Africa/Algiers",
"Africa/Bissau",
"Africa/Cairo",
"Africa/Casablanca",
"Africa/Ceuta",
"Africa/El_Aaiun",
"Africa/Johannesburg",
"Africa/Juba",
"Africa/Khartoum",
"Africa/Lagos",
"Africa/Maputo",
"Africa/Monrovia",
"Africa/Nairobi",
"Africa/Ndjamena",
"Africa/Sao_Tome",
"Africa/Tripoli",
"Africa/Tunis",
"Africa/Windhoek",
"America/Adak",
"America/Anchorage",
"America/Araguaina",
"America/Argentina/Buenos_Aires",
"America/Argentina/Catamarca",
"America/Argentina/Cordoba",
"America/Argentina/Jujuy",
"America/Argentina/La_Rioja",
"America/Argentina/Mendoza",
"America/Argentina/Rio_Gallegos",
"America/Argentina/Salta",
"America/Argentina/San_Juan",
"America/Argentina/San_Luis",
"America/Argentina/Tucuman",
"America/Argentina/Ushuaia",
"America/Asuncion",
"America/Bahia",
"America/Bahia_Banderas",
"America/Barbados",
"America/Belem",
"America/Belize",
"America/Boa_Vista",
"America/Bogota",
"America/Boise",
"America/Cambridge_Bay",
"America/Campo_Grande",
"America/Cancun",
"America/Caracas",
"America/Cayenne",
"America/Chicago",
"America/Chihuahua",
"America/Ciudad_Juarez",
"America/Costa_Rica",
"America/Cuiaba",
"America/Danmarkshavn",
"America/Dawson",
"America/Dawson_Creek",
"America/Denver",
"America/Detroit",
"America/Edmonton",
"America/Eirunepe",
"America/El_Salvador",
"America/Fort_Nelson",
"America/Fortaleza",
"America/Glace_Bay",
"America/Goose_Bay",
"America/Grand_Turk",
"America/Guatemala",
"America/Guayaquil",
"America/Guyana",
"America/Halifax",
"America/Havana",
"America/Hermosillo",
"America/Indiana/Indianapolis",
"America/Indiana/Knox",
"America/Indiana/Marengo",
"America/Indiana/Petersburg",
"America/Indiana/Tell_City",
"America/Indiana/Vevay",
"America/Indiana/Vincennes",
"America/Indiana/Winamac",
"America/Inuvik",
"America/Iqaluit",
"America/Jamaica",
"America/Juneau",
"America/Kentucky/Louisville",
"America/Kentucky/Monticello",
"America/La_Paz",
"America/Lima",
"America/Los_Angeles",
"America/Maceio",
"America/Managua",
"America/Manaus",
"America/Martinique",
"America/Matamoros",
"America/Mazatlan",
"America/Menominee",
"America/Merida",
"America/Metlakatla",
"America/Mexico_City",
"America/Miquelon",
"America/Moncton",
"America/Monterrey",
"America/Montevideo",
"America/New_York",
"America/Nome",
"America/Noronha",
"America/North_Dakota/Beulah",
"America/North_Dakota/Center",
"America/North_Dakota/New_Salem",
"America/Nuuk",
"America/Ojinaga",
"America/Panama",
"America/Paramaribo",
"America/Phoenix",
"America/Port-au-Prince",
"America/Porto_Velho",
"America/Puerto_Rico",
"America/Punta_Arenas",
"America/Rankin_Inlet",
"America/Recife",
"America/Regina",
"America/Resolute",
"America/Rio_Branco",
"America/Santarem",
"America/Santiago",
"America/Santo_Domingo",
"America/Sao_Paulo",
"America/Scoresbysund",
"America/Sitka",
"America/St_Johns",
"America/Swift_Current",
"America/Tegucigalpa",
"America/Thule",
"America/Tijuana",
"America/Toronto",
"America/Vancouver",
"America/Whitehorse",
"America/Winnipeg",
"America/Yakutat",
"Antarctica/Casey",
"Antarctica/Davis",
"Antarctica/Macquarie",
"Antarctica/Mawson",
"Antarctica/Palmer",
"Antarctica/Rothera",
"Antarctica/Troll",
"Antarctica/Vostok",
"Asia/Almaty",
"Asia/Amman",
"Asia/Anadyr",
"Asia/Aqtau",
"Asia/Aqtobe",
"Asia/Ashgabat",
"Asia/Atyrau",
"Asia/Baghdad",
"Asia/Baku",
"Asia/Bangkok",
"Asia/Barnaul",
"Asia/Beirut",
"Asia/Bishkek",
"Asia/Chita",
"Asia/Colombo",
"Asia/Damascus",
"Asia/Dhaka",
"Asia/Dili",
"Asia/Dubai",
"Asia/Dushanbe",
"Asia/Famagusta",
"Asia/Gaza",
"Asia/Hebron",
"Asia/Ho_Chi_Minh",
"Asia/Hong_Kong",
"Asia/Hovd",
"Asia/Irkutsk",
"Asia/Jakarta",
"Asia/Jayapura",
"Asia/Jerusalem",
"Asia/Kabul",
"Asia/Kamchatka",
"Asia/Karachi",
"Asia/Kathmandu",
"Asia/Khandyga",
"Asia/Kolkata",
"Asia/Krasnoyarsk",
"Asia/Kuching",
"Asia/Macau",
"Asia/Magadan",
"Asia/Makassar",
"Asia/Manila",
"Asia/Nicosia",
"Asia/Novokuznetsk",
"Asia/Novosibirsk",
"Asia/Omsk",
"Asia/Oral",
"Asia/Pontianak",
"Asia/Pyongyang",
"Asia/Qatar",
"Asia/Qostanay",
"Asia/Qyzylorda",
"Asia/Riyadh",
"Asia/Sakhalin",
"Asia/Samarkand",
"Asia/Seoul",
"Asia/Shanghai",
"Asia/Singapore",
"Asia/Srednekolymsk",
"Asia/Taipei",
"Asia/Tashkent",
"Asia/Tbilisi",
"Asia/Tehran",
"Asia/Thimphu",
"Asia/Tokyo",
"Asia/Tomsk",
"Asia/Ulaanbaatar",
"Asia/Urumqi",
"Asia/Ust-Nera",
"Asia/Vladivostok",
"Asia/Yakutsk",
"Asia/Yangon",
"Asia/Yekaterinburg",
"Asia/Yerevan",
"Atlantic/Azores",
"Atlantic/Bermuda",
"Atlantic/Canary",
"Atlantic/Cape_Verde",
"Atlantic/Faroe",
"Atlantic/Madeira",
"Atlantic/South_Georgia",
"Atlantic/Stanley",
"Australia/Adelaide",
"Australia/Brisbane",
"Australia/Broken_Hill",
"Australia/Darwin",
"Australia/Eucla",
"Australia/Hobart",
"Australia/Lindeman",
"Australia/Lord_Howe",
"Australia/Melbourne",
"Australia/Perth",
"Australia/Sydney",
"Etc/UTC",
"Europe/Andorra",
"Europe/Astrakhan",
"Europe/Athens",
"Europe/Belgrade",
"Europe/Berlin",
"Europe/Brussels",
"Europe/Bucharest",
"Europe/Budapest",
"Europe/Chisinau",
"Europe/Dublin",
"Europe/Gibraltar",
"Europe/Helsinki",
"Europe/Istanbul",
"Europe/Kaliningrad",
"Europe/Kirov",
"Europe/Kyiv",
"Europe/Lisbon",
"Europe/London",
"Europe/Madrid",
"Europe/Malta",
"Europe/Minsk",
"Europe/Moscow",
"Europe/Paris",
"Europe/Prague",
"Europe/Riga",
"Europe/Rome",
"Europe/Samara",
"Europe/Saratov",
"Europe/Simferopol",
"Europe/Sofia",
"Europe/Tallinn",
"Europe/Tirane",
"Europe/Ulyanovsk",
"Europe/Vienna",
"Europe/Vilnius",
"Europe/Volgograd",
"Europe/Warsaw",
"Europe/Zurich",
"Factory",
"Indian/Chagos",
"Indian/Maldives",
"Indian/Mauritius",
"Pacific/Apia",
"Pacific/Auckland",
"Pacific/Bougainville",
"Pacific/Chatham",
"Pacific/Easter",
"Pacific/Efate",
"Pacific/Fakaofo",
"Pacific/Fiji",
"Pacific/Galapagos",
"Pacific/Gambier",
"Pacific/Guadalcanal",
"Pacific/Guam",
"Pacific/Honolulu",
"Pacific/Kanton",
"Pacific/Kiritimati",
"Pacific/Kosrae",
"Pacific/Kwajalein",
"Pacific/Marquesas",
"Pacific/Nauru",
"Pacific/Niue",
"Pacific/Norfolk",
"Pacific/Noumea",
"Pacific/Pago_Pago",
"Pacific/Palau",
"Pacific/Pitcairn",
"Pacific/Port_Moresby",
"Pacific/Rarotonga",
"Pacific/Tahiti",
"Pacific/Tarawa",
"Pacific/Tongatapu"
]

View File

@ -0,0 +1,28 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { BaseHttpRequest } from './core/BaseHttpRequest';
import type { OpenAPIConfig } from './core/OpenAPI';
import { FetchHttpRequest } from './core/FetchHttpRequest';
import { DefaultService } from './services/DefaultService';
type HttpRequestConstructor = new (config: OpenAPIConfig) => BaseHttpRequest;
export class AvailabilitfClient {
public readonly default: DefaultService;
public readonly request: BaseHttpRequest;
constructor(config?: Partial<OpenAPIConfig>, HttpRequest: HttpRequestConstructor = FetchHttpRequest) {
this.request = new HttpRequest({
BASE: config?.BASE ?? '',
VERSION: config?.VERSION ?? '0.1.0',
WITH_CREDENTIALS: config?.WITH_CREDENTIALS ?? false,
CREDENTIALS: config?.CREDENTIALS ?? 'include',
TOKEN: config?.TOKEN,
USERNAME: config?.USERNAME,
PASSWORD: config?.PASSWORD,
HEADERS: config?.HEADERS,
ENCODE_PATH: config?.ENCODE_PATH,
});
this.default = new DefaultService(this.request);
}
}

View File

@ -0,0 +1,25 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
export class ApiError extends Error {
public readonly url: string;
public readonly status: number;
public readonly statusText: string;
public readonly body: any;
public readonly request: ApiRequestOptions;
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
super(message);
this.name = 'ApiError';
this.url = response.url;
this.status = response.status;
this.statusText = response.statusText;
this.body = response.body;
this.request = request;
}
}

View File

@ -0,0 +1,17 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiRequestOptions = {
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
readonly url: string;
readonly path?: Record<string, any>;
readonly cookies?: Record<string, any>;
readonly headers?: Record<string, any>;
readonly query?: Record<string, any>;
readonly formData?: Record<string, any>;
readonly body?: any;
readonly mediaType?: string;
readonly responseHeader?: string;
readonly errors?: Record<number, string>;
};

View File

@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ApiResult = {
readonly url: string;
readonly ok: boolean;
readonly status: number;
readonly statusText: string;
readonly body: any;
};

View File

@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export abstract class BaseHttpRequest {
constructor(public readonly config: OpenAPIConfig) {}
public abstract request<T>(options: ApiRequestOptions): CancelablePromise<T>;
}

View File

@ -0,0 +1,131 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export class CancelError extends Error {
constructor(message: string) {
super(message);
this.name = 'CancelError';
}
public get isCancelled(): boolean {
return true;
}
}
export interface OnCancel {
readonly isResolved: boolean;
readonly isRejected: boolean;
readonly isCancelled: boolean;
(cancelHandler: () => void): void;
}
export class CancelablePromise<T> implements Promise<T> {
#isResolved: boolean;
#isRejected: boolean;
#isCancelled: boolean;
readonly #cancelHandlers: (() => void)[];
readonly #promise: Promise<T>;
#resolve?: (value: T | PromiseLike<T>) => void;
#reject?: (reason?: any) => void;
constructor(
executor: (
resolve: (value: T | PromiseLike<T>) => void,
reject: (reason?: any) => void,
onCancel: OnCancel
) => void
) {
this.#isResolved = false;
this.#isRejected = false;
this.#isCancelled = false;
this.#cancelHandlers = [];
this.#promise = new Promise<T>((resolve, reject) => {
this.#resolve = resolve;
this.#reject = reject;
const onResolve = (value: T | PromiseLike<T>): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isResolved = true;
if (this.#resolve) this.#resolve(value);
};
const onReject = (reason?: any): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isRejected = true;
if (this.#reject) this.#reject(reason);
};
const onCancel = (cancelHandler: () => void): void => {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#cancelHandlers.push(cancelHandler);
};
Object.defineProperty(onCancel, 'isResolved', {
get: (): boolean => this.#isResolved,
});
Object.defineProperty(onCancel, 'isRejected', {
get: (): boolean => this.#isRejected,
});
Object.defineProperty(onCancel, 'isCancelled', {
get: (): boolean => this.#isCancelled,
});
return executor(onResolve, onReject, onCancel as OnCancel);
});
}
get [Symbol.toStringTag]() {
return "Cancellable Promise";
}
public then<TResult1 = T, TResult2 = never>(
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2> {
return this.#promise.then(onFulfilled, onRejected);
}
public catch<TResult = never>(
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
): Promise<T | TResult> {
return this.#promise.catch(onRejected);
}
public finally(onFinally?: (() => void) | null): Promise<T> {
return this.#promise.finally(onFinally);
}
public cancel(): void {
if (this.#isResolved || this.#isRejected || this.#isCancelled) {
return;
}
this.#isCancelled = true;
if (this.#cancelHandlers.length) {
try {
for (const cancelHandler of this.#cancelHandlers) {
cancelHandler();
}
} catch (error) {
console.warn('Cancellation threw an error', error);
return;
}
}
this.#cancelHandlers.length = 0;
if (this.#reject) this.#reject(new CancelError('Request aborted'));
}
public get isCancelled(): boolean {
return this.#isCancelled;
}
}

View File

@ -0,0 +1,26 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
import { BaseHttpRequest } from './BaseHttpRequest';
import type { CancelablePromise } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
import { request as __request } from './request';
export class FetchHttpRequest extends BaseHttpRequest {
constructor(config: OpenAPIConfig) {
super(config);
}
/**
* Request method
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
public override request<T>(options: ApiRequestOptions): CancelablePromise<T> {
return __request(this.config, options);
}
}

View File

@ -0,0 +1,32 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ApiRequestOptions } from './ApiRequestOptions';
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
type Headers = Record<string, string>;
export type OpenAPIConfig = {
BASE: string;
VERSION: string;
WITH_CREDENTIALS: boolean;
CREDENTIALS: 'include' | 'omit' | 'same-origin';
TOKEN?: string | Resolver<string> | undefined;
USERNAME?: string | Resolver<string> | undefined;
PASSWORD?: string | Resolver<string> | undefined;
HEADERS?: Headers | Resolver<Headers> | undefined;
ENCODE_PATH?: ((path: string) => string) | undefined;
};
export const OpenAPI: OpenAPIConfig = {
BASE: '',
VERSION: '0.1.0',
WITH_CREDENTIALS: false,
CREDENTIALS: 'include',
TOKEN: undefined,
USERNAME: undefined,
PASSWORD: undefined,
HEADERS: undefined,
ENCODE_PATH: undefined,
};

View File

@ -0,0 +1,322 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { ApiError } from './ApiError';
import type { ApiRequestOptions } from './ApiRequestOptions';
import type { ApiResult } from './ApiResult';
import { CancelablePromise } from './CancelablePromise';
import type { OnCancel } from './CancelablePromise';
import type { OpenAPIConfig } from './OpenAPI';
export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
return value !== undefined && value !== null;
};
export const isString = (value: any): value is string => {
return typeof value === 'string';
};
export const isStringWithValue = (value: any): value is string => {
return isString(value) && value !== '';
};
export const isBlob = (value: any): value is Blob => {
return (
typeof value === 'object' &&
typeof value.type === 'string' &&
typeof value.stream === 'function' &&
typeof value.arrayBuffer === 'function' &&
typeof value.constructor === 'function' &&
typeof value.constructor.name === 'string' &&
/^(Blob|File)$/.test(value.constructor.name) &&
/^(Blob|File)$/.test(value[Symbol.toStringTag])
);
};
export const isFormData = (value: any): value is FormData => {
return value instanceof FormData;
};
export const base64 = (str: string): string => {
try {
return btoa(str);
} catch (err) {
// @ts-ignore
return Buffer.from(str).toString('base64');
}
};
export const getQueryString = (params: Record<string, any>): string => {
const qs: string[] = [];
const append = (key: string, value: any) => {
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
};
const process = (key: string, value: any) => {
if (isDefined(value)) {
if (Array.isArray(value)) {
value.forEach(v => {
process(key, v);
});
} else if (typeof value === 'object') {
Object.entries(value).forEach(([k, v]) => {
process(`${key}[${k}]`, v);
});
} else {
append(key, value);
}
}
};
Object.entries(params).forEach(([key, value]) => {
process(key, value);
});
if (qs.length > 0) {
return `?${qs.join('&')}`;
}
return '';
};
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
const encoder = config.ENCODE_PATH || encodeURI;
const path = options.url
.replace('{api-version}', config.VERSION)
.replace(/{(.*?)}/g, (substring: string, group: string) => {
if (options.path?.hasOwnProperty(group)) {
return encoder(String(options.path[group]));
}
return substring;
});
const url = `${config.BASE}${path}`;
if (options.query) {
return `${url}${getQueryString(options.query)}`;
}
return url;
};
export const getFormData = (options: ApiRequestOptions): FormData | undefined => {
if (options.formData) {
const formData = new FormData();
const process = (key: string, value: any) => {
if (isString(value) || isBlob(value)) {
formData.append(key, value);
} else {
formData.append(key, JSON.stringify(value));
}
};
Object.entries(options.formData)
.filter(([_, value]) => isDefined(value))
.forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => process(key, v));
} else {
process(key, value);
}
});
return formData;
}
return undefined;
};
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
if (typeof resolver === 'function') {
return (resolver as Resolver<T>)(options);
}
return resolver;
};
export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
const [token, username, password, additionalHeaders] = await Promise.all([
resolve(options, config.TOKEN),
resolve(options, config.USERNAME),
resolve(options, config.PASSWORD),
resolve(options, config.HEADERS),
]);
const headers = Object.entries({
Accept: 'application/json',
...additionalHeaders,
...options.headers,
})
.filter(([_, value]) => isDefined(value))
.reduce((headers, [key, value]) => ({
...headers,
[key]: String(value),
}), {} as Record<string, string>);
if (isStringWithValue(token)) {
headers['Authorization'] = `Bearer ${token}`;
}
if (isStringWithValue(username) && isStringWithValue(password)) {
const credentials = base64(`${username}:${password}`);
headers['Authorization'] = `Basic ${credentials}`;
}
if (options.body !== undefined) {
if (options.mediaType) {
headers['Content-Type'] = options.mediaType;
} else if (isBlob(options.body)) {
headers['Content-Type'] = options.body.type || 'application/octet-stream';
} else if (isString(options.body)) {
headers['Content-Type'] = 'text/plain';
} else if (!isFormData(options.body)) {
headers['Content-Type'] = 'application/json';
}
}
return new Headers(headers);
};
export const getRequestBody = (options: ApiRequestOptions): any => {
if (options.body !== undefined) {
if (options.mediaType?.includes('/json')) {
return JSON.stringify(options.body)
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
return options.body;
} else {
return JSON.stringify(options.body);
}
}
return undefined;
};
export const sendRequest = async (
config: OpenAPIConfig,
options: ApiRequestOptions,
url: string,
body: any,
formData: FormData | undefined,
headers: Headers,
onCancel: OnCancel
): Promise<Response> => {
const controller = new AbortController();
const request: RequestInit = {
headers,
body: body ?? formData,
method: options.method,
signal: controller.signal,
};
if (config.WITH_CREDENTIALS) {
request.credentials = config.CREDENTIALS;
}
onCancel(() => controller.abort());
return await fetch(url, request);
};
export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
if (responseHeader) {
const content = response.headers.get(responseHeader);
if (isString(content)) {
return content;
}
}
return undefined;
};
export const getResponseBody = async (response: Response): Promise<any> => {
if (response.status !== 204) {
try {
const contentType = response.headers.get('Content-Type');
if (contentType) {
const jsonTypes = ['application/json', 'application/problem+json']
const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type));
if (isJSON) {
return await response.json();
} else {
return await response.text();
}
}
} catch (error) {
console.error(error);
}
}
return undefined;
};
export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
const errors: Record<number, string> = {
400: 'Bad Request',
401: 'Unauthorized',
403: 'Forbidden',
404: 'Not Found',
500: 'Internal Server Error',
502: 'Bad Gateway',
503: 'Service Unavailable',
...options.errors,
}
const error = errors[result.status];
if (error) {
throw new ApiError(options, result, error);
}
if (!result.ok) {
const errorStatus = result.status ?? 'unknown';
const errorStatusText = result.statusText ?? 'unknown';
const errorBody = (() => {
try {
return JSON.stringify(result.body, null, 2);
} catch (e) {
return undefined;
}
})();
throw new ApiError(options, result,
`Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}`
);
}
};
/**
* Request method
* @param config The OpenAPI configuration object
* @param options The request options from the service
* @returns CancelablePromise<T>
* @throws ApiError
*/
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
return new CancelablePromise(async (resolve, reject, onCancel) => {
try {
const url = getUrl(config, options);
const formData = getFormData(options);
const body = getRequestBody(options);
const headers = await getHeaders(config, options);
if (!onCancel.isCancelled) {
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
const responseBody = await getResponseBody(response);
const responseHeader = getResponseHeader(response, options.responseHeader);
const result: ApiResult = {
url,
ok: response.ok,
status: response.status,
statusText: response.statusText,
body: responseHeader ?? responseBody,
};
catchErrorCodes(options, result);
resolve(result.body);
}
} catch (error) {
reject(error);
}
});
};

View File

@ -0,0 +1,29 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export { AvailabilitfClient } from './AvailabilitfClient';
export { ApiError } from './core/ApiError';
export { BaseHttpRequest } from './core/BaseHttpRequest';
export { CancelablePromise, CancelError } from './core/CancelablePromise';
export { OpenAPI } from './core/OpenAPI';
export type { OpenAPIConfig } from './core/OpenAPI';
export type { AddPlayerJson } from './models/AddPlayerJson';
export type { CreateTeamJson } from './models/CreateTeamJson';
export type { PutScheduleForm } from './models/PutScheduleForm';
export type { RoleSchema } from './models/RoleSchema';
export { TeamRole } from './models/TeamRole';
export type { TeamSchema } from './models/TeamSchema';
export type { ValidationError } from './models/ValidationError';
export type { ValidationErrorElement } from './models/ValidationErrorElement';
export type { ViewAvailablePlayersForm } from './models/ViewAvailablePlayersForm';
export type { ViewScheduleForm } from './models/ViewScheduleForm';
export type { ViewScheduleResponse } from './models/ViewScheduleResponse';
export type { ViewTeamMembersResponse } from './models/ViewTeamMembersResponse';
export type { ViewTeamMembersResponseList } from './models/ViewTeamMembersResponseList';
export type { ViewTeamResponse } from './models/ViewTeamResponse';
export type { ViewTeamsResponse } from './models/ViewTeamsResponse';
export { DefaultService } from './services/DefaultService';

View File

@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TeamRole } from './TeamRole';
export type AddPlayerJson = {
isTeamLeader?: boolean;
teamRole?: TeamRole;
};

View File

@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type CreateTeamJson = {
discordWebhookUrl?: string;
leagueTimezone: string;
teamName: string;
};

View File

@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type PutScheduleForm = {
availability: Array<number>;
teamId: number;
windowSizeDays?: number;
windowStart: string;
};

View File

@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type RoleSchema = {
isMain: boolean;
role: string;
};

View File

@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* An enumeration.
*/
export enum TeamRole {
'_0' = 0,
'_1' = 1,
}

View File

@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TeamSchema = {
discordWebhookUrl?: string;
id: number;
teamName: string;
};

View File

@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ValidationErrorElement } from './ValidationErrorElement';
/**
* Model of a validation error response.
*/
export type ValidationError = Array<ValidationErrorElement>;

View File

@ -0,0 +1,14 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
/**
* Model of a validation error response element.
*/
export type ValidationErrorElement = {
ctx?: Record<string, any>;
loc: Array<string>;
msg: string;
type: string;
};

View File

@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ViewAvailablePlayersForm = {
startTime: string;
teamId: number;
};

View File

@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ViewScheduleForm = {
teamId: number;
windowSizeDays?: number;
windowStart: string;
};

View File

@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type ViewScheduleResponse = {
availability: Array<number>;
};

View File

@ -0,0 +1,11 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RoleSchema } from './RoleSchema';
export type ViewTeamMembersResponse = {
roles: Array<RoleSchema>;
steamId: string;
username: string;
};

View File

@ -0,0 +1,6 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ViewTeamMembersResponse } from './ViewTeamMembersResponse';
export type ViewTeamMembersResponseList = Array<ViewTeamMembersResponse>;

View File

@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TeamSchema } from './TeamSchema';
export type ViewTeamResponse = {
team: TeamSchema;
};

View File

@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TeamSchema } from './TeamSchema';
export type ViewTeamsResponse = {
teams: Array<TeamSchema>;
};

View File

@ -0,0 +1,264 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { AddPlayerJson } from '../models/AddPlayerJson';
import type { CreateTeamJson } from '../models/CreateTeamJson';
import type { PutScheduleForm } from '../models/PutScheduleForm';
import type { ViewScheduleResponse } from '../models/ViewScheduleResponse';
import type { ViewTeamMembersResponseList } from '../models/ViewTeamMembersResponseList';
import type { ViewTeamResponse } from '../models/ViewTeamResponse';
import type { ViewTeamsResponse } from '../models/ViewTeamsResponse';
import type { CancelablePromise } from '../core/CancelablePromise';
import type { BaseHttpRequest } from '../core/BaseHttpRequest';
export class DefaultService {
constructor(public readonly httpRequest: BaseHttpRequest) {}
/**
* debug_set_cookie <GET>
* @returns void
* @throws ApiError
*/
public getApiDebugSetCookie(): CancelablePromise<void> {
return this.httpRequest.request({
method: 'GET',
url: '/api/debug/set-cookie',
});
}
/**
* debug_set_cookie <POST>
* @returns void
* @throws ApiError
*/
public postApiDebugSetCookie(): CancelablePromise<void> {
return this.httpRequest.request({
method: 'POST',
url: '/api/debug/set-cookie',
});
}
/**
* logout <DELETE>
* @returns void
* @throws ApiError
*/
public deleteApiLogin(): CancelablePromise<void> {
return this.httpRequest.request({
method: 'DELETE',
url: '/api/login/',
});
}
/**
* index <GET>
* @returns void
* @throws ApiError
*/
public getApiLogin(): CancelablePromise<void> {
return this.httpRequest.request({
method: 'GET',
url: '/api/login/',
});
}
/**
* steam_authenticate <POST>
* @returns void
* @throws ApiError
*/
public postApiLoginAuthenticate(): CancelablePromise<void> {
return this.httpRequest.request({
method: 'POST',
url: '/api/login/authenticate',
});
}
/**
* get <GET>
* @param windowStart
* @param teamId
* @param windowSizeDays
* @returns ViewScheduleResponse OK
* @throws ApiError
*/
public getApiSchedule(
windowStart: string,
teamId: number,
windowSizeDays: number = 7,
): CancelablePromise<ViewScheduleResponse> {
return this.httpRequest.request({
method: 'GET',
url: '/api/schedule/',
query: {
'windowStart': windowStart,
'teamId': teamId,
'windowSizeDays': windowSizeDays,
},
errors: {
422: `Unprocessable Entity`,
},
});
}
/**
* put <PUT>
* @param requestBody
* @returns void
* @throws ApiError
*/
public putApiSchedule(
requestBody?: PutScheduleForm,
): CancelablePromise<void> {
return this.httpRequest.request({
method: 'PUT',
url: '/api/schedule/',
body: requestBody,
mediaType: 'application/json',
});
}
/**
* view_available <GET>
* @param startTime
* @param teamId
* @returns void
* @throws ApiError
*/
public getApiScheduleViewAvailable(
startTime: string,
teamId: number,
): CancelablePromise<void> {
return this.httpRequest.request({
method: 'GET',
url: '/api/schedule/view-available',
query: {
'startTime': startTime,
'teamId': teamId,
},
});
}
/**
* create_team <POST>
* @param requestBody
* @returns ViewTeamResponse OK
* @throws ApiError
*/
public createTeam(
requestBody?: CreateTeamJson,
): CancelablePromise<ViewTeamResponse> {
return this.httpRequest.request({
method: 'POST',
url: '/api/team/',
body: requestBody,
mediaType: 'application/json',
errors: {
403: `Forbidden`,
422: `Unprocessable Entity`,
},
});
}
/**
* view_teams <GET>
* @returns ViewTeamsResponse OK
* @throws ApiError
*/
public getTeams(): CancelablePromise<ViewTeamsResponse> {
return this.httpRequest.request({
method: 'GET',
url: '/api/team/all/',
errors: {
403: `Forbidden`,
404: `Not Found`,
422: `Unprocessable Entity`,
},
});
}
/**
* delete_team <DELETE>
* @param teamId
* @returns any OK
* @throws ApiError
*/
public deleteTeam(
teamId: string,
): CancelablePromise<any> {
return this.httpRequest.request({
method: 'DELETE',
url: '/api/team/id/{team_id}/',
path: {
'team_id': teamId,
},
errors: {
403: `Forbidden`,
404: `Not Found`,
422: `Unprocessable Entity`,
},
});
}
/**
* view_team <GET>
* @param teamId
* @returns ViewTeamResponse OK
* @throws ApiError
*/
public getTeam(
teamId: string,
): CancelablePromise<ViewTeamResponse> {
return this.httpRequest.request({
method: 'GET',
url: '/api/team/id/{team_id}/',
path: {
'team_id': teamId,
},
errors: {
403: `Forbidden`,
404: `Not Found`,
422: `Unprocessable Entity`,
},
});
}
/**
* add_player <PUT>
* @param teamId
* @param playerId
* @param requestBody
* @returns any OK
* @throws ApiError
*/
public createOrUpdatePlayer(
teamId: string,
playerId: string,
requestBody?: AddPlayerJson,
): CancelablePromise<any> {
return this.httpRequest.request({
method: 'PUT',
url: '/api/team/id/{team_id}/player/{player_id}/',
path: {
'team_id': teamId,
'player_id': playerId,
},
body: requestBody,
mediaType: 'application/json',
errors: {
403: `Forbidden`,
404: `Not Found`,
422: `Unprocessable Entity`,
},
});
}
/**
* view_team_members <GET>
* @param teamId
* @returns ViewTeamMembersResponseList OK
* @throws ApiError
*/
public getTeamMembers(
teamId: string,
): CancelablePromise<ViewTeamMembersResponseList> {
return this.httpRequest.request({
method: 'GET',
url: '/api/team/id/{team_id}/players',
path: {
'team_id': teamId,
},
errors: {
403: `Forbidden`,
404: `Not Found`,
422: `Unprocessable Entity`,
},
});
}
}

View File

@ -5,6 +5,7 @@ const model = defineModel();
const props = defineProps({
options: Array<String>,
isDisabled: Boolean,
});
const isOpen = ref(false);
@ -18,15 +19,15 @@ function selectOption(index) {
<template>
<div :class="{ 'dropdown-container': true, 'is-open': isOpen }">
<button @click="isOpen = !isOpen">
<button @click="isOpen = !isOpen" :disabled="isDisabled">
{{ selectedOption }}
<i class="bi bi-caret-down-fill"></i>
</button>
<ul class="dropdown" v-if="isOpen" @blur="isOpen = false">
<li v-for="(option, i) in options" :key="i" @click="selectOption(i)">
<button :class="{ 'is-selected': i == model }">
<option :class="{ 'is-selected': i == model, 'option': true }">
{{ option }}
</button>
</option>
</li>
</ul>
</div>
@ -38,7 +39,7 @@ function selectOption(index) {
border-radius: 8px;
}
.dropdown-container button {
.dropdown-container .option {
display: flex;
flex-direction: row;
align-items: center;
@ -53,7 +54,19 @@ function selectOption(index) {
cursor: pointer;
}
.dropdown-container button:hover {
.dropdown-container .option {
border-radius: 0;
}
.dropdown-container .option:first-child {
border-radius: 8px 8px 0 0;
}
.dropdown-container .option:last-child {
border-radius: 0 0 8px 8px;
}
.dropdown-container .option:hover {
background-color: var(--crust);
}
@ -76,14 +89,14 @@ ul.dropdown > li {
list-style-type: none;
}
.dropdown li > button {
.dropdown li > .option {
padding: 8px 16px;
font-weight: 500;
font-size: 14px;
border-radius: 0;
}
.dropdown li > button.is-selected {
.dropdown li > .option.is-selected {
background-color: var(--accent-transparent);
color: var(--accent);
}

View File

@ -226,7 +226,7 @@ onUnmounted(() => {
}
.grid {
display: flex;
display: inline-flex;
user-select: none;
}

View File

@ -0,0 +1,91 @@
<script setup lang="ts">
import { computed, defineModel, defineProps, ref } from "vue";
const model = defineModel();
const props = defineProps({
options: Array<String>,
isDisabled: Boolean,
});
const isOpen = ref(false);
const selectedOption = computed(() => props.options[model.value]);
function selectOption(index) {
model.value = index;
isOpen.value = false;
}
</script>
<template>
<div :class="{ 'dropdown-container': true, 'is-open': isOpen }">
<button @click="isOpen = !isOpen" :disabled="isDisabled">
{{ selectedOption }}
<i class="bi bi-caret-down-fill"></i>
</button>
<ul class="dropdown" v-if="isOpen" @blur="isOpen = false">
<li v-for="(option, i) in options" :key="i" @click="selectOption(i)">
<button :class="{ 'is-selected': i == model }">
{{ option }}
</button>
</li>
</ul>
</div>
</template>
<style scoped>
.dropdown-container {
display: inline-block;
border-radius: 8px;
}
.dropdown-container button {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
text-align: left;
font-weight: 700;
font-size: 16px;
padding: 4px;
transition-duration: 200ms;
background-color: transparent;
cursor: pointer;
}
.dropdown-container button:hover {
background-color: var(--crust);
}
.dropdown-container.is-open ul.dropdown {
box-shadow: 1px 1px 8px var(--shadow);
}
ul.dropdown {
display: block;
background-color: var(--base);
position: absolute;
margin-top: 8px;
padding: 0;
z-index: 2;
border-radius: 8px;
overflow: none;
}
ul.dropdown > li {
list-style-type: none;
}
.dropdown li > button {
padding: 8px 16px;
font-weight: 500;
font-size: 14px;
border-radius: 0;
}
.dropdown li > button.is-selected {
background-color: var(--accent-transparent);
color: var(--accent);
}
</style>

View File

@ -0,0 +1,125 @@
<script setup lang="ts">
import type { PlayerTeamRole } from "../player";
import { computed, type PropType } from "vue";
import { useRosterStore } from "../stores/roster";
import { type ViewTeamMembersResponse } from "@/client";
const props = defineProps({
player: Object as PropType<ViewTeamMembersResponse>,
});
const rosterStore = useRosterStore();
</script>
<template>
<tr class="player-card">
<td>
<div class="status flex-middle" :availability="player.availability">
<span class="dot"></span>
<h3>
{{ player.username }}
</h3>
</div>
</td>
<td>
<div class="role-icons flex-middle">
<i
v-for="role in player.roles"
:class="{
[rosterStore.roleIcons[role.role]]: true,
main: role.is_main,
}"
/>
</div>
</td>
<td>
{{ player.playtime.toFixed(1) }} hours
</td>
<td>
{{ new Date(player.created_at).toLocaleString() }}
</td>
<td>
<div class="edit-group">
<button>
<i class="bi bi-pencil-fill edit-icon" />
</button>
</div>
</td>
</tr>
</template>
<style scoped>
.player-card {
border-radius: 8px;
user-select: none;
gap: 1em;
align-items: center;
border: 2px solid white;
box-shadow: 1px 1px 8px var(--surface-0);
}
.player-card > td {
padding: 1em 2em;
}
.player-card h3 {
font-weight: 600;
font-size: 12pt;
}
.dot {
display: block;
border-radius: 50%;
height: 8px;
width: 8px;
background-color: var(--overlay-0);
}
.status[availability="0"] h3 {
color: var(--overlay-0);
font-weight: 400;
}
.status[availability="1"] .dot {
background-color: var(--yellow);
}
.status[availability="2"] .dot {
background-color: var(--green);
}
.flex-middle {
display: flex;
gap: 8px;
align-items: center;
}
.role-icons {
font-size: 24px;
line-height: 0;
color: var(--overlay-0);
}
.role-icons i.main {
color: var(--text);
}
.edit-group {
display: flex;
justify-content: end;
}
.edit-group > button {
background-color: transparent;
opacity: 0;
padding: 8px;
}
.edit-group > button:hover {
background-color: var(--surface-0);
}
.player-card:hover .edit-group > button {
opacity: 1;
}
</style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { useTeamsStore } from "../stores/teams";
import { RouterLink } from "vue-router";
const teams = useTeamsStore();
onMounted(() => {
teams.fetchTeams();
});
</script>
<template>
<aside>
<div>
<div class="teams-header">
<h3>Your Teams</h3>
<RouterLink to="/team/register">
<button class="small accent">
<i class="bi bi-plus-circle-fill margin"></i>
New
</button>
</RouterLink>
</div>
<div
v-if="teams.teams"
v-for="team in teams.teams"
>
<RouterLink :to="'/team/id/' + team.id">
{{ team.team_name }}
</RouterLink>
</div>
</div>
</aside>
</template>
<style scoped>
aside {
flex-basis: 256px;
}
.teams-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.teams-header h3 {
font-weight: 600;
}
</style>

View File

@ -1,14 +1,17 @@
import './assets/main.css'
import "./assets/main.css";
import "vue-select/dist/vue-select.css";
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { createApp } from "vue";
import { createPinia } from "pinia";
import VueSelect from "vue-select";
import App from './App.vue'
import router from './router'
import App from "./App.vue";
import router from "./router";
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.component("v-select", VueSelect);
app.mount('#app')
app.mount("#app")

View File

@ -3,6 +3,8 @@ import HomeView from "../views/HomeView.vue";
import ScheduleView from "../views/ScheduleView.vue";
import RosterBuilderView from "../views/RosterBuilderView.vue";
import LoginView from "../views/LoginView.vue";
import TeamRegistrationView from "../views/TeamRegistrationView.vue";
import TeamDetailsView from "../views/TeamDetailsView.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -27,6 +29,16 @@ const router = createRouter({
name: "roster-builder",
component: RosterBuilderView
},
{
path: "/team/register",
name: "team-registration",
component: TeamRegistrationView
},
{
path: "/team/id/:id",
name: "team-details",
component: TeamDetailsView
},
]
})

View File

@ -0,0 +1,39 @@
import { AvailabilitfClient } from "@/client";
import { defineStore } from "pinia";
export const useClientStore = defineStore("client", () => {
const client = new AvailabilitfClient({
//BASE: import.meta.env.VITE_API_BASE_URL
});
const calls = new Map<string, Promise<any>>();
function call<T>(
key: string,
apiCall: () => Promise<T>,
thenOnce?: (result: T) => T
): Promise<T> {
console.log("Fetching call " + key);
if (!calls.has(key)) {
const promise = apiCall();
calls.set(key, promise);
// remove from calls once completed
promise.finally(() => calls.delete(key));
// only execute this "then" once if the call was just freshly made
if (thenOnce) {
promise.then(thenOnce);
}
return promise;
}
return calls.get(key) as Promise<T>;
}
return {
client,
call,
calls,
}
});

View File

@ -177,6 +177,13 @@ export const useRosterStore = defineStore("roster", () => {
"Roamer": "tf2-FlankSoldier",
"Demoman": "tf2-Demo",
"Medic": "tf2-Medic",
"Role.PocketScout": "tf2-PocketScout",
"Role.FlankScout": "tf2-FlankScout",
"Role.PocketSoldier": "tf2-PocketSoldier",
"Role.Roamer": "tf2-FlankSoldier",
"Role.Demoman": "tf2-Demo",
"Role.Medic": "tf2-Medic",
});
function selectPlayerForRole(player: PlayerTeamRole, role: string) {

View File

@ -2,56 +2,79 @@ import { computed } from "@vue/reactivity";
import { defineStore } from "pinia";
import { reactive, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useClientStore } from "./client";
export const useScheduleStore = defineStore("schedule", () => {
const client = useClientStore().client;
const dateStart = ref(new Date(2024, 9, 21, 0, 30));
const windowStart = computed(() => Math.floor(dateStart.value.getTime() / 1000));
const availability = reactive(new Array(168));
availability.fill(0);
const route = useRoute();
const router = useRouter();
const teamId = computed({
get: () => route.query.teamId,
get: () => Number(route.query.teamId),
set: (value) => router.push({ query: { teamId: value } }),
});
watch(dateStart, () => {
availability.fill(0);
fetchSchedule();
});
watch(teamId, () => {
fetchSchedule();
});
async function fetchSchedule() {
return fetch(import.meta.env.VITE_API_BASE_URL + "/schedule?" + new URLSearchParams({
window_start: windowStart.value.toString(),
team_id: "1",
}).toString(),{
credentials: "include",
})
.then((response) => response.json())
return client.default.getApiSchedule(
Math.floor(dateStart.value.getTime() / 1000).toString(),
teamId.value,
)
.then((response) => {
response.availability.forEach((value: number, i: number) => {
response.availability.forEach((value, i) => {
availability[i] = value;
});
return response;
});
//return fetch(import.meta.env.VITE_API_BASE_URL + "/schedule?" + new URLSearchParams({
// window_start: windowStart.value.toString(),
// team_id: teamId.toString(),
//}).toString(),{
// credentials: "include",
// })
// .then((response) => response.json())
// .then((response) => {
// response.availability.forEach((value: number, i: number) => {
// availability[i] = value;
// });
// return response;
// });
}
async function saveSchedule() {
return fetch(import.meta.env.VITE_API_BASE_URL + "/schedule", {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
window_start: Math.floor(dateStart.value.getTime() / 1000),
team_id: 1,
availability: availability,
})
return client.default.putApiSchedule({
windowStart: Math.floor(dateStart.value.getTime() / 1000).toString(),
teamId: teamId.value,
availability,
});
//return fetch(import.meta.env.VITE_API_BASE_URL + "/schedule", {
// method: "PUT",
// credentials: "include",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// window_start: Math.floor(dateStart.value.getTime() / 1000),
// team_id: teamId.toString(),
// availability: availability,
// })
//});
}
return {
@ -60,5 +83,6 @@ export const useScheduleStore = defineStore("schedule", () => {
availability,
fetchSchedule,
saveSchedule,
teamId,
};
});

View File

@ -1,36 +1,81 @@
import Cacheable from "@/cacheable";
import { AvailabilitfClient, type TeamSpec, type ViewTeamMembersResponse, type ViewTeamResponse, type ViewTeamsResponse } from "@/client";
import { defineStore } from "pinia";
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
import { useClientStore } from "./client";
interface Team {
id: number,
teamName: string,
}
export type TeamMap = { [id: number]: TeamSpec };
export const useTeamsStore = defineStore("teams", () => {
//const teams: Reactive<Cacheable<Team[]>> =
// reactive(new Cacheable<Team[]>([], 0));
const teams: Ref<{ [id: number]: Team }> = ref({ });
const clientStore = useClientStore();
const client = clientStore.client;
const teams: Reactive<{ [id: number]: TeamSpec }> = reactive({ });
const teamMembers: Reactive<{ [id: number]: ViewTeamMembersResponse[] }> = reactive({ });
const isFetchingTeams = ref(false);
async function fetchTeams() {
return new Promise((res, rej) => {
fetch(import.meta.env.VITE_API_BASE_URL + "/team/view", {
credentials: "include",
})
.then((response) => response.json())
.then((response: Array<any>) => {
teams.value = response
.reduce((acc, team: Team) => {
return { ...acc, [team.id]: team }
});
res(teams.value);
})
.catch(() => rej());
return clientStore.call(
fetchTeams.name,
() => client.default.getTeams(),
(response) => {
response.teams.forEach((team) => {
teams[team.id] = team;
});
return response;
}
)
}
async function fetchTeam(id: number) {
return clientStore.call(
fetchTeam.name,
() => client.default.getTeam(id.toString()),
(response) => {
teams[response.team.id] = response.team;
return response;
}
);
}
async function fetchTeamMembers(id: number) {
return clientStore.call(
fetchTeam.name,
() => client.default.getTeamMembers(id.toString()),
(response) => {
response = response
.map((member): ViewTeamMembersResponse => {
// TODO: snake_case to camelCase
member.roles = member.roles.sort((a, b) => {
if (a.is_main == b.is_main) {
return 0;
}
return a.is_main ? -1 : 1;
});
return member;
});
console.log(response);
teamMembers[id] = response;
return response;
}
);
}
async function createTeam(teamName: string, tz: string, webhook?: string) {
return await client.default.createTeam({
teamName,
leagueTimezone: tz,
discordWebhookUrl: webhook,
});
}
return {
teams,
teamMembers,
fetchTeams,
}
fetchTeam,
fetchTeamMembers,
createTeam,
};
});

View File

@ -1,12 +1,13 @@
<script setup lang="ts">
import TeamsListSidebar from "../components/TeamsListSidebar.vue";
</script>
<template>
<TeamsListSidebar />
<main>
<h2>JustGetAHouse</h2>
<div>
test
</div>
<h1>Your Teams</h1>
</main>
</template>

View File

@ -2,17 +2,17 @@
import AvailabilityGrid from "../components/AvailabilityGrid.vue";
import AvailabilityComboBox from "../components/AvailabilityComboBox.vue";
import WeekSelectionBox from "../components/WeekSelectionBox.vue";
import { computed, onMounted, reactive, ref } from "vue";
import { computed, onMounted, reactive, ref, watch } from "vue";
import { useTeamsStore } from "../stores/teams";
import { useScheduleStore } from "../stores/schedule";
import { useRoute, useRouter } from "vue-router";
const teams = useTeamsStore();
const teamsStore = useTeamsStore();
const schedule = useScheduleStore();
const router = useRouter();
const route = useRoute();
const options = ref([
"TEAM PEPEJA forsenCD",
"The Snus Brotherhood",
]);
const options = ref([ ]);
const firstHour = computed(() => shouldShowAllHours.value ? 0 : 14);
const lastHour = computed(() => shouldShowAllHours.value ? 23 : 22);
@ -20,13 +20,20 @@ const shouldShowAllHours = ref(false);
const comboBoxIndex = ref(0);
//const availability = reactive(new Array(168));
const availability = schedule.availability;
const selectionMode = ref(1);
const isEditing = ref(false);
const selectedTeam = ref();
watch(selectedTeam, (newTeam) => {
if (newTeam) {
schedule.teamId = newTeam.id;
}
});
function saveSchedule() {
schedule.saveSchedule()
.then(() => {
@ -35,14 +42,18 @@ function saveSchedule() {
}
onMounted(() => {
teams.fetchTeams()
teamsStore.fetchTeams()
.then((teamsList) => {
options.value = Object.values(teamsList);
schedule.fetchSchedule()
.then(() => {
});
})
options.value = Object.values(teamsList.teams);
// select team with id in query parameter if exists
const queryTeam = teamsList.teams.find(x => x.id == route.query.teamId);
console.log(queryTeam);
if (queryTeam) {
selectedTeam.value = queryTeam;
//schedule.teamId = queryTeam.id;
schedule.fetchSchedule(schedule.teamId);
}
});
});
</script>
@ -52,7 +63,11 @@ onMounted(() => {
<div class="top-menu">
<div class="subtext">
Availability for
<AvailabilityComboBox :options="options" v-model="comboBoxIndex" />
<v-select
:options="options"
label="team_name"
v-model="selectedTeam"
/>
</div>
<div>
<WeekSelectionBox
@ -60,42 +75,44 @@ onMounted(() => {
:is-disabled="isEditing" />
</div>
</div>
<AvailabilityGrid v-model="availability"
:selection-mode="selectionMode"
:is-disabled="!isEditing"
:date-start="schedule.dateStart"
:first-hour="firstHour"
:last-hour="lastHour"
/>
<div class="button-group">
<button v-if="shouldShowAllHours" @click="shouldShowAllHours = false">
Show designated times
</button>
<button v-else @click="shouldShowAllHours = true">
Show all times
</button>
<template v-if="isEditing">
<div class="radio-group">
<button
:class="{ 'radio': true, 'selected': selectionMode == 1, 'left': true }"
@click="selectionMode = 1"
>
Available if needed
</button>
<button
:class="{ 'radio': true, 'selected': selectionMode == 2, 'right': true }"
@click="selectionMode = 2"
>
Definitely available
</button>
</div>
<button @click="saveSchedule()">
<i class="bi bi-check-circle-fill"></i>
<div class="grid-container">
<AvailabilityGrid v-model="availability"
:selection-mode="selectionMode"
:is-disabled="!isEditing"
:date-start="schedule.dateStart"
:first-hour="firstHour"
:last-hour="lastHour"
/>
<div class="button-group">
<button v-if="shouldShowAllHours" @click="shouldShowAllHours = false">
Show designated times
</button>
</template>
<button v-else class="accent" @click="isEditing = true">
<i class="bi bi-pencil-fill"></i>
</button>
<button v-else @click="shouldShowAllHours = true">
Show all times
</button>
<template v-if="isEditing">
<div class="radio-group">
<button
:class="{ 'radio': true, 'selected': selectionMode == 1, 'left': true }"
@click="selectionMode = 1"
>
Available if needed
</button>
<button
:class="{ 'radio': true, 'selected': selectionMode == 2, 'right': true }"
@click="selectionMode = 2"
>
Definitely available
</button>
</div>
<button @click="saveSchedule()">
<i class="bi bi-check-circle-fill"></i>
</button>
</template>
<button v-else class="accent" @click="isEditing = true">
<i class="bi bi-pencil-fill"></i>
</button>
</div>
</div>
</div>
<div v-else>
@ -106,7 +123,10 @@ onMounted(() => {
<style scoped>
.schedule-view-container {
display: inline-block;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.top-menu {
@ -115,8 +135,14 @@ onMounted(() => {
align-items: center;
}
.grid-container {
display: inline-flex;
flex-direction: column;
align-content: center;
}
.button-group {
display: flex;
display: inline-flex;
gap: 8px;
justify-content: end;
}
@ -140,10 +166,16 @@ button.radio.selected {
}
button.left {
border-radius: 8px 0 0 8px;
border-radius: 4px 0 0 4px;
}
button.right {
border-radius: 0 8px 8px 0;
border-radius: 0 4px 4px 0;
}
.v-select {
display: inline-block;
width: auto;
min-width: 11em;
}
</style>

View File

@ -0,0 +1,77 @@
<script setup lang="ts">
import { useRoute, useRouter } from "vue-router";
import { useTeamsStore } from "../stores/teams";
import { computed, onMounted } from "vue";
import PlayerTeamCard from "../components/PlayerTeamCard.vue";
const route = useRoute();
const router = useRouter();
const teamsStore = useTeamsStore();
const team = computed(() => {
return teamsStore.teams[route.params.id];
});
onMounted(() => {
teamsStore.fetchTeam(route.params.id)
.then(() => teamsStore.fetchTeamMembers(route.params.id));
});
</script>
<template>
<main>
<template v-if="team">
<h1>
{{ team.team_name }}
</h1>
<table class="member-table">
<thead>
<tr>
<th>
Name
</th>
<th>
Roles
</th>
<th>
Playtime on team
</th>
<th>
Joined
</th>
</tr>
</thead>
<tbody>
<PlayerTeamCard
v-for="member in teamsStore.teamMembers[route.params.id]"
:player="member"
/>
</tbody>
</table>
</template>
</main>
</template>
<style scoped>
h1 {
display: flex;
}
table.member-table {
width: 100%;
}
table.member-table th {
text-align: left;
padding-left: 2em;
font-weight: 700;
}
/*
div.member-grid {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
*/
</style>

View File

@ -0,0 +1,143 @@
<script lang="ts" setup>
import { ref, watch } from "vue";
import { useTeamsStore } from "../stores/teams.ts"
import timezones from "../assets/timezones.json";
import { useRouter } from "vue-router";
const teams = useTeamsStore();
const router = useRouter();
const teamName = ref("");
const timezone = ref(
Intl.DateTimeFormat().resolvedOptions().timeZone ??
"Etc/UTC"
);
const minuteOffset = ref(0);
watch(minuteOffset, (newValue) => {
minuteOffset.value = Math.min(Math.max(0, newValue), 59);
});
const webhook = ref("");
function createTeam() {
teams.createTeam(teamName.value, timezone.value, webhook.value)
.then(() => {
router.push("/");
});
}
</script>
<template>
<main>
<div class="team-registration-container">
<h1>Create a new team</h1>
<p>
Register your team to streamline match scheduling, role assignments,
and overall team communication.
</p>
<div class="form-group margin">
<h3>Team Name</h3>
<input v-model="teamName" />
</div>
<div class="form-group margin">
<div class="form-group row">
<div class="form-group">
<h3>
Timezone
<a
class="aside"
href="https://nodatime.org/TimeZones"
target="_blank"
>
(view all timezones)
</a>
</h3>
<v-select :options="timezones" v-model="timezone" />
</div>
<div class="form-group" id="minute-offset-group">
<h3>Minute Offset</h3>
<input type="number" v-model="minuteOffset" min="0" max="59" />
</div>
</div>
<em class="aside">
Matches will be scheduled against {{ timezone }} at
{{ minuteOffset }}
<span v-if="minuteOffset == 1">
minute
</span>
<span v-else>
minutes
</span>
past the hour.
</em>
</div>
<div class="form-group margin">
<h3>
Announcements Webhook URL
<span class="aside">(optional)</span>
</h3>
<input v-model="webhook" />
</div>
<div class="form-group margin">
<div class="action-buttons">
<button class="accent" @click="createTeam">Create team</button>
</div>
</div>
</div>
</main>
</template>
<style scoped>
.team-registration-container {
align-items: center;
max-width: 500px;
margin: auto;
}
.team-registration-container h3 {
font-size: 11pt;
font-weight: 700;
}
.team-registration-container .aside {
font-size: 9pt;
}
.form-group {
display: flex;
flex-direction: column;
gap: 4px;
flex-grow: 1;
}
.form-group.margin {
margin-top: 16px;
margin-bottom: 16px;
}
.form-group.row {
flex-direction: row;
margin: none;
}
#minute-offset-group {
flex-grow: unset;
flex-shrink: 1;
flex-basis: 25%;
}
input {
display: block;
width: 100%;
color: var(--text);
}
.action-buttons {
display: flex;
justify-content: end;
}
</style>

View File

@ -16,7 +16,8 @@ export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
//target: 'http://localhost:5000',
target: 'https://on-indirectly-firefly.ngrok-free.app',
changeOrigin: true,
secure: false,
configure: (proxy) => {