mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-11-04 04:57:51 +00:00 
			
		
		
		
	fix: heatmap colors for dark mode [CW-3241] (#9278)
* feat: add new heatmap colors * fix: loader * fix: move new styles to tailwind * feat: update tw classes * refactor: update styles * feat: add useI18n composable * feat: use composition api * fix: empty div * chore: don't import defineProps Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										32
									
								
								app/javascript/dashboard/composables/useI18n.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								app/javascript/dashboard/composables/useI18n.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
import { computed, getCurrentInstance } from 'vue';
 | 
			
		||||
import Vue from 'vue';
 | 
			
		||||
import VueI18n from 'vue-i18n';
 | 
			
		||||
 | 
			
		||||
let i18nInstance = VueI18n;
 | 
			
		||||
 | 
			
		||||
export function useI18n() {
 | 
			
		||||
  if (!i18nInstance) throw new Error('vue-i18n not initialized');
 | 
			
		||||
 | 
			
		||||
  const i18n = i18nInstance;
 | 
			
		||||
 | 
			
		||||
  const instance = getCurrentInstance();
 | 
			
		||||
  const vm = instance?.proxy || instance || new Vue({});
 | 
			
		||||
 | 
			
		||||
  const locale = computed({
 | 
			
		||||
    get() {
 | 
			
		||||
      return i18n.locale;
 | 
			
		||||
    },
 | 
			
		||||
    set(v) {
 | 
			
		||||
      i18n.locale = v;
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    locale,
 | 
			
		||||
    t: vm.$t.bind(vm),
 | 
			
		||||
    tc: vm.$tc.bind(vm),
 | 
			
		||||
    d: vm.$d.bind(vm),
 | 
			
		||||
    te: vm.$te.bind(vm),
 | 
			
		||||
    n: vm.$n.bind(vm),
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -1,70 +1,16 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="heatmap-container">
 | 
			
		||||
    <template v-if="isLoading">
 | 
			
		||||
      <div class="heatmap-labels">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="ii in 7"
 | 
			
		||||
          :key="ii"
 | 
			
		||||
          class="loading-cell heatmap-axis-label"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="heatmap-grid">
 | 
			
		||||
        <div v-for="ii in 7" :key="ii" class="heatmap-grid-row">
 | 
			
		||||
          <div v-for="jj in 24" :key="jj" class="heatmap-tile loading-cell">
 | 
			
		||||
            <div class="heatmap-tile__label loading-cell" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="heatmap-timeline" />
 | 
			
		||||
      <div class="heatmap-markers">
 | 
			
		||||
        <div v-for="ii in 24" :key="ii">{{ ii - 1 }} – {{ ii }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-else>
 | 
			
		||||
      <div class="heatmap-labels">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="dateKey in processedData.keys()"
 | 
			
		||||
          :key="dateKey"
 | 
			
		||||
          class="heatmap-axis-label"
 | 
			
		||||
        >
 | 
			
		||||
          {{ getDayOfTheWeek(new Date(dateKey)) }}
 | 
			
		||||
          <time>{{ formatDate(dateKey) }}</time>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="heatmap-grid">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="dateKey in processedData.keys()"
 | 
			
		||||
          :key="dateKey"
 | 
			
		||||
          class="heatmap-grid-row"
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            v-for="data in processedData.get(dateKey)"
 | 
			
		||||
            :key="data.timestamp"
 | 
			
		||||
            v-tooltip.top="getCountTooltip(data.value)"
 | 
			
		||||
            class="heatmap-tile"
 | 
			
		||||
            :class="getHeatmapLevelClass(data.value)"
 | 
			
		||||
          >
 | 
			
		||||
            <div class="heatmap-tile__label" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="heatmap-timeline" />
 | 
			
		||||
      <div class="heatmap-markers">
 | 
			
		||||
        <div v-for="ii in 24" :key="ii">{{ ii - 1 }} – {{ ii }}</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
<script>
 | 
			
		||||
import { getQuantileIntervals } from '@chatwoot/utils';
 | 
			
		||||
<script setup>
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
import format from 'date-fns/format';
 | 
			
		||||
import getDay from 'date-fns/getDay';
 | 
			
		||||
 | 
			
		||||
import { groupHeatmapByDay } from 'helpers/ReportsDataHelper';
 | 
			
		||||
import { getQuantileIntervals } from '@chatwoot/utils';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  name: 'Heatmap',
 | 
			
		||||
  props: {
 | 
			
		||||
import { groupHeatmapByDay } from 'helpers/ReportsDataHelper';
 | 
			
		||||
import { useI18n } from 'dashboard/composables/useI18n';
 | 
			
		||||
 | 
			
		||||
const { t } = useI18n();
 | 
			
		||||
const props = defineProps({
 | 
			
		||||
  heatData: {
 | 
			
		||||
    type: Array,
 | 
			
		||||
    default: () => [],
 | 
			
		||||
@@ -73,236 +19,155 @@ export default {
 | 
			
		||||
    type: Boolean,
 | 
			
		||||
    default: false,
 | 
			
		||||
  },
 | 
			
		||||
  },
 | 
			
		||||
  computed: {
 | 
			
		||||
    processedData() {
 | 
			
		||||
      return groupHeatmapByDay(this.heatData);
 | 
			
		||||
    },
 | 
			
		||||
    quantileRange() {
 | 
			
		||||
      const flattendedData = this.heatData.map(data => data.value);
 | 
			
		||||
      return getQuantileIntervals(
 | 
			
		||||
        flattendedData,
 | 
			
		||||
        [0.2, 0.4, 0.6, 0.8, 0.9, 0.99]
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  methods: {
 | 
			
		||||
    getCountTooltip(value) {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const processedData = computed(() => {
 | 
			
		||||
  return groupHeatmapByDay(props.heatData);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const quantileRange = computed(() => {
 | 
			
		||||
  const flattendedData = props.heatData.map(data => data.value);
 | 
			
		||||
  return getQuantileIntervals(flattendedData, [0.2, 0.4, 0.6, 0.8, 0.9, 0.99]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function getCountTooltip(value) {
 | 
			
		||||
  if (!value) {
 | 
			
		||||
        return this.$t(
 | 
			
		||||
          'OVERVIEW_REPORTS.CONVERSATION_HEATMAP.NO_CONVERSATIONS'
 | 
			
		||||
        );
 | 
			
		||||
    return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.NO_CONVERSATIONS');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (value === 1) {
 | 
			
		||||
        return this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATION', {
 | 
			
		||||
    return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATION', {
 | 
			
		||||
      count: value,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      return this.$t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATIONS', {
 | 
			
		||||
  return t('OVERVIEW_REPORTS.CONVERSATION_HEATMAP.CONVERSATIONS', {
 | 
			
		||||
    count: value,
 | 
			
		||||
  });
 | 
			
		||||
    },
 | 
			
		||||
    formatDate(dateString) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function formatDate(dateString) {
 | 
			
		||||
  return format(new Date(dateString), 'MMM d, yyyy');
 | 
			
		||||
    },
 | 
			
		||||
    getDayOfTheWeek(date) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getDayOfTheWeek(date) {
 | 
			
		||||
  const dayIndex = getDay(date);
 | 
			
		||||
  const days = [
 | 
			
		||||
        this.$t('DAYS_OF_WEEK.SUNDAY'),
 | 
			
		||||
        this.$t('DAYS_OF_WEEK.MONDAY'),
 | 
			
		||||
        this.$t('DAYS_OF_WEEK.TUESDAY'),
 | 
			
		||||
        this.$t('DAYS_OF_WEEK.WEDNESDAY'),
 | 
			
		||||
        this.$t('DAYS_OF_WEEK.THURSDAY'),
 | 
			
		||||
        this.$t('DAYS_OF_WEEK.FRIDAY'),
 | 
			
		||||
        this.$t('DAYS_OF_WEEK.SATURDAY'),
 | 
			
		||||
    t('DAYS_OF_WEEK.SUNDAY'),
 | 
			
		||||
    t('DAYS_OF_WEEK.MONDAY'),
 | 
			
		||||
    t('DAYS_OF_WEEK.TUESDAY'),
 | 
			
		||||
    t('DAYS_OF_WEEK.WEDNESDAY'),
 | 
			
		||||
    t('DAYS_OF_WEEK.THURSDAY'),
 | 
			
		||||
    t('DAYS_OF_WEEK.FRIDAY'),
 | 
			
		||||
    t('DAYS_OF_WEEK.SATURDAY'),
 | 
			
		||||
  ];
 | 
			
		||||
  return days[dayIndex];
 | 
			
		||||
    },
 | 
			
		||||
    getHeatmapLevelClass(value) {
 | 
			
		||||
      if (!value) return '';
 | 
			
		||||
}
 | 
			
		||||
function getHeatmapLevelClass(value) {
 | 
			
		||||
  if (!value)
 | 
			
		||||
    return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
 | 
			
		||||
 | 
			
		||||
      const level = [...this.quantileRange, Infinity].findIndex(
 | 
			
		||||
  let level = [...quantileRange.value, Infinity].findIndex(
 | 
			
		||||
    range => value <= range && value > 0
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
      if (level > 6) {
 | 
			
		||||
        return 'l6';
 | 
			
		||||
  if (level > 6) level = 5;
 | 
			
		||||
 | 
			
		||||
  if (level === 0) {
 | 
			
		||||
    return 'outline-slate-100 dark:outline-slate-700 dark:bg-slate-700/40 bg-slate-50/50';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
      return `l${level}`;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
  const classes = [
 | 
			
		||||
    'bg-woot-50 dark:bg-woot-800/40 dark:outline-woot-800/80',
 | 
			
		||||
    'bg-woot-100 dark:bg-woot-800/30 dark:outline-woot-800/80',
 | 
			
		||||
    'bg-woot-200 dark:bg-woot-500/40 dark:outline-woot-700/80',
 | 
			
		||||
    'bg-woot-300 dark:bg-woot-500/60 dark:outline-woot-600/80',
 | 
			
		||||
    'bg-woot-600 dark:bg-woot-500/80 dark:outline-woot-500/80',
 | 
			
		||||
    'bg-woot-800 dark:bg-woot-500 dark:outline-woot-400/80',
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  return classes[level - 1];
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
$heatmap-colors: (
 | 
			
		||||
  level-1: var(--w-50),
 | 
			
		||||
  level-2: var(--w-100),
 | 
			
		||||
  level-3: var(--w-300),
 | 
			
		||||
  level-4: var(--w-500),
 | 
			
		||||
  level-5: var(--w-700),
 | 
			
		||||
  level-6: var(--w-900),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
$heatmap-hover-border-color: (
 | 
			
		||||
  level-1: var(--w-25),
 | 
			
		||||
  level-2: var(--w-50),
 | 
			
		||||
  level-3: var(--w-100),
 | 
			
		||||
  level-4: var(--w-300),
 | 
			
		||||
  level-5: var(--w-500),
 | 
			
		||||
  level-6: var(--w-700),
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
$tile-height: 1.875rem;
 | 
			
		||||
$tile-gap: var(--space-smaller);
 | 
			
		||||
$container-gap-row: var(--space-one);
 | 
			
		||||
$container-gap-column: var(--space-two);
 | 
			
		||||
$marker-height: var(--space-two);
 | 
			
		||||
 | 
			
		||||
@mixin heatmap-level($level) {
 | 
			
		||||
  $color: map-get($heatmap-colors, 'level-#{$level}');
 | 
			
		||||
  background-color: $color;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    border: 1px solid map-get($heatmap-hover-border-color, 'level-#{$level}');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media screen and (max-width: 768px) {
 | 
			
		||||
  .heatmap-container {
 | 
			
		||||
    overflow-y: auto;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.loading-cell {
 | 
			
		||||
  background-color: var(--color-background-light);
 | 
			
		||||
  border: 0px;
 | 
			
		||||
 | 
			
		||||
  animation: loading-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes loading-pulse {
 | 
			
		||||
  0%,
 | 
			
		||||
  100% {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
  }
 | 
			
		||||
  50% {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.heatmap-container {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  gap: $container-gap-row $container-gap-column;
 | 
			
		||||
  grid-template-columns: 80px 1fr;
 | 
			
		||||
  min-height: calc(
 | 
			
		||||
    7 * #{$tile-height} + 6 * #{$tile-gap} + #{$container-gap-row} + #{$marker-height}
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.heatmap-labels {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-rows: 1fr;
 | 
			
		||||
  gap: $tile-gap;
 | 
			
		||||
  flex-shrink: 0;
 | 
			
		||||
 | 
			
		||||
  .heatmap-axis-label {
 | 
			
		||||
    height: $tile-height;
 | 
			
		||||
    min-width: 70px;
 | 
			
		||||
    font-size: var(--font-size-micro);
 | 
			
		||||
    font-weight: var(--font-weight-bold);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    align-items: flex-end;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
    @apply text-slate-800 dark:text-slate-200;
 | 
			
		||||
 | 
			
		||||
    time {
 | 
			
		||||
      font-size: var(--font-size-micro);
 | 
			
		||||
      font-weight: var(--font-weight-normal);
 | 
			
		||||
      @apply text-slate-700 dark:text-slate-200;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.heatmap-grid {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-rows: 1fr;
 | 
			
		||||
  gap: $tile-gap;
 | 
			
		||||
  min-width: 700px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
 | 
			
		||||
  .heatmap-grid-row {
 | 
			
		||||
    display: grid;
 | 
			
		||||
    gap: $tile-gap;
 | 
			
		||||
    grid-template-columns: repeat(24, 1fr);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .heatmap-tile {
 | 
			
		||||
    width: auto;
 | 
			
		||||
    height: $tile-height;
 | 
			
		||||
    border-radius: var(--border-radius-normal);
 | 
			
		||||
 | 
			
		||||
    &:hover {
 | 
			
		||||
      box-shadow: var(--shadow-large);
 | 
			
		||||
 | 
			
		||||
      transform: translateY(-2px);
 | 
			
		||||
      transition:
 | 
			
		||||
        transform 0.2s ease-in-out,
 | 
			
		||||
        box-shadow 0.2s ease-in-out;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &:not(.l1):not(.l2):not(.l3):not(.l4):not(.l5):not(.l6) {
 | 
			
		||||
      background-color: var(--color-background-light);
 | 
			
		||||
      border: 1px solid var(--color-border-light);
 | 
			
		||||
 | 
			
		||||
      &:hover {
 | 
			
		||||
        transform: translateY(0);
 | 
			
		||||
        box-shadow: none;
 | 
			
		||||
        border: 1px solid var(--color-border-light);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    &.l1 {
 | 
			
		||||
      @include heatmap-level(1);
 | 
			
		||||
    }
 | 
			
		||||
    &.l2 {
 | 
			
		||||
      @include heatmap-level(2);
 | 
			
		||||
    }
 | 
			
		||||
    &.l3 {
 | 
			
		||||
      @include heatmap-level(3);
 | 
			
		||||
    }
 | 
			
		||||
    &.l4 {
 | 
			
		||||
      @include heatmap-level(4);
 | 
			
		||||
    }
 | 
			
		||||
    &.l5 {
 | 
			
		||||
      @include heatmap-level(5);
 | 
			
		||||
    }
 | 
			
		||||
    &.l6 {
 | 
			
		||||
      @include heatmap-level(6);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.heatmap-markers {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: repeat(24, 1fr);
 | 
			
		||||
  gap: $tile-gap;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  font-size: var(--font-size-nano);
 | 
			
		||||
  font-weight: var(--font-weight-bold);
 | 
			
		||||
  height: $marker-height;
 | 
			
		||||
  color: var(--color-body);
 | 
			
		||||
  @apply text-slate-800 dark:text-slate-200;
 | 
			
		||||
 | 
			
		||||
  div {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: center;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
<template>
 | 
			
		||||
  <div
 | 
			
		||||
    class="grid relative w-full gap-x-4 gap-y-2.5 overflow-y-scroll md:overflow-visible grid-cols-[80px_1fr] min-h-72"
 | 
			
		||||
  >
 | 
			
		||||
    <template v-if="isLoading">
 | 
			
		||||
      <div class="grid gap-[5px] flex-shrink-0">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="ii in 7"
 | 
			
		||||
          :key="ii"
 | 
			
		||||
          class="w-full rounded-sm bg-slate-100 dark:bg-slate-900 animate-loader-pulse h-8 min-w-[70px]"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="grid gap-[5px] w-full min-w-[700px]">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="ii in 7"
 | 
			
		||||
          :key="ii"
 | 
			
		||||
          class="grid gap-[5px] grid-cols-[repeat(24,_1fr)]"
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            v-for="jj in 24"
 | 
			
		||||
            :key="jj"
 | 
			
		||||
            class="w-full h-8 rounded-sm bg-slate-100 dark:bg-slate-900 animate-loader-pulse"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div />
 | 
			
		||||
      <div
 | 
			
		||||
        class="grid grid-cols-[repeat(24,_1fr)] gap-[5px] w-full text-[8px] font-semibold h-5 text-slate-800 dark:text-slate-200"
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="ii in 24"
 | 
			
		||||
          :key="ii"
 | 
			
		||||
          class="flex items-center justify-center"
 | 
			
		||||
        >
 | 
			
		||||
          {{ ii - 1 }} – {{ ii }}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
    <template v-else>
 | 
			
		||||
      <div class="grid gap-[5px] flex-shrink-0">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="dateKey in processedData.keys()"
 | 
			
		||||
          :key="dateKey"
 | 
			
		||||
          class="h-8 min-w-[70px] text-slate-800 dark:text-slate-200 text-[10px] font-semibold flex flex-col items-end justify-center"
 | 
			
		||||
        >
 | 
			
		||||
          {{ getDayOfTheWeek(new Date(dateKey)) }}
 | 
			
		||||
          <time class="font-normal text-slate-700 dark:text-slate-200">
 | 
			
		||||
            {{ formatDate(dateKey) }}
 | 
			
		||||
          </time>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="grid gap-[5px] w-full min-w-[700px]">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="dateKey in processedData.keys()"
 | 
			
		||||
          :key="dateKey"
 | 
			
		||||
          class="grid gap-[5px] grid-cols-[repeat(24,_1fr)]"
 | 
			
		||||
        >
 | 
			
		||||
          <div
 | 
			
		||||
            v-for="data in processedData.get(dateKey)"
 | 
			
		||||
            :key="data.timestamp"
 | 
			
		||||
            v-tooltip.top="getCountTooltip(data.value)"
 | 
			
		||||
            class="h-8 rounded-sm shadow-inner dark:outline dark:outline-1 shadow-black"
 | 
			
		||||
            :class="getHeatmapLevelClass(data.value)"
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div />
 | 
			
		||||
      <div
 | 
			
		||||
        class="grid grid-cols-[repeat(24,_1fr)] gap-[5px] w-full text-[8px] font-semibold h-5 text-slate-800 dark:text-slate-200"
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="ii in 24"
 | 
			
		||||
          :key="ii"
 | 
			
		||||
          class="flex items-center justify-center"
 | 
			
		||||
        >
 | 
			
		||||
          {{ ii - 1 }} – {{ ii }}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </template>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user