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

feat(frontend): enhanced contest ui #91

Merged
merged 1 commit into from
Apr 6, 2024
Merged
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
2 changes: 2 additions & 0 deletions .yarn/versions/0d054741.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
releases:
"@aoi-js/frontend": 1.1.0-alpha.6
3 changes: 3 additions & 0 deletions apps/frontend/src/components/aoi/AoiBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
{{ t('pages.signin') }}
</VBtn>
</VToolbarItems>
<template #extension v-if="appState.navBarExtension">
<component :is="appState.navBarExtension[0]" v-bind="appState.navBarExtension[1].value" />
</template>
</VAppBar>
</template>

Expand Down
67 changes: 67 additions & 0 deletions apps/frontend/src/components/contest/ContestProgressBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import ms from 'ms'
import { ref, computed, onBeforeUnmount } from 'vue'

import type { IPlanContestDTO } from '../plan/types'

import type { IContestDTO } from './types'

export interface IContestProgressBarProps {
contest: IContestDTO | IPlanContestDTO
}

export interface IContestProgressBarEmits {
(ev: 'updated'): void
}

export function useContestProgressBar(
props: IContestProgressBarProps,
emit: IContestProgressBarEmits
) {
const now = ref(Date.now())

const items = computed(() =>
props.contest.stages.map(({ name }) => ({
title: name,
disabled: props.contest.currentStage.name !== name
}))
)

const section = computed(() => {
const stages = props.contest.stages
const i = stages.findIndex((stage) => stage.name === props.contest.currentStage.name)
if (i <= 0 || i >= stages.length - 1) {
if (i <= 0 && now.value >= stages[1].start) {
emit('updated')
}
return {
begin: stages[1].start,
end: stages[stages.length - 1].start,
progress: i <= 0 ? 0 : 100,
stopped: i >= stages.length - 1
}
}
const begin = stages[i].start
const end = stages[i + 1].start
if (now.value >= end) {
emit('updated')
}
const progress = (100 * (now.value - begin)) / (end - begin)
return {
begin,
end,
tPlus: ms(now.value - begin),
tMinus: ms(end - now.value),
progress
}
})

const intervalId = setInterval(() => {
now.value = Date.now()
}, 1000)

onBeforeUnmount(() => {
clearInterval(intervalId)
})

return { items, section, now }
}
72 changes: 18 additions & 54 deletions apps/frontend/src/components/contest/ContestProgressBar.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
<template>
<div class="u-flex u-flex-col u-items-center">
<VBreadcrumbs density="compact" :items="items" divider="-"></VBreadcrumbs>
<VBreadcrumbs density="compact" :items="items" divider="-">
<template v-slot:title="{ item }">
{{ t(`stages.${item.title}`) }}
</template>
</VBreadcrumbs>
<div class="u-self-stretch u-flex u-items-center u-gap-2">
<VChip color="blue" :text="new Date(section.begin).toLocaleString()" />
<VChip color="blue" :text="denseDateString(section.begin)" />
<VChip v-if="section.tPlus" :text="'T+' + section.tPlus" />
<div class="u-flex-1">
<VProgressLinear
Expand All @@ -13,66 +17,26 @@
/>
</div>
<VChip v-if="section.tMinus" :text="'T-' + section.tMinus" />
<VChip color="red" :text="new Date(section.end).toLocaleString()" />
<VChip color="red" :text="denseDateString(section.end)" />
</div>
</div>
</template>

<script setup lang="ts">
import ms from 'ms'
import { computed, ref, onBeforeUnmount } from 'vue'
import { useI18n } from 'vue-i18n'

import type { IPlanContestDTO } from '../plan/types'
import {
useContestProgressBar,
type IContestProgressBarProps,
type IContestProgressBarEmits
} from './ContestProgressBar'

import type { IContestDTO } from '@/components/contest/types'
import { denseDateString } from '@/utils/time'

const props = defineProps<{ contest: IContestDTO | IPlanContestDTO }>()
const emit = defineEmits<{
(ev: 'updated'): void
}>()
const now = ref(Date.now())
const props = defineProps<IContestProgressBarProps>()
const emit = defineEmits<IContestProgressBarEmits>()

const items = computed(() =>
props.contest.stages.map(({ name }) => ({
title: name,
disabled: props.contest.currentStage.name !== name
}))
)
const { t } = useI18n()

const section = computed(() => {
const stages = props.contest.stages
const i = stages.findIndex((stage) => stage.name === props.contest.currentStage.name)
if (i <= 0 || i >= stages.length - 1) {
if (i <= 0 && now.value >= stages[1].start) {
emit('updated')
}
return {
begin: stages[1].start,
end: stages[stages.length - 1].start,
progress: i <= 0 ? 0 : 100,
stopped: i >= stages.length - 1
}
}
const begin = stages[i].start
const end = stages[i + 1].start
if (now.value >= end) {
emit('updated')
}
const progress = (100 * (now.value - begin)) / (end - begin)
return {
begin,
end,
tPlus: ms(now.value - begin),
tMinus: ms(end - now.value),
progress
}
})

const intervalId = setInterval(() => {
now.value = Date.now()
}, 1000)

onBeforeUnmount(() => {
clearInterval(intervalId)
})
const { items, section } = useContestProgressBar(props, emit)
</script>
55 changes: 55 additions & 0 deletions apps/frontend/src/components/contest/ContestTabs.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<template>
<VTabs align-tabs="center" class="u-flex-1">
<VTab prepend-icon="mdi-book-outline" exact :to="rel('')" :text="t('tabs.description')" />
<VTab prepend-icon="mdi-attachment" :to="rel('attachment')" :text="t('tabs.attachments')" />
<VTab
v-show="showAdminTab || (contest?.currentStage.settings.problemEnabled && registered)"
prepend-icon="mdi-list-box-outline"
:to="rel('problem')"
:text="t('tabs.problems')"
/>
<VTab
v-show="showAdminTab || (contest?.currentStage.settings.solutionEnabled && registered)"
prepend-icon="mdi-timer-sand"
:to="rel('solution')"
:text="t('tabs.solutions')"
/>
<VTab
v-show="showAdminTab || contest?.currentStage.settings.ranklistEnabled"
prepend-icon="mdi-chevron-triple-up"
:to="rel('ranklist')"
:text="t('tabs.ranklist')"
/>
<VTab
v-show="showAdminTab || contest?.currentStage.settings.participantEnabled"
prepend-icon="mdi-account-details-outline"
:to="rel('participant')"
:text="t('tabs.participant')"
/>
<VTab
v-show="showAdminTab"
prepend-icon="mdi-cog-outline"
:to="rel('admin')"
:text="t('tabs.management')"
/>
</VTabs>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n'

import type { IContestDTO } from '@/components/contest/types'
import { useAppState } from '@/stores/app'

const { t } = useI18n()
const app = useAppState()
const props = defineProps<{
orgId: string
contestId: string
showAdminTab: boolean
registered: boolean
contest: IContestDTO
}>()

const rel = (to: string) => `/org/${props.orgId}/contest/${props.contestId}/${to}`
</script>
46 changes: 28 additions & 18 deletions apps/frontend/src/components/utils/JsonViewer.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
<template>
<AsyncState :state="data" hide-when-loading>
<AsyncState :state="data">
<template v-slot="{ value }">
<VTabs v-model="currentTab">
<VTab prepend-icon="mdi-form-textarea" value="visual" v-if="slots.default">
{{ t('visualized') }}
</VTab>
<VTab prepend-icon="mdi-code-json" value="raw" v-if="!hideRaw">
{{ t('raw') }}
</VTab>
<VTab prepend-icon="mdi-refresh" :value="currentTab" @click="data.execute()">
{{ t('action.refresh') }}
</VTab>
<VTab
prepend-icon="mdi-form-textarea"
value="visual"
:text="t('visualized')"
v-if="slots.default"
/>
<VTab prepend-icon="mdi-code-json" value="raw" :text="t('raw')" v-if="!hideRaw" />
<VBtn
:text="t('action.refresh')"
prepend-icon="mdi-refresh"
variant="text"
class="align-self-center me-4"
height="100%"
@click="data.execute()"
/>
</VTabs>
<VWindow v-model="currentTab">
<VWindowItem value="visual" v-if="slots.default">
<slot :value="value"></slot>
</VWindowItem>
<VWindowItem value="raw">
<VWindowItem value="raw" v-if="!hideRaw">
<MonacoEditor readonly language="json" :model-value="JSON.stringify(value, null, 2)" />
</VWindowItem>
</VWindow>
Expand Down Expand Up @@ -58,13 +64,17 @@ async function resolveUrl() {
throw new Error('No url or endpoint provided')
}

const data = useAsyncState(async () => {
if (props.rawData) return props.rawData
if (props.rawString) return JSON.parse(props.rawString)
const url = await resolveUrl()
const json = await ky.get(url).json<T>()
return json
}, null)
const data = useAsyncState(
async () => {
if (props.rawData) return props.rawData
if (props.rawString) return JSON.parse(props.rawString)
const url = await resolveUrl()
const json = await ky.get(url).json<T>()
return json
},
null,
{ resetOnExecute: false }
)

watch([() => props.endpoint, () => props.url, () => props.rawString], () => data.execute())
</script>
Expand Down
Loading
Loading