-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Enhance UI components, loading states, and Telegram authentication flow #86
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e136271
d48016e
b21bbac
3846f2a
0d090fd
8a97124
1a1c731
bc59527
f1f074a
b38acf6
1979806
c932dec
b12b89d
9bae4d9
2568542
8faed26
4b47bc5
59ac850
6e24eb9
f52f033
e002037
edbad53
a6b0488
51fe9ed
e42c8da
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | ||
| <title>You're offline — Bitcoin Deepa</title> | ||
| <style> | ||
| body { | ||
| margin: 0; | ||
| min-height: 100vh; | ||
| display: flex; | ||
| flex-direction: column; | ||
| align-items: center; | ||
| justify-content: center; | ||
| gap: 12px; | ||
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | ||
| background: #0b0f14; | ||
| color: #fff; | ||
| text-align: center; | ||
| padding: 24px; | ||
| } | ||
| img { | ||
| width: 64px; | ||
| height: 64px; | ||
| opacity: 0.9; | ||
| } | ||
| p { | ||
| color: #94a3b8; | ||
| font-size: 14px; | ||
| max-width: 280px; | ||
| } | ||
| button { | ||
| margin-top: 8px; | ||
| border: none; | ||
| border-radius: 12px; | ||
| padding: 10px 20px; | ||
| background: #fa7119; | ||
| color: #fff; | ||
| font-size: 14px; | ||
| font-weight: 600; | ||
| } | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <img src="/icons/icon-192.png" alt="" /> | ||
| <p>You're offline. Check your connection and try again.</p> | ||
| <button onclick="location.reload()">Retry</button> | ||
| </body> | ||
| </html> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| const CACHE_VERSION = "v1"; | ||
| const STATIC_CACHE = `bd-static-${CACHE_VERSION}`; | ||
| const OFFLINE_URL = "/offline.html"; | ||
|
|
||
| const PRECACHE_URLS = [OFFLINE_URL, "/icons/icon-192.png", "/icons/icon-512.png", "/favicon.ico"]; | ||
|
|
||
| self.addEventListener("install", (event) => { | ||
| event.waitUntil( | ||
| caches | ||
| .open(STATIC_CACHE) | ||
| .then((cache) => cache.addAll(PRECACHE_URLS)) | ||
| .then(() => self.skipWaiting()) | ||
| ); | ||
| }); | ||
|
|
||
| self.addEventListener("activate", (event) => { | ||
| event.waitUntil( | ||
| caches | ||
| .keys() | ||
| .then((keys) => | ||
| Promise.all(keys.filter((key) => key !== STATIC_CACHE).map((key) => caches.delete(key))) | ||
| ) | ||
| .then(() => self.clients.claim()) | ||
| ); | ||
| }); | ||
|
|
||
| // Navigations (HTML documents) are intentionally network-only, never cached. | ||
| // This app forces Cache-Control: no-store on HTML routes to work around a | ||
| // Telegram Desktop WebView bug that serves stale pages (see next.config.mjs) | ||
| // — caching navigations here would reintroduce that exact bug. We only fall | ||
| // back to a static offline page when the network request fails outright. | ||
| async function handleNavigation(request) { | ||
| try { | ||
| return await fetch(request); | ||
| } catch { | ||
| const cache = await caches.open(STATIC_CACHE); | ||
| return (await cache.match(OFFLINE_URL)) ?? Response.error(); | ||
| } | ||
| } | ||
|
|
||
| // Hashed Next.js build assets and local images are immutable — safe to | ||
| // cache-first indefinitely. | ||
| async function handleStaticAsset(request) { | ||
| const cache = await caches.open(STATIC_CACHE); | ||
| const cached = await cache.match(request); | ||
| if (cached) return cached; | ||
|
|
||
| const response = await fetch(request); | ||
| if (response.ok) cache.put(request, response.clone()); | ||
| return response; | ||
| } | ||
|
|
||
| self.addEventListener("fetch", (event) => { | ||
| const { request } = event; | ||
| if (request.method !== "GET") return; | ||
|
|
||
| const url = new URL(request.url); | ||
| if (url.origin !== self.location.origin) return; | ||
|
|
||
| // Never cache API responses — always fresh, subscription/wallet data included. | ||
| if (url.pathname.startsWith("/api/")) return; | ||
|
|
||
| if (request.mode === "navigate") { | ||
| event.respondWith(handleNavigation(request)); | ||
| return; | ||
| } | ||
|
|
||
| if (url.pathname.startsWith("/_next/static/") || url.pathname.startsWith("/icons/")) { | ||
| event.respondWith(handleStaticAsset(request)); | ||
| } | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| 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 <token>" 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"; | ||
|
|
||
| // Build API URL with pagination parameters | ||
| const apiUrl = new URL(`${process.env.API_BASE_URL}/transaction/bot-history`); | ||
| 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: { | ||
| "Content-Type": "application/json", | ||
| Authorization: `Bearer ${token}`, | ||
| }, | ||
| cache: "no-cache", | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.text(); | ||
| return NextResponse.json( | ||
| { | ||
| error: "Failed to fetch bot transactions", | ||
| message: errorData || `HTTP ${response.status}: ${response.statusText}`, | ||
| }, | ||
| { status: response.status } | ||
| ); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
|
|
||
| return NextResponse.json(data); | ||
| } catch (error) { | ||
| console.error("❌ Error fetching bot transactions:", error); | ||
| return NextResponse.json( | ||
| { | ||
| error: "Internal server error", | ||
| message: error instanceof Error ? error.message : "Unknown error occurred", | ||
| }, | ||
| { status: 500 } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,34 +7,56 @@ 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 { | ||||||||||||||||||||||||||||||||||||||||||
| ACTIVITY_TITLE, | ||||||||||||||||||||||||||||||||||||||||||
| getActivityCategory, | ||||||||||||||||||||||||||||||||||||||||||
| mapBotTransactionToActivityItem, | ||||||||||||||||||||||||||||||||||||||||||
| mapDcaTransactionToActivityItem, | ||||||||||||||||||||||||||||||||||||||||||
| type ActivityCategory, | ||||||||||||||||||||||||||||||||||||||||||
| } from "@/lib/activity"; | ||||||||||||||||||||||||||||||||||||||||||
| import { useStore } from "@/lib/store"; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // Only membership rewards (DCA purchases) are backed by real data today — | ||||||||||||||||||||||||||||||||||||||||||
| // sent/received/tipjar/faucet/gift and tasks have no backend endpoint yet. | ||||||||||||||||||||||||||||||||||||||||||
| const COMING_SOON_CATEGORIES: ActivityCategory[] = ["transactions", "tasks"]; | ||||||||||||||||||||||||||||||||||||||||||
| // 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(""); | ||||||||||||||||||||||||||||||||||||||||||
| const [category, setCategory] = useState<ActivityCategory>("all"); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const { transactions, isLoading, hasNextPage, isFetchingNextPage, fetchNextPage } = | ||||||||||||||||||||||||||||||||||||||||||
| useTransactionHistory(); | ||||||||||||||||||||||||||||||||||||||||||
| const activity = useMemo(() => transactions.map(mapDcaTransactionToActivityItem), [transactions]); | ||||||||||||||||||||||||||||||||||||||||||
| const dca = useTransactionHistory(); | ||||||||||||||||||||||||||||||||||||||||||
| const botHistory = useBotTransactionHistory(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const activity = useMemo( | ||||||||||||||||||||||||||||||||||||||||||
| () => [ | ||||||||||||||||||||||||||||||||||||||||||
| ...dca.transactions.map(mapDcaTransactionToActivityItem), | ||||||||||||||||||||||||||||||||||||||||||
| ...botHistory.transactions.map(mapBotTransactionToActivityItem), | ||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||
| [dca.transactions, botHistory.transactions] | ||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const showDca = category !== "transactions"; | ||||||||||||||||||||||||||||||||||||||||||
| const showBotHistory = category === "transactions"; | ||||||||||||||||||||||||||||||||||||||||||
| const isLoading = | ||||||||||||||||||||||||||||||||||||||||||
| (showDca && dca.isLoading) || (showBotHistory && botHistory.isLoading); | ||||||||||||||||||||||||||||||||||||||||||
| const hasNextPage = (showDca && dca.hasNextPage) || (showBotHistory && botHistory.hasNextPage); | ||||||||||||||||||||||||||||||||||||||||||
| const isFetchingNextPage = dca.isFetchingNextPage || botHistory.isFetchingNextPage; | ||||||||||||||||||||||||||||||||||||||||||
| const fetchNextPage = () => { | ||||||||||||||||||||||||||||||||||||||||||
| if (showDca && dca.hasNextPage) dca.fetchNextPage(); | ||||||||||||||||||||||||||||||||||||||||||
| if (showBotHistory && botHistory.hasNextPage) botHistory.fetchNextPage(); | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+39
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Load both histories in the
Proposed fix- const showDca = category !== "transactions";
- const showBotHistory = category === "transactions";
+ const showDca = category === "all" || category === "plans";
+ const showBotHistory = category === "all" || category === "transactions";📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const comingSoon = COMING_SOON_CATEGORIES.includes(category); | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const groups = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||
| if (comingSoon) return []; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const filtered = activity.filter((item) => { | ||||||||||||||||||||||||||||||||||||||||||
| if (category !== "all" && getActivityCategory(item.type) !== category) return false; | ||||||||||||||||||||||||||||||||||||||||||
| const itemCategory = getActivityCategory(item.type); | ||||||||||||||||||||||||||||||||||||||||||
| if (category === "all" ? itemCategory === "transactions" : itemCategory !== category) { | ||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+56
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Correct the When Proposed fix- if (category === "all" ? itemCategory === "transactions" : itemCategory !== category) {
+ if (category !== "all" && itemCategory !== category) {
return false;
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| if (!search.trim()) return true; | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| const query = search.trim().toLowerCase(); | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -92,7 +114,7 @@ export default function ActivityPage() { | |||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||
| onClick={() => fetchNextPage()} | ||||||||||||||||||||||||||||||||||||||||||
| disabled={isFetchingNextPage} | ||||||||||||||||||||||||||||||||||||||||||
| className="py-3 text-center text-[14px] font-semibold text-[#fa7119] disabled:opacity-50" | ||||||||||||||||||||||||||||||||||||||||||
| className="py-3 text-center text-[14px] font-semibold text-[#fa7119] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed" | ||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||
| {isFetchingNextPage ? "Loading..." : "Load more"} | ||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Use
fetchyfor the upstream request.Line 34 bypasses the repository HTTP wrapper. Migrate this request to
@/lib/fetchyand preserve the authorization header, pagination parameters, and cache behavior.As per coding guidelines, “All HTTP requests must use the custom fetchy wrapper from
@/lib/fetchy.”🤖 Prompt for AI Agents
Source: Coding guidelines