feat: Add vue-content-loader for loaders

master
John Montagu, the 4th Earl of Sandvich 2024-12-19 18:03:10 -08:00
parent 0f7995f0c2
commit 88969111ad
Signed by: sandvich
GPG Key ID: 9A39BE37E602B22D
10 changed files with 120 additions and 44 deletions

View File

@ -20,6 +20,7 @@
"pinia": "^2.2.4",
"radix-vue": "^1.9.10",
"vue": "^3.5.12",
"vue-content-loader": "^2.0.1",
"vue-router": "^4.4.5",
"vue-select": "^4.0.0-beta.6",
"vue3-tooltip": "^2.2.4"
@ -7657,6 +7658,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/vue-content-loader": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/vue-content-loader/-/vue-content-loader-2.0.1.tgz",
"integrity": "sha512-pkof4+q2xmzNEdhqelxtJejeP/vQUJtLle4/v2ueG+HURqM9Q/GIGC8GJ2bVVWeLfTDET51jqimwQdmxJTlu0g==",
"license": "MIT",
"peerDependencies": {
"vue": "^3"
}
},
"node_modules/vue-eslint-parser": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz",

View File

@ -27,6 +27,7 @@
"pinia": "^2.2.4",
"radix-vue": "^1.9.10",
"vue": "^3.5.12",
"vue-content-loader": "^2.0.1",
"vue-router": "^4.4.5",
"vue-select": "^4.0.0-beta.6",
"vue3-tooltip": "^2.2.4"

View File

@ -34,6 +34,7 @@
--surface-0: #ccd0da;
--base: #eff1f5;
--base-extra: #f5f6f7;
--mantle: #e6e9ef;
--crust: #dce0e8;

View File

@ -1,16 +1,25 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { onMounted, ref } from "vue";
import { useTeamsStore } from "../stores/teams";
import { RouterLink } from "vue-router";
import { useAuthStore } from "@/stores/auth";
import InviteKeyDialog from "./InviteKeyDialog.vue";
import { ContentLoader } from "vue-content-loader";
const teams = useTeamsStore();
const isLoading = ref(false);
const authStore = useAuthStore();
onMounted(() => {
teams.fetchTeams();
isLoading.value = true;
authStore.getUser()
.then(() => {
teams.fetchTeams()
.then(() => {
isLoading.value = false;
});
});
});
</script>
@ -34,8 +43,20 @@ onMounted(() => {
<div v-if="!authStore.isLoggedIn">
Log in to view your teams.
</div>
<div v-else-if="isLoading || true">
<ContentLoader :speed="1">
<circle cx="10" cy="20" r="8" />
<rect x="25" y="15" rx="5" ry="5" width="220" height="10" />
<circle cx="10" cy="50" r="8" />
<rect x="25" y="45" rx="5" ry="5" width="220" height="10" />
<circle cx="10" cy="80" r="8" />
<rect x="25" y="75" rx="5" ry="5" width="220" height="10" />
<circle cx="10" cy="110" r="8" />
<rect x="25" y="105" rx="5" ry="5" width="220" height="10" />
</ContentLoader>
</div>
<div
v-if="teams.teamsWithRole"
v-else-if="teams.teamsWithRole"
v-for="(team, _, i) in teams.teamsWithRole"
>
<div class="team-item">

View File

@ -2,11 +2,13 @@ import { defineStore } from "pinia";
import { ref } from "vue";
import { useClientStore } from "./client";
import { useRouter, type LocationQuery } from "vue-router";
import { type PlayerSchema } from "@/client";
export const useAuthStore = defineStore("auth", () => {
const clientStore = useClientStore();
const client = clientStore.client;
const user = ref<PlayerSchema | null>(null);
const steamId = ref("");
const username = ref("");
const isLoggedIn = ref(false);
@ -16,16 +18,27 @@ export const useAuthStore = defineStore("auth", () => {
const router = useRouter();
async function getUser() {
hasCheckedAuth.value = true;
if (hasCheckedAuth.value) {
if (!isLoggedIn.value) {
throw new Error("Not logged in");
}
return user.value;
}
return clientStore.call(
getUser.name,
() => client.default.getUser(),
(response) => {
hasCheckedAuth.value = true;
isLoggedIn.value = true;
steamId.value = response.steamId;
username.value = response.username;
user.value = response;
return response;
}
},
undefined,
() => hasCheckedAuth.value = true,
);
}

View File

@ -11,7 +11,9 @@ export const useClientStore = defineStore("client", () => {
function call<T>(
key: string,
apiCall: () => CancelablePromise<T>,
thenOnce?: (result: T) => T
thenOnce?: (result: T) => T,
catchOnce?: (error: any) => any,
finallyOnce?: () => void,
): Promise<T> {
console.log("Fetching call " + key);
if (!calls.has(key)) {
@ -26,6 +28,14 @@ export const useClientStore = defineStore("client", () => {
promise.then(thenOnce);
}
if (catchOnce) {
promise.catch(catchOnce);
}
if (finallyOnce) {
promise.finally(finallyOnce);
}
return promise;
}
return calls.get(key) as Promise<T>;

View File

@ -10,6 +10,7 @@ import EventList from "@/components/EventList.vue";
import { useTeamsEventsStore } from "@/stores/teams/events";
import MatchCard from "@/components/MatchCard.vue";
import { useMatchesStore } from "@/stores/matches";
import { ContentLoader } from "vue-content-loader";
const route = useRoute();
const teamsStore = useTeamsStore();
@ -28,14 +29,17 @@ const key = computed(() => route.query.key);
const teamsEventsStore = useTeamsEventsStore();
const events = computed(() => teamsEventsStore.teamEvents[teamId.value]);
const matches = computed(() => matchesStore.recentMatches);
const isLoading = ref(false);
onMounted(() => {
isLoading.value = true;
let doFetchTeam = () => {
teamsStore.fetchTeam(teamId.value)
.then(() => {
teamsStore.fetchTeamMembers(teamId.value);
teamsEventsStore.fetchTeamEvents(teamId.value);
matchesStore.fetchRecentMatchesForTeam(teamId.value, 5);
isLoading.value = false;
});
};
@ -55,7 +59,14 @@ onMounted(() => {
<div class="left">
<center class="margin">
<h1>
{{ team.teamName }}
<template v-if="isLoading || true">
<content-loader view-box="0 0 250 10">
<rect x="0" y="0" rx="3" ry="3" width="250" height="10" />
</content-loader>
</template>
<template v-else>
{{ team.teamName }}
</template>
</h1>
<span class="aside">
Formed on {{ creationDate }}

View File

@ -6,6 +6,7 @@ import { useTeamDetails } from "@/composables/team-details";
import { useTeamsStore } from "@/stores/teams";
import { useIntegrationsStore } from "@/stores/teams/integrations";
import { onMounted, ref } from "vue";
import { ContentLoader } from "vue-content-loader";
const teamsStore = useTeamsStore();
const integrationsStore = useIntegrationsStore();
@ -32,7 +33,14 @@ onMounted(() => {
<template>
<div class="team-integrations">
<div v-if="isLoading">
<LoaderContainer />
<ContentLoader>
<rect x="0" y="0" rx="3" ry="3" width="250" height="10" />
<rect x="20" y="20" rx="3" ry="3" width="220" height="10" />
<rect x="20" y="40" rx="3" ry="3" width="170" height="10" />
<rect x="0" y="60" rx="3" ry="3" width="250" height="10" />
<rect x="20" y="80" rx="3" ry="3" width="200" height="10" />
<rect x="20" y="100" rx="3" ry="3" width="80" height="10" />
</ContentLoader>
</div>
<template v-else>
<DiscordIntegrationForm v-model="integrationsStore.discordIntegration" />

View File

@ -20,41 +20,39 @@ onMounted(() => {
</script>
<template>
<main>
<div class="header">
<h1>
<i class="bi bi-trophy-fill margin"></i>
Matches
</h1>
<div class="button-group">
<AddMatchDialog />
</div>
<div class="header">
<h2>
<i class="bi bi-trophy-fill margin"></i>
Matches
</h2>
<div class="button-group">
<AddMatchDialog />
</div>
<table>
<thead>
<tr>
<th>RED</th>
<th>BLU</th>
<th>Team</th>
<th>Match Date</th>
<th>logs.tf URL</th>
</tr>
</thead>
<tbody>
<tr v-for="teamMatch in matches">
<td>{{ teamMatch.match.redScore }}</td>
<td>{{ teamMatch.match.blueScore }}</td>
<td>{{ teamMatch.teamColor == 'Blue' ? 'BLU' : 'RED' }}</td>
<td>{{ moment(teamMatch.match.matchTime).format("LL LT") }}</td>
<td>
<a :href="`https://logs.tf/${teamMatch.match.logsTfId}`" target="_blank">
#{{ teamMatch.match.logsTfId }}
</a>
</td>
</tr>
</tbody>
</table>
</main>
</div>
<table>
<thead>
<tr>
<th>RED</th>
<th>BLU</th>
<th>Team</th>
<th>Match Date</th>
<th>logs.tf URL</th>
</tr>
</thead>
<tbody>
<tr v-for="teamMatch in matches">
<td>{{ teamMatch.match.redScore }}</td>
<td>{{ teamMatch.match.blueScore }}</td>
<td>{{ teamMatch.teamColor == 'Blue' ? 'BLU' : 'RED' }}</td>
<td>{{ moment(teamMatch.match.matchTime).format("LL LT") }}</td>
<td>
<a :href="`https://logs.tf/${teamMatch.match.logsTfId}`" target="_blank">
#{{ teamMatch.match.logsTfId }}
</a>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>

View File

@ -100,7 +100,10 @@ class Event(app_db.BaseModel):
ringers_needed_msg = ""
if ringers_needed > 0:
ringers_needed_msg = f" **({ringers_needed} ringer(s) needed)**"
if ringers_needed == 1:
ringers_needed_msg = " **(1 ringer needed)**"
else:
ringers_needed_msg = f" **({ringers_needed} ringers needed)**"
domain = os.environ.get("DOMAIN", "availabili.tf")
@ -111,7 +114,7 @@ class Event(app_db.BaseModel):
"",
f"<t:{start_timestamp}:f>",
"\n".join(players_info),
f"Max bipartite matching size: {matchings}" + ringers_needed_msg,
f"Maximum roles filled: {matchings}" + ringers_needed_msg,
"",
"[Confirm attendance here]" +
f"(https://{domain}/team/id/{self.team.id}/events/{self.id})",