file re-org
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 53s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 53s
This commit is contained in:
140
src/pages/admin/ActivityLog.tsx
Normal file
140
src/pages/admin/ActivityLog.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
// src/components/ActivityLog.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { fetchActivityLog } from '../../services/apiClient';
|
||||
import { ActivityLogItem } from '../../types';
|
||||
import { User } from '../../types';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
|
||||
export type ActivityLogClickHandler = (log: ActivityLogItem) => void;
|
||||
|
||||
interface ActivityLogProps {
|
||||
user: User | null;
|
||||
onLogClick?: ActivityLogClickHandler;
|
||||
}
|
||||
|
||||
const renderLogDetails = (log: ActivityLogItem, onLogClick?: ActivityLogClickHandler) => {
|
||||
// With discriminated unions, we can safely access properties based on the 'action' type.
|
||||
const userName = 'user_full_name' in log.details ? log.details.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> = ({ user, onLogClick }) => {
|
||||
const [logs, setLogs] = useState<ActivityLogItem[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!user) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadLogs = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const fetchedLogs = await fetchActivityLog(20, 0);
|
||||
setLogs(fetchedLogs);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to load activity.');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadLogs();
|
||||
}, [user]);
|
||||
|
||||
if (!user) {
|
||||
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}</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.details?.user_avatar_url ? (
|
||||
<img className="h-8 w-8 rounded-full" src={log.details.user_avatar_url} alt={log.details.user_full_name || ''} />
|
||||
) : (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user