712 lines
18 KiB
Vue
712 lines
18 KiB
Vue
<template>
|
|
<div class="result-page">
|
|
<div class="result-header">
|
|
<div>
|
|
<div class="result-kicker">Keputusan Undian</div>
|
|
<h4 class="result-title">{{ String(position_id) === '0' ? 'Dashboard Keputusan' :
|
|
(getPosition(position_id) || 'Keputusan') }}</h4>
|
|
<div class="result-subtitle">Kemaskini terakhir: <b>{{ last_update }}</b></div>
|
|
</div>
|
|
<div class="result-actions">
|
|
<button class="btn btn-info" @click="refreshNominees()">
|
|
<i class="fa fa-refresh"></i> Kemaskini Keputusan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="String(position_id) === '0'" class="result-stats">
|
|
<div class="result-stat">
|
|
<div class="result-stat__label">Jumlah Undi</div>
|
|
<div class="result-stat__value">{{ totalVotes }}</div>
|
|
</div>
|
|
<div class="result-stat">
|
|
<div class="result-stat__label">Bil. Jawatan</div>
|
|
<div class="result-stat__value">{{ (data.positions || []).length }}</div>
|
|
</div>
|
|
<div class="result-stat">
|
|
<div class="result-stat__label">Calon Tertinggi</div>
|
|
<div class="result-stat__value result-stat__value--text">{{ topNomineeLabel }}</div>
|
|
<div class="result-stat__hint">{{ topNomineeVotes }} undi</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="result-filters">
|
|
<span class="result-filters__label">Jawatan:</span>
|
|
<button type="button" class="result-chip" :class="{ 'is-active': String(position_id) === '0' }"
|
|
@click="setPosition(0)">
|
|
Semua
|
|
</button>
|
|
<button v-for="position in data.positions" :key="position.id" type="button" class="result-chip"
|
|
:class="{ 'is-active': String(position.id) === String(position_id) }" @click="setPosition(position.id)">
|
|
{{ position.name }}
|
|
</button>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-7">
|
|
<div class="panel panel-default result-card">
|
|
<div class="panel-heading result-card__heading">
|
|
<div class="result-card__title">
|
|
{{ String(position_id) === '0' ? 'Pemimpin Mengikut Jawatan' : 'Kedudukan Calon' }}
|
|
</div>
|
|
<div class="result-card__meta">
|
|
{{
|
|
String(position_id) === '0' ? 'Siapa mendahului (dengan beza undi)' :
|
|
'Mengikut jawatan dipilih'
|
|
}}
|
|
</div>
|
|
</div>
|
|
<div class="panel-body result-card__body">
|
|
<template v-if="String(position_id) === '0'">
|
|
<div v-if="!leadersByPosition.length" class="result-empty">
|
|
Tiada data untuk dipaparkan.
|
|
</div>
|
|
<div v-else class="result-leaders">
|
|
<div v-for="p in leadersByPosition" :key="p.position_id" class="result-leader">
|
|
<div class="result-leader__top">
|
|
<div class="result-leader__position">{{ p.position_name }}</div>
|
|
<div class="result-leader__meta">
|
|
<span><b>{{ p.winner_votes }}</b> undi</span>
|
|
<span class="result-leader__delta">(+{{ p.delta }} beza)</span>
|
|
</div>
|
|
</div>
|
|
<div class="result-leader__name">{{ p.winner_name }}</div>
|
|
<div class="result-leader__bar">
|
|
<div class="result-leader__bar-fill" :style="{ width: p.pct + '%' }"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div v-if="!rankedNominees.length" class="result-empty">
|
|
Tiada calon untuk jawatan ini.
|
|
</div>
|
|
<div v-else class="result-rank">
|
|
<div v-for="n in rankedNominees" :key="n.id" class="result-rank__row">
|
|
<div class="result-rank__top">
|
|
<div class="result-rank__name">{{ n.name }}</div>
|
|
<div class="result-rank__votes">
|
|
<b>{{ n.votes }}</b> undi
|
|
</div>
|
|
</div>
|
|
<div class="result-rank__bar">
|
|
<div class="result-rank__bar-fill" :style="{ width: n.pct + '%' }"></div>
|
|
</div>
|
|
<div class="result-rank__meta">{{ n.pct }}%</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-5">
|
|
<div class="panel panel-default result-card">
|
|
<div class="panel-heading result-card__heading">
|
|
<div class="result-card__title">
|
|
{{ String(position_id) === '0' ? 'Analitik Ringkas' : 'Ringkasan Jawatan' }}
|
|
</div>
|
|
<div class="result-card__meta">
|
|
{{
|
|
String(position_id) === '0' ? 'Top calon & jumlah undi mengikut jawatan' : ('Jumlah undi:' +
|
|
totalVotes) }}
|
|
</div>
|
|
</div>
|
|
<div class="panel-body result-card__body">
|
|
<template v-if="String(position_id) === '0'">
|
|
<div class="result-mini-title">Top Calon (Overall)</div>
|
|
<div id="top-chart" class="result-top-chart"></div>
|
|
<div v-if="!topNominees.length" class="result-empty">Tiada data untuk dipaparkan.</div>
|
|
|
|
<div class="result-mini-title" style="margin-top: 14px;">Jumlah Undi Mengikut Jawatan</div>
|
|
<div id="votes-by-position-chart" class="result-top-chart"></div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="result-empty">
|
|
Pilih “Semua” untuk melihat analitik keseluruhan. Untuk jawatan ini, rujuk senarai di
|
|
kiri.
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
|
|
created: function () {
|
|
this.refreshNominees();
|
|
this.$nextTick(function () {
|
|
this.initCharts();
|
|
});
|
|
},
|
|
|
|
watch: {
|
|
position_id: function () {
|
|
var vm = this;
|
|
this.$nextTick(function () {
|
|
vm.initCharts();
|
|
});
|
|
}
|
|
},
|
|
|
|
methods: {
|
|
setPosition: function (id) {
|
|
// Prevent Vue Router NavigationDuplicated when clicking active chip
|
|
var target = (!id || String(id) === '0') ? '0' : String(id);
|
|
if (String(this.position_id) === target) return;
|
|
|
|
var q = Object.assign({}, this.$route.query);
|
|
if (!id || String(id) === '0') delete q.position_id;
|
|
else q.position_id = String(id);
|
|
this.$router.replace({ query: q }).catch(function () { });
|
|
},
|
|
|
|
refreshNominees: function () {
|
|
var vm = this;
|
|
this.util.notify('Refreshing results', 'loading');
|
|
// Always fetch all nominees (needed for overall dashboards and name lookups)
|
|
axios.get(config.API + 'nominee')
|
|
.then(response => {
|
|
$.notifyClose();
|
|
vm.data.nominees = response.data;
|
|
vm.refreshResults();
|
|
})
|
|
.catch(error => {
|
|
$.notifyClose();
|
|
vm.util.showResult(error);
|
|
})
|
|
},
|
|
|
|
refreshResults: function () {
|
|
var vm = this;
|
|
this.util.notify('Refreshing results', 'loading');
|
|
axios.get(config.API + 'election/results')
|
|
.then(response => {
|
|
$.notifyClose();
|
|
vm.data.results = response.data;
|
|
vm.data.last_update = new Date();
|
|
vm.$nextTick(function () {
|
|
vm.initCharts();
|
|
});
|
|
})
|
|
.catch(error => {
|
|
$.notifyClose();
|
|
vm.util.showResult(error);
|
|
})
|
|
},
|
|
|
|
initCharts: function () {
|
|
// Clear any previously rendered plots to avoid overlap when switching views
|
|
if ($('#top-chart').length) $('#top-chart').empty();
|
|
if ($('#votes-by-position-chart').length) $('#votes-by-position-chart').empty();
|
|
|
|
// All positions summary
|
|
if (String(this.position_id) === '0') {
|
|
if ($('#top-chart').length) {
|
|
var ticks = this.topNominees.map(function (n, i) { return [i, n.label]; });
|
|
var series = [{
|
|
label: 'Undi',
|
|
data: this.topNominees.map(function (n, i) { return [i, n.votes]; }),
|
|
bars: { show: true, barWidth: 0.6, align: 'center', fill: 0.85, lineWidth: 0 }
|
|
}];
|
|
$.plot($('#top-chart'), series, {
|
|
xaxis: { ticks: ticks, rotateTicks: 45 },
|
|
yaxis: { min: 0, tickDecimals: 0 },
|
|
grid: { hoverable: true, borderColor: '#e5e7eb' }
|
|
});
|
|
}
|
|
|
|
if ($('#votes-by-position-chart').length) {
|
|
var pt = this.positionTotals;
|
|
var ptTicks = pt.map(function (p, i) { return [i, p.label]; });
|
|
var ptSeries = [{
|
|
label: 'Undi',
|
|
data: pt.map(function (p, i) { return [i, p.votes]; }),
|
|
bars: { show: true, barWidth: 0.6, align: 'center', fill: 0.55, lineWidth: 0 }
|
|
}];
|
|
$.plot($('#votes-by-position-chart'), ptSeries, {
|
|
xaxis: { ticks: ptTicks, rotateTicks: 45 },
|
|
yaxis: { min: 0, tickDecimals: 0 },
|
|
grid: { hoverable: true, borderColor: '#e5e7eb' }
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
// For per-position view, we intentionally do not render charts (UI request).
|
|
},
|
|
|
|
getPosition: function (id) {
|
|
let positions = this.data.positions;
|
|
for (var i in positions)
|
|
if (id == positions[i]['id']) return positions[i]['name'];
|
|
return '';
|
|
},
|
|
|
|
getNominee: function (id) {
|
|
let nominees = this.data.nominees;
|
|
for (var i in nominees)
|
|
if (id == nominees[i]['id']) return nominees[i]['name'];
|
|
return '';
|
|
},
|
|
|
|
getVotes: function (id) {
|
|
let results = this.data.results;
|
|
for (var i in results) {
|
|
if (results[i]['nominee_id'] == id) return results[i]['votes'];
|
|
}
|
|
return 0;
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
last_update: function () {
|
|
let x = this.data.last_update;
|
|
if (!x) return '-';
|
|
return x.toDateString() + ' ' + x.toLocaleTimeString();
|
|
},
|
|
|
|
position_id: function () {
|
|
return this.$route.query.position_id ? this.$route.query.position_id : 0;
|
|
},
|
|
|
|
datas: function () {
|
|
//var results =
|
|
var data = [];
|
|
var nominees = this.data.nominees;
|
|
for (var i in nominees) {
|
|
if (nominees[i]['position_id'] == this.position_id) {
|
|
let row = [];
|
|
row['label'] = nominees[i]['name'];
|
|
row['data'] = [[1, this.getVotes(nominees[i]['id'])]];
|
|
data.push(row);
|
|
}
|
|
}
|
|
return data;
|
|
},
|
|
|
|
positionTotals: function () {
|
|
// Total votes per position (for "Semua" dashboard)
|
|
var results = this.data.results || [];
|
|
var totals = {};
|
|
for (var i = 0; i < results.length; i++) {
|
|
var pid = results[i].position_id;
|
|
var v = Number(results[i].votes || 0);
|
|
totals[pid] = (totals[pid] || 0) + v;
|
|
}
|
|
var positions = this.data.positions || [];
|
|
var rows = positions.map(function (p) {
|
|
return { position_id: p.id, label: p.name, votes: totals[p.id] || 0 };
|
|
});
|
|
rows.sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
|
return rows;
|
|
},
|
|
|
|
topNominees: function () {
|
|
// Highest vote nominees across all positions (sum by nominee_id)
|
|
var results = this.data.results || [];
|
|
var sum = {};
|
|
for (var i = 0; i < results.length; i++) {
|
|
var nid = results[i].nominee_id;
|
|
var v = Number(results[i].votes || 0);
|
|
sum[nid] = (sum[nid] || 0) + v;
|
|
}
|
|
var nominees = this.data.nominees || [];
|
|
var nameById = {};
|
|
for (var j = 0; j < nominees.length; j++) {
|
|
nameById[nominees[j].id] = nominees[j].name;
|
|
}
|
|
var rows = Object.keys(sum).map(function (nid) {
|
|
return { id: Number(nid), label: nameById[nid] || ('Nominee #' + nid), votes: sum[nid] };
|
|
});
|
|
rows.sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
|
return rows.slice(0, 8);
|
|
},
|
|
|
|
topNomineeLabel: function () {
|
|
return this.topNominees && this.topNominees[0] ? this.topNominees[0].label : '-';
|
|
},
|
|
|
|
topNomineeVotes: function () {
|
|
return this.topNominees && this.topNominees[0] ? (this.topNominees[0].votes || 0) : 0;
|
|
},
|
|
|
|
leadersByPosition: function () {
|
|
// For each position: winner + runner-up + margin
|
|
var positions = this.data.positions || [];
|
|
var nominees = this.data.nominees || [];
|
|
var results = this.data.results || [];
|
|
|
|
var posName = {};
|
|
positions.forEach(function (p) { posName[p.id] = p.name; });
|
|
|
|
var nomineeName = {};
|
|
nominees.forEach(function (n) { nomineeName[n.id] = n.name; });
|
|
|
|
var byPos = {};
|
|
for (var i = 0; i < results.length; i++) {
|
|
var r = results[i];
|
|
var pid = r.position_id;
|
|
if (!byPos[pid]) byPos[pid] = [];
|
|
byPos[pid].push({ nominee_id: r.nominee_id, votes: Number(r.votes || 0) });
|
|
}
|
|
|
|
var rows = [];
|
|
for (var j = 0; j < positions.length; j++) {
|
|
var pid2 = positions[j].id;
|
|
var arr = (byPos[pid2] || []).slice().sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
|
var w = arr[0] || { nominee_id: null, votes: 0 };
|
|
var r2 = arr[1] || { nominee_id: null, votes: 0 };
|
|
var total = arr.reduce(function (s, x) { return s + (x.votes || 0); }, 0);
|
|
var pct = total > 0 ? Math.round((w.votes / total) * 100) : 0;
|
|
rows.push({
|
|
position_id: pid2,
|
|
position_name: posName[pid2] || ('Jawatan #' + pid2),
|
|
winner_id: w.nominee_id,
|
|
winner_name: nomineeName[w.nominee_id] || '-',
|
|
winner_votes: w.votes || 0,
|
|
runnerup_id: r2.nominee_id,
|
|
runnerup_name: nomineeName[r2.nominee_id] || '-',
|
|
runnerup_votes: r2.votes || 0,
|
|
delta: Math.max(0, (w.votes || 0) - (r2.votes || 0)),
|
|
pct: pct
|
|
});
|
|
}
|
|
// Sort: show positions with most votes first
|
|
rows.sort(function (a, b) { return (b.winner_votes || 0) - (a.winner_votes || 0); });
|
|
return rows;
|
|
},
|
|
|
|
rankedNominees: function () {
|
|
var nominees = this.data.nominees || [];
|
|
var rows = [];
|
|
for (var i = 0; i < nominees.length; i++) {
|
|
if (String(nominees[i].position_id) !== String(this.position_id)) continue;
|
|
var votes = this.getVotes(nominees[i].id);
|
|
rows.push({
|
|
id: nominees[i].id,
|
|
name: nominees[i].name,
|
|
votes: votes
|
|
});
|
|
}
|
|
rows.sort(function (a, b) { return (b.votes || 0) - (a.votes || 0); });
|
|
var total = rows.reduce(function (s, r) { return s + (r.votes || 0); }, 0);
|
|
return rows.map(function (r) {
|
|
var pct = total > 0 ? Math.round((r.votes / total) * 100) : 0;
|
|
return Object.assign({}, r, { pct: pct });
|
|
});
|
|
},
|
|
|
|
totalVotes: function () {
|
|
// In "All" view use all results; otherwise use per-position ranking
|
|
if (String(this.position_id) === '0') {
|
|
var results = this.data.results || [];
|
|
return results.reduce(function (s, r) { return s + Number(r.votes || 0); }, 0);
|
|
}
|
|
var rows = this.rankedNominees;
|
|
return rows.reduce(function (s, r) { return s + (r.votes || 0); }, 0);
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.result-page {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.result-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.result-kicker {
|
|
color: #2f80ed;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.result-title {
|
|
margin: 0 0 6px;
|
|
font-weight: 800;
|
|
color: #17324d;
|
|
}
|
|
|
|
.result-subtitle {
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.result-actions {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.result-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 10px;
|
|
margin: 10px 0 14px;
|
|
}
|
|
|
|
.result-stat {
|
|
border: 1px solid #e6edf5;
|
|
border-radius: 16px;
|
|
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
|
padding: 12px 14px;
|
|
box-shadow: 0 10px 22px rgba(35, 64, 97, 0.06);
|
|
}
|
|
|
|
.result-stat__label {
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.result-stat__value {
|
|
font-size: 22px;
|
|
font-weight: 900;
|
|
color: #17324d;
|
|
}
|
|
|
|
.result-stat__value--text {
|
|
font-size: 14px;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.result-stat__hint {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.result-filters {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin: 10px 0 16px;
|
|
}
|
|
|
|
.result-filters__label {
|
|
color: #6b7280;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
margin-right: 4px;
|
|
}
|
|
|
|
.result-chip {
|
|
border: 1px solid #d1d5db;
|
|
background: #fff;
|
|
color: #374151;
|
|
padding: 6px 12px;
|
|
border-radius: 999px;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
line-height: 1;
|
|
}
|
|
|
|
.result-chip:hover {
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
.result-chip.is-active {
|
|
background: #1976d2;
|
|
border-color: #1976d2;
|
|
color: #fff;
|
|
}
|
|
|
|
.result-card {
|
|
border: 0;
|
|
border-radius: 16px;
|
|
overflow: hidden;
|
|
box-shadow: 0 12px 28px rgba(35, 64, 97, 0.08);
|
|
}
|
|
|
|
.result-card__heading {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
background: linear-gradient(135deg, #1d4e89, #2f80ed);
|
|
color: #fff;
|
|
border: 0 !important;
|
|
}
|
|
|
|
.result-card__title {
|
|
font-weight: 800;
|
|
}
|
|
|
|
.result-card__meta {
|
|
opacity: 0.95;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.result-card__body {
|
|
background: #fff;
|
|
}
|
|
|
|
.result-top-chart {
|
|
width: 100%;
|
|
height: 280px;
|
|
}
|
|
|
|
.result-mini-title {
|
|
font-size: 12px;
|
|
font-weight: 900;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
color: #17324d;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.result-empty {
|
|
margin-top: 10px;
|
|
text-align: center;
|
|
color: #6b7280;
|
|
font-style: italic;
|
|
}
|
|
|
|
.result-leaders {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.result-leader {
|
|
padding: 12px 12px;
|
|
border: 1px solid #eef2f7;
|
|
border-radius: 14px;
|
|
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
|
}
|
|
|
|
.result-leader__top {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.result-leader__position {
|
|
font-weight: 900;
|
|
color: #17324d;
|
|
}
|
|
|
|
.result-leader__meta {
|
|
display: flex;
|
|
gap: 10px;
|
|
font-size: 12px;
|
|
color: #17324d;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.result-leader__delta {
|
|
color: #2f80ed;
|
|
}
|
|
|
|
.result-leader__name {
|
|
font-weight: 800;
|
|
color: #17324d;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.result-leader__bar {
|
|
height: 10px;
|
|
border-radius: 999px;
|
|
background: #e8f0fb;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.result-leader__bar-fill {
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
background: linear-gradient(90deg, #2f80ed, #1d4e89);
|
|
}
|
|
|
|
.result-leader__sub {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-top: 8px;
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.result-rank {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.result-rank__row {
|
|
padding: 12px 12px;
|
|
border: 1px solid #eef2f7;
|
|
border-radius: 14px;
|
|
background: linear-gradient(180deg, #ffffff 0%, #f7fbff 100%);
|
|
}
|
|
|
|
.result-rank__top {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 10px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.result-rank__name {
|
|
font-weight: 800;
|
|
color: #17324d;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.result-rank__votes {
|
|
color: #17324d;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.result-rank__bar {
|
|
height: 10px;
|
|
border-radius: 999px;
|
|
background: #e8f0fb;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.result-rank__bar-fill {
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
background: linear-gradient(90deg, #2f80ed, #1d4e89);
|
|
}
|
|
|
|
.result-rank__meta {
|
|
margin-top: 6px;
|
|
font-size: 12px;
|
|
color: #6b7280;
|
|
text-align: right;
|
|
}
|
|
|
|
@media (max-width: 991px) {
|
|
.result-header {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.result-stats {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style> |