All files / components/base BaseWidget.vue

100% Statements 39/39
100% Branches 7/7
100% Functions 0/0
100% Lines 39/39

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85  1x   1x 1x 1x 1x 5x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 19x   19x 19x 19x   19x 19x 19x 24x       24x 24x 24x 1x 1x 1x 1x 1x 3x 3x   3x 1x 1x                                                         1x                 1x    
<template>
  <BaseCard class="h-full">
    <!-- Header -->
    <template #header>
      <div class="flex items-center justify-between">
        <div class="flex items-center gap-2">
          <span v-if="icon" class="text-lg" :aria-hidden="true">{{ icon }}</span>
          <h2 class="text-lg font-semibold text-secondary-900">
            {{ t(titleKey) }}
            <span
              v-if="count !== undefined"
              class="text-sm font-normal text-secondary-500 ml-1"
            >
              ({{ count }})
            </span>
          </h2>
        </div>
        <div class="flex items-center gap-2">
          <slot name="actions" />
          <NuxtLink
            v-if="viewAllLink"
            :to="viewAllLink"
            class="text-sm text-primary-600 hover:text-primary-700 hover:underline focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded"
          >
            {{ t('widgets.viewAll') }}
          </NuxtLink>
        </div>
      </div>
    </template>
 
    <!-- Content -->
    <div v-if="loading" class="flex items-center justify-center py-8">
      <BaseSpinner size="md" />
    </div>
    <div v-else-if="isEmpty" class="py-8 text-center">
      <slot name="empty">
        <p class="text-secondary-500">
          {{ emptyMessage || t('widgets.noContent') }}
        </p>
      </slot>
    </div>
    <div v-else>
      <slot />
    </div>
  </BaseCard>
</template>
 
<script setup lang="ts">
/**
 * BaseWidget - Displays content in a consistent card container with translatable header and optional view-all link.
 *
 * @slot default - Main content area
 * @slot empty - Custom empty state content
 * @slot actions - Additional header actions (buttons, links)
 */
 
interface Props {
  /** i18n key for the widget title (required) */
  titleKey: string
  /** Optional icon to display before title */
  icon?: string
  /** Optional link for "View All" action */
  viewAllLink?: string
  /** Optional count to display next to title */
  count?: number
  /** Loading state */
  loading?: boolean
  /** Whether the widget content is empty */
  isEmpty?: boolean
  /** Custom empty state message (i18n key or text) */
  emptyMessage?: string
}
 
const props = withDefaults(defineProps<Props>(), {
  icon: undefined,
  viewAllLink: undefined,
  count: undefined,
  loading: false,
  isEmpty: false,
  emptyMessage: undefined
})
 
const { t } = useI18n()
</script>