From c1bab8ac4affaa34909ce035eaa925bb6bf49374 Mon Sep 17 00:00:00 2001 From: Face <69168154+face-hh@users.noreply.github.com> Date: Sat, 31 May 2025 13:38:17 +0300 Subject: [PATCH] feat: transactions page --- .../src/routes/api/transactions/+server.ts | 97 +++- website/src/routes/transactions/+page.svelte | 471 ++++++++++++++++++ 2 files changed, 543 insertions(+), 25 deletions(-) create mode 100644 website/src/routes/transactions/+page.svelte diff --git a/website/src/routes/api/transactions/+server.ts b/website/src/routes/api/transactions/+server.ts index 96b4658..56348d5 100644 --- a/website/src/routes/api/transactions/+server.ts +++ b/website/src/routes/api/transactions/+server.ts @@ -2,18 +2,63 @@ import { auth } from '$lib/auth'; import { error, json } from '@sveltejs/kit'; import { db } from '$lib/server/db'; import { transaction, coin } from '$lib/server/db/schema'; -import { eq, desc } from 'drizzle-orm'; +import { eq, desc, asc, and, or, ilike, sql } from 'drizzle-orm'; -export async function GET({ request }) { - const session = await auth.api.getSession({ +export async function GET({ request, url }) { + const authSession = await auth.api.getSession({ headers: request.headers }); - if (!session?.user) { + if (!authSession?.user) { throw error(401, 'Not authenticated'); } - const userId = Number(session.user.id); + const userId = Number(authSession.user.id); + const searchQuery = url.searchParams.get('search') || ''; + const typeFilter = url.searchParams.get('type') || 'all'; + const sortBy = url.searchParams.get('sortBy') || 'timestamp'; + const sortOrder = url.searchParams.get('sortOrder') || 'desc'; + const page = parseInt(url.searchParams.get('page') || '1'); + const limit = Math.min(parseInt(url.searchParams.get('limit') || '20'), 50); + + let whereConditions = and(eq(transaction.userId, userId)); + + if (searchQuery) { + whereConditions = and( + whereConditions, + or( + ilike(coin.name, `%${searchQuery}%`), + ilike(coin.symbol, `%${searchQuery}%`) + ) + ); + } + + if (typeFilter !== 'all') { + whereConditions = and(whereConditions, eq(transaction.type, typeFilter as 'BUY' | 'SELL')); + } + + let sortColumn; + switch (sortBy) { + case 'totalBaseCurrencyAmount': + sortColumn = transaction.totalBaseCurrencyAmount; + break; + case 'quantity': + sortColumn = transaction.quantity; + break; + case 'pricePerCoin': + sortColumn = transaction.pricePerCoin; + break; + default: + sortColumn = transaction.timestamp; + } + + const orderBy = sortOrder === 'asc' ? asc(sortColumn) : desc(sortColumn); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(transaction) + .leftJoin(coin, eq(transaction.coinId, coin.id)) + .where(whereConditions); const transactions = await db .select({ @@ -23,29 +68,31 @@ export async function GET({ request }) { pricePerCoin: transaction.pricePerCoin, totalBaseCurrencyAmount: transaction.totalBaseCurrencyAmount, timestamp: transaction.timestamp, - coinSymbol: coin.symbol, - coinName: coin.name, - coinIcon: coin.icon + coin: { + id: coin.id, + name: coin.name, + symbol: coin.symbol, + icon: coin.icon + } }) .from(transaction) - .innerJoin(coin, eq(transaction.coinId, coin.id)) - .where(eq(transaction.userId, userId)) - .orderBy(desc(transaction.timestamp)) - .limit(100); + .leftJoin(coin, eq(transaction.coinId, coin.id)) + .where(whereConditions) + .orderBy(orderBy) + .limit(limit) + .offset((page - 1) * limit); + + const formattedTransactions = transactions.map(tx => ({ + ...tx, + quantity: Number(tx.quantity), + pricePerCoin: Number(tx.pricePerCoin), + totalBaseCurrencyAmount: Number(tx.totalBaseCurrencyAmount) + })); return json({ - transactions: transactions.map(t => ({ - id: t.id, - type: t.type, - quantity: Number(t.quantity), - pricePerCoin: Number(t.pricePerCoin), - totalBaseCurrencyAmount: Number(t.totalBaseCurrencyAmount), - timestamp: t.timestamp, - coin: { - symbol: t.coinSymbol, - name: t.coinName, - icon: t.coinIcon - } - })) + transactions: formattedTransactions, + total: count, + page, + limit }); } diff --git a/website/src/routes/transactions/+page.svelte b/website/src/routes/transactions/+page.svelte new file mode 100644 index 0000000..3ba440e --- /dev/null +++ b/website/src/routes/transactions/+page.svelte @@ -0,0 +1,471 @@ + + + + +
+
+
+

Transactions

+

+ Complete record of your trading activity and transactions +

+ +
+
+ + +
+ + + + + + +
+
+ +
+ + + + +
+
+ +
+ + + + {currentSortOrderLabel} + + + + {#each sortOrderOptions as option} + + {option.label} + + {/each} + + + +
+ +
+ + + + {currentTypeFilterLabel} + + + + {#each typeFilterOptions as option} + + {option.label} + + {/each} + + + +
+ +
+ + +
+
+
+
+ + +
+
+
+ + + {#if !loading && totalCount > 0} +
+
+ Showing {startIndex}-{endIndex} of {totalCount} transactions +
+ {#if hasActiveFilters} + + {/if} +
+ {/if} + + + + + + + History + + Complete record of your trading activity + + + {#if loading} +
+ {#each Array(10) as _} +
+ {/each} +
+ {:else} + goto(`/coin/${tx.coin.symbol}`)} + emptyIcon={Receipt} + emptyTitle="No transactions found" + emptyDescription={hasActiveFilters + ? 'No transactions match your current filters. Try adjusting your search criteria.' + : "You haven't made any trades yet. Start by buying or selling coins."} + /> + {/if} +
+
+ + + {#if !loading && totalPages > 1} +
+ + {#snippet children({ pages, currentPage: paginationCurrentPage })} + + + + + + + + {#each pages as page (page.key)} + {#if page.type === 'ellipsis'} + + + + {:else} + + + {page.value} + + + {/if} + {/each} + + + + + + + + {/snippet} + +
+ {/if} +