Improve usability
parent
afba73e1e8
commit
cb9e29b402
|
@ -11,7 +11,6 @@ const baseUrl = window.location.origin;
|
||||||
<h1>availabili.tf</h1>
|
<h1>availabili.tf</h1>
|
||||||
<RouterLink to="/">Home</RouterLink>
|
<RouterLink to="/">Home</RouterLink>
|
||||||
<RouterLink to="/schedule">Schedule</RouterLink>
|
<RouterLink to="/schedule">Schedule</RouterLink>
|
||||||
<RouterLink to="/schedule/roster">Roster Builder</RouterLink>
|
|
||||||
<form action="https://steamcommunity.com/openid/login" method="get">
|
<form action="https://steamcommunity.com/openid/login" method="get">
|
||||||
<input type="hidden" name="openid.identity"
|
<input type="hidden" name="openid.identity"
|
||||||
value="http://specs.openid.net/auth/2.0/identifier_select" />
|
value="http://specs.openid.net/auth/2.0/identifier_select" />
|
||||||
|
|
|
@ -142,6 +142,9 @@ main {
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
color: var(--text);
|
||||||
padding: 6px 9px;
|
padding: 6px 9px;
|
||||||
border: none;
|
border: none;
|
||||||
/*outline: 1px solid var(--overlay-0);*/
|
/*outline: 1px solid var(--overlay-0);*/
|
||||||
|
@ -164,3 +167,25 @@ input {
|
||||||
'Helvetica Neue',
|
'Helvetica Neue',
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group .action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, defineModel, defineProps, reactive, ref, onMounted, onUnmounted, type PropType } from "vue";
|
import { computed, defineModel, defineProps, reactive, ref, onMounted, onUnmounted, type PropType } from "vue";
|
||||||
import moment, { type Moment } from "moment";
|
import moment, { type Moment } from "moment";
|
||||||
|
import { useScheduleStore } from "../stores/schedule";
|
||||||
|
|
||||||
|
const scheduleStore = useScheduleStore();
|
||||||
|
|
||||||
const model = defineModel();
|
const model = defineModel();
|
||||||
|
|
||||||
|
@ -196,6 +199,17 @@ function getAvailabilityCell(day: number, hour: number) {
|
||||||
}
|
}
|
||||||
return model.value[index];
|
return model.value[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentTimezone = computed(() =>
|
||||||
|
Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||||
|
|
||||||
|
function getHour(offset, tz) {
|
||||||
|
let time = props.dateStart.clone()
|
||||||
|
if (tz) {
|
||||||
|
time = time.tz(tz);
|
||||||
|
}
|
||||||
|
return time.add(offset, "hours");
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -204,12 +218,18 @@ function getAvailabilityCell(day: number, hour: number) {
|
||||||
<div class="height-48px"></div>
|
<div class="height-48px"></div>
|
||||||
<div class="height-24px hour-marker-container" v-for="hour, i in hours" :key="i">
|
<div class="height-24px hour-marker-container" v-for="hour, i in hours" :key="i">
|
||||||
<span class="hour-marker" v-if="i % 2 == 0 || i == hours.length">
|
<span class="hour-marker" v-if="i % 2 == 0 || i == hours.length">
|
||||||
{{ hour % 24 }}:30 / {{ (hour + 3) % 24 }}:30 EST
|
{{ getHour(hour).format("HH:mm z") }}
|
||||||
|
<span v-if="scheduleStore.team.tzTimezone != currentTimezone">
|
||||||
|
/ {{ getHour(hour, scheduleStore.team.tzTimezone).format("HH:mm z") }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="height-24px hour-marker-container">
|
<div class="height-24px hour-marker-container">
|
||||||
<span class="hour-marker">
|
<span class="hour-marker">
|
||||||
{{ (lastHour + 1) % 24 }}:30 / {{ (lastHour + 4) % 24 }}:30 EST
|
{{ getHour(hour + 1).format("HH:mm z") }}
|
||||||
|
<span v-if="scheduleStore.team.tzTimezone != currentTimezone">
|
||||||
|
/ {{ getHour(hour + 1, scheduleStore.team.tzTimezone).format("HH:mm z") }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { useTeamsStore } from "../stores/teams";
|
||||||
import { computed, onMounted, ref } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import PlayerTeamCard from "../components/PlayerTeamCard.vue";
|
import PlayerTeamCard from "../components/PlayerTeamCard.vue";
|
||||||
import InviteEntry from "../components/InviteEntry.vue";
|
import InviteEntry from "../components/InviteEntry.vue";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -13,6 +14,12 @@ const team = computed(() => {
|
||||||
return teamsStore.teams[route.params.id];
|
return teamsStore.teams[route.params.id];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const creationDate = computed(() => {
|
||||||
|
if (team.value) {
|
||||||
|
return moment(team.value.createdAt).format("L");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const invites = computed(() => {
|
const invites = computed(() => {
|
||||||
return teamsStore.teamInvites[route.params.id];
|
return teamsStore.teamInvites[route.params.id];
|
||||||
});
|
});
|
||||||
|
@ -68,8 +75,16 @@ onMounted(async () => {
|
||||||
<template>
|
<template>
|
||||||
<main>
|
<main>
|
||||||
<template v-if="team">
|
<template v-if="team">
|
||||||
<h1>
|
<center class="team-info">
|
||||||
{{ team.teamName }}
|
<h1>
|
||||||
|
{{ team.teamName }}
|
||||||
|
</h1>
|
||||||
|
<span class="aside">
|
||||||
|
Formed on {{ creationDate }}
|
||||||
|
</span>
|
||||||
|
</center>
|
||||||
|
<div class="member-list-header">
|
||||||
|
<h2>Members</h2>
|
||||||
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
|
<em class="aside" v-if="teamsStore.teamMembers[route.params.id]">
|
||||||
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
|
{{ teamsStore.teamMembers[route.params.id]?.length }} member(s),
|
||||||
{{ availableMembers?.length }} currently available,
|
{{ availableMembers?.length }} currently available,
|
||||||
|
@ -89,7 +104,7 @@ onMounted(async () => {
|
||||||
Leave
|
Leave
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</div>
|
||||||
<table class="member-table">
|
<table class="member-table">
|
||||||
<!--thead>
|
<!--thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -157,13 +172,17 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
h1 {
|
.team-info {
|
||||||
|
margin: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5em;
|
gap: 0.5em;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 > em.aside {
|
.member-list-header > .aside {
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from datetime import datetime
|
from datetime import UTC, datetime
|
||||||
from sqlalchemy.orm import mapped_column, relationship
|
from sqlalchemy.orm import mapped_column, relationship
|
||||||
from sqlalchemy.orm.attributes import Mapped
|
from sqlalchemy.orm.attributes import Mapped
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
@ -25,11 +25,19 @@ class Team(app_db.BaseModel):
|
||||||
class TeamSchema(spec.BaseModel):
|
class TeamSchema(spec.BaseModel):
|
||||||
id: int
|
id: int
|
||||||
team_name: str
|
team_name: str
|
||||||
discord_webhook_url: str | None
|
|
||||||
tz_timezone: str
|
tz_timezone: str
|
||||||
minute_offset: int
|
minute_offset: int
|
||||||
#players: list[PlayerTeamSpec] | None
|
created_at: datetime
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_model(cls, team: Team):
|
||||||
|
return cls(
|
||||||
|
id=team.id,
|
||||||
|
team_name=team.team_name,
|
||||||
|
tz_timezone=team.tz_timezone,
|
||||||
|
minute_offset=team.minute_offset,
|
||||||
|
created_at=team.created_at,
|
||||||
|
)
|
||||||
|
|
||||||
from models.player_team import PlayerTeam
|
from models.player_team import PlayerTeam
|
||||||
from models.team_integration import TeamIntegration
|
from models.team_integration import TeamIntegration
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import UTC, datetime, timedelta, timezone
|
||||||
from random import randint, random
|
from random import randint, random
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
@ -22,15 +22,6 @@ import pytz
|
||||||
|
|
||||||
api_team = Blueprint("team", __name__, url_prefix="/team")
|
api_team = Blueprint("team", __name__, url_prefix="/team")
|
||||||
|
|
||||||
def map_team_to_schema(team: Team):
|
|
||||||
return TeamSchema(
|
|
||||||
id=team.id,
|
|
||||||
team_name=team.team_name,
|
|
||||||
discord_webhook_url=None,
|
|
||||||
tz_timezone=team.tz_timezone,
|
|
||||||
minute_offset=team.minute_offset
|
|
||||||
)
|
|
||||||
|
|
||||||
def map_player_to_schema(player: Player):
|
def map_player_to_schema(player: Player):
|
||||||
return PlayerSchema(
|
return PlayerSchema(
|
||||||
steam_id=str(player.steam_id),
|
steam_id=str(player.steam_id),
|
||||||
|
@ -40,6 +31,7 @@ def map_player_to_schema(player: Player):
|
||||||
class CreateTeamJson(BaseModel):
|
class CreateTeamJson(BaseModel):
|
||||||
team_name: str
|
team_name: str
|
||||||
discord_webhook_url: str | None = None
|
discord_webhook_url: str | None = None
|
||||||
|
minute_offset: int = 0
|
||||||
league_timezone: str
|
league_timezone: str
|
||||||
|
|
||||||
@validator("league_timezone")
|
@validator("league_timezone")
|
||||||
|
@ -75,6 +67,7 @@ def create_team(json: CreateTeamJson, player: Player, **kwargs):
|
||||||
team = Team(
|
team = Team(
|
||||||
team_name=json.team_name,
|
team_name=json.team_name,
|
||||||
tz_timezone=json.league_timezone,
|
tz_timezone=json.league_timezone,
|
||||||
|
minute_offset=json.minute_offset,
|
||||||
)
|
)
|
||||||
if json.discord_webhook_url:
|
if json.discord_webhook_url:
|
||||||
team.discord_webhook_url = json.discord_webhook_url
|
team.discord_webhook_url = json.discord_webhook_url
|
||||||
|
@ -91,8 +84,8 @@ def create_team(json: CreateTeamJson, player: Player, **kwargs):
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
response = ViewTeamResponse(team=map_team_to_schema(team))
|
response = ViewTeamResponse(team=TeamSchema.from_model(team))
|
||||||
return jsonify(response.dict(by_alias=True))
|
return response.dict(by_alias=True), 200
|
||||||
|
|
||||||
@api_team.delete("/id/<team_id>/")
|
@api_team.delete("/id/<team_id>/")
|
||||||
@spec.validate(
|
@spec.validate(
|
||||||
|
@ -255,7 +248,7 @@ def view_teams(**kwargs):
|
||||||
player: Player = kwargs["player"]
|
player: Player = kwargs["player"]
|
||||||
response = fetch_teams_for_player(player, None)
|
response = fetch_teams_for_player(player, None)
|
||||||
if isinstance(response, ViewTeamsResponse):
|
if isinstance(response, ViewTeamsResponse):
|
||||||
return jsonify(response.dict(by_alias=True))
|
return response.dict(by_alias=True)
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
@api_team.get("/id/<team_id>/")
|
@api_team.get("/id/<team_id>/")
|
||||||
|
@ -272,7 +265,7 @@ def view_team(team_id: int, **kwargs):
|
||||||
player: Player = kwargs["player"]
|
player: Player = kwargs["player"]
|
||||||
response = fetch_teams_for_player(player, team_id)
|
response = fetch_teams_for_player(player, team_id)
|
||||||
if isinstance(response, ViewTeamResponse):
|
if isinstance(response, ViewTeamResponse):
|
||||||
return jsonify(response.dict(by_alias=True))
|
return response.dict(by_alias=True)
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def fetch_teams_for_player(player: Player, team_id: int | None):
|
def fetch_teams_for_player(player: Player, team_id: int | None):
|
||||||
|
@ -292,13 +285,13 @@ def fetch_teams_for_player(player: Player, team_id: int | None):
|
||||||
if team_id is None:
|
if team_id is None:
|
||||||
teams = q.all()
|
teams = q.all()
|
||||||
return ViewTeamsResponse(
|
return ViewTeamsResponse(
|
||||||
teams=list(map(map_team_to_schema, teams))
|
teams=list(map(TeamSchema.from_model, teams))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
team = q.one_or_none()
|
team = q.one_or_none()
|
||||||
if team:
|
if team:
|
||||||
return ViewTeamResponse(
|
return ViewTeamResponse(
|
||||||
team=map_team_to_schema(team)
|
team=TeamSchema.from_model(team)
|
||||||
)
|
)
|
||||||
|
|
||||||
class ViewTeamMembersResponse(PlayerSchema):
|
class ViewTeamMembersResponse(PlayerSchema):
|
||||||
|
|
Loading…
Reference in New Issue