Component Design

This guide outlines the principles and patterns for creating components in the Employ platform. Following these guidelines will help ensure that your components are reusable, maintainable, and consistent with the rest of the codebase.

Component Hierarchy

Our components follow a clear hierarchy:

  1. Page Components: Found in /pages, these use layout and module components
  2. Layout Components: Define the overall structure of a page
  3. Module Components: Feature-specific components in /modules/{feature}/components
  4. UI Components: Reusable, generic UI elements

Component Structure

Every component should follow this basic structure:

<script setup lang="ts">
// 1. Imports (if needed)
import { SomeIcon } from 'lucide-vue-next';

// 2. Component props with TypeScript interface
interface Props {
  title: string;
  description?: string;
  items: any[];
  loading?: boolean;
}

// 3. Props with defaults
const props = withDefaults(defineProps<Props>(), {
  description: '',
  loading: false,
});

// 4. Emits
const emit = defineEmits(['itemSelected', 'refresh']);

// 5. Local state and computeds
const selectedItem = ref(null);

// 6. Methods/Functions
function handleItemClick(item) {
  selectedItem.value = item;
  emit('itemSelected', item);
}
</script>

<template>
  <div>
    <!-- Component template with clear structure -->
    <div v-if="loading">Loading...</div>
    <div v-else>
      <h2>{{ title }}</h2>
      <p v-if="description">{{ description }}</p>
      
      <!-- List rendering with events -->
      <ul>
        <li 
          v-for="item in items" 
          :key="item.id"
          @click="handleItemClick(item)"
        >
          {{ item.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

Best Practices

1. Props Design

  • Use TypeScript interfaces for all props
  • Provide default values for optional props
  • Document complex props
interface ApplicationListProps {
  // Required props
  applications: Application[];
  
  // Optional props with comments
  /** Controls loading state display */
  loading?: boolean;
  
  /** Error object to display error states */
  error?: Error | null;
  
  /** Title displayed at the top of the component */
  title?: string;
}

2. Events and Emits

  • Define all emits the component can trigger
  • Use descriptive event names
  • Pass relevant data with events
const emit = defineEmits([
  'viewApplication',  // When user clicks to view an application
  'changeStatus',     // When application status is changed
  'refresh',          // When data needs to be refreshed
]);

function viewApplication(id: string) {
  emit('viewApplication', id);
}

3. Component Composition

Break large components into smaller, focused components:

<!-- Parent component -->
<template>
  <div>
    <ApplicationHeader :title="title" />
    <ApplicationFilters @filter="applyFilters" />
    <ApplicationTable 
      :items="filteredApplications"
      @viewApplication="viewApplication" 
    />
    <ApplicationPagination :totalPages="totalPages" />
  </div>
</template>

4. Loading and Error States

Always handle loading and error states:

<template>
  <div>
    <!-- Loading state -->
    <div v-if="loading" class="loading-container">
      <Spinner />
      <p>Loading applications...</p>
    </div>
    
    <!-- Error state -->
    <div v-else-if="error" class="error-container">
      <AlertCircle class="text-red-500" />
      <p>{{ error.message || 'Failed to load applications' }}</p>
      <Button @click="retry">Retry</Button>
    </div>
    
    <!-- Content -->
    <div v-else>
      <!-- Main content here -->
    </div>
  </div>
</template>

Real-World Example: ApplicationList

Let's examine the ApplicationList component as an example:

<script setup lang="ts">
// Props with TypeScript interface
interface Props {
    applications: any[];
    loading?: boolean;
    error?: Error | null;
    searchTerm?: string;
    filterStatus?: string;
    // Additional props...
}

// Default props
const props = withDefaults(defineProps<Props>(), {
    title: "Applications",
    description: "View and manage all job applications",
    emptyTitle: "No applications found",
    // Additional defaults...
});

// Events
const emit = defineEmits(["viewApplication"]);

// Helper functions
function formatDate(date: Date | string | undefined) {
    if (!date) return "";
    return new Date(date).toLocaleDateString();
}

function getStatusColor(status: string) {
    switch (status) {
        case "APPLIED":
            return props.compact ? "blue" : "bg-blue-500";
        case "REVIEWED":
            return props.compact ? "yellow" : "bg-yellow-500";
        // Additional cases...
    }
}

// Event handlers
function viewApplication(applicationId: string) {
    emit("viewApplication", applicationId);
}
</script>

<template>
    <ListingContainer 
        :title="title" 
        :icon="Users" 
        :description="description" 
        :isLoading="loading" 
        :error="error"
        :isEmpty="applications.length === 0" 
        :emptyTitle="emptyTitle" 
        :emptyDescription="emptyDescription"
        :emptyIcon="FileQuestion">
        
        <!-- Filters section -->
        <template v-if="!compact && !hideFilters" #filters>
            <Filters 
                ref="filters" 
                :items="{ items: applications }"
                @update:filteredItems="filteredData = $event" 
                @update:paginatedItems="paginatedData = $event"
                :filterFields="{ /* filter configurations */ }" 
                :advancedOptions="{ /* options */ }"
                :sortOptions="[ /* sort options */ ]" 
                :searchPlaceholder="'Search applications...'" 
                class="w-full" />
        </template>
        
        <!-- Pagination -->
        <template #pagination>
            <LoadMore 
                :total-count="filteredData.length" 
                :item-count="paginatedData.length" 
                :batch-size="3"
                :filters="filters" 
                loadedText="All applications loaded"
                buttonClass="min-w-[240px] font-medium"
                class="w-full mt-4" />
        </template>
        
        <!-- Application items rendering -->
        <!-- Content implementation... -->
    </ListingContainer>
</template>

Component Documentation

For complex components, include documentation in comments:

<script setup lang="ts">
/**
 * ApplicationList Component
 * 
 * Displays a list of job applications with filtering, sorting, and pagination.
 * 
 * @component
 * @example
 * <ApplicationList 
 *   :applications="applications"
 *   :loading="isLoading"
 *   @viewApplication="handleViewApplication" 
 * />
 */
</script>

Component Directory Structure

Organize your components in feature-specific directories:

modules/
  platform/
    application/
      components/
        ApplicationList.vue          # Main listing component
        ApplicationListItem.vue      # Individual item component
        ApplicationDetails.vue       # Details view
        ApplicationFilters.vue       # Filtering component
        ApplicationStatusBadge.vue   # Status display component

By following these guidelines, you'll create components that are easy to use, maintain, and integrate into the Employ platform.