diff --git a/backend/package.json b/backend/package.json index 6856d8f..90dd0a3 100644 --- a/backend/package.json +++ b/backend/package.json @@ -39,12 +39,15 @@ "@types/express": "^5.0.6", "@types/node": "^25.2.3", "@types/pg": "^8.16.0", + "@types/supertest": "^7.2.0", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", "nodemon": "^3.1.11", "prisma": "^7.4.1", + "supertest": "^7.2.2", "ts-node": "^10.9.2", "tsx": "^4.19.2", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vitest": "^4.0.18" } } diff --git a/backend/src/controllers/user.controller.ts b/backend/src/controllers/user.controller.ts index e68c121..b6f2dcc 100644 --- a/backend/src/controllers/user.controller.ts +++ b/backend/src/controllers/user.controller.ts @@ -62,3 +62,31 @@ export const getUser = async (req: Request, res: Response, next: NextFunction) = next(error); } }; + +/** + * Get user events (history) + */ +export const getUserEvents = async (req: Request, res: Response, next: NextFunction) => { + try { + const { publicKey } = req.params; + + const events = await prisma.streamEvent.findMany({ + where: { + stream: { + OR: [ + { sender: publicKey }, + { recipient: publicKey } + ] + } + }, + orderBy: { timestamp: 'desc' }, + include: { + stream: true + } + }); + + return res.status(200).json(events); + } catch (error) { + next(error); + } +}; diff --git a/backend/src/routes/v1/user.routes.ts b/backend/src/routes/v1/user.routes.ts index 405995d..711ad59 100644 --- a/backend/src/routes/v1/user.routes.ts +++ b/backend/src/routes/v1/user.routes.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import { registerUser, getUser } from '../../controllers/user.controller.js'; +import { registerUser, getUser, getUserEvents } from '../../controllers/user.controller.js'; const router = Router(); @@ -66,4 +66,33 @@ const router = Router(); router.post('/', registerUser); router.get('/:publicKey', getUser); +/** + * @openapi + * /v1/users/{publicKey}/events: + * get: + * tags: + * - Users + * summary: Fetch user activity history + * description: Returns a chronological history of all stream events associated with the user. + * parameters: + * - in: path + * name: publicKey + * required: true + * schema: + * type: string + * description: Stellar public key + * responses: + * 200: + * description: List of user events + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/StreamEvent' + * 404: + * description: User not found + */ +router.get('/:publicKey/events', getUserEvents); + export default router; diff --git a/frontend/components/Dashboard.tsx b/frontend/components/Dashboard.tsx index fa8d952..cff7f5c 100644 --- a/frontend/components/Dashboard.tsx +++ b/frontend/components/Dashboard.tsx @@ -1,5 +1,7 @@ -import React from 'react'; -import { downloadCSV } from '../utils/csvExport'; +import { ActivityHistory } from './dashboard/ActivityHistory'; +import { fetchUserEvents } from '@/lib/dashboard'; +import { useWallet } from '@/context/wallet-context'; +import { BackendStreamEvent } from '@/lib/api-types'; interface StreamData extends Record { id: string; @@ -21,6 +23,30 @@ const mockStreams: StreamData[] = [ ]; const Dashboard: React.FC = () => { + const { session } = useWallet(); + const [activeTab, setActiveTab] = React.useState<'streams' | 'activity'>('streams'); + const [events, setEvents] = React.useState([]); + const [isLoadingEvents, setIsLoadingEvents] = React.useState(false); + + React.useEffect(() => { + if (activeTab === 'activity' && session?.publicKey) { + loadEvents(); + } + }, [activeTab, session?.publicKey]); + + const loadEvents = async () => { + if (!session?.publicKey) return; + setIsLoadingEvents(true); + try { + const data = await fetchUserEvents(session.publicKey); + setEvents(data); + } catch (error) { + console.error(error); + } finally { + setIsLoadingEvents(false); + } + }; + const handleExport = () => { downloadCSV(mockStreams, 'flowfi-stream-history.csv'); }; @@ -37,59 +63,78 @@ const Dashboard: React.FC = () => { return (
-

Stream History

- +
+ + +
+ {activeTab === 'streams' && ( + + )}
-
- - - - - - - - - - - - - - {mockStreams.map((stream) => ( - - - - - - - - + {activeTab === 'streams' ? ( +
+
DateRecipientDepositedWithdrawnTokenStatusActions
{stream.date}{stream.recipient}{stream.deposited} {stream.token}{stream.withdrawn} {stream.token}{stream.token} - - {stream.status} - - - {stream.status === 'Active' && ( - - )} -
+ + + + + + + + + - ))} - -
DateRecipientDepositedWithdrawnTokenStatusActions
-
+ + + {mockStreams.map((stream) => ( + + {stream.date} + {stream.recipient} + {stream.deposited} {stream.token} + {stream.withdrawn} {stream.token} + {stream.token} + + + {stream.status} + + + + {stream.status === 'Active' && ( + + )} + + + ))} + + +
+ ) : ( + + )} ); }; diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index fe0d5f8..67ea5e6 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -1,9 +1,12 @@ -import React from "react"; +import { NotificationDropdown } from "./NotificationDropdown"; +import { useWallet } from "@/context/wallet-context"; import { Button } from "./ui/Button"; import { ModeToggle } from "./ModeToggle"; import { WalletButton } from "./wallet/WalletButton"; export const Navbar = () => { + const { session, status } = useWallet(); + return (