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", "pinia": "^2.2.4",
"radix-vue": "^1.9.10", "radix-vue": "^1.9.10",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-content-loader": "^2.0.1",
"vue-router": "^4.4.5", "vue-router": "^4.4.5",
"vue-select": "^4.0.0-beta.6", "vue-select": "^4.0.0-beta.6",
"vue3-tooltip": "^2.2.4" "vue3-tooltip": "^2.2.4"
@ -7657,6 +7658,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/vue-eslint-parser": {
"version": "9.4.3", "version": "9.4.3",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", "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", "pinia": "^2.2.4",
"radix-vue": "^1.9.10", "radix-vue": "^1.9.10",
"vue": "^3.5.12", "vue": "^3.5.12",
"vue-content-loader": "^2.0.1",
"vue-router": "^4.4.5", "vue-router": "^4.4.5",
"vue-select": "^4.0.0-beta.6", "vue-select": "^4.0.0-beta.6",
"vue3-tooltip": "^2.2.4" "vue3-tooltip": "^2.2.4"

View File

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

View File

@ -1,16 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted } from "vue"; import { onMounted, ref } from "vue";
import { useTeamsStore } from "../stores/teams"; import { useTeamsStore } from "../stores/teams";
import { RouterLink } from "vue-router"; import { RouterLink } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import InviteKeyDialog from "./InviteKeyDialog.vue"; import InviteKeyDialog from "./InviteKeyDialog.vue";
import { ContentLoader } from "vue-content-loader";
const teams = useTeamsStore(); const teams = useTeamsStore();
const isLoading = ref(false);
const authStore = useAuthStore(); const authStore = useAuthStore();
onMounted(() => { onMounted(() => {
teams.fetchTeams(); isLoading.value = true;
authStore.getUser()
.then(() => {
teams.fetchTeams()
.then(() => {
isLoading.value = false;
});
});
}); });
</script> </script>
@ -34,8 +43,20 @@ onMounted(() => {
<div v-if="!authStore.isLoggedIn"> <div v-if="!authStore.isLoggedIn">
Log in to view your teams. Log in to view your teams.
</div> </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 <div
v-if="teams.teamsWithRole" v-else-if="teams.teamsWithRole"
v-for="(team, _, i) in teams.teamsWithRole" v-for="(team, _, i) in teams.teamsWithRole"
> >
<div class="team-item"> <div class="team-item">

View File

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

View File

@ -11,7 +11,9 @@ export const useClientStore = defineStore("client", () => {
function call<T>( function call<T>(
key: string, key: string,
apiCall: () => CancelablePromise<T>, apiCall: () => CancelablePromise<T>,
thenOnce?: (result: T) => T thenOnce?: (result: T) => T,
catchOnce?: (error: any) => any,
finallyOnce?: () => void,
): Promise<T> { ): Promise<T> {
console.log("Fetching call " + key); console.log("Fetching call " + key);
if (!calls.has(key)) { if (!calls.has(key)) {
@ -26,6 +28,14 @@ export const useClientStore = defineStore("client", () => {
promise.then(thenOnce); promise.then(thenOnce);
} }
if (catchOnce) {
promise.catch(catchOnce);
}
if (finallyOnce) {
promise.finally(finallyOnce);
}
return promise; return promise;
} }
return calls.get(key) as Promise<T>; 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 { useTeamsEventsStore } from "@/stores/teams/events";
import MatchCard from "@/components/MatchCard.vue"; import MatchCard from "@/components/MatchCard.vue";
import { useMatchesStore } from "@/stores/matches"; import { useMatchesStore } from "@/stores/matches";
import { ContentLoader } from "vue-content-loader";
const route = useRoute(); const route = useRoute();
const teamsStore = useTeamsStore(); const teamsStore = useTeamsStore();
@ -28,14 +29,17 @@ const key = computed(() => route.query.key);
const teamsEventsStore = useTeamsEventsStore(); const teamsEventsStore = useTeamsEventsStore();
const events = computed(() => teamsEventsStore.teamEvents[teamId.value]); const events = computed(() => teamsEventsStore.teamEvents[teamId.value]);
const matches = computed(() => matchesStore.recentMatches); const matches = computed(() => matchesStore.recentMatches);
const isLoading = ref(false);
onMounted(() => { onMounted(() => {
isLoading.value = true;
let doFetchTeam = () => { let doFetchTeam = () => {
teamsStore.fetchTeam(teamId.value) teamsStore.fetchTeam(teamId.value)
.then(() => { .then(() => {
teamsStore.fetchTeamMembers(teamId.value); teamsStore.fetchTeamMembers(teamId.value);
teamsEventsStore.fetchTeamEvents(teamId.value); teamsEventsStore.fetchTeamEvents(teamId.value);
matchesStore.fetchRecentMatchesForTeam(teamId.value, 5); matchesStore.fetchRecentMatchesForTeam(teamId.value, 5);
isLoading.value = false;
}); });
}; };
@ -55,7 +59,14 @@ onMounted(() => {
<div class="left"> <div class="left">
<center class="margin"> <center class="margin">
<h1> <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> </h1>
<span class="aside"> <span class="aside">
Formed on {{ creationDate }} Formed on {{ creationDate }}

View File

@ -6,6 +6,7 @@ import { useTeamDetails } from "@/composables/team-details";
import { useTeamsStore } from "@/stores/teams"; import { useTeamsStore } from "@/stores/teams";
import { useIntegrationsStore } from "@/stores/teams/integrations"; import { useIntegrationsStore } from "@/stores/teams/integrations";
import { onMounted, ref } from "vue"; import { onMounted, ref } from "vue";
import { ContentLoader } from "vue-content-loader";
const teamsStore = useTeamsStore(); const teamsStore = useTeamsStore();
const integrationsStore = useIntegrationsStore(); const integrationsStore = useIntegrationsStore();
@ -32,7 +33,14 @@ onMounted(() => {
<template> <template>
<div class="team-integrations"> <div class="team-integrations">
<div v-if="isLoading"> <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> </div>
<template v-else> <template v-else>
<DiscordIntegrationForm v-model="integrationsStore.discordIntegration" /> <DiscordIntegrationForm v-model="integrationsStore.discordIntegration" />

View File

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

View File

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