Skip to main content
Glama
HistoryView.jsx11.5 kB
import React, { useMemo, useState } from 'react'; import { useReactTable, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, flexRender, } from '@tanstack/react-table'; import { useTranslation } from 'react-i18next'; function HistoryView({ data = [], loading = false, error = '', onViewTasks, onBack, profileId }) { const { t } = useTranslation(); const [showNotesModal, setShowNotesModal] = useState(false); const [editingEntry, setEditingEntry] = useState(null); const [notesText, setNotesText] = useState(''); const [savedNotes, setSavedNotes] = useState({}); const [savingNote, setSavingNote] = useState(false); // Load notes from localStorage on mount React.useEffect(() => { if (!profileId) return; const storageKey = `history-notes-${profileId}`; const storedNotes = localStorage.getItem(storageKey); if (storedNotes) { try { setSavedNotes(JSON.parse(storedNotes)); } catch (e) { console.error('Failed to parse stored notes:', e); } } }, [profileId]); // Save notes to localStorage whenever they change React.useEffect(() => { if (!profileId) return; const storageKey = `history-notes-${profileId}`; localStorage.setItem(storageKey, JSON.stringify(savedNotes)); }, [savedNotes, profileId]); const columns = useMemo(() => [ { accessorKey: 'timestamp', header: t('dateTime'), cell: ({ getValue }) => { const timestamp = getValue(); // Convert to local time using the user's timezone const date = new Date(timestamp); return date.toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true }); }, size: 200, }, { accessorKey: 'notes', header: t('notes'), cell: ({ row }) => { const noteKey = `${row.original.timestamp}`; const noteText = savedNotes[noteKey]; const hasNote = !!noteText; return ( <div className="notes-cell" onClick={(e) => { e.stopPropagation(); setEditingEntry(row.original); setNotesText(savedNotes[noteKey] || ''); setShowNotesModal(true); }} style={{ cursor: 'pointer' }} > {hasNote ? ( <div className="notes-preview" title={noteText} > {noteText.slice(0, 150)}{noteText.length > 150 ? '...' : ''} </div> ) : ( <button className="edit-notes-button" onClick={(e) => { e.stopPropagation(); setEditingEntry(row.original); setNotesText(''); setShowNotesModal(true); }} title={t('addNote')} > ✏️ </button> )} </div> ); }, size: 200, }, { accessorKey: 'taskCount', header: t('taskCount'), cell: ({ getValue }) => ( <div className="task-count">{getValue()}</div> ), size: 100, }, { accessorKey: 'stats', header: t('statusSummary'), cell: ({ getValue }) => { const stats = getValue(); return ( <div className="status-summary"> <span className="status-item completed" title={t('completed')}> <span className="status-count">{stats.completed}</span> {t('completed')} </span> <span className="status-divider">•</span> <span className="status-item in-progress" title={t('inProgress')}> <span className="status-count">{stats.inProgress}</span> {t('inProgress')} </span> <span className="status-divider">•</span> <span className="status-item pending" title={t('pending')}> <span className="status-count">{stats.pending}</span> {t('pending')} </span> </div> ); }, size: 300, }, { accessorKey: 'actions', header: t('actions'), cell: ({ row }) => ( <div className="actions-cell"> <button className="action-button view-button" onClick={(e) => { e.stopPropagation(); if (onViewTasks) { onViewTasks(row.original); } }} title={t('viewTasks')} disabled={row.original.taskCount === 0} > 👁️ </button> </div> ), size: 100, }, ], [onViewTasks, t]); const table = useReactTable({ data, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), initialState: { pagination: { pageSize: 15 }, sorting: [{ id: 'timestamp', desc: true }] }, }); if (loading) { return ( <div className="loading"> {t('loading')} ⏳ </div> ); } if (error) { return ( <div className="error"> {error} </div> ); } if (data.length === 0) { return ( <div className="loading"> {t('noHistoryFound')} </div> ); } return ( <div className="history-view"> <div className="history-view-header"> <div className="header-content" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%' }}> <h2>📚 {t('projectHistory')}</h2> {onBack && ( <button className="back-button" onClick={onBack} title={t('backToTasks')} style={{ backgroundColor: '#8b5cf6', color: 'white', border: 'none', padding: '8px 16px', borderRadius: '6px', cursor: 'pointer', fontSize: '14px', fontWeight: '500', transition: 'background-color 0.2s' }} onMouseEnter={(e) => e.target.style.backgroundColor = '#7c3aed'} onMouseLeave={(e) => e.target.style.backgroundColor = '#8b5cf6'} > ← {t('backToTasks')} </button> )} </div> </div> <table className="table"> <thead> {table.getHeaderGroups().map(headerGroup => ( <tr key={headerGroup.id}> {headerGroup.headers.map(header => ( <th key={header.id} style={{ width: header.getSize() }} onClick={header.column.getCanSort() ? header.column.getToggleSortingHandler() : undefined} className={header.column.getCanSort() ? 'sortable' : ''} > {flexRender(header.column.columnDef.header, header.getContext())} {header.column.getIsSorted() && ( <span style={{ marginLeft: '8px' }}> {header.column.getIsSorted() === 'desc' ? '↓' : '↑'} </span> )} </th> ))} </tr> ))} </thead> <tbody> {table.getRowModel().rows.map(row => ( <tr key={row.id} className="clickable-row history-row" onClick={() => { if (onViewTasks && row.original.taskCount > 0) { onViewTasks(row.original); } }} style={{ cursor: row.original.taskCount > 0 ? 'pointer' : 'default' }} title={row.original.taskCount > 0 ? t('clickToViewTasks') : t('noTasksAvailable')} > {row.getVisibleCells().map(cell => ( <td key={cell.id}> {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ))} </tr> ))} </tbody> </table> <div className="pagination"> <div className="pagination-info"> {t('showing')} {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} {t('to')}{' '} {Math.min( (table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length )}{' '} {t('of')} {table.getFilteredRowModel().rows.length} {t('historyEntries')} </div> <div className="pagination-controls"> <button onClick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()} > {'<<'} </button> <button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > {'<'} </button> <span> {t('page')} {table.getState().pagination.pageIndex + 1} {t('of')}{' '} {table.getPageCount()} </span> <button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > {'>'} </button> <button onClick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()} > {'>>'} </button> </div> </div> {/* Notes Modal */} {showNotesModal && ( <div className="modal-overlay" onClick={() => setShowNotesModal(false)}> <div className="modal-content notes-modal" onClick={(e) => e.stopPropagation()}> <h3>{t('editNote')}</h3> <p className="modal-subtitle"> {editingEntry && new Date(editingEntry.timestamp).toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: true })} </p> <textarea className="notes-textarea" value={notesText} onChange={(e) => setNotesText(e.target.value)} placeholder={t('enterNoteHere')} rows={6} autoFocus /> <div className="modal-actions"> <button className="primary-btn" onClick={() => { if (editingEntry) { const noteKey = `${editingEntry.timestamp}`; setSavedNotes(prev => ({ ...prev, [noteKey]: notesText })); } setShowNotesModal(false); }} > {t('save')} </button> <button className="secondary-btn" onClick={() => setShowNotesModal(false)} > {t('cancel')} </button> </div> </div> </div> )} </div> ); } export default HistoryView;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cjo4m06/mcp-shrimp-task-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server