Skip to content
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
98 changes: 98 additions & 0 deletions src/components/Portal/Stats/StatsAlert.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<template>
<div class="stats_alert" :class="`stats_alert--${severity}`" role="status">
<img class="stats_alert__icon" :src="iconSrc" alt="" width="20" height="20" />
<div class="stats_alert__body">
<slot>{{ text }}</slot>
</div>
</div>
</template>

<script>
/**
* Light MUI-ish alert for Stats empty / unavailable / note states.
* Tinted background + icon + short text; uses overlay / toast tokens.
*
* @module StatsAlert
*/
import infoIcon from "@/assets/img/general/portal/info.svg";
import warnIcon from "@/assets/img/general/toast/connection-warn.svg";
import errorIcon from "@/assets/img/general/toast/connection-error.svg";

const ICONS = {
info: infoIcon,
warning: warnIcon,
error: errorIcon,
};

export default {
name: "StatsAlert",
props: {
/** Visual tone: info | warning | error */
severity: {
type: String,
default: "info",
validator: (v) => ["info", "warning", "error"].includes(v),
},
/** Plain text when no slot content is provided */
text: {
type: String,
default: "",
},
},
computed: {
iconSrc() {
return ICONS[this.severity] || ICONS.info;
},
},
};
</script>

<style scoped>
.stats_alert {
display: flex;
flex-flow: row nowrap;
align-items: flex-start;
gap: 10px;
width: 100%;
box-sizing: border-box;
padding: calc(var(--padding-overlay-input) * 0.75) var(--padding-overlay-input);
border-radius: var(--radius-overlay-input);
font-size: 0.95em;
line-height: 1.35;
color: var(--color-on-overlay-input);
background: var(--color-overlay-input);
}
.stats_alert__icon {
flex: 0 0 auto;
width: 20px;
height: 20px;
margin-top: 1px;
object-fit: contain;
filter: var(--filter-icon);
}
.stats_alert--warning .stats_alert__icon,
.stats_alert--error .stats_alert__icon {
/* toast icons are already colored discs; don't invert them */
filter: none;
}
.stats_alert__body {
flex: 1 1 auto;
min-width: 0;
}
.stats_alert--info {
background: var(--color-overlay-link-hover);
color: var(--color-text);
}
.stats_alert--warning {
background: var(--toast-bg);
color: var(--toast-text);
}
.stats_alert--error {
background: var(--color-overlay-link-remove-hover);
color: var(--color-on-overlay-link-remove-hover);
}
.stats_alert :deep(a) {
color: inherit;
text-decoration: underline;
}
</style>
86 changes: 86 additions & 0 deletions src/components/Portal/Stats/StatsProgressDots.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<template>
<div class="progress_display" v-if="total > 1">
<span
class="progress_display__dot_container"
v-for="i in total"
:key="i"
@click="$emit('open', i - 1)"
>
<span
class="progress_display__dot"
:class="{ dot_active: i == current, dot_finished: i < current }"
></span>
</span>
</div>
</template>

<script>
/**
* Survey-style progress dots for Stats modals.
* Markup/classes/tokens match Modal.vue (`progress_display` / `__dot` / `dot_*`).
*
* @module StatsProgressDots
*/
export default {
name: "StatsProgressDots",
props: {
/** 1-based current page index (same as Modal progress.current) */
current: {
type: Number,
required: true,
},
/** Total page count */
total: {
type: Number,
required: true,
},
},
emits: ["open"],
};
</script>

<style scoped>
.progress_display {
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: center;
margin-left: 0;
user-select: none;
height: var(--size-modal-progress, auto);
}
.progress_display span {
display: block;
}
.progress_display__dot_container {
padding: var(--margin-modal-progress-dot);
cursor: pointer;
}
.progress_display__dot {
width: var(--size-modal-progress-dot);
height: var(--size-modal-progress-dot);
border-radius: 50%;
background-color: var(--color-modal-progress-dot);
}
.progress_display__dot.dot_active {
background-color: var(--color-modal-progress-dot-active);
}
.progress_display__dot.dot_finished {
transform: scale(0.75);
opacity: 0.6;
}
</style>

<!-- Unscoped: beat global .bottom_actions > * button chrome (same layout as Modal.vue). -->
<style>
.bottom_actions > .progress_display {
padding: 0;
height: auto;
min-height: var(--height-overlay-action);
border-radius: 0;
background: transparent;
color: inherit;
cursor: default;
font-size: inherit;
}
</style>
127 changes: 88 additions & 39 deletions src/views/Portal/StatsModal.vue
Original file line number Diff line number Diff line change
@@ -1,56 +1,68 @@
<template>
<main class="entercode">
<header class="modal_header">
<h2 class="header_style modal_header_title">View Statistics</h2>
<h2 class="header_style modal_header_title">{{ pageTitle }}</h2>
</header>
<div class="overlay_contents" ref="contents">
<div class="overlay_contents_text">
Your daily workload and check-ins are shown below. Days without a survey still appear when
task counts are available.
</div>
<br />
<StatsFilterBar
:filters="filters"
:active="active"
:is-ready="is_ready"
:toolbar="toolbar"
@update:active="active = $event"
@update:toolbar="toolbar = $event"
/>
<StatsChartCard
:rows="surveys"
:graphs="graphs"
:toolbar="toolbar"
:is-ready="is_ready"
/>
<br />
<div class="overlay_contents_text">
We appreciate your contributions towards our research. If you have any questions, please
<router-link to="/contact">contact us</router-link>!
</div>
<!-- Intro -->
<template v-if="page === 0">
<div class="overlay_contents_text">Your recent task counts and check-ins.</div>
<br />
<StatsAlert severity="info">
Days without a survey still show when task counts are available.
</StatsAlert>
</template>

<!-- Chart -->
<template v-else-if="page === 1">
<StatsFilterBar
:filters="filters"
:active="active"
:is-ready="is_ready"
:toolbar="toolbar"
@update:active="active = $event"
@update:toolbar="toolbar = $event"
/>
<StatsAlert v-if="load_error" severity="error" text="Couldn't load your stats. Try again later." />
<StatsAlert
v-else-if="is_ready && !surveys.length"
severity="warning"
text="No stats to show yet."
/>
<StatsChartCard
v-else
:rows="surveys"
:graphs="graphs"
:toolbar="toolbar"
:is-ready="is_ready"
/>
</template>

<!-- Contact -->
<template v-else>
<StatsAlert severity="info">
Questions?
<router-link to="/contact">Contact us</router-link>.
</StatsAlert>
</template>
</div>
<div class="bottom_actions">
<button
class="close_action click_escape"
@click="
$router.push({
name: 'settings',
query: $route.query,
})
"
>
Back
</button>
<button class="close_action click_escape" @click="onBack">Back</button>
<StatsProgressDots :current="page + 1" :total="pageCount" @open="goTo" />
<div class="flex_spacer"></div>
<button class="continue_action click_ctrlenter" @click="$emit('close')">Close</button>
<button class="continue_action click_ctrlenter" @click="onContinue">
{{ page < pageCount - 1 ? "Next" : "Close" }}
</button>
</div>
</main>
</template>

<script>
import { ErrorToast, WarningToast } from "@svonk/util";
import StatsAlert from "@/components/Portal/Stats/StatsAlert.vue";
import StatsChartCard from "@/components/Portal/Stats/StatsChartCard.vue";
import StatsFilterBar from "@/components/Portal/Stats/StatsFilterBar.vue";
import StatsProgressDots from "@/components/Portal/Stats/StatsProgressDots.vue";
import {
PERSONAL_STAT_FILTERS,
buildGraphs,
Expand All @@ -60,13 +72,16 @@ import {

export default {
name: "StatsModal",
components: { StatsChartCard, StatsFilterBar },
components: { StatsAlert, StatsChartCard, StatsFilterBar, StatsProgressDots },
emits: ["close"],
data() {
return {
page: 0,
pageCount: 3,
is_ready: false,
can_update: true,
toolbar: false,
load_error: false,
min_delay: 1000 * 15,
surveys: [],
active: ["mood", "stress", "upcoming"],
Expand All @@ -79,8 +94,34 @@ export default {
graphs() {
return buildGraphs(this.surveys, this.filters, this.active);
},
pageTitle() {
if (this.page === 0) return "View Statistics";
if (this.page === 1) return "Your stats";
return "Help";
},
},
methods: {
goTo(index) {
if (index < 0 || index >= this.pageCount) return;
this.page = index;
},
onBack() {
if (this.page > 0) {
this.page -= 1;
return;
}
this.$router.push({
name: "settings",
query: this.$route.query,
});
},
onContinue() {
if (this.page < this.pageCount - 1) {
this.page += 1;
return;
}
this.$emit("close");
},
try_update() {
if (this.can_update) {
this.update(true);
Expand All @@ -91,6 +132,7 @@ export default {
}
},
process(data) {
this.load_error = false;
this.surveys = sortStatRows((data || []).filter((survey) => !survey.error));
try {
this.surveys.forEach((survey, index) => {
Expand All @@ -105,19 +147,20 @@ export default {
update(force = false) {
this.is_ready = false;
this.can_update = false;
this.load_error = false;
this.last_update = Date.now();
this.surveys = [];
this.$store
.get_stats(undefined, force)
.then((data) => {
this.process(data);
// set a timeout to allow the user to update again
setTimeout(() => {
this.can_update = true;
}, this.min_delay);
})
.catch((err) => {
new ErrorToast("Failed to get statistics", err, 5000);
this.load_error = true;
this.is_ready = true;
this.can_update = true;
});
Expand All @@ -128,3 +171,9 @@ export default {
},
};
</script>

<style scoped>
.overlay_contents > .stats_alert {
margin-bottom: calc(var(--padding-overlay-input) / 2);
}
</style>
Loading
Loading