A quick-start reference for contributors to the Scavenger frontend.
frontend/src/
├── api/ # ScavengerClient, types, and API index
├── components/
│ ├── layout/ # AppShell, navigation
│ ├── modals/ # Transaction modals (RegisterWaste, TransferWaste, CreateIncentive)
│ └── ui/ # Reusable primitives (Button, Card, Dialog, TransactionConfirmDialog…)
├── context/ # React Context providers (Auth, Wallet, Contract, Theme)
├── hooks/ # Custom hooks for contract mutations and queries
├── lib/ # Utilities, contract re-export, Stellar helpers, error parsing
├── pages/ # One file per route
├── router.tsx # React Router config with lazy-loaded pages
├── main.tsx # App entry point, providers, QueryClient setup
└── index.css # Tailwind base styles
The app uses two complementary layers:
React Context — for global, low-frequency state:
AuthContext— authenticated user (address, role, name), persisted tolocalStorage.WalletContext— Freighter wallet connection state andconnect/disconnecthelpers.ContractContext— active RPC URL, network, and contract ID (can be changed in Settings).
TanStack React Query — for all server/contract state:
- Queries cache on-chain reads (participant info, waste lists, incentives).
- Mutations handle write transactions and automatically invalidate related queries on success.
- Global error toasts are wired in
main.tsxviaQueryCache/MutationCacheonError.
- Create
src/pages/MyNewPage.tsxand export a named component:
export function MyNewPage() {
return <div>My new page</div>
}- Add a lazy import in
src/router.tsx:
const MyNewPage = lazy(() =>
import('@/pages/MyNewPage').then((m) => ({ default: m.MyNewPage }))
)- Add a route inside the protected
childrenarray (or at the top level for public routes):
{ path: 'my-page', element: <MyNewPage /> },The PageFallback wrapper in ProtectedLayout automatically shows a skeleton while the chunk loads.
All contract calls go through ScavengerClient in src/api/client.ts. The pattern is:
// src/hooks/useMyData.ts
import { useQuery } from '@tanstack/react-query'
import { ScavengerClient } from '@/api/client'
import { useWallet } from '@/context/WalletContext'
import { useContract } from '@/context/ContractContext'
import { networkConfig } from '@/lib/stellar'
export function useMyData() {
const { address } = useWallet()
const { config } = useContract()
return useQuery({
queryKey: ['my-data', address],
queryFn: async () => {
const client = new ScavengerClient({
rpcUrl: config.rpcUrl,
networkPassphrase: networkConfig.networkPassphrase,
contractId: config.contractId,
})
return client.myReadMethod(address!)
},
enabled: !!address,
})
}// src/hooks/useMyAction.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { ScavengerClient } from '@/api/client'
import { useWallet } from '@/context/WalletContext'
import { useContract } from '@/context/ContractContext'
import { networkConfig } from '@/lib/stellar'
export function useMyAction() {
const { address } = useWallet()
const { config } = useContract()
const queryClient = useQueryClient()
return useMutation({
mutationFn: async (params: { foo: string }) => {
if (!address) throw new Error('Wallet not connected.')
const client = new ScavengerClient({
rpcUrl: config.rpcUrl,
networkPassphrase: networkConfig.networkPassphrase,
contractId: config.contractId,
})
return client.myWriteMethod(params.foo, address)
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['my-data'] })
},
})
}If the action requires a confirmation step before signing, use TransactionConfirmDialog:
import { TransactionConfirmDialog } from '@/components/ui/TransactionConfirmDialog'
// In your modal/page:
<TransactionConfirmDialog
open={showConfirm}
action="My Action"
params={[{ label: 'Param', value: someValue }]}
isPending={mutation.isPending}
onConfirm={() => mutation.mutate(params)}
onCancel={() => setShowConfirm(false)}
/>Add the method to src/api/client.ts following the existing pattern:
async myWriteMethod(arg: string, signer: string): Promise<void> {
return this.invoke<void>(
'my_contract_fn',
[nativeToScVal(arg, { type: 'string' })],
signer
)
}- Pass
signerfor mutating calls (triggers Freighter signing). - Omit
signerfor read-only calls (simulation only).
cd frontend
cp .env.example .env # fill in CONTRACT_ID and RPC_URL
npm install
npm run devLint and format:
npm run lint
npm run format