Improve usability

master
John Montagu, the 4th Earl of Sandvich 2024-11-15 19:40:28 -08:00
parent afba73e1e8
commit cb9e29b402
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
6 changed files with 91 additions and 27 deletions

View File

@ -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" />

View File

@ -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;
}

View File

@ -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>

View File

@ -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;
} }

View File

@ -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

View File

@ -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):