diff --git a/.github/workflows/frontend-ci.yml b/.github/workflows/frontend-ci.yml new file mode 100644 index 0000000..d1ff7ea --- /dev/null +++ b/.github/workflows/frontend-ci.yml @@ -0,0 +1,53 @@ +name: CI – Frontend Build & Typecheck + +on: + push: + paths: + - "frontend/**" + pull_request: + paths: + - "frontend/**" + workflow_dispatch: + +jobs: + build: + name: Build & Typecheck Frontend + runs-on: ubuntu-latest + + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "24" + cache: "pnpm" + cache-dependency-path: frontend/pnpm-lock.yaml + + - name: Clean workspace + run: git clean -fdx + + - name: Install dependencies + run: pnpm install --no-frozen-lockfile + + - name: Setup environment + run: cp .env.ci .env.local + + - name: TypeScript typecheck + run: pnpm exec tsc --noEmit + + - name: Lint + run: pnpm run lint + + - name: Build + run: pnpm run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9efbc77..36782a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,11 @@ name: CI – Build & Test Soroban Contracts on: push: + paths: + - 'smartcontract/**' pull_request: + paths: + - 'smartcontract/**' workflow_dispatch: diff --git a/README.md b/README.md index 6e2b315..67a4982 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@ # JointSave - [![CI – Build & Test Soroban Contracts](https://github.com/Sendi0011/Joint_Save/actions/workflows/test.yml/badge.svg)](https://github.com/Sendi0011/Joint_Save/actions/workflows/test.yml) +[![CI – Build & Test Soroban Contracts](https://github.com/Sendi0011/Joint_Save/actions/workflows/test.yml/badge.svg)](https://github.com/Sendi0011/Joint_Save/actions/workflows/test.yml) +[![CI – Frontend Build & Typecheck](https://github.com/Sendi0011/Joint_Save/actions/workflows/frontend-ci.yml/badge.svg)](https://github.com/Sendi0011/Joint_Save/actions/workflows/frontend-ci.yml) + +A decentralized community savings platform built on Stellar, enabling trusted groups to automate contributions, payouts, and transparency using Soroban smart contracts. - A decentralized community savings platform built on Stellar, enabling trusted groups to automate contributions, payouts, and transparency using Soroban smart contracts. ## Live Demo @@ -52,11 +54,13 @@ The frontend is built with Next.js and integrates with multiple Stellar wallets ## Technology Stack ### Smart Contracts + - **Rust** with Soroban SDK for smart contract development - **WebAssembly (WASM)** compilation for efficient execution - **Stellar Testnet** for secure, low-cost transactions ### Frontend + - **Next.js 14** with App Router for modern React development - **TypeScript** for type-safe development - **Tailwind CSS** for responsive, mobile-first styling @@ -65,6 +69,7 @@ The frontend is built with Next.js and integrates with multiple Stellar wallets - **Stellar Wallets Kit** for multi-wallet support ### Infrastructure + - **Vercel** for frontend deployment and hosting - **Supabase** for off-chain metadata and user data - **GitHub Actions** for automated CI/CD pipeline @@ -73,14 +78,14 @@ The frontend is built with Next.js and integrates with multiple Stellar wallets All contracts are deployed on **Stellar Testnet**: -| Contract | Address | -|----------|---------| -| **Factory** | `CBZNGP52FLFZ4BOGC265FUAMP5KFMAYPQK3KTI5UHMYVMM3QCST3IMRI` | +| Contract | Address | +| ------------------- | ------------------------------------------------------------------ | +| **Factory** | `CBZNGP52FLFZ4BOGC265FUAMP5KFMAYPQK3KTI5UHMYVMM3QCST3IMRI` | | **Rotational WASM** | `d350a325d8734263a3d7150c875555d8956e13a527fb3497d5141b8b3f3d2c74` | -| **Target WASM** | `133a62226501fc5443e70007d79deeeb0b33fdf8c85c7fcd3cf16293bb5c7292` | -| **Flexible WASM** | `df6ff088fd79f13d8d03e72160434517fdb4a83b8c7bfdd887be4369805e0d6b` | +| **Target WASM** | `133a62226501fc5443e70007d79deeeb0b33fdf8c85c7fcd3cf16293bb5c7292` | +| **Flexible WASM** | `df6ff088fd79f13d8d03e72160434517fdb4a83b8c7bfdd887be4369805e0d6b` | -*Deployed on April 16, 2026* +_Deployed on April 16, 2026_ For complete API documentation — functions, events, storage keys, error conditions, and CLI examples — see **[docs/contract-api.md](docs/contract-api.md)**. @@ -96,12 +101,14 @@ For complete API documentation — functions, events, storage keys, error condit ### Quick Start 1. **Clone the repository** + ```bash git clone https://github.com/Sendi0011/Joint_Save.git cd Joint_Save ``` 2. **Set up the frontend** + ```bash cd frontend npm install @@ -109,7 +116,6 @@ For complete API documentation — functions, events, storage keys, error condit ``` 3. **Configure environment variables** - Use the inline comments in `frontend/.env.example` as the source of truth for each value. The frontend validates required variables during startup, so missing values fail fast with a clear `Missing required env var: ...` error. ```env NEXT_PUBLIC_SUPABASE_URL=your_supabase_url @@ -121,6 +127,7 @@ For complete API documentation — functions, events, storage keys, error condit ``` 4. **Start the development server** + ```bash npm run dev ``` @@ -189,11 +196,10 @@ JointSave demonstrates several advanced Soroban patterns:
- - ### Desktop Features + ![CI/CD Pipeline](docs/ci-screenshot.png) -*Automated testing and deployment* +_Automated testing and deployment_ ### Transaction Export ![Transaction History with CSV Export](docs/transactions-screenshot.png) @@ -204,6 +210,7 @@ JointSave demonstrates several advanced Soroban patterns: ## Roadmap ### 🚀 Phase 1 - Foundation (Current) + - ✅ Core savings pool functionality (Rotational, Target, Flexible) - ✅ Multi-wallet Stellar integration - ✅ Factory contract for pool discovery @@ -211,7 +218,13 @@ JointSave demonstrates several advanced Soroban patterns: - ✅ Real-time on-chain state synchronization - ✅ Automated CI/CD pipeline +### CI Checks + +- **Smart Contracts** – Rust/Soroban contracts are built and tested on push/PR (any branch) +- **Frontend** – TypeScript typecheck, lint, and Next.js build run on PRs affecting `frontend/**` + ### 🔧 Phase 2 - Enhancement + - **DeFi Integration** – Connect flexible pools to Stellar DeFi protocols for yield - **Mobile App** – Native iOS and Android applications - **Advanced Analytics** – Detailed savings insights and projections @@ -219,6 +232,7 @@ JointSave demonstrates several advanced Soroban patterns: - **Reputation System** – Trust scores based on participation history ### 🌍 Phase 3 - Scale + - **Mainnet Deployment** – Production-ready contracts on Stellar mainnet - **Fiat Integration** – Direct bank transfers and credit card support - **Social Features** – Friend invitations and community building @@ -230,17 +244,20 @@ JointSave demonstrates several advanced Soroban patterns: We welcome contributions from the community! Here's how you can help: ### Development + - 🐛 **Bug Reports** – Found an issue? [Open an issue](https://github.com/Sendi0011/Joint_Save/issues) - 💡 **Feature Requests** – Have an idea? We'd love to hear it - 🔧 **Code Contributions** – Submit pull requests for improvements - 📖 **Documentation** – Help improve our docs and guides ### Testing + - 🧪 **Testnet Testing** – Try the app and report issues - 📱 **Device Testing** – Test on different devices and browsers - 🔍 **Security Review** – Help audit smart contracts and frontend code ### Community + - 💬 **Discussions** – Join conversations in GitHub Discussions - 🌟 **Spread the Word** – Share JointSave with your network - 🎓 **Education** – Help others learn about decentralized savings @@ -272,7 +289,7 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file **Built with ❤️ for communities worldwide** -*Powered by [Stellar](https://stellar.org) • [Soroban](https://soroban.stellar.org) • [Next.js](https://nextjs.org)* +_Powered by [Stellar](https://stellar.org) • [Soroban](https://soroban.stellar.org) • [Next.js](https://nextjs.org)_ [Live Demo](https://joint-save.vercel.app) • [Watch Video](https://youtu.be/Iuy-As9im7A) • [View Code](https://github.com/Sendi0011/Joint_Save) diff --git a/frontend/.env.ci b/frontend/.env.ci new file mode 100644 index 0000000..ac1c5a6 --- /dev/null +++ b/frontend/.env.ci @@ -0,0 +1,19 @@ +# Supabase +NEXT_PUBLIC_SUPABASE_URL=https://placeholder.supabase.co +NEXT_PUBLIC_SUPABASE_ANON_KEY=placeholder-key + +# Stellar +NEXT_PUBLIC_STELLAR_RPC_URL=https://soroban-testnet.stellar.org +NEXT_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org + +# JointSave Contracts +NEXT_PUBLIC_FACTORY_CONTRACT_ID=CBZNGP52FLFZ4BOGC265FUAMP5KFMAYPQK3KTI5UHMYVMM3QCST3IMRI +NEXT_PUBLIC_TOKEN_CONTRACT_ID=native + +# Pool WASM hashes +NEXT_PUBLIC_ROTATIONAL_WASM_HASH=d350a325d8734263a3d7150c875555d8956e13a527fb3497d5141b8b3f3d2c74 +NEXT_PUBLIC_TARGET_WASM_HASH=133a62226501fc5443e70007d79deeeb0b33fdf8c85c7fcd3cf16293bb5c7292 +NEXT_PUBLIC_FLEXIBLE_WASM_HASH=df6ff088fd79f13d8d03e72160434517fdb4a83b8c7bfdd887be4369805e0d6b + +# Dev Tooling +NEXT_PUBLIC_DEBUG_DATA_LAYER=false \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index f639e21..a385d07 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -16,9 +16,10 @@ yarn-debug.log* yarn-error.log* .pnpm-debug.log* -# env files (but keep example) +# env files (but keep example and ci) .env* !.env.example +!.env.ci # vercel .vercel diff --git a/frontend/components/dashboard/my-groups.tsx b/frontend/components/dashboard/my-groups.tsx index 5552c04..ccd94c6 100644 --- a/frontend/components/dashboard/my-groups.tsx +++ b/frontend/components/dashboard/my-groups.tsx @@ -1,8 +1,8 @@ "use client" import { Card } from "@/components/ui/card" -import { Skeleton } from "@/components/ui/skeleton" -import { Input } from "@/components/ui/input" +import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" import { Pagination, PaginationContent, @@ -10,15 +10,19 @@ import { PaginationNext, PaginationPrevious, } from "@/components/ui/pagination" -import { Search } from "lucide-react" +import { Users, TrendingUp, Calendar, ArrowRight, Loader2 } from "lucide-react" +import Link from "next/link" import { motion } from "framer-motion" import { useState, useEffect, useCallback } from "react" import { useRouter, useSearchParams } from "next/navigation" import { useStellar } from "@/components/web3-provider" -import { EmptyState } from "@/components/dashboard/empty-state" -import { FirstPoolTooltip } from "@/components/dashboard/first-pool-tooltip" -import { PoolCard, PoolCardSkeleton, type Pool } from "@/components/dashboard/pool-card" -import { useDebouncedValue } from "@/hooks/use-debounced-value" +import { usePoolData } from "@/lib/data-layer/PoolDataProvider" +import { + stroopsToXlm, + RotationalPoolState, + TargetPoolState, + FlexiblePoolState, +} from "@/hooks/useJointSaveContracts" const PAGE_SIZE = 6 @@ -26,18 +30,156 @@ interface MyGroupsProps { onCreateClick?: () => void } -const container = { - hidden: { opacity: 0 }, - show: { opacity: 1, transition: { staggerChildren: 0.1 } }, +type PoolWithLive = Pool & { + liveTotalSaved?: number + liveProgress?: number + progressLabel?: string +} + +const container = { hidden: { opacity: 0 }, show: { opacity: 1, transition: { staggerChildren: 0.1 } } } +const item = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } } +const formatXlm = (n: number) => `${n.toFixed(2)} XLM` + +// ── Per-pool card that hydrates live balances from the unified cache ────────── +function PoolCard({ pool }: { pool: Pool }) { + // Use contract address as cache key for deployed pools, fallback to DB id + const cacheKey = + pool.contract_address && pool.contract_address !== "pending_deployment" + ? pool.contract_address + : pool.id + + const { data, isLoading } = usePoolData(cacheKey) + + // Derive live stats from cache when available + const getLiveStats = (): { totalSaved: number; progress: number; progressLabel: string } => { + const onchain = data?.onchain ?? null + + if (pool.type === "rotational" && onchain) { + const s = onchain as RotationalPoolState + const totalMembers = s.members.length || pool.members_count || 1 + const progress = Math.min(100, Math.round((s.currentRound / totalMembers) * 100)) + const perRound = (pool.contribution_amount || 0) * totalMembers + const totalSaved = s.currentRound * perRound + return { + totalSaved, + progress, + progressLabel: `Round ${s.currentRound + 1} of ${totalMembers}`, + } + } + + if (pool.type === "target" && onchain) { + const s = onchain as TargetPoolState + const saved = stroopsToXlm(s.totalDeposited) + const target = pool.target_amount || stroopsToXlm(s.targetAmount) || 1 + const progress = Math.min(100, Math.round((saved / target) * 100)) + return { + totalSaved: saved, + progress, + progressLabel: `${saved.toFixed(2)} / ${target.toFixed(2)} XLM`, + } + } + + if (pool.type === "flexible" && onchain) { + const s = onchain as FlexiblePoolState + const totalSaved = stroopsToXlm(s.totalBalance) + const softGoal = (pool.minimum_deposit || 0) * (pool.members_count || 1) + const progress = + softGoal > 0 + ? Math.min(100, Math.round((totalSaved / softGoal) * 100)) + : s.isActive + ? 50 + : 100 + return { + totalSaved, + progress, + progressLabel: + softGoal > 0 + ? `${totalSaved.toFixed(2)} / ${softGoal.toFixed(2)} XLM` + : `${totalSaved.toFixed(2)} XLM saved`, + } + } + + // Fallback to DB data + return { totalSaved: pool.total_saved ?? 0, progress: pool.progress ?? 0, progressLabel: "" } + } + + const { totalSaved, progress, progressLabel } = getLiveStats() + + return ( + + +
+
+

{pool.name}

+ {pool.type.charAt(0).toUpperCase() + pool.type.slice(1)} +
+ {pool.status} +
+ +
+
+ + Members + + {pool.members_count} +
+
+ + Total Saved + + + {isLoading && !data?.onchain ? ( + + ) : ( + formatXlm(totalSaved) + )} + +
+
+ + + {pool.type === "rotational" ? "Frequency" : "Status"} + + {pool.frequency || pool.status} +
+
+ +
+
+ Progress + {progress.toFixed(1)}% +
+
+ +
+ {progressLabel && ( +

{progressLabel}

+ )} +
+ + +
+
+ ) } // ── Main MyGroups component ─────────────────────────────────────────────────── + export function MyGroups({ onCreateClick }: MyGroupsProps) { const { address } = useStellar() const router = useRouter() const searchParams = useSearchParams() - const [pools, setPools] = useState([]) + const [pools, setPools] = useState([]) const [total, setTotal] = useState(0) const [loading, setLoading] = useState(true) const [error, setError] = useState("") @@ -48,14 +190,11 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) { const debouncedSearchInput = useDebouncedValue(searchInput, 300) const totalPages = Math.ceil(total / PAGE_SIZE) - const setPage = useCallback( - (p: number) => { - const params = new URLSearchParams(searchParams.toString()) - params.set("page", String(p)) - router.push(`?${params.toString()}`, { scroll: false }) - }, - [router, searchParams] - ) + const setPage = useCallback((p: number) => { + const params = new URLSearchParams(searchParams.toString()) + params.set("page", String(p)) + router.push(`?${params.toString()}`, { scroll: false }) + }, [router, searchParams]) const setSearchTerm = useCallback( (term: string) => { @@ -83,10 +222,7 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) { }, [debouncedSearchInput, searchTerm, setSearchTerm]) useEffect(() => { - if (!address) { - setLoading(false) - return - } + if (!address) { setLoading(false); return } loadPools(page) }, [address, page]) @@ -97,9 +233,13 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) { const res = await fetch(`/api/pools?creator=${address?.toLowerCase()}&page=${currentPage}`) if (!res.ok) throw new Error("Failed to fetch pools") const json = await res.json() - const data: Pool[] = Array.isArray(json) ? json : (json.data ?? []) - setPools(data) - setTotal(json.total ?? data.length) + const base: Pool[] = Array.isArray(json) ? json : (json.data ?? []) + setTotal(json.total ?? base.length) + + // Render DB data immediately + setPools(base as PoolWithLive[]) + setLoading(false) + } catch (err) { setError(err instanceof Error ? err.message : "Failed to fetch pools") setPools([]) @@ -108,59 +248,30 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) { } } - // Client-side filtering by pool name - // Note: This filters only the currently loaded page (6 pools max). - // For a full cross-page search, we would need backend API support. - const filteredPools = searchTerm - ? pools.filter((pool) => pool.name.toLowerCase().includes(searchTerm.toLowerCase())) - : pools - - if (loading) { - return ( -
-
-

My Groups

- -
-
- {Array.from({ length: PAGE_SIZE }).map((_, i) => ( - - ))} -
+ if (loading) return ( +
+

My Groups

+
+
- ) - } +
+ ) - if (error) { - return ( -
-
-

My Groups

-
- -

{error}

-
-
- ) - } + if (error) return ( +
+

My Groups

+

{error}

+
+ ) return (
- +

My Groups

- {total === 0 - ? "Manage your savings circles" - : `${total} active group${total !== 1 ? "s" : ""}`} + {total === 0 ? "Manage your savings circles" : `${total} active group${total !== 1 ? "s" : ""}`}

@@ -178,41 +289,93 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) {
{pools.length === 0 ? ( - - ) : filteredPools.length === 0 ? ( - -
- -
-

No pools match your search

-

- Try adjusting your search term or{" "} - - . -

-
+ + +
+
+ +
+

No groups yet

+

Create your first savings group or join an existing one

+ +
+
+
) : ( <> - - - - {filteredPools.map((pool) => ( - - ))} - + + {pools.map((pool) => { + const totalSaved = pool.liveTotalSaved ?? pool.total_saved ?? 0 + const progress = pool.liveProgress ?? pool.progress ?? 0 + return ( + + +
+
+

{pool.name}

+ {pool.type.charAt(0).toUpperCase() + pool.type.slice(1)} +
+ {pool.status} +
+ +
+
+ + Members + + {pool.members_count} +
+
+ + Total Saved + + {formatXlm(totalSaved)} +
+
+ + + {pool.type === "rotational" ? "Frequency" : "Status"} + + {pool.frequency || pool.status} +
+
+ +
+
+ Progress + {progress.toFixed(1)}% +
+
+ +
+ {pool.progressLabel && ( +

{pool.progressLabel}

+ )} +
+ + +
+
+ ) + })} +
{totalPages > 1 && (

- Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total}{" "} - pools + Showing {page * PAGE_SIZE + 1}–{Math.min((page + 1) * PAGE_SIZE, total)} of {total} pools

@@ -227,9 +390,7 @@ export function MyGroups({ onCreateClick }: MyGroupsProps) { setPage(page + 1)} aria-disabled={page >= totalPages - 1} - className={ - page >= totalPages - 1 ? "pointer-events-none opacity-50" : "cursor-pointer" - } + className={page >= totalPages - 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} /> diff --git a/frontend/components/group/group-members.tsx b/frontend/components/group/group-members.tsx index 06651af..54f6e6c 100644 --- a/frontend/components/group/group-members.tsx +++ b/frontend/components/group/group-members.tsx @@ -1,22 +1,20 @@ "use client" -import { Card } from "@/components/ui/card" -import { Avatar, AvatarFallback } from "@/components/ui/avatar" -import { Badge } from "@/components/ui/badge" -import { Skeleton } from "@/components/ui/skeleton" -import { Button } from "@/components/ui/button" -import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" -import { CheckCircle2, Clock, XCircle, AlertCircle, Award, Copy, Check } from "lucide-react" -import { useState, useEffect } from "react" -import { usePoolData } from "@/lib/data-layer/PoolDataProvider" -import { useOptimisticTransactions } from "@/hooks/useOptimisticTransactions" +import { Card } from "@/components/ui/card"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Badge } from "@/components/ui/badge"; import { - RotationalPoolState, - fetchReputation, - type ReputationScore, -} from "@/hooks/useJointSaveContracts" -import { useToast } from "@/hooks/use-toast" -import { countPendingMembers, filterPendingMembers } from "@/lib/member-filters" + CheckCircle2, + Clock, + XCircle, + Loader2, + AlertCircle, + Award, +} from "lucide-react"; +import { useState, useEffect } from "react"; +import { usePoolData } from "@/lib/data-layer/PoolDataProvider"; +import { useOptimisticTransactions } from "@/hooks/useOptimisticTransactions"; +import { RotationalPoolState, fetchReputation, type ReputationScore } from "@/hooks/useJointSaveContracts"; interface Member { id: string @@ -117,23 +115,9 @@ export function GroupMembers({ groupId, contractAddress, poolType }: GroupMember if (isLoading && members.length === 0) { return ( - - -
- {Array.from({ length: 4 }).map((_, i) => ( -
-
- {/* avatar */} - -
- - -
-
- {/* status icon */} - -
- ))} + +
+
) diff --git a/frontend/components/group/yield-dashboard.tsx b/frontend/components/group/yield-dashboard.tsx index 234e71e..348293c 100644 --- a/frontend/components/group/yield-dashboard.tsx +++ b/frontend/components/group/yield-dashboard.tsx @@ -20,6 +20,8 @@ import { Account, Transaction, } from "@stellar/stellar-sdk" +import { STELLAR_RPC_URL } from "@/components/web3-provider" +import { STELLAR_NETWORK_PASSPHRASE } from "@/components/web3-provider" import { getRpc } from "@/hooks/useJointSaveContracts" const TX_TIMEOUT = 300 @@ -95,10 +97,21 @@ export function YieldDashboard({ poolAddress }: YieldDashboardProps) { strategyAddress = null } else { // scvOption wrapping an address - const inner = - stratVal.switch().name === "scvAddress" - ? stratVal - : (stratVal.value() as unknown as xdr.ScVal) + const kind = stratVal.switch().name; + + let inner: xdr.ScVal; + + if (kind === "scvAddress") { + inner = stratVal; + } else { + const value = stratVal.value(); + + if (!(value instanceof xdr.ScVal)) { + throw new Error("Expected ScVal"); + } + + inner = value; + } strategyAddress = Address.fromScVal(inner).toString() } } catch {} diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs index 225b947..edc1ba6 100644 --- a/frontend/eslint.config.mjs +++ b/frontend/eslint.config.mjs @@ -1,52 +1,42 @@ -import tseslint from "typescript-eslint" -import nextPlugin from "@next/eslint-plugin-next" -import reactHooksPlugin from "eslint-plugin-react-hooks" -import prettierConfig from "eslint-config-prettier" +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import tsPlugin from "@typescript-eslint/eslint-plugin"; +import reactHooks from "eslint-plugin-react-hooks"; -export default tseslint.config( - // Files to lint +export default [ { - files: ["**/*.ts", "**/*.tsx"], + ignores: [".next/**", "node_modules/**", "out/**", "build/**"], }, - - // Next.js core-web-vitals flat config (includes @next/next plugin + rules) - nextPlugin.configs["core-web-vitals"], - - // TypeScript recommended rules - ...tseslint.configs.recommended, - - // React hooks rules { + files: ["**/*.{js,jsx,ts,tsx}"], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + parser: tsParser, + parserOptions: { + ecmaVersion: 2024, + sourceType: "module", + }, + }, plugins: { - "react-hooks": reactHooksPlugin, + "@typescript-eslint": tsPlugin, + "react-hooks": reactHooks, }, rules: { + ...tsPlugin.configs.recommended.rules, "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", }, }, - - // Project-specific rule overrides { - rules: { - "no-console": "warn", - "@typescript-eslint/no-explicit-any": "error", - "@typescript-eslint/no-unused-vars": [ - "error", - { - varsIgnorePattern: "^_", - argsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], + files: ["**/*.{ts,tsx}"], + languageOptions: { + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, }, }, - - // Prettier must be last — disables all formatting rules that could conflict - prettierConfig, - - // Ignore patterns - { - ignores: ["node_modules/**", ".next/**", "out/**", "e2e/**", "playwright.config.ts"], - } -) +]; diff --git a/frontend/hooks/useJointSaveContracts.ts b/frontend/hooks/useJointSaveContracts.ts index 7bc27ae..10a3cdc 100644 --- a/frontend/hooks/useJointSaveContracts.ts +++ b/frontend/hooks/useJointSaveContracts.ts @@ -796,12 +796,9 @@ async function fetchContractStorage( if (entry && typeof entry === "object") { if ("xdr" in entry) { - rawXdr = entry.xdr as string - } else if ( - entry.val && - typeof (entry.val as unknown as { toXDR?: () => string }).toXDR === "function" - ) { - rawXdr = (entry.val as unknown as { toXDR: (encoding: string) => string }).toXDR("base64") + rawXdr = (entry.xdr as string) + } else if (entry.val && typeof (entry.val as any).toXDR === "function") { + rawXdr = (entry.val as any).toXDR("base64") } } if (!rawXdr) return null diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index f5cbc38..4cd9948 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,8 +1,5 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - eslint: { - ignoreDuringBuilds: true, - }, typescript: { ignoreBuildErrors: true, }, diff --git a/frontend/package.json b/frontend/package.json index 94456c4..00c7e28 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,16 +4,9 @@ "private": true, "scripts": { "build": "next build", - "dev": "next dev --webpack", - "lint": "eslint . --ext .ts,.tsx", - "lint:fix": "eslint . --ext .ts,.tsx --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "start": "next start", - "test:unit": "tsx --test lib/csv-export.test.ts lib/analytics.test.ts lib/pool-health.test.ts lib/form-validation.test.ts lib/member-filters.test.ts hooks/use-keyboard-shortcuts.test.ts app/api/admin/audit-log/route.test.ts app/api/admin/actions/route.test.ts", - "test:e2e": "playwright test", - "test:e2e:ui": "playwright test --ui", - "test:e2e:report": "playwright show-report" + "dev": "next dev", + "lint": "eslint .", + "start": "next start" }, "dependencies": { "@creit.tech/stellar-wallets-kit": "^1.2.0", @@ -90,12 +83,10 @@ "@types/papaparse": "^5.5.2", "@types/react": "^19", "@types/react-dom": "^19", - "@typescript-eslint/eslint-plugin": "^8.62.0", - "@typescript-eslint/parser": "^8.62.0", - "eslint": "^10.6.0", - "eslint-config-next": "^16.2.9", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react-hooks": "^7.1.1", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", + "eslint": "^9", + "eslint-plugin-react-hooks": "^7.0.1", "postcss": "^8.5", "prettier": "^3.9.1", "tailwindcss": "^4.1.9",