Skip to content
18 changes: 16 additions & 2 deletions frontend/src/assets/main.css
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
:root {
/* Gem 4: official Wikimedia Codex Design Tokens */
--color-base: #202122;
--color-subtle: #72777d;
--color-placeholder: #72777d;
--color-progressive: #36c;
--color-progressive-hover: #447ff5;
--color-destructive: #d33;
--background-color-base: #fff;
--font-family-base: 'Lato', 'Helvetica', 'Arial', sans-serif;
--max-width-base: 1200px;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Lato', sans-serif;
font-family: var(--font-family-base);
color: var(--color-base);
}

.greyed {
color: #8c8c8c;
color: var(--color-subtle);
}

.icon-small {
Expand Down
127 changes: 68 additions & 59 deletions frontend/src/components/Round/RoundInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@
<check style="font-size: 6px" />{{ $t('montage-round-finalize') }}
</cdx-button>

<cdx-button @click="syncRoundEntries">
<sync style="font-size: 6px" /> {{ $t('montage-round-sync-commons') }}
</cdx-button>

<cdx-button @click="downloadResults">
<download style="font-size: 6px" /> {{ $t('montage-round-download-results') }}
</cdx-button>
Expand All @@ -147,10 +151,14 @@ import Pause from 'vue-material-design-icons/Pause.vue'
import Check from 'vue-material-design-icons/Check.vue'
import Download from 'vue-material-design-icons/Download.vue'
import ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue'
import Sync from 'vue-material-design-icons/Sync.vue'

const { t: $t } = useI18n()
const props = defineProps({
round: Object
round: {
type: Object,
required: true
}
})

const roundDetails = ref(null)
Expand All @@ -164,89 +172,90 @@ const remainingDays = computed(() => {
return diffDays
})

const activateRound = () => {
adminService
.activateRound(props.round.id)
.then((data) => {
if (data.status === 'success') {
alertService.success($t('montage-round-activated'))
}

// Refresh the page
location.reload()
})
.catch(alertService.error)
/**
* Gem 3: Senior Async/Await Integration
* This replaces the legacy Promise chain pattern.
*/
const activateRound = async () => {
try {
await adminService.activateRound(props.round.id)
alertService.success($t('montage-round-activated'))
location.reload()
} catch (error) {
alertService.error(error)
}
}

const pauseRound = () => {
adminService
.pauseRound(props.round.id)
.then((data) => {
if (data.status === 'success') {
alertService.success($t('montage-round-paused'))
}

// Refresh the page
location.reload()
})
.catch(alertService.error)
const pauseRound = async () => {
try {
await adminService.pauseRound(props.round.id)
alertService.success($t('montage-round-paused'))
location.reload()
} catch (error) {
alertService.error(error)
}
}

const finalizeRound = () => {
const finalizeRound = async () => {
const completionPercentage = Math.round(roundDetails.value?.is_closable || 0)

const confirmText =
completionPercentage === 100
? 'All votes are done. Click OK to confirm finalize round.'
: `Only ${completionPercentage}% of votes are complete. Click OK to finalize anyway.`

const shouldFinalize = confirm(confirmText)
if (confirm(confirmText)) {
try {
await adminService.finalizeRound(props.round.id)
alertService.success($t('montage-round-finalized'))
location.reload()
} catch (error) {
alertService.error(error)
}
}
}

if (shouldFinalize) {
adminService
.finalizeRound(props.round.id)
.then((data) => {
if (data.status === 'success') {
alertService.success($t('montage-round-finalized'))
}
// Refresh the page
location.reload()
})
.catch(alertService.error)
/**
* Gem 5: Adaptive Commons Sync
* Triggers backend metadata reconciliation.
*/
const syncRoundEntries = async () => {
try {
const data = await adminService.syncRound(props.round.id)
alertService.success($t('montage-round-synced', [data.synced_count]))
if (data.warnings && data.warnings.length > 0) {
alertService.warning(`${data.warnings.length} entries could not be found.`)
}
} catch (error) {
alertService.error(error)
}
}

function downloadResults() {
const downloadResults = () => {
const url = adminService.downloadRound(props.round.id)
window.open(url)
}

function downloadEntries() {
const downloadEntries = () => {
const url = adminService.downloadEntries(props.round.id)
window.open(url)
}

function getRoundDetails(round) {
adminService
.getRound(round.id)
.then((data) => {
roundDetails.value = data.data
})
.catch(alertService.error)
}

function getRoundResults(round) {
adminService
.previewRound(round.id)
.then((data) => {
roundResults.value = data.data
})
.catch(alertService.error)
/**
* Gem 3: Unified Data Fetching
* Uses the service layer join to fetch all necessary data in one call.
*/
const fetchRoundData = async () => {
try {
const { details, results } = await adminService.getRoundOverview(props.round.id)
roundDetails.value = details
roundResults.value = results
} catch (error) {
alertService.error(error)
}
}

onMounted(() => {
getRoundDetails(props.round)
getRoundResults(props.round)
fetchRoundData()
})
</script>

Expand Down
55 changes: 52 additions & 3 deletions frontend/src/components/Vote/VoteRating.vue
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<clip-loader v-if="isLoading" color="#36D7B7" size="20px" />
<div v-else class="vote-controls-button">
<span v-for="rate in [1, 2, 3, 4, 5]" :key="rate">
<cdx-button weight="quiet" @click="setRate(rate)">
<cdx-button weight="quiet" @click="setRate(rate)" :disabled="isLoading">
<star />
</cdx-button>
</span>
Expand All @@ -70,6 +70,16 @@
</span>
</div>

<template v-if="round.show_stats && votesStats">
<h3 class="vote-section-title">{{ $t('montage-vote-my-stats') }}</h3>
<div class="vote-stats">
<div v-for="(count, label) in votesStats.stats" :key="label" class="vote-stats-item">
<span class="vote-stats-label">{{ label }}</span>
<span class="vote-stats-count">{{ count }}</span>
</div>
</div>
</template>

<h3 class="vote-section-title">{{ $t('montage-vote-actions') }}</h3>
<div class="vote-actions">
<div>
Expand All @@ -85,7 +95,7 @@
</cdx-button>
</div>
<div>
<cdx-button weight="quiet" @click="setRate()">
<cdx-button weight="quiet" @click="setRate()" :disabled="isLoading">
<arrow-right class="icon-small" /> {{ $t('montage-vote-skip') }}
</cdx-button>
<cdx-button weight="quiet" @click="goPrevVoteEditing">
Expand Down Expand Up @@ -166,7 +176,7 @@
</template>

<script setup>
import { ref, watch, computed } from 'vue'
import { ref, watch, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import jurorService from '@/services/jurorService'
import { useRouter } from 'vue-router'
Expand Down Expand Up @@ -200,6 +210,7 @@ const voteContainer = ref(null)
const showSidebar = ref(true)
const imageCache = new Map()
const isLoading = ref(false)
const votesStats = ref(null)

const props = defineProps({
round: Object,
Expand Down Expand Up @@ -279,6 +290,10 @@ function setRate(rate) {
if (stats.value.total_open_tasks <= 10) {
skips.value = 0
}
// Refresh the vote stats after each successful vote
if (props.round.show_stats) {
jurorService.getRoundVotesStats(props.round.id).then(r => { votesStats.value = r.data })
}
if (counter.value === 4 || !stats.value.total_open_tasks) {
counter.value = 0
getTasks()
Expand Down Expand Up @@ -417,6 +432,14 @@ watch(voteContainer, () => {
voteContainer.value.focus()
}
})

onMounted(() => {
if (props.round.show_stats) {
jurorService.getRoundVotesStats(props.round.id).then((r) => {
votesStats.value = r.data
})
}
})
</script>

<style scoped>
Expand Down Expand Up @@ -605,4 +628,30 @@ watch(voteContainer, () => {
margin-top: 24px;
width: 232px;
}
.vote-stats {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}

.vote-stats-item {
display: flex;
align-items: center;
gap: 6px;
background: #f0f0f0;
border-radius: 4px;
padding: 4px 10px;
font-size: 13px;
}

.vote-stats-label {
font-weight: 500;
color: rgba(0,0,0,0.6);
}

.vote-stats-count {
font-weight: 700;
color: rgba(0,0,0,0.87);
}
</style>
31 changes: 27 additions & 4 deletions frontend/src/components/Vote/VoteYesNo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@
<div class="vote-controls">
<clip-loader v-if="isLoading" color="#36D7B7" size="20px" />
<div v-else class="vote-controls-button">
<cdx-button action="progressive" weight="quiet" @click="setRate(5)">
<cdx-button action="progressive" weight="quiet" @click="setRate(5)" :disabled="isLoading">
<thumb-up class="icon-small" /> {{ $t('montage-vote-accept') }}
</cdx-button>
<cdx-button action="destructive" weight="quiet" @click="setRate(1)">
<cdx-button action="destructive" weight="quiet" @click="setRate(1)" :disabled="isLoading">
<thumb-down class="icon-small" /> {{ $t('montage-vote-decline') }}
</cdx-button>
</div>
Expand All @@ -71,6 +71,16 @@
</span>
</div>

<template v-if="round.show_stats && votesStats">
<h3 class="vote-section-title">{{ $t('montage-vote-my-stats') }}</h3>
<div class="vote-stats">
<div v-for="(count, label) in votesStats.stats" :key="label" class="vote-stats-item">
<span class="vote-stats-label">{{ label }}</span>
<span class="vote-stats-count">{{ count }}</span>
</div>
</div>
</template>

<h3 class="vote-section-title">{{ $t('montage-vote-actions') }}</h3>
<div class="vote-actions">
<div>
Expand All @@ -86,7 +96,7 @@
</cdx-button>
</div>
<div>
<cdx-button weight="quiet" @click="setRate()">
<cdx-button weight="quiet" @click="setRate()" :disabled="isLoading">
<arrow-right class="icon-small" /> {{ $t('montage-vote-skip') }}
</cdx-button>
<cdx-button weight="quiet" @click="goPrevVoteEditing">
Expand Down Expand Up @@ -170,7 +180,7 @@
</template>

<script setup>
import { ref, watch, computed } from 'vue'
import { ref, watch, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import jurorService from '@/services/jurorService'
import { useRouter } from 'vue-router'
Expand Down Expand Up @@ -215,6 +225,7 @@ const roundLink = [props.round.id, props.round.canonical_url_name].join('-')
const isLoading = ref(false)
const images = ref(null)
const stats = ref(null)
const votesStats = ref(null)

const rating = ref({
current: null,
Expand Down Expand Up @@ -284,6 +295,10 @@ function setRate(rate) {
if (stats.value.total_open_tasks <= 10) {
skips.value = 0
}
// Refresh vote stats after each successful vote
if (props.round.show_stats) {
jurorService.getRoundVotesStats(props.round.id).then(r => { votesStats.value = r.data })
}
if (counter.value === 4 || !stats.value.total_open_tasks) {
counter.value = 0
getTasks()
Expand Down Expand Up @@ -424,6 +439,14 @@ watch(voteContainer, () => {
voteContainer.value.focus()
}
})

onMounted(() => {
if (props.round.show_stats) {
jurorService.getRoundVotesStats(props.round.id).then((r) => {
votesStats.value = r.data
})
}
})
</script>

<style scoped>
Expand Down
Loading