Files
zen-kanji/client/src/components/dashboard/WidgetHeatmap.vue
T
Crylia 049eaa1807
Release Build / build-docker (push) Successful in 1m9s
Release Build / build-android-and-release (push) Successful in 7m14s
feat: add sound effects, improved stroke recognition, instant locale switching with scramble text animation
- Add SoundManager with Web Audio API synthesized sounds (stroke error, drawing complete, session complete)
- Add sound effects toggle in settings (persisted via localStorage)
- Improve stroke recognition: curvature similarity, max point deviation, stroke length ratio checks
- Loosen thresholds for long strokes and first-stroke positioning
- Implement instant locale switching (no save required)
- Add ScrambleText component with character-by-character morphing animation
- Apply ScrambleText globally across all views, widgets, and dialogs
- Add i18n keys for sound effects settings (EN/DE/JA)
2026-04-18 22:19:36 +02:00

84 lines
2.5 KiB
Vue

<template lang="pug">
DashboardWidget(
:title="$t('stats.consistency')"
icon="mdi-calendar-check"
)
template(#header-right)
.legend-container
span.text-caption.text-medium-emphasis.mr-1
ScrambleText(:text="$t('stats.less')")
.legend-box.level-0
.legend-box.level-1
.legend-box.level-2
.legend-box.level-3
.legend-box.level-4
span.text-caption.text-medium-emphasis.ml-1
ScrambleText(:text="$t('stats.more')")
.heatmap-container
.heatmap-year
.heatmap-week(v-for="(week, wIdx) in weeks" :key="wIdx")
.heatmap-day-wrapper(v-for="(day, dIdx) in week" :key="dIdx")
v-tooltip(location="top" open-delay="100")
template(v-slot:activator="{ props }")
.heatmap-cell(
v-bind="props"
:class="[isToday(day.date) ? 'today-cell' : '', getHeatmapClass(day.count)]"
)
.text-center
.font-weight-bold {{ formatDate(day.date) }}
div
ScrambleText(:text="$t('stats.reviewsCount', { count: day.count })")
</template>
<script setup>
/* eslint-disable no-unused-vars */
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import DashboardWidget from './DashboardWidget.vue';
const { locale } = useI18n();
const props = defineProps({
heatmapData: { type: Object, default: () => ({}) },
});
const weeks = computed(() => {
const data = props.heatmapData || {};
const w = [];
const today = new Date();
const startDate = new Date(today);
startDate.setDate(today.getDate() - (52 * 7));
startDate.setDate(startDate.getDate() - startDate.getDay());
let currentWeek = [];
for (let i = 0; i < 371; i += 1) {
const d = new Date(startDate);
d.setDate(startDate.getDate() + i);
const dateStr = d.toISOString().split('T')[0];
currentWeek.push({
date: dateStr,
count: data[dateStr] || 0,
});
if (currentWeek.length === 7) {
w.push(currentWeek);
currentWeek = [];
}
}
return w;
});
const getHeatmapClass = (count) => {
if (count === 0) return 'level-0';
if (count <= 20) return 'level-1';
if (count <= 50) return 'level-2';
if (count <= 100) return 'level-3';
return 'level-4';
};
const formatDate = (dateStr) => new Date(dateStr).toLocaleDateString(locale.value, { month: 'short', day: 'numeric' });
const isToday = (dateStr) => dateStr === new Date().toISOString().split('T')[0];
</script>