From 76097bb253ad4912ff59e5a5f90eab2b17033365 Mon Sep 17 00:00:00 2001 From: Official-Krish Date: Mon, 1 Jun 2026 17:45:44 +0530 Subject: [PATCH 01/11] [feat]: better error management and toast notis --- web-services/apps/backend/index.ts | 14 +- web-services/apps/backend/redis.ts | 25 +- web-services/apps/backend/routes/depinVm.ts | 38 +- web-services/apps/backend/routes/indexer.ts | 24 +- web-services/apps/backend/routes/user.ts | 10 +- web-services/apps/backend/routes/vm.ts | 22 +- .../apps/backend/routes/vmInstance.ts | 22 +- web-services/apps/backend/utils/helpers.ts | 16 +- .../components/DepinDeployment/EscrowCard.tsx | 15 +- .../DepinDeployment/SettlementSummary.tsx | 8 +- .../components/DepinDeployment/StopDialog.tsx | 13 +- .../components/DepinHostDashboard/Table.tsx | 35 +- .../src/components/DeployImage/Form.tsx | 26 +- .../components/DeployImage/PaymentGateway.tsx | 23 +- .../frontend/src/components/ErrorBoundary.tsx | 85 +++-- .../components/RentVm/NavigationButton.tsx | 12 +- .../src/components/vmDetail/BillingStatus.tsx | 24 +- .../src/components/vmDetail/Header.tsx | 8 +- web-services/apps/frontend/src/lib/api.ts | 64 +++- web-services/apps/frontend/src/lib/depin.ts | 10 +- web-services/apps/frontend/src/lib/toast.ts | 30 ++ web-services/apps/frontend/src/lib/vm.ts | 27 +- .../apps/frontend/src/pages/Admin.tsx | 10 +- .../apps/frontend/src/pages/ClaimRewards.tsx | 21 +- .../apps/frontend/src/pages/Dashboard.tsx | 13 +- .../frontend/src/pages/DepinDeployment.tsx | 8 +- .../apps/frontend/src/pages/HostDashboard.tsx | 24 +- .../apps/frontend/src/pages/HostMachine.tsx | 19 +- .../frontend/src/pages/HostMachineDetails.tsx | 14 +- .../apps/frontend/src/pages/RentVm.tsx | 66 ++-- .../apps/frontend/src/pages/Signin.tsx | 9 +- .../apps/frontend/src/pages/Signup.tsx | 9 +- .../apps/frontend/src/pages/vmDetail.tsx | 16 +- web-services/apps/worker/index.ts | 349 ++++++++---------- .../packages/utilities/authMiddleware.ts | 44 ++- web-services/packages/utilities/errors.ts | 41 ++ web-services/packages/utilities/index.ts | 16 + web-services/packages/utilities/logger.ts | 40 ++ web-services/packages/utilities/package.json | 5 +- web-services/packages/utilities/redis.ts | 6 +- web-services/packages/utilities/response.ts | 75 ++++ 41 files changed, 728 insertions(+), 608 deletions(-) create mode 100644 web-services/apps/frontend/src/lib/toast.ts create mode 100644 web-services/packages/utilities/errors.ts create mode 100644 web-services/packages/utilities/logger.ts create mode 100644 web-services/packages/utilities/response.ts diff --git a/web-services/apps/backend/index.ts b/web-services/apps/backend/index.ts index 0651f8d..b031aaa 100644 --- a/web-services/apps/backend/index.ts +++ b/web-services/apps/backend/index.ts @@ -1,6 +1,7 @@ import express from "express"; import cors from "cors"; import type { NextFunction, Request, Response } from "express"; +import { sendError, logger } from "@axion/utilities"; import UserRouter from "./routes/user"; import vmInstance from "./routes/vmInstance"; import vm from "./routes/vm"; @@ -8,11 +9,14 @@ import depinVM from "./routes/depinVm"; import indexerRouter from "./routes/indexer"; process.on("unhandledRejection", (reason) => { - console.error("Unhandled Rejection:", reason); + logger.error( + "Unhandled Rejection", + reason instanceof Error ? reason : new Error(String(reason)), + ); }); process.on("uncaughtException", (error) => { - console.error("Uncaught Exception:", error); + logger.error("Uncaught Exception", error); }); const app = express(); @@ -26,10 +30,10 @@ app.use("/api/v2/user/depin", depinVM); app.use("/api/v2/indexer", indexerRouter); app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => { - console.error("Unhandled error:", err); - res.status(500).json({ error: "Internal server error" }); + logger.error("Unhandled route error", err); + sendError(res, err); }); app.listen(3000, () => { - console.log("Backend server is running on http://localhost:3000"); + logger.info("Backend server started", { port: 3000 }); }); diff --git a/web-services/apps/backend/redis.ts b/web-services/apps/backend/redis.ts index f25b9ca..575bdbb 100644 --- a/web-services/apps/backend/redis.ts +++ b/web-services/apps/backend/redis.ts @@ -1,7 +1,22 @@ import { createQueue } from "@axion/utilities/redis"; -export const vmQueue = createQueue("vm-termination"); -export const terminateDepinVMQueue = createQueue("terminate-depin-vm"); -export const activateHostQueue = createQueue("changeVMStatus"); -export const initialiseAccount = createQueue("initialise-host-pda"); -export const claimRewardsQueue = createQueue("claim-rewards"); +const DEFAULT_RETRY = { + attempts: 3, + backoff: { type: "exponential" as const, delay: 2000 }, +}; + +export const vmQueue = createQueue("vm-termination", { + defaultJobOptions: DEFAULT_RETRY, +}); +export const terminateDepinVMQueue = createQueue("terminate-depin-vm", { + defaultJobOptions: DEFAULT_RETRY, +}); +export const activateHostQueue = createQueue("changeVMStatus", { + defaultJobOptions: DEFAULT_RETRY, +}); +export const initialiseAccount = createQueue("initialise-host-pda", { + defaultJobOptions: DEFAULT_RETRY, +}); +export const claimRewardsQueue = createQueue("claim-rewards", { + defaultJobOptions: DEFAULT_RETRY, +}); diff --git a/web-services/apps/backend/routes/depinVm.ts b/web-services/apps/backend/routes/depinVm.ts index 0b234eb..0828491 100644 --- a/web-services/apps/backend/routes/depinVm.ts +++ b/web-services/apps/backend/routes/depinVm.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { authMiddleware } from "@axion/utilities/auth"; +import { authMiddleware, logger } from "@axion/utilities"; import prisma from "@axion/db"; import { ChangeVMStatusSchema, @@ -250,8 +250,8 @@ depinVM.post("/deploy", authMiddleware, async (req, res) => { name: txn.name, }); } catch (error) { - console.error("Error deploying image:", error); - fail(res, 500, "Internal server error"); + logger.error("Error deploying image", error); + fail(res, 500, "Internal server error", "DEPLOY_FAILED"); } }); @@ -306,8 +306,8 @@ depinVM.delete("/terminate/:id", authMiddleware, async (req, res) => { ok(res, { message: "Termination request sent successfully" }); } catch (error) { - console.error("Error terminating VM:", error); - fail(res, 500, "Internal server error"); + logger.error("Error terminating VM", error); + fail(res, 500, "Internal server error", "TERMINATE_FAILED"); } }); @@ -404,8 +404,8 @@ depinVM.post("/depinVerification", async (req, res) => { tunnelId: vm.tunnelId, }); } catch (error) { - console.error("Error in depin verification:", error); - fail(res, 500, "Internal server error"); + logger.error("Error in depin verification", error); + fail(res, 500, "Internal server error", "VERIFICATION_FAILED"); } }); @@ -461,8 +461,8 @@ depinVM.post("/register", authMiddleware, async (req, res) => { ok(res, { message: "VM registered successfully", vm }); } catch (error) { - console.error("Error registering VM:", error); - fail(res, 500, "Internal server error"); + logger.error("Error registering VM", error); + fail(res, 500, "Internal server error", "REGISTER_FAILED"); } }); @@ -506,8 +506,8 @@ depinVM.post("/changeVisibility", authMiddleware, async (req, res) => { ok(res, { message: "VM visibility updated successfully" }); } catch (error) { - console.error("Error fetching VM:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching VM", error); + fail(res, 500, "Internal server error", "VM_FETCH_FAILED"); } }); @@ -524,8 +524,8 @@ depinVM.get("/getAll", authMiddleware, async (req, res) => { }); ok(res, vms); } catch (error) { - console.error("Error fetching VMs:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching VMs", error); + fail(res, 500, "Internal server error", "VMS_FETCH_FAILED"); } }); @@ -544,8 +544,8 @@ depinVM.get("/getById", authMiddleware, async (req, res) => { } ok(res, vm); } catch (error) { - console.error("Error fetching VM:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching VM by ID", error); + fail(res, 500, "Internal server error", "VM_BY_ID_FAILED"); } }); @@ -576,8 +576,8 @@ depinVM.post("/claimSOL", authMiddleware, async (req, res) => { }); ok(res, { message: "Claim request submitted" }); } catch (error) { - console.error("Error claiming SOL:", error); - fail(res, 500, "Internal server error"); + logger.error("Error claiming SOL", error); + fail(res, 500, "Internal server error", "CLAIM_SOL_FAILED"); } }); @@ -594,8 +594,8 @@ depinVM.get("/settlement/:id", authMiddleware, async (req, res) => { }); ok(res, { settlement }); } catch (error) { - console.error("Error fetching settlement:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching settlement", error); + fail(res, 500, "Internal server error", "SETTLEMENT_FETCH_FAILED"); } }); diff --git a/web-services/apps/backend/routes/indexer.ts b/web-services/apps/backend/routes/indexer.ts index bc47a4c..0d120da 100644 --- a/web-services/apps/backend/routes/indexer.ts +++ b/web-services/apps/backend/routes/indexer.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import type { Request, Response } from "express"; import prisma from "@axion/db"; +import { logger } from "@axion/utilities"; const router = Router(); @@ -32,14 +33,24 @@ interface IndexerEvent { router.post("/webhook", async (req: Request, res: Response) => { const token = req.headers["x-indexer-token"]; if (token !== INDEXER_TOKEN) { - res.status(401).json({ error: "Unauthorized" }); + res + .status(401) + .json({ + success: false, + error: { code: "UNAUTHORIZED", message: "Unauthorized" }, + }); return; } const event: IndexerEvent = req.body; if (!event.instruction || !event.signature) { - res.status(400).json({ error: "Invalid event payload" }); + res + .status(400) + .json({ + success: false, + error: { code: "INVALID_PAYLOAD", message: "Invalid event payload" }, + }); return; } @@ -61,8 +72,13 @@ router.post("/webhook", async (req: Request, res: Response) => { await handleInstruction(event); res.status(200).json({ received: true }); } catch (error) { - console.error(`[Indexer] Error handling ${event.instruction}:`, error); - res.status(500).json({ error: "Failed to process event" }); + logger.error(`[Indexer] Error handling ${event.instruction}`, error); + res + .status(500) + .json({ + success: false, + error: { code: "INDEXER_ERROR", message: "Failed to process event" }, + }); } }); diff --git a/web-services/apps/backend/routes/user.ts b/web-services/apps/backend/routes/user.ts index 1ddf9e9..9f97515 100644 --- a/web-services/apps/backend/routes/user.ts +++ b/web-services/apps/backend/routes/user.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import { SignInSchema, SignUpSchema } from "@axion/types"; import prisma from "@axion/db"; -import { authMiddleware } from "@axion/utilities/auth"; +import { authMiddleware, logger } from "@axion/utilities"; import { fail, getUserOr404, @@ -41,8 +41,8 @@ UserRouter.post("/signup", async (req, res) => { return; } } - console.error("Error during signup:", error); - fail(res, 500, "Internal server error"); + logger.error("Error during signup", error); + fail(res, 500, "Internal server error", "SIGNUP_FAILED"); } }); @@ -64,8 +64,8 @@ UserRouter.post("/login", async (req, res) => { token: signToken({ userId: user.id }, "1Day"), }); } catch (error) { - console.error("Error during login:", error); - fail(res, 500, "Internal server error"); + logger.error("Error during login", error); + fail(res, 500, "Internal server error", "LOGIN_FAILED"); } }); diff --git a/web-services/apps/backend/routes/vm.ts b/web-services/apps/backend/routes/vm.ts index 39444aa..92ecac2 100644 --- a/web-services/apps/backend/routes/vm.ts +++ b/web-services/apps/backend/routes/vm.ts @@ -2,7 +2,7 @@ import "dotenv/config"; import prisma from "@axion/db"; import { Router } from "express"; import axios from "axios"; -import { authMiddleware } from "@axion/utilities/auth"; +import { authMiddleware, logger } from "@axion/utilities"; import { EscrowTopUpSchema } from "@axion/types"; import { vmQueue } from "../redis"; import { fail, getUserOr404, ok, MINUTE_MS } from "../utils/helpers"; @@ -33,8 +33,8 @@ vm.get("/calculatePrice", authMiddleware, async (req, res) => { const solPrice = await getSolPrice(); ok(res, { price: totalPrice / solPrice }); } catch (error) { - console.error("Error calculating price:", error); - fail(res, 500, "Internal server error"); + logger.error("Error calculating price", error); + fail(res, 500, "Internal server error", "PRICE_CALC_FAILED"); } }); @@ -43,8 +43,8 @@ vm.get("/getVMTypes", authMiddleware, async (req, res) => { const vmTypes = await prisma.vMTypes.findMany(); ok(res, vmTypes); } catch (error) { - console.error("Error fetching VM types:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching VM types", error); + fail(res, 500, "Internal server error", "VM_TYPES_FAILED"); } }); @@ -60,8 +60,8 @@ vm.get("/getAll", authMiddleware, async (req, res) => { }); ok(res, vms); } catch (error) { - console.error("Error fetching VMs:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching VMs", error); + fail(res, 500, "Internal server error", "VM_FETCH_FAILED"); } }); @@ -77,8 +77,8 @@ vm.get("/checkNameAvailability", authMiddleware, async (req, res) => { }); ok(res, { available: !existingVM }); } catch (error) { - console.error("Error checking name availability:", error); - fail(res, 500, "Internal server error"); + logger.error("Error checking name availability", error); + fail(res, 500, "Internal server error", "NAME_CHECK_FAILED"); } }); @@ -140,8 +140,8 @@ vm.post("/topup", authMiddleware, async (req, res) => { ok(res, { msg: "Top up successful", vm: updatedVM }); } catch (error) { - console.error("Error during top up:", error); - fail(res, 500, "Internal server error"); + logger.error("Error during top up", error); + fail(res, 500, "Internal server error", "TOPUP_FAILED"); } }); diff --git a/web-services/apps/backend/routes/vmInstance.ts b/web-services/apps/backend/routes/vmInstance.ts index 0f11461..cb2e651 100644 --- a/web-services/apps/backend/routes/vmInstance.ts +++ b/web-services/apps/backend/routes/vmInstance.ts @@ -1,6 +1,6 @@ import "dotenv/config"; import { Router } from "express"; -import { authMiddleware } from "@axion/utilities/auth"; +import { authMiddleware, logger } from "@axion/utilities"; import { VmInstanceSchema } from "@axion/types"; import prisma from "@axion/db"; import { createInstance } from "../utils/createVm"; @@ -125,8 +125,8 @@ vmInstance.post("/create", authMiddleware, async (req, res) => { PrivateKey: transaction.privateKey, }); } catch (error) { - console.error("Error during VM instance creation:", error); - fail(res, 500, "Internal server error"); + logger.error("Error during VM instance creation", error); + fail(res, 500, "Internal server error", "VM_CREATE_FAILED"); } }); @@ -170,8 +170,8 @@ vmInstance.get("/pollStatus", authMiddleware, async (req, res) => { }); ok(res, { vmId, status }); } catch (e) { - console.error("Error during VM status polling:", e); - fail(res, 500, "Internal server error"); + logger.error("Error during VM status polling", e); + fail(res, 500, "Internal server error", "VM_POLL_FAILED"); } }); @@ -204,8 +204,8 @@ vmInstance.delete("/destroy", authMiddleware, async (req, res) => { remainingTime: remainingTime > 0 ? remainingTime : 0, }); } catch (error) { - console.error("Error during VM instance deletion:", error); - fail(res, 500, "Internal server error"); + logger.error("Error during VM instance deletion", error); + fail(res, 500, "Internal server error", "VM_DELETE_FAILED"); } }); @@ -218,8 +218,8 @@ vmInstance.get("/getAll", authMiddleware, async (req, res) => { }); ok(res, { vms }); } catch (error) { - console.error("Error fetching VM instances:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching VM instances", error); + fail(res, 500, "Internal server error", "VM_FETCH_FAILED"); } }); @@ -241,8 +241,8 @@ vmInstance.get("/getDetails", authMiddleware, async (req, res) => { } ok(res, { vmInstance }); } catch (error) { - console.error("Error fetching VM instance details:", error); - fail(res, 500, "Internal server error"); + logger.error("Error fetching VM instance details", error); + fail(res, 500, "Internal server error", "VM_DETAILS_FAILED"); } }); diff --git a/web-services/apps/backend/utils/helpers.ts b/web-services/apps/backend/utils/helpers.ts index fdabfed..65e17d4 100644 --- a/web-services/apps/backend/utils/helpers.ts +++ b/web-services/apps/backend/utils/helpers.ts @@ -11,18 +11,26 @@ export function ok(res: Response, data: unknown, status = 200) { res.status(status).json(data); } -export function fail(res: Response, status: number, msg: string) { - res.status(status).json({ error: msg }); +export function fail( + res: Response, + status: number, + msg: string, + code = "ERROR", +) { + res.status(status).json({ + success: false, + error: { code, message: msg }, + }); } export async function getUserOr404(res: Response, userId?: string) { if (!userId) { - fail(res, 400, "User ID is required"); + fail(res, 400, "User ID is required", "MISSING_USER_ID"); return null; } const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user) { - fail(res, 404, "User not found"); + fail(res, 404, "User not found", "USER_NOT_FOUND"); return null; } return user; diff --git a/web-services/apps/frontend/src/components/DepinDeployment/EscrowCard.tsx b/web-services/apps/frontend/src/components/DepinDeployment/EscrowCard.tsx index 2820fb6..d75fe47 100644 --- a/web-services/apps/frontend/src/components/DepinDeployment/EscrowCard.tsx +++ b/web-services/apps/frontend/src/components/DepinDeployment/EscrowCard.tsx @@ -16,8 +16,7 @@ import { useState } from "react"; import { toast } from "sonner"; import { TopUpEscrowSession } from "@/lib/Escrow"; import { useAnchorWallet } from "@solana/wallet-adapter-react"; -import axios from "axios"; -import { BACKEND_URL } from "@/config"; +import { api } from "@/lib/api"; import { useTxConfirm } from "@/lib/useTxConfirm"; import type { VM } from "types/vm"; @@ -48,13 +47,11 @@ export const EscrowCard = ({ vm }: { vm: VM }) => { watch(tx.signature, { onConfirmed: async () => { try { - await axios.post( - `${BACKEND_URL}/vm/topup`, - { id: vm.id, instanceId: vm.instanceId, amount }, - { - headers: { Authorization: `${localStorage.getItem("token")}` }, - }, - ); + await api.post("/vm/topup", { + id: vm.id, + instanceId: vm.instanceId, + amount, + }); toast.success("Escrow topped up!", { position: "top-right" }); } catch { toast.error("On-chain confirmed but backend update failed", { diff --git a/web-services/apps/frontend/src/components/DepinDeployment/SettlementSummary.tsx b/web-services/apps/frontend/src/components/DepinDeployment/SettlementSummary.tsx index 63cae78..20b0945 100644 --- a/web-services/apps/frontend/src/components/DepinDeployment/SettlementSummary.tsx +++ b/web-services/apps/frontend/src/components/DepinDeployment/SettlementSummary.tsx @@ -1,8 +1,7 @@ import { motion } from "motion/react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Receipt, Clock, ExternalLink } from "lucide-react"; -import axios from "axios"; -import { BACKEND_URL } from "@/config"; +import { api } from "@/lib/api"; import { useEffect, useState } from "react"; interface SettlementData { @@ -22,10 +21,7 @@ export const SettlementSummary = ({ vmId }: { vmId: string }) => { useEffect(() => { const fetch = async () => { try { - const res = await axios.get( - `${BACKEND_URL}/user/depin/settlement/${vmId}`, - { headers: { Authorization: `${localStorage.getItem("token")}` } }, - ); + const res = await api.get(`/user/depin/settlement/${vmId}`); setSettlement(res.data.settlement); } catch { /* not settled yet */ diff --git a/web-services/apps/frontend/src/components/DepinDeployment/StopDialog.tsx b/web-services/apps/frontend/src/components/DepinDeployment/StopDialog.tsx index 743ad50..ae5cc74 100644 --- a/web-services/apps/frontend/src/components/DepinDeployment/StopDialog.tsx +++ b/web-services/apps/frontend/src/components/DepinDeployment/StopDialog.tsx @@ -15,8 +15,7 @@ import { Clock, } from "lucide-react"; import { toast } from "sonner"; -import axios from "axios"; -import { BACKEND_URL } from "@/config"; +import { api } from "@/lib/api"; type Stage = | "idle" @@ -56,10 +55,7 @@ export const StopDialog = ({ setStage("submitting"); setErrorMsg(""); try { - const res = await axios.delete( - `${BACKEND_URL}/user/depin/terminate/${vmId}`, - { headers: { Authorization: `${localStorage.getItem("token")}` } }, - ); + const res = await api.delete(`/user/depin/terminate/${vmId}`); if (res.status !== 200) { setStage("failed"); @@ -77,10 +73,7 @@ export const StopDialog = ({ pollRef.current = setInterval(async () => { attempts++; try { - const sr = await axios.get( - `${BACKEND_URL}/user/depin/settlement/${vmId}`, - { headers: { Authorization: `${localStorage.getItem("token")}` } }, - ); + const sr = await api.get(`/user/depin/settlement/${vmId}`); if (sr.data.settlement) { stopPolling(); setStage("done"); diff --git a/web-services/apps/frontend/src/components/DepinHostDashboard/Table.tsx b/web-services/apps/frontend/src/components/DepinHostDashboard/Table.tsx index 3a75f09..56ff850 100644 --- a/web-services/apps/frontend/src/components/DepinHostDashboard/Table.tsx +++ b/web-services/apps/frontend/src/components/DepinHostDashboard/Table.tsx @@ -19,8 +19,7 @@ import { Button } from "../ui/button"; import { motion } from "motion/react"; import { Badge } from "../ui/badge"; import { toast } from "sonner"; -import axios from "axios"; -import { BACKEND_URL } from "@/config"; +import { api } from "@/lib/api"; import { useState } from "react"; import { useNavigate } from "react-router-dom"; import { @@ -91,20 +90,12 @@ export const DashboardTable = ({ return; } try { - const res = await axios.post( - `${BACKEND_URL}/user/depin/changeVisibility`, - { - id: machineId, - pubKey: wallet?.publicKey?.toBase58(), - status: !isActive, - Key: key, - }, - { - headers: { - Authorization: `${localStorage.getItem("token")}`, - }, - }, - ); + const res = await api.post("/user/depin/changeVisibility", { + id: machineId, + pubKey: wallet?.publicKey?.toBase58(), + status: !isActive, + Key: key, + }); if (res.status === 200) { toast.success( `Machine ${!isActive ? "activated" : "deactivated"} successfully`, @@ -118,8 +109,7 @@ export const DashboardTable = ({ ); setMachines(updatedMachines); } - } catch (error) { - console.error("Error changing machine status:", error); + } catch { toast.error("Failed to change machine status. Please try again."); } }; @@ -128,11 +118,10 @@ export const DashboardTable = ({ if (!wallet?.publicKey) return; setClaimingId(machineId); try { - const res = await axios.post( - `${BACKEND_URL}/user/depin/claimSOL`, - { id: machineId, pubKey: wallet.publicKey.toBase58() }, - { headers: { Authorization: `${localStorage.getItem("token")}` } }, - ); + const res = await api.post("/user/depin/claimSOL", { + id: machineId, + pubKey: wallet.publicKey.toBase58(), + }); if (res.status === 200) { toast.success("Claim request submitted. Rewards will arrive shortly."); } diff --git a/web-services/apps/frontend/src/components/DeployImage/Form.tsx b/web-services/apps/frontend/src/components/DeployImage/Form.tsx index f408e9d..271dc34 100644 --- a/web-services/apps/frontend/src/components/DeployImage/Form.tsx +++ b/web-services/apps/frontend/src/components/DeployImage/Form.tsx @@ -18,8 +18,7 @@ import { SelectValue, } from "@/components/ui/select"; import { toast } from "sonner"; -import axios from "axios"; -import { BACKEND_URL } from "@/config"; +import { api } from "@/lib/api"; import type { Machine } from "types/depinMachines"; import { motion } from "motion/react"; @@ -54,20 +53,12 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); try { - const res = await axios.post( - `${BACKEND_URL}/user/depin/findVM`, - { - cpu: formData.cpu, - ram: formData.ram, - diskSize: formData.diskSize, - dockerImage: formData.dockerImage, - }, - { - headers: { - Authorization: `${localStorage.getItem("token")}`, - }, - }, - ); + const res = await api.post("/user/depin/findVM", { + cpu: formData.cpu, + ram: formData.ram, + diskSize: formData.diskSize, + dockerImage: formData.dockerImage, + }); if (res.status === 200) { toast.success("VM found successfully!"); setVm(res.data.vm); @@ -75,8 +66,7 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { } else { toast.error("Failed to find VM. Please try again."); } - } catch (error) { - console.error("Error submitting form:", error); + } catch { toast.error("Failed to find vm. Please try again."); } }; diff --git a/web-services/apps/frontend/src/components/DeployImage/PaymentGateway.tsx b/web-services/apps/frontend/src/components/DeployImage/PaymentGateway.tsx index 80cbcb0..42917e6 100644 --- a/web-services/apps/frontend/src/components/DeployImage/PaymentGateway.tsx +++ b/web-services/apps/frontend/src/components/DeployImage/PaymentGateway.tsx @@ -13,8 +13,7 @@ import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; import { Button } from "../ui/button"; import { useState } from "react"; import { toast } from "sonner"; -import axios from "axios"; -import { BACKEND_URL } from "@/config"; +import { api } from "@/lib/api"; import { useNavigate } from "react-router-dom"; import type { Machine } from "types/depinMachines"; import { StartRentalSessionWithEscrow } from "@/lib/Escrow"; @@ -87,18 +86,14 @@ export const PaymentGateway = ({ PricePerHour > 0 ? Math.max(1, Math.floor((escrowAmount / PricePerHour) * 60)) : 60; // default 60 minutes if price not set - const res = await axios.post( - `${BACKEND_URL}/user/depin/deploy`, - { - ...form, - escrowAmount, - endTime, - VmId: vmId, - id, - ports: form.ports.split(",").map((p) => p.trim()), - }, - { headers: { Authorization: `${localStorage.getItem("token")}` } }, - ); + const res = await api.post("/user/depin/deploy", { + ...form, + escrowAmount, + endTime, + VmId: vmId, + id, + ports: form.ports.split(",").map((p) => p.trim()), + }); if (res.status === 200) { toast.success("Payment successful! Your VM is being deployed."); } else { diff --git a/web-services/apps/frontend/src/components/ErrorBoundary.tsx b/web-services/apps/frontend/src/components/ErrorBoundary.tsx index 71ec90b..bd060c2 100644 --- a/web-services/apps/frontend/src/components/ErrorBoundary.tsx +++ b/web-services/apps/frontend/src/components/ErrorBoundary.tsx @@ -1,55 +1,60 @@ import { Component } from "react"; import { Button } from "@/components/ui/button"; +import { toast } from "sonner"; interface Props { - children: React.ReactNode; + children: React.ReactNode; } interface State { - hasError: boolean; - error: Error | null; + hasError: boolean; + error: Error | null; } export class ErrorBoundary extends Component { - constructor(props: Props) { - super(props); - this.state = { hasError: false, error: null }; - } + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null }; + } - static getDerivedStateFromError(error: Error): State { - return { hasError: true, error }; - } + static getDerivedStateFromError(error: Error): State { + return { hasError: true, error }; + } - componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { - console.error("ErrorBoundary caught:", error, errorInfo); - } + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("ErrorBoundary caught:", error, errorInfo); + toast.error("An unexpected error occurred. Please try again.", { + position: "bottom-right", + duration: 5000, + }); + } - render() { - if (this.state.hasError) { - return ( -
-
-
- ! -
-

Something went wrong

-

- {this.state.error?.message || "An unexpected error occurred"} -

- -
-
- ); - } - - return this.props.children; + render() { + if (this.state.hasError) { + return ( +
+
+
+ ! +
+

Something went wrong

+

+ {this.state.error?.message || "An unexpected error occurred"} +

+ +
+
+ ); } + + return this.props.children; + } } diff --git a/web-services/apps/frontend/src/components/RentVm/NavigationButton.tsx b/web-services/apps/frontend/src/components/RentVm/NavigationButton.tsx index 006aa0b..7739565 100644 --- a/web-services/apps/frontend/src/components/RentVm/NavigationButton.tsx +++ b/web-services/apps/frontend/src/components/RentVm/NavigationButton.tsx @@ -10,8 +10,7 @@ import { } from "../ui/dialog"; import { motion } from "motion/react"; import { toast } from "sonner"; -import axios from "axios"; -import { BACKEND_URL } from "@/config"; +import { api } from "@/lib/api"; interface NavigationButtonProps { currentStep: number; @@ -93,14 +92,7 @@ export const NavigationButton = ({ + {/* CTA / Wallet */}
@@ -127,6 +165,63 @@ export const Appbar = () => { onClose={() => setUserDropdownOpen(false)} />
+ + {/* Mobile menu */} + + {mobileMenuOpen && ( + <> + setMobileMenuOpen(false)} + aria-hidden="true" + /> + +
+ + Menu + + +
+ +
+ + )} +
); diff --git a/web-services/apps/frontend/src/components/DeployImage/Form.tsx b/web-services/apps/frontend/src/components/DeployImage/Form.tsx index 271dc34..f137c5e 100644 --- a/web-services/apps/frontend/src/components/DeployImage/Form.tsx +++ b/web-services/apps/frontend/src/components/DeployImage/Form.tsx @@ -21,6 +21,8 @@ import { toast } from "sonner"; import { api } from "@/lib/api"; import type { Machine } from "types/depinMachines"; import { motion } from "motion/react"; +import { Loader2 } from "lucide-react"; +import { useState } from "react"; interface FormProps { formData: { @@ -50,8 +52,47 @@ interface FormProps { } export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { + const [isSearching, setIsSearching] = useState(false); + const [errors, setErrors] = useState>({}); + + const validate = () => { + const newErrors: Record = {}; + if (!formData.appName) { + newErrors.appName = "Application name is required"; + } + if (!formData.dockerImage) { + newErrors.dockerImage = "Docker image is required"; + } else if ( + !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*(:[a-zA-Z0-9._-]+)?$/.test( + formData.dockerImage, + ) + ) { + newErrors.dockerImage = "Invalid image format (e.g. nginx:latest)"; + } + if (!formData.cpu) { + newErrors.cpu = "CPU is required"; + } else if (Number(formData.cpu) <= 0) { + newErrors.cpu = "CPU must be greater than 0"; + } + if (!formData.ram) { + newErrors.ram = "RAM is required"; + } else if (Number(formData.ram) <= 0) { + newErrors.ram = "RAM must be greater than 0"; + } + if (!formData.diskSize) { + newErrors.diskSize = "Disk size is required"; + } else if (Number(formData.diskSize) <= 0) { + newErrors.diskSize = "Disk size must be greater than 0"; + } + return newErrors; + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + const validationErrors = validate(); + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) return; + setIsSearching(true); try { const res = await api.post("/user/depin/findVM", { cpu: formData.cpu, @@ -69,6 +110,7 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { } catch { toast.error("Failed to find vm. Please try again."); } + setIsSearching(false); }; return ( { -
+ {/* Basic Info */}
@@ -100,11 +146,15 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { id="appName" placeholder="my-awesome-app" value={formData.appName} - onChange={(e) => - setFormData({ ...formData, appName: e.target.value }) - } + onChange={(e) => { + setFormData({ ...formData, appName: e.target.value }); + setErrors((prev) => ({ ...prev, appName: "" })); + }} required /> + {errors.appName && ( +

{errors.appName}

+ )}
@@ -115,11 +165,17 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { id="dockerImage" placeholder="nginx:latest or myregistry/myapp:v1.0" value={formData.dockerImage} - onChange={(e) => - setFormData({ ...formData, dockerImage: e.target.value }) - } + onChange={(e) => { + setFormData({ ...formData, dockerImage: e.target.value }); + setErrors((prev) => ({ ...prev, dockerImage: "" })); + }} required /> + {errors.dockerImage && ( +

+ {errors.dockerImage} +

+ )}
@@ -157,9 +213,10 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { + {errors.cpu && ( +

{errors.cpu}

+ )}
@@ -184,9 +244,10 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { + {errors.ram && ( +

{errors.ram}

+ )}
@@ -214,12 +278,18 @@ export const Form = ({ formData, setFormData, setVm, setStep }: FormProps) => { id="storage" type="number" value={formData.diskSize} - onChange={(e) => - setFormData({ ...formData, diskSize: e.target.value }) - } + onChange={(e) => { + setFormData({ ...formData, diskSize: e.target.value }); + setErrors((prev) => ({ ...prev, diskSize: "" })); + }} min="1" max="1000" /> + {errors.diskSize && ( +

+ {errors.diskSize} +

+ )}
- ); - } - return (
( diff --git a/web-services/apps/frontend/src/pages/ClaimRewards.tsx b/web-services/apps/frontend/src/pages/ClaimRewards.tsx index 2a8e8f9..48a9e7d 100644 --- a/web-services/apps/frontend/src/pages/ClaimRewards.tsx +++ b/web-services/apps/frontend/src/pages/ClaimRewards.tsx @@ -1,9 +1,10 @@ -import { useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { motion, useInView } from "motion/react"; import { useWallet } from "@solana/wallet-adapter-react"; import { Link } from "react-router-dom"; import { type Machine } from "../../types/depinMachines"; import { toast } from "sonner"; +import { RefreshCw, AlertCircle } from "lucide-react"; import { api } from "@/lib/api"; // per-machine claim status @@ -96,30 +97,39 @@ export default function ClaimRewards() { const wallet = useWallet(); const [machines, setMachines] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); // per-machine claim status const [claimStatuses, setClaimStatuses] = useState< Record >({}); - useEffect(() => { - if (!wallet.publicKey) return; - api - .get(`/user/depin/getAll?userPublicKey=${wallet.publicKey.toBase58()}`) - .then((r) => setMachines(r.data)) - .catch(() => {}) - .finally(() => setLoading(false)); + const fetchMachines = useCallback(async () => { + setLoading(true); + setError(false); + try { + const r = await api.get( + `/user/depin/getAll?userPublicKey=${wallet.publicKey!.toBase58()}`, + ); + setMachines(r.data); + } catch { + setError(true); + } + setLoading(false); }, [wallet.publicKey]); + useEffect(() => { + fetchMachines(); + }, [fetchMachines]); + const setStatus = (id: string, s: ClaimStatus) => setClaimStatuses((p) => ({ ...p, [id]: s })); const handleClaim = async (id: string) => { - if (!wallet.publicKey) return; setStatus(id, "submitted"); try { const res = await api.post("/user/depin/claimSOL", { id, - pubKey: wallet.publicKey.toBase58(), + pubKey: wallet.publicKey!.toBase58(), }); if (res.status === 200) { setStatus(id, "confirmed"); @@ -133,32 +143,13 @@ export default function ClaimRewards() { } }; - if (!wallet.publicKey || !localStorage.getItem("token")) { - return ( -
- -

- Sign in to claim rewards -

- - Sign in → - -
-
- ); - } - const total = machines.reduce((s, m) => s + m.claimedSOL, 0); return ( -
+
+ ) : error ? ( +
+ +

+ Failed to load machines. +

+ +
) : machines.length === 0 ? (

diff --git a/web-services/apps/frontend/src/pages/Dashboard.tsx b/web-services/apps/frontend/src/pages/Dashboard.tsx index d07e8b2..4e4df56 100644 --- a/web-services/apps/frontend/src/pages/Dashboard.tsx +++ b/web-services/apps/frontend/src/pages/Dashboard.tsx @@ -3,7 +3,13 @@ import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { StatusBadge } from "@/components/StatusBadge"; -import { Plus, Search, ExternalLink } from "lucide-react"; +import { + Plus, + Search, + ExternalLink, + RefreshCw, + AlertCircle, +} from "lucide-react"; import { Link, useNavigate } from "react-router-dom"; import { api } from "@/lib/api"; import { type VM } from "../../types/vm"; @@ -13,14 +19,37 @@ import { getVmDetails } from "@/lib/vm"; import { toast } from "sonner"; import { useIndexerEvents } from "@/lib/useIndexerEvents"; +function SkeletonCard() { + return ( +

+
+
+
+
+
+ {[...Array(4)].map((_, i) => ( +
+
+
+
+ ))} +
+
+
+
+
+ ); +} + export function Dashboard() { const [searchQuery, setSearchQuery] = useState(""); const [filter, setFilter] = useState("all"); const [vms, setVMs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); const navigate = useNavigate(); const wallet = useWallet(); - // Real-time VM status updates from indexer useIndexerEvents({ account: wallet.publicKey?.toBase58(), onEvent: (event) => { @@ -57,19 +86,20 @@ export function Dashboard() { }, }); - useEffect(() => { - if (!wallet || !localStorage.getItem("token")) { - return; + const fetchVMs = async () => { + setLoading(true); + setError(false); + try { + const res = await api.get("/vmInstance/getAll"); + setVMs(res.data.vms); + } catch { + setError(true); } - const getVMs = async () => { - try { - const res = await api.get("/vmInstance/getAll"); - setVMs(res.data.vms); - } catch { - /* toast handled by api interceptor */ - } - }; - getVMs(); + setLoading(false); + }; + + useEffect(() => { + fetchVMs(); }, [wallet]); const filteredVMs = vms.filter((vm) => { @@ -80,30 +110,11 @@ export function Dashboard() { return matchesSearch && matchesFilter; }); - if (!wallet.connected || !localStorage.getItem("token")) { - return ( -
- -

Please SignIn

-

- Please connect your wallet and signInto manage your virtual - machines. -

- - - -
-
- ); - } - return ( -
- {/* Header */} +
- {/* Controls */} setSearchQuery(e.target.value)} className="pl-10" + aria-label="Search" />
@@ -164,98 +175,140 @@ export function Dashboard() {
- {/* VM List */} -
- {filteredVMs.map((vm, index) => ( - { - if (vm.status === "RUNNING") { - navigate( - vm.provider === "LOCAL" - ? `/depin/deployment/${vm.id}` - : `/vm/${vm.id}`, - ); - } else { - toast.info("This VM is not running.", { - position: "top-right", - }); - } - }} - > -
-
-
-
- {vm.name} -
- -
- -
-
+ {error && ( + + +

Failed to load VMs

+

+ Something went wrong while fetching your virtual machines. +

+ +
+ )} -
-
- - {vm.region} - - Region -
-
- - {vm.VMConfig?.os || vm.VMImage?.os || "N/A"} - - Operating System -
-
- - {vm.provider === "LOCAL" - ? `${vm.VMImage?.cpu || 0}vCPUs • ${vm.VMImage?.ram || 0}Gb Ram` - : `${getVmDetails(vm.VMConfig?.machineType).cpu}vCPUs • ${getVmDetails(vm.VMConfig?.machineType).ram}Gb Ram`} - - Resources + {loading && !error && ( +
+ {[...Array(3)].map((_, i) => ( + + ))} +
+ )} + + {!loading && !error && ( +
+ {filteredVMs.map((vm, index) => ( + { + if (vm.status === "RUNNING") { + navigate( + vm.provider === "LOCAL" + ? `/depin/deployment/${vm.id}` + : `/vm/${vm.id}`, + ); + } else { + toast.info("This VM is not running.", { + position: "top-right", + }); + } + }} + > +
+
+
+
+ {vm.name} +
+ +
+ +
-
- - {Number(vm.price).toFixed(6)} SOL - - Rented for + +
+
+ + {vm.region} + + Region +
+
+ + {vm.VMConfig?.os || vm.VMImage?.os || "N/A"} + + Operating System +
+
+ + {vm.provider === "LOCAL" + ? `${vm.VMImage?.cpu || 0} vCPUs • ${vm.VMImage?.ram || 0} GB RAM` + : `${getVmDetails(vm.VMConfig?.machineType).cpu} vCPUs • ${getVmDetails(vm.VMConfig?.machineType).ram} GB RAM`} + + Resources +
+
+ + {Number(vm.price).toFixed(6)} SOL + + Rented for +
-
-
- - Created On: {formatter.format(new Date(vm.createdAt))} - - {vm.provider !== "LOCAL" && ( - Instance Id: {vm.instanceId} - )} - {vm.provider === "LOCAL" && ( - - Image Deployed: {vm.VMImage?.dockerImage} +
+ + Created On: {formatter.format(new Date(vm.createdAt))} - )} -
- - ))} -
+ {vm.provider !== "LOCAL" && ( + + Instance Id: {vm.instanceId} + + )} + {vm.provider === "LOCAL" && ( + + Image Deployed: {vm.VMImage?.dockerImage} + + )} +
+ + ))} +
+ )} - {filteredVMs.length === 0 && ( + {!loading && !error && filteredVMs.length === 0 && ( (null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); const isTerminated = vm?.status === "DELETED" || vm?.status === "TERMINATED"; useIndexerEvents({ @@ -37,33 +39,62 @@ export function DepinDeployment() { }, }); - useEffect(() => { + const fetchDeployment = useCallback(async () => { if (!id) return; - const fetch = async () => { - try { - const res = await api.get(`/vmInstance/getDetails?id=${id}`); - setVm(res.data.vmInstance); - } catch { - toast.error("Failed to load deployment details", { - position: "bottom-right", - }); - } - setLoading(false); - }; - fetch(); + setLoading(true); + setError(false); + try { + const res = await api.get(`/vmInstance/getDetails?id=${id}`); + setVm(res.data.vmInstance); + } catch { + setError(true); + toast.error("Failed to load deployment details", { + position: "bottom-right", + }); + } + setLoading(false); }, [id]); + useEffect(() => { + fetchDeployment(); + }, [fetchDeployment]); + if (loading) { return ( -
-

- Loading deployment details... -

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
); } - if (!vm) { + if (error) { return (
-

Deployment Not Found

+ +

Failed to load deployment

- This deployment does not exist or has been removed. + Something went wrong while fetching this deployment.

- - - +
+ + + + +
); } - if (!wallet || !localStorage.getItem("token")) { + if (!vm) { return (
-

Please Sign In

+

Deployment Not Found

- Connect your wallet and sign in to view your deployment. + This deployment does not exist or has been removed.

- - + +
diff --git a/web-services/apps/frontend/src/pages/Docs.tsx b/web-services/apps/frontend/src/pages/Docs.tsx index 066e10e..a9f67bd 100644 --- a/web-services/apps/frontend/src/pages/Docs.tsx +++ b/web-services/apps/frontend/src/pages/Docs.tsx @@ -296,6 +296,7 @@ export default function Docs() { value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Search…" + aria-label="Search" className="bg-transparent text-xs text-zinc-700 dark:text-zinc-300 placeholder:text-zinc-400 focus:outline-none flex-1 w-full" /> {!search && ( @@ -392,6 +393,7 @@ export default function Docs() { @@ -402,6 +404,7 @@ export default function Docs() { diff --git a/web-services/apps/frontend/src/pages/HostDashboard.tsx b/web-services/apps/frontend/src/pages/HostDashboard.tsx index 05889f5..120284b 100644 --- a/web-services/apps/frontend/src/pages/HostDashboard.tsx +++ b/web-services/apps/frontend/src/pages/HostDashboard.tsx @@ -1,17 +1,48 @@ import { motion } from "motion/react"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { type Machine } from "../../types/depinMachines"; import { api } from "@/lib/api"; import { useWallet } from "@solana/wallet-adapter-react"; -import { useNavigate, Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import { DashboardTable } from "@/components/DepinHostDashboard/Table"; import { useIndexerEvents } from "@/lib/useIndexerEvents"; import { toast } from "sonner"; +import { RefreshCw, AlertCircle } from "lucide-react"; + +function SkeletonSummary() { + return ( +
+ {[...Array(3)].map((_, i) => ( +
+
+
+
+ ))} +
+ ); +} + +function SkeletonRow() { + return ( +
+
+
+
+
+
+
+
+
+
+ ); +} export function HostDashboard() { const wallet = useWallet(); const navigate = useNavigate(); const [machines, setMachines] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); useIndexerEvents({ account: wallet.publicKey?.toBase58(), @@ -42,47 +73,32 @@ export function HostDashboard() { }, }); - useEffect(() => { + const fetchMachines = useCallback(async () => { const pubKey = wallet.publicKey?.toBase58(); if (!pubKey) return; - const fetchMachines = async () => { - try { - const r = await api.get(`/user/depin/getAll?userPublicKey=${pubKey}`); - if (r.status === 200) setMachines(r.data); - } catch { - /* toast handled by api interceptor */ - } - }; - fetchMachines(); - }, [wallet]); + setLoading(true); + setError(false); + try { + const r = await api.get(`/user/depin/getAll?userPublicKey=${pubKey}`); + if (r.status === 200) setMachines(r.data); + } catch { + setError(true); + } + setLoading(false); + }, [wallet.publicKey]); - if (!wallet.publicKey || !localStorage.getItem("token")) { - return ( -
- -

- Sign in to view your host dashboard -

- - Sign in → - -
-
- ); - } + useEffect(() => { + fetchMachines(); + }, [fetchMachines]); const active = machines.filter((m) => m.isActive).length; const totalEarned = machines.reduce((s, m) => s + m.claimedSOL, 0); return ( -
+
- {/* header */}
- {/* summary strip */} - - {[ - { label: "Total machines", value: String(machines.length) }, - { label: "Active", value: String(active), accent: active > 0 }, - { - label: "Total earned", - value: `${totalEarned.toFixed(4)} SOL`, - green: true, - }, - ].map((s) => ( -
- - {s.label} - - - {s.value} - + {error ? ( + + +

+ Failed to load machines +

+

+ Something went wrong while fetching your machines. +

+ +
+ ) : loading ? ( + <> + +
+ {[...Array(3)].map((_, i) => ( + + ))}
- ))} - + + ) : ( + <> + + {[ + { label: "Total machines", value: String(machines.length) }, + { label: "Active", value: String(active), accent: active > 0 }, + { + label: "Total earned", + value: `${totalEarned.toFixed(4)} SOL`, + green: true, + }, + ].map((s) => ( +
+ + {s.label} + + + {s.value} + +
+ ))} +
- {/* table */} - - {machines.length === 0 ? ( -
-

- No machines registered yet. -

- -
- ) : ( - - )} -
+ + {machines.length === 0 ? ( +
+

+ No machines registered yet. +

+ +
+ ) : ( + + )} +
+ + )}
); diff --git a/web-services/apps/frontend/src/pages/HostMachine.tsx b/web-services/apps/frontend/src/pages/HostMachine.tsx index ac8af80..0676ba2 100644 --- a/web-services/apps/frontend/src/pages/HostMachine.tsx +++ b/web-services/apps/frontend/src/pages/HostMachine.tsx @@ -5,7 +5,6 @@ import { Step1, type Step1FormData } from "@/components/DepinHosting/Step1"; import { Step2 } from "@/components/DepinHosting/Step2"; import { Step3 } from "@/components/DepinHosting/Step3"; import { api } from "@/lib/api"; -import { Link } from "react-router-dom"; import { useWallet } from "@solana/wallet-adapter-react"; import { IconCheck, IconCoins, IconTrendingUp } from "@tabler/icons-react"; @@ -262,31 +261,6 @@ export function HostRegister() { setIsLoading(false); }; - if (!wallet.publicKey || !localStorage.getItem("token")) { - return ( -
- -

- Connect your wallet to register a machine -

- - Sign in → - -
-
- ); - } - return (
(null); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); - useEffect(() => { - if (!wallet.publicKey) return; - api - .get(`/user/depin/getAll?userPublicKey=${wallet.publicKey.toBase58()}`) - .then((r) => setMachine(r.data.find((m: Machine) => m.id === id) ?? null)) - .catch(() => {}) - .finally(() => setLoading(false)); + const fetchMachine = useCallback(async () => { + setLoading(true); + setError(false); + try { + const r = await api.get( + `/user/depin/getAll?userPublicKey=${wallet.publicKey!.toBase58()}`, + ); + setMachine(r.data.find((m: Machine) => m.id === id) ?? null); + } catch { + setError(true); + } + setLoading(false); }, [wallet.publicKey, id]); - if (!wallet.publicKey || !localStorage.getItem("token")) { - return ( -
- -

Sign in to view machine

- - Sign in → - -
-
- ); - } + useEffect(() => { + fetchMachine(); + }, [fetchMachine]); if (loading) { return ( @@ -75,6 +66,30 @@ export function HostMachineDetails() { ); } + if (error) { + return ( +
+ + +

+ Failed to load machine details. +

+ +
+
+ ); + } + if (!machine) { return (
diff --git a/web-services/apps/frontend/src/pages/Hosting.tsx b/web-services/apps/frontend/src/pages/Hosting.tsx index 94d1892..369df11 100644 --- a/web-services/apps/frontend/src/pages/Hosting.tsx +++ b/web-services/apps/frontend/src/pages/Hosting.tsx @@ -1,6 +1,5 @@ import { motion } from "motion/react"; import { Link } from "react-router-dom"; -import { useWallet } from "@solana/wallet-adapter-react"; const STEPS = [ { @@ -27,30 +26,6 @@ const STEPS = [ ]; export function Hosting() { - const wallet = useWallet(); - - if (!wallet.publicKey || !localStorage.getItem("token")) { - return ( -
- -

- Connect your wallet to continue -

- - Sign in → - -
-
- ); - } - return (
{label} - - {value} - + {children ?? ( + + {value} + + )} +
+ ); +} + +function SkeletonRow() { + return ( +
+
+
); } const SECTIONS = [ - { - label: "Identity", - rows: (pk: string, email: string) => [ - { label: "Wallet", value: pk, mono: true }, - { label: "Email", value: email || "—" }, - { label: "Network", value: "Solana Devnet" }, - ], - }, { label: "Security", rows: () => [ @@ -58,31 +67,49 @@ const SECTIONS = [ export default function Profile() { const { publicKey } = useWallet(); + const [loading, setLoading] = useState(true); + const [isEditing, setIsEditing] = useState(false); + const [formData, setFormData] = useState({ name: "", email: "" }); + const [saving, setSaving] = useState(false); - if (!publicKey || !localStorage.getItem("token")) { - return ( -
- -

- Sign in to access settings -

- - Sign in → - -
-
- ); - } + useEffect(() => { + api + .get("/user/profile") + .then((res) => { + const d = res.data?.data ?? res.data; + setFormData({ + name: d.name ?? "", + email: d.email ?? localStorage.getItem("email") ?? "", + }); + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, [publicKey]); + + const handleSave = async () => { + setSaving(true); + try { + await api.put("/user/profile", formData); + localStorage.setItem("email", formData.email); + showSuccess("Profile updated"); + setIsEditing(false); + } catch { + showError("Failed to update profile"); + } finally { + setSaving(false); + } + }; - const pk = publicKey.toBase58(); - const email = localStorage.getItem("email") ?? ""; + const handleCancel = () => { + setFormData({ + name: "", + email: localStorage.getItem("email") ?? "", + }); + setIsEditing(false); + }; + + const pk = publicKey!.toBase58(); + const email = formData.email ?? localStorage.getItem("email") ?? ""; return (
@@ -124,18 +151,106 @@ export default function Profile() { {/* sections */}
+ {/* Identity — manually rendered for edit support */} + +
+ + Identity + + {!loading && !isEditing && ( + + )} +
+
+ {loading ? ( + <> + + + + + ) : ( + <> + + {isEditing ? ( + <> + + + setFormData((prev) => ({ + ...prev, + name: e.target.value, + })) + } + className="h-8 w-48 text-sm text-right" + placeholder="Your name" + /> + + + + setFormData((prev) => ({ + ...prev, + email: e.target.value, + })) + } + className="h-8 w-48 text-sm text-right" + placeholder="your@email.com" + /> + + + ) : ( + <> + + + + )} + + {isEditing && ( +
+ + +
+ )} + + )} +
+
+ {SECTIONS.map((section, i) => ( {section.label}
- {section.rows(pk, email).map((r) => ( + {section.rows().map((r) => ( ))}
diff --git a/web-services/apps/frontend/src/pages/RentVm.tsx b/web-services/apps/frontend/src/pages/RentVm.tsx index e9922b7..1a40da1 100644 --- a/web-services/apps/frontend/src/pages/RentVm.tsx +++ b/web-services/apps/frontend/src/pages/RentVm.tsx @@ -10,8 +10,6 @@ import { NavigationButton } from "@/components/RentVm/NavigationButton"; import { CostSummary } from "@/components/RentVm/CostSummary"; import { CredentialModal } from "@/components/RentVm/CredentialModal"; import { toast } from "sonner"; -import { Link } from "react-router-dom"; -import { Button } from "@/components/ui/button"; import { useAnchorWallet } from "@solana/wallet-adapter-react"; import { TransferToVaultAndStartRental } from "@/lib/contract"; @@ -39,6 +37,23 @@ export const RentVM = () => { ); const [escrowAmount, setEscrowAmount] = useState(0); const [currentVmId, setCurrentVmId] = useState(null); + const [errors, setErrors] = useState>({}); + + useEffect(() => { + if (errors.disk) setErrors((prev) => ({ ...prev, disk: "" })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [diskSize]); + + useEffect(() => { + if (errors.duration) setErrors((prev) => ({ ...prev, duration: "" })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [duration]); + + useEffect(() => { + if (errors.cpu || errors.ram) + setErrors((prev) => ({ ...prev, cpu: "", ram: "" })); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedConfig]); const wallet = useAnchorWallet(); @@ -120,6 +135,20 @@ export const RentVM = () => { ); const handlePayment = async () => { + const validationErrors: Record = {}; + if (!selectedConfig) { + validationErrors.cpu = "CPU configuration is required"; + validationErrors.ram = "RAM configuration is required"; + } + if (!diskSize || Number(diskSize) <= 0) { + validationErrors.disk = "Disk size must be greater than 0"; + } + if (!duration || duration <= 0) { + validationErrors.duration = "Duration must be greater than 0"; + } + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) return; + setIsConfirmOpen(false); setPaymentStatus("Pending"); const id = crypto.randomUUID().substring(0, 32); @@ -187,26 +216,6 @@ export const RentVM = () => { } }; - if (!wallet || !localStorage.getItem("token")) { - return ( -
- -

Please SignIn

-

- Please connect your wallet and ensure you are signed in to proceed. -

- - - -
-
- ); - } - if (paymentStatus === "Pending") { return (
@@ -347,6 +356,15 @@ export const RentVM = () => { /> )} + {Object.keys(errors).length > 0 && ( +
+ {Object.entries(errors).map(([key, msg]) => ( +

+ {msg} +

+ ))} +
+ )} {/* Navigation Buttons */} >({}); + + const validate = () => { + const newErrors: Record = {}; + if (!formData.email) { + newErrors.email = "Email is required"; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + newErrors.email = "Invalid email format"; + } + if (!formData.password) { + newErrors.password = "Password is required"; + } else if (formData.password.length < 6) { + newErrors.password = "Password must be at least 6 characters"; + } + return newErrors; + }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + const validationErrors = validate(); + setErrors(validationErrors); + if (Object.keys(validationErrors).length > 0) { + setIsLoading(false); + return; + } setIsLoading(true); if (!wallet?.adapter.connected) { toast.error("Please connect your wallet first."); @@ -43,7 +66,7 @@ export function SignIn() { toast.success("Successfully signed in!"); localStorage.setItem("token", `Bearer ${res.data.token}`); localStorage.setItem("email", formData.email); - setFormData({ email: "" }); + setFormData({ email: "", password: "" }); navigate("/dashboard"); } else { toast.error("Failed to sign in. Please try again."); @@ -60,6 +83,7 @@ export function SignIn() { ...prev, [e.target.name]: e.target.value, })); + setErrors((prev) => ({ ...prev, [e.target.name]: "" })); }; return ( @@ -137,11 +161,39 @@ export function SignIn() { required className="transition-all duration-200 focus:ring-2 focus:ring-primary/20" /> + {errors.email && ( +

{errors.email}

+ )} +
+
+ + + {errors.password && ( +

{errors.password}

+ )}
- +
+ + + + +
); } - if (!wallet || !localStorage.getItem("token")) { + if (loading && !vm) { return ( -
+
+
+
+
+ +
+
+ +
+
+
+ +
+
+
+ ); + } + + if (!vm) return null; + + if (vm.status === "DELETED") { + return ( +
-

Please SignIn

+

VM Deleted

- Please connect your wallet and ensure you are signed in to proceed. + This virtual machine has been deleted.

- - + +
@@ -108,11 +145,9 @@ export function VMDetails() { return (
- {/* Header */}
- {/* Main Content */}
{vm.PaymentType === "ESCROW" && } @@ -120,7 +155,6 @@ export function VMDetails() { {vm.provider != "LOCAL" && }
- {/* Sidebar */}
From 1d144fdebd1179df32ceaa6f8520831746cc2727 Mon Sep 17 00:00:00 2001 From: Official-Krish Date: Mon, 1 Jun 2026 18:22:49 +0530 Subject: [PATCH 04/11] [feat]: Added labels, lazy loading --- web-services/apps/frontend/src/App.tsx | 328 +++++++++--------- .../src/components/RentVm/CredentialModal.tsx | 5 + .../frontend/src/components/RentVm/Step1.tsx | 78 +++-- .../frontend/src/components/RentVm/Step2.tsx | 10 + .../src/components/vmDetail/Header.tsx | 45 ++- .../src/components/vmDetail/Overview.tsx | 2 + .../frontend/src/components/vmDetail/SSH.tsx | 3 + .../apps/frontend/src/pages/Admin.tsx | 147 +++++--- .../apps/frontend/src/pages/Contact.tsx | 2 + .../apps/frontend/src/pages/Dashboard.tsx | 18 + .../apps/frontend/src/pages/HostMachine.tsx | 1 + .../apps/frontend/src/pages/Profile.tsx | 6 +- .../apps/frontend/src/pages/RentVm.tsx | 81 +++-- .../apps/frontend/src/pages/Signin.tsx | 7 +- .../apps/frontend/src/pages/Signup.tsx | 7 +- .../apps/frontend/src/pages/Terminal.tsx | 13 +- .../apps/frontend/src/pages/vmDetail.tsx | 5 +- 17 files changed, 490 insertions(+), 268 deletions(-) diff --git a/web-services/apps/frontend/src/App.tsx b/web-services/apps/frontend/src/App.tsx index 6bf0d5f..0c20ba5 100644 --- a/web-services/apps/frontend/src/App.tsx +++ b/web-services/apps/frontend/src/App.tsx @@ -1,23 +1,17 @@ +import { lazy, Suspense } from "react"; import { Routes, Route } from "react-router-dom"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { RequireAuth } from "./components/RequireAuth"; -import Landing from "./pages/Landing"; import { Dashboard } from "./pages/Dashboard"; -import { RentVM } from "./pages/RentVm"; import { VMDetails } from "./pages/vmDetail"; import { Hosting } from "./pages/Hosting"; import { SignUp } from "./pages/Signup"; import "@solana/wallet-adapter-react-ui/styles.css"; import { SignIn } from "./pages/Signin"; -import SSHTerminal from "./pages/Terminal"; -import { AdminPage } from "./pages/Admin"; import { ComingSoon } from "./components/ComingSoon"; -import { HostRegister } from "./pages/HostMachine"; import { DepinDeployment } from "./pages/DepinDeployment"; import { HostDashboard } from "./pages/HostDashboard"; -import { DeployApp } from "./pages/deployImage"; import { HostMachineDetails } from "./pages/HostMachineDetails"; -import Docs from "./pages/Docs"; import ApiReference from "./pages/ApiReference"; import Tutorials from "./pages/Tutorials"; import Status from "./pages/Status"; @@ -34,158 +28,182 @@ import Roadmap from "./pages/Roadmap"; import ClaimRewards from "./pages/ClaimRewards"; import Host from "./pages/Host"; +const Landing = lazy(() => import("./pages/Landing")); +const RentVM = lazy(() => + import("./pages/RentVm").then((m) => ({ default: m.RentVM })), +); +const AdminPage = lazy(() => + import("./pages/Admin").then((m) => ({ default: m.AdminPage })), +); +const SSHTerminal = lazy(() => import("./pages/Terminal")); +const HostRegister = lazy(() => + import("./pages/HostMachine").then((m) => ({ default: m.HostRegister })), +); +const Docs = lazy(() => import("./pages/Docs")); +const DeployApp = lazy(() => + import("./pages/deployImage").then((m) => ({ default: m.DeployApp })), +); + function App() { return ( - - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + +

Loading...

+
+ } + > + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - } /> - + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + } /> + + ); } diff --git a/web-services/apps/frontend/src/components/RentVm/CredentialModal.tsx b/web-services/apps/frontend/src/components/RentVm/CredentialModal.tsx index 8c4835b..fd24f6e 100644 --- a/web-services/apps/frontend/src/components/RentVm/CredentialModal.tsx +++ b/web-services/apps/frontend/src/components/RentVm/CredentialModal.tsx @@ -112,6 +112,7 @@ export const CredentialModal = ({ `ssh -i ${vmName}-key.pem axion@${finalConfig?.ipAddress}`, ); }} + aria-label="Copy SSH command" > @@ -140,6 +141,7 @@ export const CredentialModal = ({ element.click(); document.body.removeChild(element); }} + aria-label="Download private key" > Download Private Key @@ -176,6 +178,7 @@ export const CredentialModal = ({ onClick={() => copyToClipboard(`chmod 600 ${vmName}-key.pem`) } + aria-label="Copy chmod command" > @@ -193,6 +196,7 @@ export const CredentialModal = ({ `ssh -i ${vmName}-key.pem axion@${finalConfig?.ipAddress}`, ) } + aria-label="Copy SSH command" > @@ -208,6 +212,7 @@ export const CredentialModal = ({ onClick={() => copyToClipboard(finalConfig?.AuthToken || "") } + aria-label="Copy auth token" > diff --git a/web-services/apps/frontend/src/components/RentVm/Step1.tsx b/web-services/apps/frontend/src/components/RentVm/Step1.tsx index fcfb0e1..df6bb7f 100644 --- a/web-services/apps/frontend/src/components/RentVm/Step1.tsx +++ b/web-services/apps/frontend/src/components/RentVm/Step1.tsx @@ -114,39 +114,53 @@ export const Step1 = ({
- {vms.map((config) => ( -
setSelectedConfig(config.id)} - className={`p-4 rounded-lg border cursor-pointer transition-all hover:border-primary/50 ${ - selectedConfig === config.id - ? "border-primary bg-primary/5" - : "border-border bg-background" - }`} - > -
-
-
-
- - {config.machineType} - + {vms.length === 0 ? ( +

+ No configurations available. Please try again later. +

+ ) : ( + vms.map((config) => ( +
setSelectedConfig(config.id)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setSelectedConfig(config.id); + } + }} + role="button" + tabIndex={0} + className={`p-4 rounded-lg border cursor-pointer transition-all hover:border-primary/50 ${ + selectedConfig === config.id + ? "border-primary bg-primary/5" + : "border-border bg-background" + }`} + > +
+
+
+
+ + {config.machineType} + +
+

+ {config.description} +

-

- {config.description} -

-
-
-
- {prices[config.id] - ? `${prices[config.id].toFixed(6)} SOL/hr` - : "Calculating..."} +
+
+ {prices[config.id] + ? `${prices[config.id].toFixed(6)} SOL/hr` + : "Calculating..."} +
-
- ))} + )) + )}
@@ -194,6 +208,14 @@ export const Step1 = ({
setOs(osOption.value)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setOs(osOption.value); + } + }} + role="button" + tabIndex={0} className={`p-3 rounded-lg border cursor-pointer transition-all hover:border-primary/50 ${ os === osOption.value ? "border-primary bg-primary/5" diff --git a/web-services/apps/frontend/src/components/RentVm/Step2.tsx b/web-services/apps/frontend/src/components/RentVm/Step2.tsx index 7a7bb9f..fc10d9c 100644 --- a/web-services/apps/frontend/src/components/RentVm/Step2.tsx +++ b/web-services/apps/frontend/src/components/RentVm/Step2.tsx @@ -113,6 +113,11 @@ export const Step2 = ({ } className="mt-2 w-33" /> + {duration !== undefined && duration <= 0 && ( +

+ Duration must be greater than 0. +

+ )} )}
@@ -158,6 +163,11 @@ export const Step2 = ({ placeholder="Enter amount in SOL" className="my-4 w-32" /> + {escrowAmount !== undefined && escrowAmount <= 0 && ( +

+ Escrow amount must be greater than 0. +

+ )}

≈{" "} {selectedVMConfig diff --git a/web-services/apps/frontend/src/components/vmDetail/Header.tsx b/web-services/apps/frontend/src/components/vmDetail/Header.tsx index 6ee8afe..1c20494 100644 --- a/web-services/apps/frontend/src/components/vmDetail/Header.tsx +++ b/web-services/apps/frontend/src/components/vmDetail/Header.tsx @@ -1,7 +1,15 @@ import { motion } from "motion/react"; import { Link, useNavigate } from "react-router-dom"; import { Button } from "@/components/ui/button"; -import { ArrowLeft, Trash2 } from "lucide-react"; +import { AlertTriangle, ArrowLeft, Trash2 } from "lucide-react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; import { api } from "@/lib/api"; import type { VM } from "types/vm"; import { calculatePrice } from "@/lib/vm"; @@ -27,6 +35,7 @@ export const Header = ({ vm }: { vm: VM }) => { const wallet = useAnchorWallet(); const { watch } = useTxConfirm(wallet?.publicKey?.toBase58()); const [txStatus, setTxStatus] = useState("idle"); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const navigate = useNavigate(); const handleDelete = async () => { @@ -123,12 +132,44 @@ export const Header = ({ vm }: { vm: VM }) => { variant="destructive" size="sm" className={`cursor-pointer ${busy ? "opacity-50" : ""}`} - onClick={handleDelete} + onClick={() => setShowDeleteConfirm(true)} disabled={busy} > {DELETE_LABEL[txStatus]} +

+ + + + + Delete Virtual Machine + + + Are you sure you want to delete {vm.name}? This action will + terminate your instance, and you may incur charges for + remaining time. This action cannot be undone. + + + + + + + +

diff --git a/web-services/apps/frontend/src/components/vmDetail/Overview.tsx b/web-services/apps/frontend/src/components/vmDetail/Overview.tsx index ea00d82..b9e7dd7 100644 --- a/web-services/apps/frontend/src/components/vmDetail/Overview.tsx +++ b/web-services/apps/frontend/src/components/vmDetail/Overview.tsx @@ -56,6 +56,7 @@ export const Overview = ({ vm }: { vm: VM }) => { size="sm" onClick={() => copyToClipboard(vm.ipAddress)} className="h-6 w-6 p-0 cursor-pointer" + aria-label="Copy IP address" > @@ -80,6 +81,7 @@ export const Overview = ({ vm }: { vm: VM }) => { size="sm" onClick={() => copyToClipboard(vm.VMImage?.applicationUrl)} className="h-6 w-6 p-0 cursor-pointer" + aria-label="Copy application URL" > diff --git a/web-services/apps/frontend/src/components/vmDetail/SSH.tsx b/web-services/apps/frontend/src/components/vmDetail/SSH.tsx index f0abc92..95049e2 100644 --- a/web-services/apps/frontend/src/components/vmDetail/SSH.tsx +++ b/web-services/apps/frontend/src/components/vmDetail/SSH.tsx @@ -45,6 +45,7 @@ export const SSH = ({ vm }: { vm: VM }) => { onClick={() => copyToClipboard(`ssh-keygen -R ${vm.ipAddress}`) } + aria-label="Copy command" > @@ -64,6 +65,7 @@ export const SSH = ({ vm }: { vm: VM }) => { onClick={() => copyToClipboard(`chmod 600 ${vm.name}-key.pem`) } + aria-label="Copy command" > @@ -84,6 +86,7 @@ export const SSH = ({ vm }: { vm: VM }) => { `ssh -i ${vm.name}-key.pem axion@${vm.ipAddress}`, ) } + aria-label="Copy command" > diff --git a/web-services/apps/frontend/src/pages/Admin.tsx b/web-services/apps/frontend/src/pages/Admin.tsx index 48a39ba..32b87a7 100644 --- a/web-services/apps/frontend/src/pages/Admin.tsx +++ b/web-services/apps/frontend/src/pages/Admin.tsx @@ -139,6 +139,7 @@ export function AdminPage() { const [withdrawVault, setWithdrawVault] = useState({ amount: "" }); const [errors, setErrors] = useState>({}); const [vms, setVMs] = useState([]); + const [loadingVms, setLoadingVms] = useState(false); // Track pending signatures → which op they belong to const pendingSigs = useRef void>>( @@ -179,12 +180,14 @@ export function AdminPage() { useEffect(() => { if (activeTab !== "vms") return; + setLoadingVms(true); api .get(`/vm/getAll?adminKey=${wallet?.publicKey}`) .then((res) => { if (res.status === 200) setVMs(res.data || []); }) - .catch(() => toast.error("Failed to load virtual machines")); + .catch(() => toast.error("Failed to load virtual machines")) + .finally(() => setLoadingVms(false)); }, [activeTab, wallet?.publicKey]); // ── Helper: submit tx, show "submitted", wait for indexer event ── @@ -400,7 +403,10 @@ export function AdminPage() { const busy = (op: OpState) => op.status === "submitted"; return ( -

+
@@ -626,7 +632,40 @@ export function AdminPage() { {/* ── VM tab ────────────────────────────────────────────── */} - {vms.length === 0 ? ( + {loadingVms ? ( + + +
+
+ + +
+ + + + {[...Array(10)].map((_, i) => ( + +
+ + ))} + + + + {[...Array(3)].map((_, row) => ( + + {[...Array(10)].map((_, cell) => ( + +
+ + ))} + + ))} + +
+
+
+ + ) : vms.length === 0 ? ( No virtual machines found. @@ -643,57 +682,59 @@ export function AdminPage() {
- - - - VM ID - Name - Status - Type - Region - Resources - IP Address - Cost - Duration - Created At - - - - {paginatedVMs.map((vm) => ( - - - {vm.instanceId} - - - {vm.name} - - {getStatusBadge(vm.status)} - {vm.VMConfig.machineType} - {vm.region} - -
{vm.VMConfig.os}
-
- {vm.VMConfig.diskSize} GB -
-
- - {vm.ipAddress} - - - {Number(vm.price).toFixed(6)} SOL - - - {vm.endTime - ? `${Math.floor((new Date(vm.endTime).getTime() - new Date(vm.createdAt).getTime()) / 60000)} min` - : "N/A"} - - - {formatter.format(new Date(vm.createdAt))} - +
+
+ + + VM ID + Name + Status + Type + Region + Resources + IP Address + Cost + Duration + Created At - ))} - -
+ + + {paginatedVMs.map((vm) => ( + + + {vm.instanceId} + + + {vm.name} + + {getStatusBadge(vm.status)} + {vm.VMConfig.machineType} + {vm.region} + +
{vm.VMConfig.os}
+
+ {vm.VMConfig.diskSize} GB +
+
+ + {vm.ipAddress} + + + {Number(vm.price).toFixed(6)} SOL + + + {vm.endTime + ? `${Math.floor((new Date(vm.endTime).getTime() - new Date(vm.createdAt).getTime()) / 60000)} min` + : "N/A"} + + + {formatter.format(new Date(vm.createdAt))} + +
+ ))} +
+ +
{totalPages > 1 && ( diff --git a/web-services/apps/frontend/src/pages/Contact.tsx b/web-services/apps/frontend/src/pages/Contact.tsx index be7438a..73477d0 100644 --- a/web-services/apps/frontend/src/pages/Contact.tsx +++ b/web-services/apps/frontend/src/pages/Contact.tsx @@ -118,6 +118,8 @@ export default function Contact() { ) : ( { diff --git a/web-services/apps/frontend/src/pages/Dashboard.tsx b/web-services/apps/frontend/src/pages/Dashboard.tsx index 4e4df56..f4470f3 100644 --- a/web-services/apps/frontend/src/pages/Dashboard.tsx +++ b/web-services/apps/frontend/src/pages/Dashboard.tsx @@ -212,6 +212,8 @@ export function Dashboard() { className="group p-6 rounded-2xl border border-border/50 bg-card/50 hover:bg-card/80 transition-all duration-300" whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} + role="button" + tabIndex={0} onClick={() => { if (vm.status === "RUNNING") { navigate( @@ -225,6 +227,22 @@ export function Dashboard() { }); } }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + if (vm.status === "RUNNING") { + navigate( + vm.provider === "LOCAL" + ? `/depin/deployment/${vm.id}` + : `/vm/${vm.id}`, + ); + } else { + toast.info("This VM is not running.", { + position: "top-right", + }); + } + } + }} >
diff --git a/web-services/apps/frontend/src/pages/HostMachine.tsx b/web-services/apps/frontend/src/pages/HostMachine.tsx index 0676ba2..2eae3b3 100644 --- a/web-services/apps/frontend/src/pages/HostMachine.tsx +++ b/web-services/apps/frontend/src/pages/HostMachine.tsx @@ -263,6 +263,7 @@ export function HostRegister() { return (
diff --git a/web-services/apps/frontend/src/pages/Profile.tsx b/web-services/apps/frontend/src/pages/Profile.tsx index f30bfc2..78baf8d 100644 --- a/web-services/apps/frontend/src/pages/Profile.tsx +++ b/web-services/apps/frontend/src/pages/Profile.tsx @@ -112,7 +112,10 @@ export default function Profile() { const email = formData.email ?? localStorage.getItem("email") ?? ""; return ( -
+
- - - - - {/* Fund Vault */} - - - - Fund - Vault - - - Add funds to your vault account - - - -
- - { - setFundVault({ ...fundVault, amount: e.target.value }); - setErrors((prev) => ({ ...prev, fundAmount: "" })); - }} - /> - {errors.fundAmount && ( -

- {errors.fundAmount} -

- )} -
- - -
-
- - {/* Withdraw Funds */} - - - - {" "} - Withdraw Funds - - - Withdraw funds from your vault to an address - - - -
- - { - setWithdrawVault({ - ...withdrawVault, - amount: e.target.value, - }); - setErrors((prev) => ({ ...prev, withdrawAmount: "" })); - }} - /> - {errors.withdrawAmount && ( -

- {errors.withdrawAmount} -

- )} -
- - -
-
- - {/* Check Balance */} - - - - Check Balance - - - View your current vault balance - - - - - - - -
- - - {/* ── VM tab ────────────────────────────────────────────── */} - - {loadingVms ? ( - - -
-
- - -
- - - - {[...Array(10)].map((_, i) => ( - -
- - ))} - - - - {[...Array(3)].map((_, row) => ( - - {[...Array(10)].map((_, cell) => ( - -
- - ))} - - ))} - -
+ + {activeTab === "vault" && ( + +
+ {/* Initialize Vault */} + + + + Initialize + Vault + + + {vaultExists + ? "Vault is already initialized" + : "Create the platform vault account"} + + + + + + + + + {/* Fund Vault */} + + + + {" "} + Fund Vault + + + Add funds to your vault account + + + +
+ + { + setFundVault({ + ...fundVault, + amount: e.target.value, + }); + setErrors((prev) => ({ ...prev, fundAmount: "" })); + }} + /> + {errors.fundAmount && ( +

+ {errors.fundAmount} +

+ )} +
+ + +
+
+ + {/* Withdraw Funds */} + + + + {" "} + Withdraw Funds + + + Withdraw funds from your vault to an address + + + +
+ + { + setWithdrawVault({ + ...withdrawVault, + amount: e.target.value, + }); + setErrors((prev) => ({ + ...prev, + withdrawAmount: "", + })); + }} + /> + {errors.withdrawAmount && ( +

+ {errors.withdrawAmount} +

+ )} +
+ + +
+
+ + {/* Check Balance */} + + + + Check + Balance + + + View your current vault balance + + + + + + + +
+
+ )} + {activeTab === "vms" && ( + + {timedOut ? ( +
+

+ Loading is taking longer than expected. Please try again. +

+
- - - ) : vms.length === 0 ? ( - - No virtual machines found. - - ) : ( - - - - Virtual Machines ( - {vms.length} total) - - - Monitor and manage all deployed virtual machines - - - -
-
- - - - VM ID - Name - Status - Type - Region - Resources - IP Address - Cost - Duration - Created At - - - - {paginatedVMs.map((vm) => ( - - - {vm.instanceId} - - - {vm.name} - - {getStatusBadge(vm.status)} - {vm.VMConfig.machineType} - {vm.region} - -
{vm.VMConfig.os}
-
- {vm.VMConfig.diskSize} GB -
-
- - {vm.ipAddress} - - - {Number(vm.price).toFixed(6)} SOL - - - {vm.endTime - ? `${Math.floor((new Date(vm.endTime).getTime() - new Date(vm.createdAt).getTime()) / 60000)} min` - : "N/A"} - - - {formatter.format(new Date(vm.createdAt))} - + ) : loadingVms ? ( + + +
+
+ + +
+
+ + + {[...Array(10)].map((_, i) => ( + +
+ + ))} - ))} - -
-
-
- - {totalPages > 1 && ( -
-
- Showing {startIndex + 1}– - {Math.min(startIndex + itemsPerPage, totalItems)} of{" "} - {totalItems} VMs + + + {[...Array(3)].map((_, row) => ( + + {[...Array(10)].map((_, cell) => ( + +
+ + ))} + + ))} + +
-
- - {Array.from( - { length: Math.min(5, totalPages) }, - (_, i) => i + 1, - ).map((page) => ( - - ))} - {totalPages > 5 && ( - <> - + + + ) : vms.length === 0 ? ( + + + No virtual machines found. + + + ) : ( + + + + Virtual Machines ( + {vms.length} total) + + + Monitor and manage all deployed virtual machines + + + +
+
+ + + + VM ID + Name + Status + Type + Region + Resources + IP Address + Cost + Duration + Created At + + + + {paginatedVMs.map((vm) => ( + + + {vm.instanceId} + + + {vm.name} + + + {getStatusBadge(vm.status)} + + + {vm.VMConfig.machineType} + + {vm.region} + +
{vm.VMConfig.os}
+
+ {vm.VMConfig.diskSize} GB +
+
+ + {vm.ipAddress} + + + {Number(vm.price).toFixed(6)} SOL + + + {vm.endTime + ? `${Math.floor((new Date(vm.endTime).getTime() - new Date(vm.createdAt).getTime()) / 60000)} min` + : "N/A"} + + + {formatter.format(new Date(vm.createdAt))} + +
+ ))} +
+
+
+
+ + {totalPages > 1 && ( +
+
+ Showing {startIndex + 1}– + {Math.min(startIndex + itemsPerPage, totalItems)} of{" "} + {totalItems} VMs +
+
+ {Array.from( + { length: Math.min(5, totalPages) }, + (_, i) => i + 1, + ).map((page) => ( + + ))} + {totalPages > 5 && ( + <> + + + + )} + - - )} - -
-
- )} -
-
+
+
+ )} + + + )} + )} - +
diff --git a/web-services/apps/frontend/src/pages/ApiReference.tsx b/web-services/apps/frontend/src/pages/ApiReference.tsx index dde02b5..98b0639 100644 --- a/web-services/apps/frontend/src/pages/ApiReference.tsx +++ b/web-services/apps/frontend/src/pages/ApiReference.tsx @@ -1,5 +1,6 @@ import { useRef, useState } from "react"; import { motion, useInView } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; const METHODS = ["GET", "POST", "PUT", "DELETE"] as const; type Method = (typeof METHODS)[number]; @@ -186,13 +187,11 @@ function GroupSection({ export default function ApiReference() { return ( -
-
+
diff --git a/web-services/apps/frontend/src/pages/Billing.tsx b/web-services/apps/frontend/src/pages/Billing.tsx index 6ba8545..07658fa 100644 --- a/web-services/apps/frontend/src/pages/Billing.tsx +++ b/web-services/apps/frontend/src/pages/Billing.tsx @@ -1,92 +1,93 @@ import { motion } from "motion/react"; import { useWallet } from "@solana/wallet-adapter-react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; export default function Billing() { const { publicKey } = useWallet(); return ( -
-
+ +
+ + +
+
+ + + + Billing + + +

+ + + Transactions + + +

+
+ + + {[ + { + label: "Total spent", + value: "—", + color: "text-zinc-900 dark:text-white", + }, + { label: "Total earned", value: "—", color: "text-emerald-500" }, + { + label: "Wallet", + value: `${publicKey?.toBase58()?.slice(0, 4) ?? "…"}…${publicKey?.toBase58()?.slice(-4) ?? ""}`, + color: "text-zinc-500 dark:text-zinc-400", + }, + ].map((s) => ( +
+ + {s.label} + + + {s.value} + +
+ ))} +
-
-
- - - Billing + + Activity +
+

+ No transactions yet. Rent a VM or host a machine to get started. +

+
-

- - - Transactions - - -

- - - {[ - { - label: "Total spent", - value: "—", - color: "text-zinc-900 dark:text-white", - }, - { label: "Total earned", value: "—", color: "text-emerald-500" }, - { - label: "Wallet", - value: `${publicKey!.toBase58().slice(0, 4)}…${publicKey!.toBase58().slice(-4)}`, - color: "text-zinc-500 dark:text-zinc-400", - }, - ].map((s) => ( -
- - {s.label} - - - {s.value} - -
- ))} -
- - - - Activity - -
-

- No transactions yet. Rent a VM or host a machine to get started. -

-
-
-
+ ); } diff --git a/web-services/apps/frontend/src/pages/Blog.tsx b/web-services/apps/frontend/src/pages/Blog.tsx index af54716..3a39acc 100644 --- a/web-services/apps/frontend/src/pages/Blog.tsx +++ b/web-services/apps/frontend/src/pages/Blog.tsx @@ -1,44 +1,46 @@ import { motion } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; export default function Blog() { return ( -
-
-
-
- - - - Blog - - -

- +
+ +
+
+ - Updates & insights - -

-
-
-

- No posts yet. Product updates and technical deep-dives coming soon. -

+ + + Blog + + +

+ + Updates & insights + +

+
+
+

+ No posts yet. Product updates and technical deep-dives coming + soon. +

+
-
+ ); } diff --git a/web-services/apps/frontend/src/pages/Careers.tsx b/web-services/apps/frontend/src/pages/Careers.tsx index 8a00942..5cbc14b 100644 --- a/web-services/apps/frontend/src/pages/Careers.tsx +++ b/web-services/apps/frontend/src/pages/Careers.tsx @@ -1,48 +1,49 @@ import { motion } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; export default function Careers() { return ( -
-
-
-
- - - - Careers - - -

- +
+ +
+
+ - Join the team - -

-

- We're building the future of decentralized compute. Check back soon - for open roles. -

-
-
-

- No open roles right now. Follow us on X for updates. -

+ + + Careers + + +

+ + Join the team + +

+

+ We're building the future of decentralized compute. Check back + soon for open roles. +

+
+
+

+ No open roles right now. Follow us on X for updates. +

+
-
+ ); } diff --git a/web-services/apps/frontend/src/pages/ClaimRewards.tsx b/web-services/apps/frontend/src/pages/ClaimRewards.tsx index 48a9e7d..99bb83a 100644 --- a/web-services/apps/frontend/src/pages/ClaimRewards.tsx +++ b/web-services/apps/frontend/src/pages/ClaimRewards.tsx @@ -1,10 +1,13 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { motion, useInView } from "motion/react"; import { useWallet } from "@solana/wallet-adapter-react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; import { Link } from "react-router-dom"; import { type Machine } from "../../types/depinMachines"; import { toast } from "sonner"; import { RefreshCw, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useLoadingTimeout } from "@/hooks/useLoadingTimeout"; import { api } from "@/lib/api"; // per-machine claim status @@ -98,6 +101,7 @@ export default function ClaimRewards() { const [machines, setMachines] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const timedOut = useLoadingTimeout(loading, 30000); // per-machine claim status const [claimStatuses, setClaimStatuses] = useState< Record @@ -108,7 +112,7 @@ export default function ClaimRewards() { setError(false); try { const r = await api.get( - `/user/depin/getAll?userPublicKey=${wallet.publicKey!.toBase58()}`, + `/user/depin/getAll?userPublicKey=${wallet.publicKey?.toBase58() ?? ""}`, ); setMachines(r.data); } catch { @@ -129,7 +133,7 @@ export default function ClaimRewards() { try { const res = await api.post("/user/depin/claimSOL", { id, - pubKey: wallet.publicKey!.toBase58(), + pubKey: wallet.publicKey?.toBase58() ?? "", }); if (res.status === 200) { setStatus(id, "confirmed"); @@ -147,15 +151,13 @@ export default function ClaimRewards() { return (
-
@@ -202,7 +204,16 @@ export default function ClaimRewards() { {/* machines */}
- {loading ? ( + {timedOut && !error ? ( +
+

+ Loading is taking longer than expected. Please try again. +

+ +
+ ) : loading ? (
(null); @@ -14,13 +15,11 @@ export default function Contact() { }`; return ( -
-
+
diff --git a/web-services/apps/frontend/src/pages/Dashboard.tsx b/web-services/apps/frontend/src/pages/Dashboard.tsx index f4470f3..de5332b 100644 --- a/web-services/apps/frontend/src/pages/Dashboard.tsx +++ b/web-services/apps/frontend/src/pages/Dashboard.tsx @@ -1,6 +1,7 @@ -import { motion } from "motion/react"; +import { motion, AnimatePresence } from "motion/react"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; +import { useLoadingTimeout } from "@/hooks/useLoadingTimeout"; import { Input } from "@/components/ui/input"; import { StatusBadge } from "@/components/StatusBadge"; import { @@ -47,6 +48,7 @@ export function Dashboard() { const [vms, setVMs] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const timedOut = useLoadingTimeout(loading, 30000); const navigate = useNavigate(); const wallet = useWallet(); @@ -193,6 +195,17 @@ export function Dashboard() { )} + {timedOut && !error && ( +
+

+ Loading is taking longer than expected. Please try again. +

+ +
+ )} + {loading && !error && (
{[...Array(3)].map((_, i) => ( @@ -202,12 +215,14 @@ export function Dashboard() { )} {!loading && !error && ( -
+ {filteredVMs.map((vm, index) => ( ))} -
+ )} {!loading && !error && filteredVMs.length === 0 && ( diff --git a/web-services/apps/frontend/src/pages/DepinDeployment.tsx b/web-services/apps/frontend/src/pages/DepinDeployment.tsx index ff9d220..1df1912 100644 --- a/web-services/apps/frontend/src/pages/DepinDeployment.tsx +++ b/web-services/apps/frontend/src/pages/DepinDeployment.tsx @@ -5,6 +5,7 @@ import { useCallback, useEffect, useState } from "react"; import { api } from "@/lib/api"; import { useAnchorWallet } from "@solana/wallet-adapter-react"; import { RefreshCw, AlertCircle } from "lucide-react"; +import { useLoadingTimeout } from "@/hooks/useLoadingTimeout"; import { toast } from "sonner"; import { type VM } from "../../types/vm"; import { useIndexerEvents } from "@/lib/useIndexerEvents"; @@ -21,6 +22,7 @@ export function DepinDeployment() { const [vm, setVm] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const timedOut = useLoadingTimeout(loading, 30000); const isTerminated = vm?.status === "DELETED" || vm?.status === "TERMINATED"; useIndexerEvents({ @@ -59,6 +61,24 @@ export function DepinDeployment() { fetchDeployment(); }, [fetchDeployment]); + if (timedOut && !error) { + return ( +
+
+

+ Loading is taking longer than expected. Please try again. +

+ +
+
+ ); + } + if (loading) { return (
-
+ {/* top bar */} -
+
+ + ) : error ? ( - {/* Subtle radial glow */} -
diff --git a/web-services/apps/frontend/src/pages/HostMachineDetails.tsx b/web-services/apps/frontend/src/pages/HostMachineDetails.tsx index 6b030bc..36081e0 100644 --- a/web-services/apps/frontend/src/pages/HostMachineDetails.tsx +++ b/web-services/apps/frontend/src/pages/HostMachineDetails.tsx @@ -2,7 +2,10 @@ import { useCallback, useEffect, useState } from "react"; import { motion } from "motion/react"; import { useParams, Link } from "react-router-dom"; import { useWallet } from "@solana/wallet-adapter-react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; import { RefreshCw, AlertCircle } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { useLoadingTimeout } from "@/hooks/useLoadingTimeout"; import { api } from "@/lib/api"; import { type Machine } from "../../types/depinMachines"; @@ -35,13 +38,14 @@ export function HostMachineDetails() { const [machine, setMachine] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const timedOut = useLoadingTimeout(loading, 30000); const fetchMachine = useCallback(async () => { setLoading(true); setError(false); try { const r = await api.get( - `/user/depin/getAll?userPublicKey=${wallet.publicKey!.toBase58()}`, + `/user/depin/getAll?userPublicKey=${wallet.publicKey?.toBase58() ?? ""}`, ); setMachine(r.data.find((m: Machine) => m.id === id) ?? null); } catch { @@ -54,9 +58,22 @@ export function HostMachineDetails() { fetchMachine(); }, [fetchMachine]); - if (loading) { + if (timedOut && !error) { return (
+
+

+ Loading is taking longer than expected. Please try again. +

+ +
+
+ ); + } + + if (loading) { + return ( +
+
+
-
+
diff --git a/web-services/apps/frontend/src/pages/Hosting.tsx b/web-services/apps/frontend/src/pages/Hosting.tsx index 369df11..4771a2d 100644 --- a/web-services/apps/frontend/src/pages/Hosting.tsx +++ b/web-services/apps/frontend/src/pages/Hosting.tsx @@ -1,5 +1,6 @@ import { motion } from "motion/react"; import { Link } from "react-router-dom"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; const STEPS = [ { @@ -27,13 +28,11 @@ const STEPS = [ export function Hosting() { return ( -
-
+
diff --git a/web-services/apps/frontend/src/pages/Legal.tsx b/web-services/apps/frontend/src/pages/Legal.tsx index 6442a0b..7aab303 100644 --- a/web-services/apps/frontend/src/pages/Legal.tsx +++ b/web-services/apps/frontend/src/pages/Legal.tsx @@ -1,4 +1,5 @@ import { motion } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; function LegalPage({ title, @@ -12,13 +13,11 @@ function LegalPage({ sections: { heading: string; body: string }[]; }) { return ( -
-
+
diff --git a/web-services/apps/frontend/src/pages/Notifications.tsx b/web-services/apps/frontend/src/pages/Notifications.tsx index 37bfa1a..b3d1047 100644 --- a/web-services/apps/frontend/src/pages/Notifications.tsx +++ b/web-services/apps/frontend/src/pages/Notifications.tsx @@ -1,14 +1,13 @@ import { motion } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; export default function Notifications() { return ( -
-
+
diff --git a/web-services/apps/frontend/src/pages/Profile.tsx b/web-services/apps/frontend/src/pages/Profile.tsx index 78baf8d..ecadc87 100644 --- a/web-services/apps/frontend/src/pages/Profile.tsx +++ b/web-services/apps/frontend/src/pages/Profile.tsx @@ -1,11 +1,14 @@ import { useEffect, useState } from "react"; import { motion } from "motion/react"; import { useWallet } from "@solana/wallet-adapter-react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; import { api } from "@/lib/api"; import { Input } from "@/components/ui/input"; import { showSuccess, showError } from "@/lib/toast"; import { toast } from "sonner"; +import { useLoadingTimeout } from "@/hooks/useLoadingTimeout"; +import { Button } from "@/components/ui/button"; function Row({ label, @@ -68,22 +71,31 @@ const SECTIONS = [ export default function Profile() { const { publicKey } = useWallet(); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const timedOut = useLoadingTimeout(loading, 30000); const [isEditing, setIsEditing] = useState(false); const [formData, setFormData] = useState({ name: "", email: "" }); const [saving, setSaving] = useState(false); + const fetchProfile = async () => { + setLoading(true); + setError(false); + try { + const res = await api.get("/user/profile"); + const d = res.data?.data ?? res.data; + setFormData({ + name: d.name ?? "", + email: d.email ?? localStorage.getItem("email") ?? "", + }); + } catch { + setError(true); + showError("Failed to load profile"); + } + setLoading(false); + }; + useEffect(() => { - api - .get("/user/profile") - .then((res) => { - const d = res.data?.data ?? res.data; - setFormData({ - name: d.name ?? "", - email: d.email ?? localStorage.getItem("email") ?? "", - }); - }) - .catch(() => {}) - .finally(() => setLoading(false)); + fetchProfile(); }, [publicKey]); const handleSave = async () => { @@ -108,20 +120,18 @@ export default function Profile() { setIsEditing(false); }; - const pk = publicKey!.toBase58(); + const pk = publicKey?.toBase58() ?? ""; const email = formData.email ?? localStorage.getItem("email") ?? ""; return (
-
@@ -174,7 +184,16 @@ export default function Profile() { )}
- {loading ? ( + {timedOut && !error ? ( +
+

+ Loading is taking longer than expected. Please try again. +

+ +
+ ) : loading ? ( <> diff --git a/web-services/apps/frontend/src/pages/RentVm.tsx b/web-services/apps/frontend/src/pages/RentVm.tsx index 79853a1..6d29f48 100644 --- a/web-services/apps/frontend/src/pages/RentVm.tsx +++ b/web-services/apps/frontend/src/pages/RentVm.tsx @@ -1,6 +1,8 @@ import { motion } from "motion/react"; import { useEffect, useState } from "react"; import { ChevronRight, Loader2 } from "lucide-react"; +import { useLoadingTimeout } from "@/hooks/useLoadingTimeout"; +import { Button } from "@/components/ui/button"; import type { FinalConfig, VMTypes } from "types/vm"; import { api } from "@/lib/api"; import { calculateEscrowEndTime, calculatePrice } from "@/lib/vm"; @@ -29,6 +31,8 @@ export const RentVM = () => { const [isCredentialsOpen, setIsCredentialsOpen] = useState(false); const [vms, setVms] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); + const timedOut = useLoadingTimeout(loading, 30000); const [finalConfig, setFinalConfig] = useState(); const [paymentStatus, setPaymentStatus] = useState< "Pending" | "Success" | "Failed" | "not_started" @@ -85,17 +89,19 @@ export const RentVM = () => { ]; const [isNameAvailable, setIsNameAvailable] = useState(false); + const fetchVMConfigs = async () => { + setLoading(true); + setError(false); + try { + const res = await api.get("/vm/getVMTypes"); + setVms(res.data); + } catch { + setError(true); + } + setLoading(false); + }; + useEffect(() => { - const fetchVMConfigs = async () => { - setLoading(true); - try { - const res = await api.get("/vm/getVMTypes"); - setVms(res.data); - } catch { - /* toast handled by api interceptor */ - } - setLoading(false); - }; fetchVMConfigs(); }, []); @@ -313,7 +319,16 @@ export const RentVM = () => {
{/* Step 1: Configuration */} {currentStep === 1 && - (loading ? ( + (timedOut && !error ? ( +
+

+ Loading is taking longer than expected. Please try again. +

+ +
+ ) : loading ? (
diff --git a/web-services/apps/frontend/src/pages/Roadmap.tsx b/web-services/apps/frontend/src/pages/Roadmap.tsx index a0149de..c4c54e9 100644 --- a/web-services/apps/frontend/src/pages/Roadmap.tsx +++ b/web-services/apps/frontend/src/pages/Roadmap.tsx @@ -1,49 +1,50 @@ import { motion } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; export default function Roadmap() { return ( -
-
-
-
- - - - Roadmap - - -

- +
+ +
+
+ - What's next - -

-

- We're working on expanding the network, adding new regions, and - improving the developer experience. The roadmap will be published - here as milestones are defined. -

-
-
-

- Roadmap details coming soon. -

+ + + Roadmap + + +

+ + What's next + +

+

+ We're working on expanding the network, adding new regions, and + improving the developer experience. The roadmap will be published + here as milestones are defined. +

+
+
+

+ Roadmap details coming soon. +

+
-
+ ); } diff --git a/web-services/apps/frontend/src/pages/Signin.tsx b/web-services/apps/frontend/src/pages/Signin.tsx index c3e65c8..7ba0e92 100644 --- a/web-services/apps/frontend/src/pages/Signin.tsx +++ b/web-services/apps/frontend/src/pages/Signin.tsx @@ -70,7 +70,7 @@ export function SignIn() { navigate("/dashboard"); } else { toast.error("Failed to sign in. Please try again."); - console.error("Failed to sign in:", res.data); + /* login error handled by toast above */ } } catch { /* toast handled by api interceptor */ diff --git a/web-services/apps/frontend/src/pages/Status.tsx b/web-services/apps/frontend/src/pages/Status.tsx index 5df29c6..716ecaa 100644 --- a/web-services/apps/frontend/src/pages/Status.tsx +++ b/web-services/apps/frontend/src/pages/Status.tsx @@ -1,41 +1,42 @@ import { motion } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; export default function Status() { return ( -
-
-
-
- - - - Status - - -
- - - - -

- All systems operational -

+ +
+ +
+
+ + + + Status + + +
+ + + + +

+ All systems operational +

+
+

+ Real-time service status will appear here as the network grows. +

-

- Real-time service status will appear here as the network grows. -

-
+ ); } diff --git a/web-services/apps/frontend/src/pages/Tutorials.tsx b/web-services/apps/frontend/src/pages/Tutorials.tsx index 1f3d4fa..dd419bb 100644 --- a/web-services/apps/frontend/src/pages/Tutorials.tsx +++ b/web-services/apps/frontend/src/pages/Tutorials.tsx @@ -1,5 +1,6 @@ import { useRef } from "react"; import { motion, useInView } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; import { Link } from "react-router-dom"; const TUTORIALS = [ @@ -107,13 +108,11 @@ function TutRow({ t, i }: { t: (typeof TUTORIALS)[0]; i: number }) { export default function Tutorials() { return ( -
-
+
diff --git a/web-services/apps/frontend/src/pages/deployImage.tsx b/web-services/apps/frontend/src/pages/deployImage.tsx index f8f496e..b3c6e01 100644 --- a/web-services/apps/frontend/src/pages/deployImage.tsx +++ b/web-services/apps/frontend/src/pages/deployImage.tsx @@ -1,4 +1,5 @@ import { motion, AnimatePresence } from "motion/react"; +import { BackgroundGlow } from "@/components/BackgroundGlow"; import { useState } from "react"; import { Form } from "@/components/DeployImage/Form"; import { CostEstimation } from "@/components/DeployImage/CostEstimation"; @@ -21,13 +22,11 @@ export function DeployApp() { }); return ( -
-
+
diff --git a/web-services/apps/frontend/src/pages/vmDetail.tsx b/web-services/apps/frontend/src/pages/vmDetail.tsx index 8edfcb5..18e7a74 100644 --- a/web-services/apps/frontend/src/pages/vmDetail.tsx +++ b/web-services/apps/frontend/src/pages/vmDetail.tsx @@ -14,6 +14,7 @@ import { BillingStatus } from "@/components/vmDetail/BillingStatus"; import { useIndexerEvents } from "@/lib/useIndexerEvents"; import { toast } from "sonner"; import { RefreshCw, AlertCircle, ArrowLeft } from "lucide-react"; +import { useLoadingTimeout } from "@/hooks/useLoadingTimeout"; function SkeletonBlock() { return ( @@ -31,6 +32,7 @@ export function VMDetails() { const [vm, setVm] = useState(); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); + const timedOut = useLoadingTimeout(loading, 30000); useIndexerEvents({ account: wallet?.publicKey?.toBase58(), @@ -101,6 +103,21 @@ export function VMDetails() { ); } + if (timedOut && !error) { + return ( +
+
+

+ Loading is taking longer than expected. Please try again. +

+ +
+
+ ); + } + if (loading && !vm) { return (
From ce6283103b4b23108729c6dd38b3a5843d230eb3 Mon Sep 17 00:00:00 2001 From: Official-Krish Date: Tue, 2 Jun 2026 16:19:56 +0530 Subject: [PATCH 06/11] [feat]: added Debounced search on Dashboard, Parallax on Landing hero --- web-services/apps/frontend/src/App.css | 71 ++-- web-services/apps/frontend/src/App.tsx | 310 +++++++++--------- .../apps/frontend/src/components/Appbar.tsx | 4 +- .../src/components/DepinHosting/Step1.tsx | 6 +- .../src/components/DeployImage/Form.tsx | 17 +- .../apps/frontend/src/components/Footer.tsx | 3 + .../components/LandingPage/HeroSection.tsx | 21 +- .../src/components/ReadingProgress.tsx | 13 + .../apps/frontend/src/components/Skeleton.tsx | 18 + .../src/components/vmDetail/Overview.tsx | 2 + .../frontend/src/components/vmDetail/SSH.tsx | 3 + .../apps/frontend/src/hooks/useDebounce.ts | 12 + web-services/apps/frontend/src/lib/toast.ts | 44 ++- web-services/apps/frontend/src/main.tsx | 2 +- .../apps/frontend/src/pages/Admin.tsx | 15 +- .../apps/frontend/src/pages/Contact.tsx | 14 +- .../apps/frontend/src/pages/Dashboard.tsx | 32 +- .../frontend/src/pages/DepinDeployment.tsx | 25 +- web-services/apps/frontend/src/pages/Docs.tsx | 2 + .../apps/frontend/src/pages/HostDashboard.tsx | 15 +- .../apps/frontend/src/pages/Profile.tsx | 23 +- .../apps/frontend/src/pages/RentVm.tsx | 178 +++++----- .../apps/frontend/src/pages/Signin.tsx | 4 +- .../apps/frontend/src/pages/Signup.tsx | 6 +- .../apps/frontend/src/pages/Tutorials.tsx | 2 + .../apps/frontend/src/pages/vmDetail.tsx | 9 +- 26 files changed, 504 insertions(+), 347 deletions(-) create mode 100644 web-services/apps/frontend/src/components/ReadingProgress.tsx create mode 100644 web-services/apps/frontend/src/components/Skeleton.tsx create mode 100644 web-services/apps/frontend/src/hooks/useDebounce.ts diff --git a/web-services/apps/frontend/src/App.css b/web-services/apps/frontend/src/App.css index e2f960e..2892ada 100644 --- a/web-services/apps/frontend/src/App.css +++ b/web-services/apps/frontend/src/App.css @@ -1,42 +1,55 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem 0; - text-align: center; +.skeleton-shimmer { + position: relative; + overflow: hidden; } -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); +.skeleton-shimmer::after { + content: ""; + position: absolute; + inset: 0; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(255, 255, 255, 0.06) 50%, + transparent 100% + ); + animation: shimmer 2s infinite; } -@keyframes logo-spin { - from { - transform: rotate(0deg); +@keyframes shimmer { + 0% { + transform: translateX(-100%); } - to { - transform: rotate(360deg); + 100% { + transform: translateX(100%); } } -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); } + 20%, 40%, 60%, 80% { transform: translateX(2px); } } -.card { - padding: 2em; +.animate-shake { + animation: shake 0.5s ease-in-out; } -.read-the-docs { - color: #888; +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + .skeleton-shimmer::after { + background: none; + } + + .skeleton-shimmer { + background: rgba(128, 128, 128, 0.15); + } } diff --git a/web-services/apps/frontend/src/App.tsx b/web-services/apps/frontend/src/App.tsx index 0c20ba5..571059e 100644 --- a/web-services/apps/frontend/src/App.tsx +++ b/web-services/apps/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { lazy, Suspense } from "react"; -import { Routes, Route } from "react-router-dom"; +import { Routes, Route, useLocation } from "react-router-dom"; +import { AnimatePresence, motion } from "motion/react"; import { ErrorBoundary } from "./components/ErrorBoundary"; import { RequireAuth } from "./components/RequireAuth"; import { Dashboard } from "./pages/Dashboard"; @@ -45,164 +46,171 @@ const DeployApp = lazy(() => ); function App() { + const location = useLocation(); return ( +

Loading...

-
+ } > - - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - } /> - } /> - } /> - + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + } + /> + + + + } + /> + + + + } + /> + } /> + } /> + } /> + + ); diff --git a/web-services/apps/frontend/src/components/Appbar.tsx b/web-services/apps/frontend/src/components/Appbar.tsx index 86c95f3..45c86bd 100644 --- a/web-services/apps/frontend/src/components/Appbar.tsx +++ b/web-services/apps/frontend/src/components/Appbar.tsx @@ -85,7 +85,7 @@ export const Appbar = () => { {navItems.map((item, idx) => ( setHovered(idx)} @@ -116,6 +116,7 @@ export const Appbar = () => {