first set of user progress flow fixes / refactor

This commit is contained in:
austinkelsay 2025-06-08 14:44:13 -05:00
parent 5d1e9d32bb
commit e8c5c161dd
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
6 changed files with 216 additions and 56 deletions

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import { useNDKContext } from '@/context/NDKContext';
import { parseEvent } from '@/utils/nostr';
import { parseEvent, parseCourseEvent } from '@/utils/nostr';
import { ProgressSpinner } from 'primereact/progressspinner';
import { nip19 } from 'nostr-tools';
import appConfig from '@/config/appConfig';
@ -8,50 +8,73 @@ import appConfig from '@/config/appConfig';
const PurchasedListItem = ({ eventId, category }) => {
const { ndk } = useNDKContext();
const [event, setEvent] = useState(null);
const [naddr, setNaddr] = useState(null);
useEffect(() => {
const fetchEvent = async () => {
if (!eventId) return;
if (!eventId || !ndk) return;
try {
await ndk.connect();
const event = await ndk.fetchEvent(eventId);
if (event) {
setEvent(parseEvent(event));
const filter = category === 'courses'
? {
kinds: [30004],
authors: appConfig.authorPubkeys,
'#d': [eventId],
}
: {
kinds: [30023, 30402],
authors: appConfig.authorPubkeys,
'#d': [eventId],
};
const fetchedEvent = await ndk.fetchEvent(filter);
if (fetchedEvent) {
setEvent(category === 'courses' ? parseCourseEvent(fetchedEvent) : parseEvent(fetchedEvent));
}
} catch (error) {
console.error('Error fetching event:', error);
console.error('Error fetching event in PurchasedListItem:', error);
setEvent(null);
}
};
fetchEvent();
}, [eventId, ndk]);
}, [eventId, ndk, category]);
useEffect(() => {
if (event) {
encodeNaddr();
}
}, [event]);
const encodeNaddr = () => {
setNaddr(
nip19.naddrEncode({
const encodeNaddrForLink = () => {
if (!event || !event.pubkey || !event.d) return null;
try {
return nip19.naddrEncode({
pubkey: event.pubkey,
identifier: event.d,
kind: event.kind,
kind: category === 'courses' ? 30004 : event.kind,
relays: appConfig.defaultRelayUrls,
})
);
});
} catch (error) {
console.error("Error encoding naddr:", error);
return null;
}
};
return !event || !ndk ? (
<ProgressSpinner className="w-[40px] h-[40px]" />
) : (
if (!ndk) {
return <ProgressSpinner className="w-[40px] h-[40px]" title="Initializing..." />;
}
if (!eventId) {
return <span className="text-gray-500">Missing item ID</span>
}
if (!event) {
return <ProgressSpinner className="w-[40px] h-[40px]" />;
}
const naddrValue = encodeNaddrForLink();
if (!naddrValue) {
return <span className="text-red-500">Error generating link. (Invalid Event ID?)</span>;
}
return (
<a
className="text-blue-500 underline hover:text-blue-600"
href={category === 'courses' ? `/course/${naddr}` : `/details/${naddr}`}
href={category === 'courses' ? `/course/${naddrValue}` : `/details/${naddrValue}`}
>
{event.title}
{event.name || event.title || 'Unnamed Item'}
</a>
);
};

View File

@ -1,6 +1,5 @@
import React from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import GenericDataTable from '@/components/ui/DataTables/DataTable';
import ProgressListItem from '@/components/content/lists/ProgressListItem';
import { formatDateTime } from '@/utils/time';
import { ProgressSpinner } from 'primereact/progressspinner';
@ -92,7 +91,7 @@ const UserProgressTable = ({ session, ndk }) => {
return progressData.sort((a, b) => new Date(b.date) - new Date(a.date));
};
const header = (
const tableHeader = (
<div className="flex flex-wrap align-items-center justify-content-between gap-2">
<span className="text-xl text-900 font-bold text-[#f8f8ff]">Progress</span>
</div>
@ -161,6 +160,13 @@ const UserProgressTable = ({ session, ndk }) => {
);
};
const columns = [
{ field: 'type', header: 'Type', body: typeTemplate },
{ field: 'eventType', header: 'Event', body: eventTemplate },
{ field: 'name', header: 'Name', body: nameTemplate },
{ field: 'date', header: 'Date', body: dateTemplate },
];
if (!session || !session?.user || !ndk) {
return (
<div className="w-full h-full flex items-center justify-center">
@ -170,10 +176,11 @@ const UserProgressTable = ({ session, ndk }) => {
}
return (
<DataTable
<GenericDataTable
emptyMessage="No Courses or Milestones completed"
value={prepareProgressData()}
header={header}
header={tableHeader}
columns={columns}
className="mt-2 mx-2 max-lap:mx-0"
style={{
width: '100%',
@ -199,12 +206,10 @@ const UserProgressTable = ({ session, ndk }) => {
},
}}
stripedRows
dataKey="id"
>
<Column field="type" header="Type" body={typeTemplate}></Column>
<Column field="eventType" header="Event" body={eventTemplate}></Column>
<Column field="name" header="Name" body={nameTemplate}></Column>
<Column field="date" body={dateTemplate} header="Date"></Column>
</DataTable>
{/* Original Column definitions removed */}
</GenericDataTable>
);
};

View File

@ -1,6 +1,5 @@
import React from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import GenericDataTable from '@/components/ui/DataTables/DataTable';
import PurchasedListItem from '@/components/content/lists/PurchasedListItem';
import { formatDateTime } from '@/utils/time';
@ -47,13 +46,22 @@ const UserPurchaseTable = ({ session, windowWidth }) => {
);
};
// Define columns for GenericDataTable
const columns = [
{ field: 'amountPaid', header: 'Cost', body: costTemplate },
{ field: 'name', header: 'Name', body: nameTemplate },
{ field: 'category', header: 'Category', body: categoryTemplate },
{ field: 'createdAt', header: 'Date', body: dateTemplate },
];
return (
session &&
session?.user && (
<DataTable
<GenericDataTable
emptyMessage="No purchases"
value={session.user?.purchased}
header={purchasesHeader}
columns={columns}
className="mt-2 mx-2 max-lap:mx-0"
style={{
width: '100%',
@ -80,11 +88,8 @@ const UserPurchaseTable = ({ session, windowWidth }) => {
}}
stripedRows
>
<Column field="amountPaid" header="Cost" body={costTemplate}></Column>
<Column field="name" header="Name" body={nameTemplate}></Column>
<Column field="category" header="Category" body={categoryTemplate}></Column>
<Column field="createdAt" header="Date" body={dateTemplate}></Column>
</DataTable>
{/* Original Column definitions removed */}
</GenericDataTable>
)
);
};

View File

@ -1,6 +1,5 @@
import React, { useState, useEffect, useCallback } from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import GenericDataTable from '@/components/ui/DataTables/DataTable';
import { InputText } from 'primereact/inputtext';
import GenericButton from '@/components/buttons/GenericButton';
import { useToast } from '@/hooks/useToast';
@ -54,7 +53,7 @@ const UserRelaysTable = ({ ndk, userRelays, setUserRelays, reInitializeNDK }) =>
}
};
const header = (
const tableHeader = (
<div className="text-[#f8f8ff]">
<div className="flex items-center justify-between">
<div>
@ -62,9 +61,10 @@ const UserRelaysTable = ({ ndk, userRelays, setUserRelays, reInitializeNDK }) =>
</div>
<GenericButton
outlined
icon="pi pi-plus"
label="Add Relay"
icon={collapsed ? 'pi pi-plus' : 'pi pi-minus'}
label={collapsed ? 'Add Relay' : 'Hide'}
severity="success"
className="w-fit"
onClick={() => setCollapsed(!collapsed)}
/>
</div>
@ -77,12 +77,14 @@ const UserRelaysTable = ({ ndk, userRelays, setUserRelays, reInitializeNDK }) =>
onChange={e => setNewRelayUrl(e.target.value)}
className="flex-1"
/>
<GenericButton label="+" severity="success" outlined onClick={addRelay} />
<GenericButton className="w-fit" label="+" severity="success" outlined onClick={addRelay} />
</div>
)}
</div>
);
const relayUrlBody = rowData => rowData;
const relayStatusBody = url => {
const isConnected = relayStatuses[url];
return (
@ -118,13 +120,23 @@ const UserRelaysTable = ({ ndk, userRelays, setUserRelays, reInitializeNDK }) =>
);
};
// Define columns for GenericDataTable
const columns = [
{ field: 'url', header: 'Relay URL', body: relayUrlBody },
{ header: 'Status', body: relayStatusBody },
{ header: 'Actions', body: relayActionsBody },
];
return (
<div className="w-full">
<DataTable value={userRelays} className="border-none" header={header}>
<Column field={url => url} header="Relay URL"></Column>
<Column body={relayStatusBody} header="Status"></Column>
<Column body={relayActionsBody} header="Actions"></Column>
</DataTable>
<GenericDataTable
value={userRelays}
columns={columns}
className="border-none"
header={tableHeader}
dataKey={rowData => rowData}
>
</GenericDataTable>
</div>
);
};

View File

@ -0,0 +1,114 @@
import React from 'react';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
const GenericDataTable = ({
value,
columns,
header,
emptyMessage = "No records found.",
className,
style,
pt,
stripedRows = false,
dataKey,
// Pagination props
paginator = false,
rows,
rowsPerPageOptions,
paginatorTemplate = "FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown",
currentPageReportTemplate = "Showing {first} to {last} of {totalRecords} entries",
// Sorting props
sortMode,
sortField,
sortOrder,
onSort,
// Filtering props
filters,
onFilter,
globalFilter,
globalFilterFields,
filterDisplay = "menu",
// Selection props
selection,
onSelectionChange,
selectionMode,
// Row expansion
rowExpansionTemplate,
expandedRows,
onRowToggle,
// Context Menu
onContextMenu,
contextMenuSelection,
onContextMenuSelectionChange,
// Other common props
loading = false,
...rest // Allows passing any other DataTable props
}) => {
return (
<DataTable
value={value}
header={header}
emptyMessage={emptyMessage}
className={className}
style={style}
pt={pt}
stripedRows={stripedRows}
dataKey={dataKey}
// Pagination
paginator={paginator}
rows={rows}
rowsPerPageOptions={rowsPerPageOptions}
paginatorTemplate={paginatorTemplate}
currentPageReportTemplate={currentPageReportTemplate}
// Sorting
sortMode={sortMode}
sortField={sortField}
sortOrder={sortOrder}
onSort={onSort}
// Filtering
filters={filters}
onFilter={onFilter}
globalFilter={globalFilter}
globalFilterFields={globalFilterFields}
filterDisplay={filterDisplay}
// Selection
selection={selection}
onSelectionChange={onSelectionChange}
selectionMode={selectionMode}
// Row Expansion
rowExpansionTemplate={rowExpansionTemplate}
expandedRows={expandedRows}
onRowToggle={onRowToggle}
// Context Menu
onContextMenu={onContextMenu}
contextMenuSelection={contextMenuSelection}
onContextMenuSelectionChange={onContextMenuSelectionChange}
// Loading state
loading={loading}
{...rest}
>
{columns && columns.map((col, i) => (
<Column
key={col.field || i}
field={col.field}
header={col.header}
body={col.body}
sortable={col.sortable}
filter={col.filter}
filterPlaceholder={col.filterPlaceholder}
filterElement={col.filterElement}
selectionMode={col.selectionMode}
headerStyle={col.headerStyle}
bodyStyle={col.bodyStyle}
style={col.style}
expander={col.expander}
// Add other column props as needed
{...col.columnProps} // Allows passing any other Column props
/>
))}
</DataTable>
);
};
export default GenericDataTable;

View File

@ -11,7 +11,8 @@ const appConfig = {
],
authorPubkeys: [
'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741',
'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345'
'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345',
'6260f29fa75c91aaa292f082e5e87b438d2ab4fdf96af398567b01802ee2fcd4'
],
customLightningAddresses: [
{