-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
23 changed files
with
767 additions
and
53 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,259 @@ | ||
<script setup lang="ts"> | ||
const props = withDefaults(defineProps<{ | ||
scramble: Scramble | ||
competition: Competition | ||
submissions: Submission[] | ||
type?: string | ||
allowUnlimited?: boolean | ||
}>(), { | ||
type: 'weekly', | ||
allowUnlimited: true, | ||
}) | ||
const emit = defineEmits<{ | ||
submitted: [] | ||
}>() | ||
const { t } = useI18n() | ||
const submissionsMap = computed(() => { | ||
const ret: Record<number, Submission> = {} | ||
for (const submission of props.submissions) ret[submission.mode] = submission | ||
return ret | ||
}) | ||
const form = reactive({ | ||
mode: CompetitionMode.REGULAR, | ||
solution: '', | ||
comment: '', | ||
}) | ||
// if there is an unlimited submission, use unlimited mode | ||
if (submissionsMap.value[CompetitionMode.UNLIMITED]) | ||
form.mode = CompetitionMode.UNLIMITED | ||
const localForm = useLocalStorage<Record<number, Record<number, { solution: string, comment: string }>>>(`form.${props.type}.${props.competition.alias}`, {}) | ||
onMounted(() => { | ||
const localValue = submissionsMap.value[form.mode] || localForm.value[props.scramble.number]?.[form.mode] | ||
if (localValue) { | ||
form.solution = localValue.solution | ||
form.comment = localValue.comment | ||
} | ||
}) | ||
watch(form, (state) => { | ||
localForm.value = { | ||
...localForm.value, | ||
[props.scramble.number]: { | ||
...localForm.value[props.scramble.number], | ||
[form.mode]: { | ||
solution: state.solution, | ||
comment: state.comment, | ||
}, | ||
}, | ||
} | ||
}) | ||
watch(() => form.mode, (mode) => { | ||
const localValue = submissionsMap.value[mode] || localForm.value[props.scramble.number]?.[mode] | ||
if (localValue) { | ||
form.solution = localValue.solution | ||
form.comment = localValue.comment | ||
} | ||
else { | ||
form.solution = '' | ||
form.comment = '' | ||
} | ||
if (mode === CompetitionMode.REGULAR && submissionsMap.value[CompetitionMode.UNLIMITED] && !submissionsMap.value[CompetitionMode.REGULAR]) | ||
form.solution = t('weekly.regular.unlimitedSubmitted') | ||
}) | ||
const { moves, isSolved } = useComputedState(props, form) | ||
const solutionState = computed<boolean | null>(() => { | ||
if (form.solution.length === 0) | ||
return null | ||
if (!isSolved.value) | ||
return false | ||
return moves.value !== DNF | ||
}) | ||
const solutionDisabled = computed<boolean>(() => { | ||
if (form.mode === CompetitionMode.UNLIMITED) | ||
return false | ||
if (props.submissions.length === 0) | ||
return false | ||
return true | ||
}) | ||
const unlimitedWorse = computed<boolean>(() => { | ||
return form.mode === CompetitionMode.UNLIMITED && props.submissions.some(s => s.moves < moves.value * 100) | ||
}) | ||
const formState = computed<boolean>(() => { | ||
if (form.mode === CompetitionMode.REGULAR && !submissionsMap.value[CompetitionMode.REGULAR] && submissionsMap.value[CompetitionMode.UNLIMITED]) | ||
return false | ||
if (unlimitedWorse.value) | ||
return false | ||
return solutionState.value !== null | ||
}) | ||
const { confirm, cancel, reveal, isRevealed } = useConfirmDialog() | ||
const loading = ref(false) | ||
const confirmMessage = ref(t('weekly.confirmDNF')) | ||
async function submit() { | ||
loading.value = true | ||
try { | ||
if (form.mode === CompetitionMode.REGULAR && submissionsMap.value[CompetitionMode.REGULAR]) { | ||
const { data, refresh } = await useApiPost<Submission>(`/${props.type}/${props.competition.alias}/${submissionsMap.value[CompetitionMode.REGULAR].id}`, { | ||
body: { | ||
comment: form.comment, | ||
}, | ||
immediate: false, | ||
}) | ||
await refresh() | ||
if (data.value) | ||
emit('submitted') | ||
} | ||
else { | ||
if (moves.value === DNF) { | ||
confirmMessage.value = t('weekly.confirmDNF') | ||
const { isCanceled } = await reveal() | ||
if (isCanceled) | ||
return | ||
} | ||
const { data, refresh } = await useApiPost<Submission>(`/${props.type}/${props.competition.alias}`, { | ||
body: { | ||
scrambleId: props.scramble.id, | ||
mode: form.mode, | ||
solution: form.solution, | ||
comment: form.comment, | ||
}, | ||
immediate: false, | ||
}) | ||
await refresh() | ||
if (data.value) | ||
emit('submitted') | ||
} | ||
} | ||
catch (e: any) { | ||
if (e.response && e.response.data && e.response.data.message) | ||
alert(e.response.data.message) | ||
else alert(e.message) | ||
} | ||
finally { | ||
loading.value = false | ||
} | ||
} | ||
async function turnToUnlimited() { | ||
try { | ||
confirmMessage.value = t('weekly.turnToUnlimited.confirm') | ||
const { isCanceled } = await reveal() | ||
if (isCanceled) | ||
return | ||
const { data, refresh } = await useApiPost<Submission>(`/weekly/${props.competition.alias}/${submissionsMap.value[CompetitionMode.REGULAR].id}/unlimited`, { | ||
immediate: false, | ||
}) | ||
await refresh() | ||
if (data.value) { | ||
emit('submitted') | ||
form.solution = t('weekly.regular.unlimitedSubmitted') | ||
} | ||
} | ||
catch (e: any) { | ||
if (e.response && e.response.data && e.response.data.message) | ||
alert(e.response.data.message) | ||
else alert(e.message) | ||
} | ||
} | ||
function reset() { | ||
if (!submissionsMap.value[form.mode]) | ||
form.solution = '' | ||
form.comment = '' | ||
} | ||
</script> | ||
|
||
<template> | ||
<div class="mt-6"> | ||
<form class="relative" @submit="submit" @reset="reset"> | ||
<FormSignInRequired /> | ||
<FormInput | ||
v-if="allowUnlimited" | ||
v-model="form.mode" | ||
type="radio" | ||
:label="$t('weekly.mode.label')" | ||
:state="null" | ||
:attrs="{ required: true }" | ||
:options="[ | ||
{ label: $t('weekly.regular.label'), description: $t('weekly.regular.description'), value: CompetitionMode.REGULAR }, | ||
{ label: $t('weekly.unlimited.label'), description: $t('weekly.unlimited.description'), value: CompetitionMode.UNLIMITED }, | ||
]" | ||
/> | ||
<FormInput | ||
v-model="form.solution" | ||
type="textarea" | ||
:rows="4" | ||
:label="$t('weekly.solution.label') + (submissionsMap[form.mode] ? $t('weekly.submitted') : '')" | ||
:state="solutionState" | ||
:attrs="{ required: true, disabled: solutionDisabled }" | ||
> | ||
<template #description> | ||
<div v-if="unlimitedWorse" class="text-red-500"> | ||
{{ $t('weekly.unlimited.invalid') }} | ||
</div> | ||
<div v-if="moves !== DNF" class="text-green-500 text-bold"> | ||
{{ $t('common.moves', { moves }) }} | ||
</div> | ||
<div v-else class="text-red-500 text-bold"> | ||
DNF | ||
</div> | ||
<I18nT keypath="if.scramble.description" tag="p" scope="global"> | ||
<template #notation> | ||
<Notation /> | ||
</template> | ||
</I18nT> | ||
</template> | ||
</FormInput> | ||
<FormInput | ||
v-model="form.comment" | ||
type="textarea" | ||
:rows="4" | ||
:label="$t('weekly.comment.label')" | ||
:state="null" | ||
class="mt-4" | ||
> | ||
<template #description> | ||
<p class="py-1" v-html="$t('weekly.comment.description')" /> | ||
</template> | ||
</FormInput> | ||
<div class="mt-4"> | ||
<button | ||
class="px-2 py-1 text-white bg-blue-500 focus:outline-none" | ||
:class="{ 'bg-opacity-50 cursor-not-allowed': !formState }" | ||
:disabled="!formState" | ||
@click.prevent="submit" | ||
> | ||
<Spinner v-if="loading" class="w-4 h-4 text-white border-[3px]" /> | ||
<template v-else> | ||
{{ $t('form.submit') }} | ||
</template> | ||
</button> | ||
<button class="px-2 py-1 text-white bg-gray-500 focus:outline-none ml-2" @click.prevent="reset"> | ||
{{ $t('form.reset') }} | ||
</button> | ||
<button | ||
v-if="allowUnlimited && form.mode === CompetitionMode.REGULAR && submissionsMap[CompetitionMode.REGULAR] && !submissionsMap[CompetitionMode.UNLIMITED]" | ||
class="px-2 py-1 text-white bg-orange-500 focus:outline-none ml-2" | ||
@click.prevent="turnToUnlimited" | ||
> | ||
{{ $t('weekly.turnToUnlimited.label') }} | ||
</button> | ||
</div> | ||
</form> | ||
</div> | ||
<Teleport to="body"> | ||
<Modal v-if="isRevealed" :cancel="cancel"> | ||
<div class="mb-5 font-bold"> | ||
{{ confirmMessage }} | ||
</div> | ||
<div class="flex gap-2 justify-end"> | ||
<button class="bg-rose-500 hover:bg-opacity-90 text-white cursor-pointer px-2 py-1" @click="confirm"> | ||
{{ $t('form.confirm') }} | ||
</button> | ||
<button class="bg-gray-300 hover:bg-opacity-80 cursor-pointer px-2 py-1" @click="cancel"> | ||
{{ $t('form.cancel') }} | ||
</button> | ||
</div> | ||
</Modal> | ||
</Teleport> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
<script setup lang="ts"> | ||
const props = defineProps<{ | ||
competition: Practice | ||
}>() | ||
const index = computed(() => props.competition.alias.split('-').reverse()[0]) | ||
const link = computed(() => { | ||
const { user } = props.competition | ||
return `/practice/${user.wcaId || user.id}/${index.value}` | ||
}) | ||
</script> | ||
|
||
<template> | ||
<div class="pt-2"> | ||
<NuxtLink :to="link" class="col-span-12 md:col-span-6 lg:col-span-4 flex items-center gap-2 text-blue-500"> | ||
<div> | ||
{{ $t('practice.number', { number: index }) }} | ||
</div> | ||
<UserAvatarName :user="competition.user" :link="false" /> | ||
</NuxtLink> | ||
<div class="flex text-sm gap-2 mt-2"> | ||
<div> | ||
{{ $t(`common.${CompetitionFormat[competition.format].toLowerCase()}`) }} | ||
</div> | ||
<div> | ||
{{ $t('endless.progress.competitors', { competitors: competition.attendees }) }} | ||
</div> | ||
<div> | ||
{{ $dayjs(competition.startTime).format('YYYY-MM-DD HH:mm:ss') }} | ||
</div> | ||
</div> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<script setup lang="ts"> | ||
const props = withDefaults(defineProps<{ | ||
show: boolean | ||
spoiled: string | ||
}>(), { | ||
show: false, | ||
}) | ||
const showSpoiler = ref(props.show) | ||
</script> | ||
|
||
<template> | ||
<div class="relative"> | ||
<div | ||
v-if="!showSpoiler" | ||
class="absolute inset-0 z-50 bg-indigo-500 text-white flex flex-col items-center justify-center cursor-pointer" | ||
@click="showSpoiler = true" | ||
> | ||
<I18nT keypath="common.spoiler" tag="div" scope="global" class="flex items-center"> | ||
<template #for> | ||
<span class="text-lg font-bold"> | ||
{{ spoiled }} | ||
</span> | ||
</template> | ||
</I18nT> | ||
</div> | ||
<slot /> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,20 @@ | ||
<script setup lang="ts"> | ||
import { NuxtLink } from '#components' | ||
withDefaults(defineProps<{ | ||
user: User | ||
link?: boolean | ||
size?: number | ||
link?: boolean | ||
}>(), { | ||
link: true, | ||
size: 6, | ||
link: true, | ||
}) | ||
</script> | ||
|
||
<template> | ||
<NuxtLink :to="`/profile/${user.wcaId || user.id}`" class="flex items-center"> | ||
<component :is="link ? NuxtLink : 'div'" :to="`/profile/${user.wcaId || user.id}`" class="flex items-center"> | ||
<UserAvatar :user="user" class="mr-1" :size="size" :link="false" /> | ||
<div class="whitespace-nowrap text-blue-500"> | ||
{{ localeName(user.name, $i18n.locale) }} | ||
</div> | ||
<UserName :user="user" :class="{ 'text-blue-500': link }" /> | ||
<slot /> | ||
</NuxtLink> | ||
</component> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
<script setup lang="ts"> | ||
defineProps<{ | ||
user: User | ||
}>() | ||
</script> | ||
|
||
<template> | ||
<div class="whitespace-nowrap"> | ||
{{ localeName(user.name, $i18n.locale) }} | ||
</div> | ||
</template> |
Oops, something went wrong.