Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Leaderboard Edit Page #23

Draft
wants to merge 24 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,36 @@ import Login from './components/Login.vue'
import Navbar from './components/Navbar.vue'
import { useUserDetails } from './composables/useUserDetails'

const user = useUserDetails()
const loggedIn = computed(() => user.value?.role === 'Administrator')
const { state, isLoading, isReady } = useUserDetails()
const loggedIn = computed(() => state.value?.role === 'Administrator')
</script>

<template>
<div class="root">
<div v-if="loggedIn">
<Navbar />
<RouterView />
<Teleport to="body" v-if="isLoading">
<div class="loader"><p>Loading...</p></div>
</Teleport>
<div v-show="isReady">
<template v-if="loggedIn">
<Navbar />
<RouterView />
</template>
<Login v-else />
</div>
<Login v-else />
</div>
</template>

<style scoped>
.root {
width: 100%;
}

.loader {
position: fixed;
z-index: 999;
top: 45%;
left: 50%;
padding: 1rem;
background-color: black;
}
</style>
138 changes: 138 additions & 0 deletions src/components/Edit.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import { computed, ref } from 'vue'
import { useApi } from '../composables/useApi'
import { useAuth } from '../composables/useAuth'
import { useSessionToken } from '../composables/useSessionToken'
import { Leaderboards } from '../lib/api/Leaderboards'
import { UpdateLeaderboardRequest } from '../lib/api/data-contracts'

const props = defineProps<{
id: number
}>()

const updateError = ref('')

const token = useSessionToken()

const leaderboards = new Leaderboards({
baseUrl: import.meta.env.VITE_BACKEND_URL
})

const {
state: board,
error,
isLoading,
execute
} = useAsyncState(async () => {
// TODO: Add param in BE that allows also fetching deleted boards
const resp = await leaderboards.getLeaderboard(props.id)
return resp.data
}, null)

const updateRequest = computed<UpdateLeaderboardRequest>(() => ({
name: board.value?.name,
info: board.value?.info,
slug: board.value?.slug,
}))

async function submit() {
useApi(
() => leaderboards.updateLeaderboard(props.id, updateRequest.value, useAuth(token.value)),
async (data) => {
console.log(await data.json())
execute()
},
(error) => {
updateError.value =
'Failed to update: ' + (error as Response).status.toString(10)
}
)
}
</script>

<template>
<div class="container">
<div v-if="isLoading">Loading...</div>
<div v-else-if="error" class="error-container">
<p class="errorText">{{ error }}</p>
<button @click="execute()" class="button">Reload</button>
</div>

<div v-else class="main-content">
<RouterLink class="back-link" :to="{ name: 'leaderboardView', params: { id } }"
>&lt; Back</RouterLink
>

<p v-if="updateError" class="error-text">{{ updateError }}</p>

<form @submit.prevent="submit">
<table>
<tbody>
<tr>
<th>ID:</th>
<td>{{ board?.id }}</td>
</tr>
<tr>
<th><label for="name">Name:</label></th>
<input v-model="updateRequest.name" id="name" />
</tr>
<tr>
<th><label for="slug">Slug:</label></th>
<input v-model="updateRequest.slug" id="slug" />
</tr>
<tr>
<th>Created:</th>
<td>{{ board?.createdAt }}</td>
</tr>
<tr>
<th>Deleted:</th>
<td v-if="board?.deletedAt">{{ board?.deletedAt }}</td>
<td v-else class="dim">&lt;Not deleted&gt;</td>
</tr>
<tr>
<th><label for="info">Info:</label></th>
<input v-model="updateRequest.info" id="info" />
</tr>
</tbody>
</table>
<button>Submit</button>
</form>
</div>
</div>
</template>

<style lang="css" scoped>
.container {
padding: 1rem;
}

.error-container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: start;
}

.main-content {
display: flex;
flex-direction: column;
margin-top: 1rem;
row-gap: 1rem;
}

.back-link {
justify-self: start;
padding: 0.5rem;
}

.error-text {
grid-column: span 2 / span 2;
color: crimson;
}

.dim {
color: grey;
font-style: italic;
}
</style>
174 changes: 174 additions & 0 deletions src/components/Leaderboard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<script setup lang="ts">
import { useAsyncState } from '@vueuse/core'
import { ref } from 'vue'
import { useAuth } from '../composables/useAuth'
import { useSessionToken } from '../composables/useSessionToken'
import { Leaderboards } from '../lib/api/Leaderboards'
import { useApi } from '../composables/useApi'

const props = defineProps<{
id: number
}>()

const updateError = ref('')

const token = useSessionToken()

const leaderboards = new Leaderboards({
baseUrl: import.meta.env.VITE_BACKEND_URL
})

const {
state: board,
error,
isLoading,
execute
} = useAsyncState(async () => {
// TODO: Add param in BE that allows also fetching deleted boards
const resp = await leaderboards.getLeaderboard(props.id)
return resp.data
}, null)

async function revealDelete() {
if (
confirm('Really delete this leaderboard? (This action can be reversed)')
) {
useApi(
() => leaderboards.deleteLeaderboard(props.id, useAuth(token.value)),
() => execute(),
(error) => {
updateError.value =
'Failed to delete: ' + (error as Response).status.toString(10)
}
)
}
}

async function revealRestore() {
if (
confirm('Really restore this leaderboard? (This action can be reversed)')
) {
useApi(
() => leaderboards.restoreLeaderboard(props.id, useAuth(token.value)),
() => execute(),
(error) => {
updateError.value =
'Failed to restore: ' + (error as Response).status.toString(10)
}
)
}
}
</script>

<template>
<div class="container">
<div v-if="isLoading">Loading...</div>
<div v-else-if="error" class="error-container">
<p class="errorText">{{ error }}</p>
<button @click="execute()" class="button">Reload</button>
</div>

<div v-else class="main-content">
<RouterLink class="back-link" :to="{ name: 'leaderboardsList' }"
>&lt; Back</RouterLink
>
<div class="action-button-container">
<RouterLink :to="{ name: 'leaderboardEdit', params: { id } }"><button class="action-button">✎</button></RouterLink>
<button
v-if="board?.deletedAt === null"
class="action-button delete-button"
@click="revealDelete"
>
Delete
</button>
<button v-else class="action-button" @click="revealRestore">
Restore
</button>
</div>

<p v-if="updateError" class="error-text">{{ updateError }}</p>

<table>
<tbody>
<tr>
<th>ID:</th>
<td>{{ board?.id }}</td>
</tr>
<tr>
<th>Name:</th>
<td>{{ board?.name }}</td>
</tr>
<tr>
<th>Slug:</th>
<!-- TODO: Convert this to a link to the board on the main site -->
<td>/{{ board?.slug }}</td>
</tr>
<tr>
<th>Created:</th>
<td>{{ board?.createdAt }}</td>
</tr>
<tr>
<th>Deleted:</th>
<td v-if="board?.deletedAt">{{ board?.deletedAt }}</td>
<td v-else class="dim">&lt;Not deleted&gt;</td>
</tr>
<tr>
<th>Info:</th>
<td v-if="board?.info">{{ board?.info }}</td>
<td v-else class="dim">&lt;none&gt;</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>

<style lang="css" scoped>
.container {
padding: 1rem;
}

.error-container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: start;
}

.main-content {
display: grid;
grid-template-columns: repeat(2, 1fr);
margin-top: 1rem;
row-gap: 1rem;
}

.back-link {
justify-self: start;
padding: 0.5rem;
}

.action-button-container {
display: flex;
gap: 0.5rem;
justify-self: end;
}

.action-button {
width: 3rem;
height: 3rem;
}

.delete-button {
background: crimson;
}

.error-text {
grid-column: span 2 / span 2;
color: crimson;
}

.dim {
color: grey;
font-style: italic;
}
</style>
4 changes: 2 additions & 2 deletions src/components/Login.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const password = ref('')
const loginError = ref(false)
const submitted = ref(false)
const token = useSessionToken()
const user = useUserDetails()
const { state: user } = useUserDetails()
const loginFailed = computed(
() =>
loginError.value ||
Expand Down Expand Up @@ -47,7 +47,7 @@ async function submit() {
<h2>Admin Panel Login</h2>
<form class="loginForm" @submit.prevent="submit">
<label for="email">Email</label>
<input v-model="email" id="email" type="text" />
<input v-model="email" id="email" type="text" autofocus />
<label for="password">Password</label>
<input v-model="password" id="password" type="password" />
<input type="submit" value="Login" />
Expand Down
6 changes: 4 additions & 2 deletions src/components/Navbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ function logoutClicked() {
token.value = ''
}

const user = useUserDetails()
const { state: user } = useUserDetails()
</script>

<template>
<div class="navbar">
<div class="navbar-left">
<p class="title select-none">Leaderboards.gg Admin Panel</p>
<RouterLink class="navlink" :to="{ name: 'leaderboardsList' }">Leaderboards</RouterLink>
<RouterLink class="navlink" :to="{ name: 'leaderboardsList' }"
>Leaderboards</RouterLink
>
</div>
<div class="navbar-right">
<p>Signed in as {{ user?.username }}</p>
Expand Down
Loading