All files / src/pages/admin ActivityLog.tsx

100% Statements 22/22
100% Branches 47/47
100% Functions 7/7
100% Lines 22/22

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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136                              1x   24x 24x 24x   5x           4x       1x                 4x           4x       1x                 4x       1x                 3x       1x   8x   8x 1x     7x                       24x       7x 7x       7x                                                            
// src/pages/admin/ActivityLog.tsx
import React from 'react';
import { ActivityLogItem } from '../../types';
import { UserProfile } from '../../types';
import { formatDistanceToNow } from 'date-fns';
import { useActivityLogQuery } from '../../hooks/queries/useActivityLogQuery';
import { logger } from '../../services/logger.client';
 
export type ActivityLogClickHandler = (log: ActivityLogItem) => void;
 
interface ActivityLogProps {
  userProfile: UserProfile | null;
  onLogClick?: ActivityLogClickHandler;
}
 
const renderLogDetails = (log: ActivityLogItem, onLogClick?: ActivityLogClickHandler) => {
  // With discriminated unions, we can safely access properties based on the 'action' type.
  const userName = log.user_full_name || 'A user';
  const isClickable = onLogClick !== undefined;
  switch (log.action) {
    case 'flyer_processed':
      return (
        <span>
          A new flyer for <strong>{log.details.store_name || 'a store'}</strong> was added.
        </span>
      );
    case 'recipe_created':
      return (
        <span>
          {userName} added a new recipe:{' '}
          <strong
            onClick={isClickable ? () => onLogClick(log) : undefined}
            className={isClickable ? 'text-blue-500 hover:underline cursor-pointer' : ''}
          >
            {log.details.recipe_name || 'Untitled Recipe'}
          </strong>
          .
        </span>
      );
    case 'user_registered':
      return (
        <span>
          <strong>{log.details.full_name || 'A new user'}</strong> just joined!
        </span>
      );
    case 'recipe_favorited':
      return (
        <span>
          {userName} favorited the recipe:{' '}
          <strong
            onClick={isClickable ? () => onLogClick(log) : undefined}
            className={isClickable ? 'text-blue-500 hover:underline cursor-pointer' : ''}
          >
            {log.details.recipe_name || 'a recipe'}
          </strong>
          .
        </span>
      );
    case 'list_shared':
      return (
        <span>
          {userName} shared the list "
          <strong
            onClick={isClickable ? () => onLogClick(log) : undefined}
            className={isClickable ? 'text-blue-500 hover:underline cursor-pointer' : ''}
          >
            {log.details.list_name || 'a shopping list'}
          </strong>
          " with <strong>{log.details.shared_with_name || 'another user'}</strong>.
        </span>
      );
    default:
      return <span>An unknown activity occurred.</span>;
  }
};
 
export const ActivityLog: React.FC<ActivityLogProps> = ({ userProfile, onLogClick }) => {
  // Use TanStack Query for data fetching (ADR-0005 Phase 5)
  const { data: logs = [], isLoading, error } = useActivityLogQuery(20, 0);
 
  if (!userProfile) {
    return null; // Don't render the component if the user is not logged in
  }
 
  return (
    <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 shadow-sm p-4">
      <h3 className="text-lg font-semibold text-gray-800 dark:text-gray-100 mb-4">
        Recent Activity
      </h3>
      {isLoading && <p className="text-gray-500 dark:text-gray-400">Loading activity...</p>}
      {error && <p className="text-red-500">{error.message}</p>}
      {!isLoading && !error && logs.length === 0 && (
        <p className="text-gray-500 dark:text-gray-400">No recent activity to show.</p>
      )}
      <ul className="space-y-4">
        {logs.map((log) => (
          <li key={log.activity_log_id} className="flex items-start space-x-3">
            <div className="shrink-0">
              {log.user_avatar_url ? (
                (() => {
                  const altText = log.user_full_name || 'User Avatar';
                  logger.debug(
                    { activityLogId: log.activity_log_id, altText },
                    '[ActivityLog] Rendering avatar',
                  );
                  return (
                    <img className="h-8 w-8 rounded-full" src={log.user_avatar_url} alt={altText} />
                  );
                })()
              ) : (
                <span className="h-8 w-8 rounded-full bg-gray-300 dark:bg-gray-600 flex items-center justify-center">
                  <svg
                    className="h-5 w-5 text-gray-500 dark:text-gray-400"
                    fill="currentColor"
                    viewBox="0 0 20 20"
                  >
                    <path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" />
                  </svg>
                </span>
              )}
            </div>
            <div className="flex-1">
              <p className="text-sm text-gray-700 dark:text-gray-300">
                {renderLogDetails(log, onLogClick)}
              </p>
              <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
                {formatDistanceToNow(new Date(log.created_at), { addSuffix: true })}
              </p>
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
};