diff --git a/src/app/api/transaction/bot-history/route.ts b/src/app/api/transaction/history/route.ts similarity index 67% rename from src/app/api/transaction/bot-history/route.ts rename to src/app/api/transaction/history/route.ts index 0046d98..859bf7d 100644 --- a/src/app/api/transaction/bot-history/route.ts +++ b/src/app/api/transaction/history/route.ts @@ -1,9 +1,12 @@ import { NextResponse } from "next/server"; import { headers } from "next/headers"; +// Proxies the backend's single unified GET /transaction/history?type=plan|bot|all +// endpoint. Kept server-side because API_BASE_URL is a server-only env var and +// the backend isn't set up for direct browser CORS. Response is passed through +// as-is (no reshaping) — callers unwrap the `plan`/`bot` fields themselves. export async function GET(request: Request) { try { - // Get authorization header const headersList = await headers(); const authorization = headersList.get("authorization"); @@ -17,20 +20,18 @@ export async function GET(request: Request) { ); } - // Extract token from "Bearer " format const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : authorization; - // Get pagination parameters from URL const url = new URL(request.url); - const limit = url.searchParams.get("limit") || "100"; - const offset = url.searchParams.get("offset") || "0"; + const type = url.searchParams.get("type") || "all"; + const page = url.searchParams.get("page") || "1"; + const limit = url.searchParams.get("limit") || "10"; - // Build API URL with pagination parameters - const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/bot-history`); + const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/history`); + apiUrl.searchParams.set("type", type); + apiUrl.searchParams.set("page", page); apiUrl.searchParams.set("limit", limit); - apiUrl.searchParams.set("offset", offset); - // Make request to external API to get current user's bot (sats send/receive) transactions const response = await fetch(apiUrl.toString(), { method: "GET", headers: { @@ -44,7 +45,7 @@ export async function GET(request: Request) { const errorData = await response.text(); return NextResponse.json( { - error: "Failed to fetch bot transactions", + error: "Failed to fetch transaction history", message: errorData || `HTTP ${response.status}: ${response.statusText}`, }, { status: response.status } @@ -52,10 +53,9 @@ export async function GET(request: Request) { } const data = await response.json(); - return NextResponse.json(data); } catch (error) { - console.error("❌ Error fetching bot transactions:", error); + console.error("❌ Error fetching transaction history:", error); return NextResponse.json( { error: "Internal server error", diff --git a/src/app/api/transaction/list/route.ts b/src/app/api/transaction/list/route.ts deleted file mode 100644 index 9ee4297..0000000 --- a/src/app/api/transaction/list/route.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { NextResponse } from "next/server"; -import { headers } from "next/headers"; - -export async function GET(request: Request) { - try { - // Get authorization header - const headersList = await headers(); - const authorization = headersList.get("authorization"); - - if (!authorization) { - return NextResponse.json( - { - error: "Unauthorized", - message: "Authorization header is required", - }, - { status: 401 } - ); - } - - // Extract token from "Bearer " format - const token = authorization.startsWith("Bearer ") ? authorization.slice(7) : authorization; - - // Get pagination parameters from URL - const url = new URL(request.url); - const page = url.searchParams.get("page") || "1"; - const limit = url.searchParams.get("limit") || "10"; - - // Build API URL with pagination parameters - const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/list`); - apiUrl.searchParams.set("page", page); - apiUrl.searchParams.set("limit", limit); - - // Make request to external API to get current user transactions - const response = await fetch(apiUrl.toString(), { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, - cache: "no-cache", - }); - - if (!response.ok) { - if (response.status === 404) { - // No transactions found - return NextResponse.json( - { - transactions: [], - message: "No transactions found", - }, - { status: 200 } - ); - } - - const errorData = await response.text(); - return NextResponse.json( - { - error: "Failed to fetch transactions", - message: errorData || `HTTP ${response.status}: ${response.statusText}`, - }, - { status: response.status } - ); - } - - const transactions = await response.json(); - - return NextResponse.json({ - transactions, - message: "Transactions fetched successfully", - }); - } catch (error) { - console.error("❌ Error fetching current transactions:", error); - return NextResponse.json( - { - error: "Internal server error", - message: error instanceof Error ? error.message : "Unknown error occurred", - }, - { status: 500 } - ); - } -} diff --git a/src/app/dashboard/activity/page.tsx b/src/app/dashboard/activity/page.tsx index ecce05f..46264ee 100644 --- a/src/app/dashboard/activity/page.tsx +++ b/src/app/dashboard/activity/page.tsx @@ -6,8 +6,7 @@ import { ActivitySearchBar } from "@/components/dashboard/activity/ActivitySearc import { ActivityTabs } from "@/components/dashboard/activity/ActivityTabs"; import { ActivityGroup } from "@/components/dashboard/activity/ActivityGroup"; import { ActivitySkeleton } from "@/components/dashboard/activity/ActivitySkeleton"; -import { useTransactionHistory } from "@/hooks/query/useTransactionHistory"; -import { useBotTransactionHistory } from "@/hooks/query/useBotTransactionHistory"; +import { useTransactionHistory, useBotTransactionHistory } from "@/hooks/query/useTransactionHistory"; import { ACTIVITY_TITLE, getActivityCategory, @@ -17,9 +16,6 @@ import { } from "@/lib/activity"; import { useStore } from "@/lib/store"; -// Tipjar/faucet/gift and tasks have no backend endpoint yet. -const COMING_SOON_CATEGORIES: ActivityCategory[] = ["tasks"]; - export default function ActivityPage() { const { balanceVisible, toggleBalanceVisible } = useStore(); const [search, setSearch] = useState(""); @@ -36,8 +32,8 @@ export default function ActivityPage() { [dca.transactions, botHistory.transactions] ); - const showDca = category !== "transactions"; - const showBotHistory = category === "transactions"; + const showDca = category === "all" || category === "plans"; + const showBotHistory = category === "all" || category === "transactions"; const isLoading = (showDca && dca.isLoading) || (showBotHistory && botHistory.isLoading); const hasNextPage = (showDca && dca.hasNextPage) || (showBotHistory && botHistory.hasNextPage); @@ -47,14 +43,10 @@ export default function ActivityPage() { if (showBotHistory && botHistory.hasNextPage) botHistory.fetchNextPage(); }; - const comingSoon = COMING_SOON_CATEGORIES.includes(category); - const groups = useMemo(() => { - if (comingSoon) return []; - const filtered = activity.filter((item) => { const itemCategory = getActivityCategory(item.type); - if (category === "all" ? itemCategory === "transactions" : itemCategory !== category) { + if (category !== "all" && itemCategory !== category) { return false; } if (!search.trim()) return true; @@ -80,7 +72,7 @@ export default function ActivityPage() { } return Array.from(byDay.entries()); - }, [activity, comingSoon, search, category]); + }, [activity, search, category]); return (
@@ -89,14 +81,13 @@ export default function ActivityPage() { onChange={setSearch} visible={balanceVisible} onToggleVisible={toggleBalanceVisible} + disabled /> {isLoading ? ( - ) : comingSoon ? ( -

Coming soon.

) : groups.length === 0 ? (

No activity found.

) : ( diff --git a/src/components/dashboard/activity/ActivityRow.tsx b/src/components/dashboard/activity/ActivityRow.tsx index e81286f..ef2da37 100644 --- a/src/components/dashboard/activity/ActivityRow.tsx +++ b/src/components/dashboard/activity/ActivityRow.tsx @@ -63,7 +63,7 @@ export function ActivityRow({ item, visible }: ActivityRowProps) { } after={ -
+

void; visible: boolean; onToggleVisible: () => void; + disabled?: boolean; } export function ActivitySearchBar({ @@ -17,18 +19,32 @@ export function ActivitySearchBar({ onChange, visible, onToggleVisible, + disabled = false, }: ActivitySearchBarProps) { return (

- onChange(e.target.value)} /> + onChange(e.target.value)} + disabled={disabled} + /> + + + - - -
); } diff --git a/src/components/dashboard/activity/ActivityTabs.tsx b/src/components/dashboard/activity/ActivityTabs.tsx index c44c6f1..2a30123 100644 --- a/src/components/dashboard/activity/ActivityTabs.tsx +++ b/src/components/dashboard/activity/ActivityTabs.tsx @@ -7,9 +7,8 @@ import type { ActivityCategory } from "@/lib/activity"; const TABS: { value: ActivityCategory; label: string }[] = [ { value: "all", label: "All Activities" }, - { value: "transactions", label: "Transactions" }, - { value: "tasks", label: "Tasks" }, { value: "plans", label: "Plans" }, + { value: "transactions", label: "Transactions" }, ]; export interface ActivityTabsProps { diff --git a/src/hooks/query/useBotTransactionHistory.ts b/src/hooks/query/useBotTransactionHistory.ts deleted file mode 100644 index b773a68..0000000 --- a/src/hooks/query/useBotTransactionHistory.ts +++ /dev/null @@ -1,75 +0,0 @@ -"use client"; - -import { useMemo } from "react"; -import { useInfiniteQuery } from "@tanstack/react-query"; -import { queryKeys } from "@/lib/query-keys"; -import { getAuthTokenFromStorage } from "@/lib/auth"; -import fetchy from "@/lib/fetchy"; - -export interface BotTransaction { - id: number; - time: string; - direction: "incoming" | "outgoing"; - from_id: number; - to_id: number; - from_user?: string; - to_user?: string; - type: string; - amount: number; - amount_lkr: string; - memo?: string; - success: boolean; -} - -interface BotTransactionsResponse { - success: boolean; - count: number; - limit: number; - offset: number; - transactions: BotTransaction[]; - message?: string; -} - -const PAGE_SIZE = 50; - -// Sats sent/received to other Telegram users via the bot, paginated -// oldest page first via offset/limit, sorted oldest-first once flattened. -export function useBotTransactionHistory() { - const authToken = getAuthTokenFromStorage(); - - const query = useInfiniteQuery({ - queryKey: queryKeys.botTransactions, - queryFn: async ({ pageParam }) => { - const data = await fetchy.get( - `/api/transaction/bot-history?limit=${PAGE_SIZE}&offset=${pageParam}`, - { - headers: { Authorization: `Bearer ${authToken}` }, - shouldCache: false, - } - ); - - const transactions = data.success ? data.transactions : []; - const hasMore = data.success && data.offset + transactions.length < data.count; - - return { - transactions, - nextOffset: data.offset + PAGE_SIZE, - hasMore, - }; - }, - initialPageParam: 0, - getNextPageParam: (lastPage) => (lastPage.hasMore ? lastPage.nextOffset : undefined), - enabled: !!authToken, - staleTime: 1000 * 60 * 5, - }); - - const transactions = useMemo( - () => - (query.data?.pages ?? []) - .flatMap((page) => page.transactions) - .sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()), - [query.data] - ); - - return { ...query, transactions }; -} diff --git a/src/hooks/query/useTransactionHistory.ts b/src/hooks/query/useTransactionHistory.ts index dffcd4d..5f6394a 100644 --- a/src/hooks/query/useTransactionHistory.ts +++ b/src/hooks/query/useTransactionHistory.ts @@ -30,13 +30,29 @@ interface RawTransaction { settled?: boolean; } -interface PaginatedTransactions { - transactions: RawTransaction[]; - has_more?: boolean; +export interface BotTransaction { + id: number; + time: string; + direction: "incoming" | "outgoing"; + from_id: number; + to_id: number; + from_user?: string; + to_user?: string; + type: string; + amount: number; + amount_lkr: string; + memo?: string; + success: boolean; } -interface TransactionListResponse { - transactions: RawTransaction[] | PaginatedTransactions; +interface PlanHistoryResponse { + transactions: { plan?: RawTransaction }[]; + has_more: boolean; +} + +interface BotHistoryResponse { + transactions: { bot?: BotTransaction }[]; + has_more: boolean; } const PAGE_SIZE = 20; @@ -69,22 +85,21 @@ export function useTransactionHistory() { const query = useInfiniteQuery({ queryKey: queryKeys.transactions, queryFn: async ({ pageParam }) => { - const data = await fetchy.get( - `/api/transaction/list?page=${pageParam}&limit=${PAGE_SIZE}`, + const data = await fetchy.get( + `/api/transaction/history?type=plan&page=${pageParam}&limit=${PAGE_SIZE}`, { headers: { Authorization: `Bearer ${authToken}` }, shouldCache: false, } ); - const nested = Array.isArray(data.transactions) ? null : data.transactions; - const raw = Array.isArray(data.transactions) - ? data.transactions - : (nested?.transactions ?? []); + const raw = data.transactions + .map((item) => item.plan) + .filter((tx): tx is RawTransaction => Boolean(tx)); return { transactions: raw.map(normalize), - hasMore: nested?.has_more ?? raw.length === PAGE_SIZE, + hasMore: data.has_more, }; }, initialPageParam: 1, @@ -103,3 +118,45 @@ export function useTransactionHistory() { return { ...query, transactions }; } + +// Sats sent/received to other Telegram users via the bot, paginated +// oldest page first via `has_more`, sorted oldest-first once flattened. +export function useBotTransactionHistory() { + const authToken = getAuthTokenFromStorage(); + + const query = useInfiniteQuery({ + queryKey: queryKeys.botTransactions, + queryFn: async ({ pageParam }) => { + const data = await fetchy.get( + `/api/transaction/history?type=bot&page=${pageParam}&limit=${PAGE_SIZE}`, + { + headers: { Authorization: `Bearer ${authToken}` }, + shouldCache: false, + } + ); + + const transactions = data.transactions + .map((item) => item.bot) + .filter((tx): tx is BotTransaction => Boolean(tx)); + + return { + transactions, + hasMore: data.has_more, + }; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => (lastPage.hasMore ? allPages.length + 1 : undefined), + enabled: !!authToken, + staleTime: 1000 * 60 * 5, + }); + + const transactions = useMemo( + () => + (query.data?.pages ?? []) + .flatMap((page) => page.transactions) + .sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()), + [query.data] + ); + + return { ...query, transactions }; +} diff --git a/src/lib/activity.ts b/src/lib/activity.ts index c66913e..87a0c88 100644 --- a/src/lib/activity.ts +++ b/src/lib/activity.ts @@ -1,6 +1,5 @@ import type { ActivityItem, ActivityType } from "@/lib/types"; -import type { DcaTransaction } from "@/hooks/query/useTransactionHistory"; -import type { BotTransaction } from "@/hooks/query/useBotTransactionHistory"; +import type { DcaTransaction, BotTransaction } from "@/hooks/query/useTransactionHistory"; import { fmtActivityTime } from "@/lib/formatters"; type StatusDot = NonNullable; @@ -40,7 +39,7 @@ export const ACTIVITY_TITLE: Record = { membership_reward: "Membership Reward", }; -export type ActivityCategory = "all" | "transactions" | "tasks" | "plans"; +export type ActivityCategory = "all" | "transactions" | "plans"; const TRANSACTION_TYPES: ActivityType[] = [ "sent", @@ -54,7 +53,6 @@ const TRANSACTION_TYPES: ActivityType[] = [ ]; export function getActivityCategory(type: ActivityType): ActivityCategory { - if (type === "tasks_reward") return "tasks"; if (type === "membership_reward") return "plans"; if (TRANSACTION_TYPES.includes(type)) return "transactions"; return "all";