TruCycle is a London-based logistics and web application platform for household waste management that enables users to list, exchange, donate, or acquire household items while tracking their environmental impact through CO2 savings.
- Overview
- Quick Start
- Environment Variables
- Project Structure
- Application Architecture
- Pages & Routes
- Components Reference
- Real-time Features
- API Integration
- State Management
- Known Issues & Caveats
- Code Navigation Tips
- Development Workflow
- Deployment & Handover Notes
- License
- User Authentication: Registration, login, email verification, and profile management
- Item Listing: Create, edit, and manage item listings with photos, categories, and conditions
- Search & Browse: Filter items by category, location, condition, and price
- Exchange System: Request, approve, and track item exchanges and donations
- Drop-off Points: Interactive map showing 20 partner drop-off locations
- Carbon Tracking: Calculate and display CO2 savings from sustainable actions
- Real-time Messaging: WebSocket-based chat between users
- Real-time Notifications: WebSocket-based notifications for claims, messages, and system events
- QR Code System: Shop partners can scan items for drop-offs and collections
- Partner Portal: Separate interface for shop partners to manage inventory
- Frontend: React 18.3.1 + TypeScript + Vite
- Styling: Tailwind CSS 4.x + Radix UI components
- State Management: React hooks + custom KV store (localStorage-based)
- Real-time: Socket.IO client for WebSocket connections
- Forms: React Hook Form + Zod validation
- Maps: React Leaflet for drop-off location maps
- Icons: Phosphor Icons + Heroicons
- Animations: Framer Motion
- Image Upload: Cloudinary (unsigned upload)
- Node.js 18+ and npm 9+
- A backend API server running (see
VITE_API_BASE_URL) - Cloudinary account for image uploads
-
Clone the repository
git clone <repository-url> cd trucycle-sustainable
-
Install dependencies
npm install
-
Configure environment variables
cp .env.example .env # Edit .env with your values (see Environment Variables section) -
Start development server
npm run dev
The app will be available at
http://localhost:5173(or the next available port)
npm run dev- Start Vite development server with hot reloadnpm run build- Build for production (outputs todist/)npm run preview- Preview production build locallynpm run lint- Run ESLint to check code qualitynpm test- Run Vitest unit testsnpm run kill- Kill process running on port 5000
All environment variables are prefixed with VITE_ to be accessible in the frontend. Create a .env file in the root directory:
- Purpose: Base URL of the backend API server
- Example:
https://api.trucycle.comorhttp://localhost:3000 - Usage: All API calls are prefixed with this URL
- Critical: Application will not function without this
- Location:
src/lib/api/client.ts
- Purpose: Your Cloudinary cloud name for image uploads
- Example:
dxxxxxxxxxxxx - How to get: Sign up at cloudinary.com and find in dashboard
- Usage: Used for uploading item photos and chat images
- Location:
src/lib/cloudinary.ts
- Purpose: Unsigned upload preset name from Cloudinary
- Example:
trucycle_unsigned - Setup: Create an unsigned preset in Cloudinary settings β Upload β Upload presets
- Security: Must be "unsigned" for frontend uploads
- Location:
src/lib/cloudinary.ts
- Purpose: Folder path in Cloudinary where images are stored
- Default:
trucycle/items - Example:
production/trucycle/itemsordev/trucycle/items - Best Practice: Use different folders for dev/staging/prod
- Location:
src/lib/cloudinary.ts
VITE_API_BASE_URL=http://localhost:3000
VITE_CLOUDINARY_CLOUD_NAME=dxxxxxxxxxxxx
VITE_CLOUDINARY_UPLOAD_PRESET=trucycle_unsigned
VITE_CLOUDINARY_FOLDER=trucycle/items- Missing
VITE_API_BASE_URLwill throw an error on first API call - Missing Cloudinary config will throw an error when trying to upload images
- Check browser console for configuration errors
trucycle-sustainable/
βββ src/
β βββ components/ # React components
β β βββ auth/ # Authentication components
β β βββ messaging/ # Chat and messaging components
β β βββ partner/ # Partner portal components
β β βββ skeletons/ # Loading skeleton components
β β βββ ui/ # Radix UI component wrappers
β βββ hooks/ # Custom React hooks
β βββ lib/ # Utility libraries
β β βββ api/ # API client and types
β β βββ messaging/ # Messaging WebSocket logic
β β βββ notifications/ # Notification WebSocket logic
β βββ types/ # TypeScript type definitions
β βββ styles/ # Global styles
β βββ App.tsx # Main consumer app component
β βββ PartnerRouter.tsx # Partner portal router
β βββ RootRouter.tsx # Root application router
β βββ main.tsx # Application entry point
βββ backend_docs/ # Backend API documentation
βββ .env.example # Environment variable template
βββ package.json # Dependencies and scripts
βββ vite.config.ts # Vite configuration
βββ tailwind.config.js # Tailwind CSS configuration
βββ tsconfig.json # TypeScript configuration
The app uses a custom client-side router without external routing libraries:
-
RootRouter (
src/RootRouter.tsx)- Top-level router that handles path parsing
- Routes authentication pages:
/auth/verify,/auth/forgot-password,/auth/reset-password - Routes partner portal:
/partner/* - Default route goes to main App
-
PartnerRouter (
src/PartnerRouter.tsx)- Handles partner portal routes:
/partner/home,/partner/items,/partner/shops,/partner/profile - Manages authentication state for partners
- Redirects unauthenticated users to
/partner/login
- Handles partner portal routes:
-
Main App (
src/App.tsx)- Tab-based navigation:
home,search,list,map,profile,mylistings - All navigation is handled via state, not URL changes
- Modal-based interface for most interactions
- Tab-based navigation:
-
Consumer Users
- Sign up via
AuthDialogcomponent - Email verification required (check inbox for link)
- Post-signup onboarding for user type (donor/collector) and location
- JWT tokens stored in localStorage via
kvStore - Auto-logout after 24 hours of inactivity
- Sign up via
-
Partner Users (Shop Owners)
- Separate registration at
/partner/register - Must provide shop details during registration
- Access to partner portal with different features
- Same JWT authentication but different user type
- Separate registration at
- localStorage-based key-value store with type safety
- Supports serialization of complex objects
- Used for: user profiles, auth tokens, preferences, onboarding state
- Hook:
useKV(key, defaultValue)provides reactive state
- Global loading state management
- Tracks multiple concurrent operations by key
- Used for API calls and async operations
- Functions:
startLoading(key),finishLoading(key)
@tanstack/react-queryis installed but not actively used- Most data fetching is done via custom hooks and manual state management
- Component:
<Homepage /> - Purpose: Landing page with hero, featured items, and quick actions
- Features:
- Hero section with app introduction
- Quick action buttons (List Item, Browse, Find Drop-offs)
- Environmental impact stats
- Demo guide for first-time users
- Navigation: Default tab on app load
- Component:
<ItemListing /> - Purpose: Browse and search all available items
- Features:
- Search bar with real-time filtering
- Category filters (Furniture, Electronics, Clothing, etc.)
- Condition filters (New, Like New, Good, Fair)
- Action type filters (Exchange, Donate, Recycle)
- Grid view of item cards
- Item detail modals with claim/request functionality
- QR code generation for items
- API:
GET /items/search
- Component:
<ItemListingForm /> - Purpose: Create new item listings
- Features:
- Multi-step form (Category β Details β Photos β Location β Action)
- Photo upload (up to 5 images via Cloudinary)
- Category selection (predefined categories)
- Condition selection
- Action type selection (Exchange, Donate, Recycle)
- Location/postcode input
- Drop-off location selector for donations
- Draft saving capability
- API:
POST /items - Caveat: Must be authenticated to list items
- Component:
<DropOffMap /> - Purpose: Find partner drop-off locations
- Features:
- Interactive Leaflet map with 20 partner locations
- Location markers with shop details
- Filter by shop capabilities (donations, recycling)
- Directions and contact information
- Shop hours and capacity indicators
- Data: Static shop locations from
src/components/dropOffLocations.ts - Note: Map tiles load from OpenStreetMap (requires internet)
- Component:
<ProfileDashboard /> - Purpose: User profile, stats, and settings
- Features:
- Profile information and avatar
- Environmental impact stats (CO2 saved, items exchanged)
- Trust score and verification badges
- Verification center (email, identity, address)
- Rewards balance
- Settings dialog (edit profile, change preferences)
- Logout functionality
- API:
GET /users/me,GET /users/me/impact
- Component:
<MyListingsView /> - Purpose: Manage user's own listings and collections
- Features:
- Two sections: "My Items" and "My Collections"
- Edit/delete own listings
- View and manage claims on listed items
- Approve/reject claim requests
- Mark items as collected
- View collected items and their status
- QR code generation for pickups
- API:
GET /items/me/listed,GET /items/me/collected
- Component:
<VerifyEmailPage /> - Purpose: Email verification after signup
- Triggered: When user clicks verification link in email
- Query Params:
?token=xxx - Flow: Auto-verifies and redirects to app
- Component:
<ForgotPasswordPage /> - Purpose: Request password reset email
- Input: Email address
- API:
POST /auth/forgot-password
- Component:
<ResetPasswordPage /> - Purpose: Set new password with reset token
- Query Params:
?token=xxx - API:
POST /auth/reset-password
- Component:
<PartnerLoginPage /> - Purpose: Login for shop partners
- Different from consumer login: Separate authentication flow
- Component:
<PartnerRegisterPage /> - Purpose: Shop registration
- Required Info: Shop name, address, contact, capabilities
- Component:
<PartnerHome /> - Purpose: Partner dashboard with stats
- Component:
<PartnerItems /> - Purpose: View items dropped off at partner shops
- Component:
<PartnerShops /> - Purpose: Manage shop locations and details
- Component:
<PartnerProfile /> - Purpose: Partner account settings
- Landing page component
- Features: Hero, stats, CTAs, demo guide
- Props: Navigation callbacks, auth state
- Search and browse items interface
- Features: Filters, search, grid view, item details
- Complex component with multiple sub-components
- Manages: Search state, filter state, selected item
- Multi-step item creation form
- Largest component (~1400 lines)
- Steps: Category β Details β Photos β Location β Action
- Features: Draft saving, photo upload, validation
- Props: Callbacks for completion, draft state
- User's listings and collections manager
- Two tabs: Listed items and Collected items
- Features: Edit, delete, claim management
- Complex claim approval workflow
- User profile and stats display
- Features: Impact metrics, verification status, trust score
- Integrates: VerificationCenter, TrustScore, CarbonTracker
- Interactive map component using React Leaflet
- Shows 20 partner drop-off locations
- Features: Markers, popups, filters, directions
- Note: Lazy-loads Leaflet to reduce initial bundle size
- Modal dialog for login/signup
- Switches between signin and signup modes
- Features: Email/password auth, validation, error handling
- API:
POST /auth/login,POST /auth/register
- Post-signup onboarding flow
- Collects: User type (donor/collector), location, preferences
- Can be reopened for editing profile
- API:
PUT /users/me
- Real-time chat interface
- Two views: Chats list and Request inbox
- Features: Message sending, image attachments, presence indicators
- WebSocket:
/messagesnamespace - See Messaging System for details
- Reusable message UI components
- Components: MessageBubble, ChatHeader, RoomList
- Notification dropdown in header
- Shows recent notifications with unread count badge
- Types: Claims, collections, drop-offs, system messages
- WebSocket:
/notificationsnamespace - Features: Mark as read, navigation to related items
- Generates QR codes for items
- Used for: Item pickups, drop-offs, partner scans
- Encodes: Item ID and action type
- Collector's QR scanner for quick claims
- Scans item QR codes to instantly claim
- Uses browser camera API
- Library: jsqr for QR decoding
- Partner's scanner for drop-offs and collections
- Handles: Drop-in scanning, claim-out scanning
- Validates items and processes transactions
- API:
POST /scan/dropin,POST /scan/claimout
- Displays CO2 savings visualization
- Animated progress bars and stats
- Data from user impact metrics
- User trust/reputation score display
- Based on: Transactions, ratings, verifications
- Visual: Stars and percentage
- Shows verification status badges
- Types: Email verified, Identity verified, Address verified
- Used in: Profile, listings, messages
- Interactive tutorial overlay
- Shows on first visit (dismissible)
- Guides users through key features
Radix UI wrappers for design system consistency:
- Button, Input, Select, Checkbox, Radio, Switch
- Dialog, Sheet, Popover, Tooltip, Dropdown Menu
- Card, Badge, Avatar, Separator, Tabs
- Alert Dialog, Toast (Sonner), Progress Bar
- All styled with Tailwind CSS and CVA (Class Variance Authority)
The app uses Socket.IO for real-time WebSocket communication. There are two separate namespaces:
// src/lib/messaging/socket.ts
import { io } from 'socket.io-client'
const socket = io(API_BASE_URL + '/messages', {
auth: { token: accessToken },
transports: ['websocket'],
autoConnect: false
})- JWT token required in handshake:
{ auth: { token } } - Alternative: Query param
?token=xxxor headerAuthorization: Bearer xxx - Connection rejected if token missing/invalid
room:join - Create/join a direct message room
socket.emit('room:join', { otherUserId: 'user-uuid' })
socket.once('room:joined', (room: ActiveRoomViewModel) => {
// Room ready
})message:send - Send text or image message
socket.emit('message:send', {
roomId: 'room-uuid',
text: 'Hello!',
files: [{ name: 'image.jpg', type: 'image/jpeg', data: base64Data }]
})
socket.once('message:sent', (message: MessageViewModel) => {
// Message sent
})message:new- New message in a roomroom:activity- Room last activity updatedpresence:update- User online/offline status changedroom:cleared- Room history clearedroom:deleted- Room deleted
POST /messages/rooms- Ensure room existsGET /messages/rooms/active- List active roomsGET /messages/rooms/:id/messages- Message history (paginated)POST /messages/rooms/:id/messages/image- Upload image (for large files)DELETE /messages/rooms/:id- Delete room
Custom hook in src/hooks/useMessaging.ts:
- Manages socket connection lifecycle
- Handles room joining and message sending
- Provides reactive state for rooms and messages
- Auto-reconnects on token changes
- Image files sent via WebSocket are base64-encoded (size limit ~1MB)
- For larger images, use HTTP upload endpoint
- Messages are automatically marked with direction (
incoming/outgoing) per viewer - Presence is tracked per socket (user online if any socket connected)
See full documentation: backend_docs/messaging_websocket.md
// src/lib/notifications/socket.ts
const socket = io(API_BASE_URL + '/notifications', {
auth: { token: accessToken },
transports: ['websocket'],
autoConnect: false
})notification:new - New notification received
socket.on('notification:new', (notification: NotificationViewModel) => {
// Update badge count, show toast, update list
})notification:read - Mark notification(s) as read
// Single
socket.emit('notification:read', { id: 'notif-uuid' })
// Multiple
socket.emit('notification:read', { ids: ['uuid1', 'uuid2'] })
socket.once('notification:read:ack', ({ count }) => {
console.log(`Marked ${count} as read`)
})GET /notifications- List notifications (with?unread=truefilter)GET /notifications/unread-count- Get unread count for badge
item.claim.request- Someone requested to claim your itemitem.claim.approved- Your claim was approveditem.collection- Item was collected/delivereddropin.created- Item dropped off at partner shopdropoff.created- Drop-off recordedpickup.created- Pickup scheduled (future feature)general- System messages
Custom hook in src/hooks/useNotifications.ts:
- Manages socket connection
- Maintains notification list and unread count
- Plays sound on new notifications (optional)
- Provides
markRead()function
- Notifications are persisted in database (not lost if offline)
- Unread count should be reconciled with server on app start
- Sound file:
src/lib/notificationSound.ts(uses Web Audio API) - Desktop notifications not implemented (only in-app)
See full documentation: backend_docs/notification_websocket.md
Located in src/lib/api/client.ts, this module handles all HTTP communication with the backend.
export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || ''- JWT-based authentication
- Access token stored in localStorage (key:
auth.tokens) - Refresh token used for token renewal
- Auto-logout after 24 hours of inactivity
- Token included in all requests via
Authorization: Bearer <token>header
class ApiError extends Error {
status: number // HTTP status code
details?: unknown // Additional error details
}Common status codes:
400- Bad request (validation error)401- Unauthorized (invalid/expired token)403- Forbidden (insufficient permissions)404- Not found422- Unprocessable entity (business logic error)500- Server error
register(dto: RegisterDto): Promise<RegisterResponse>login(dto: LoginDto): Promise<LoginResponse>verify(dto: VerifyDto): Promise<void>forgotPassword(dto: ForgetPasswordDto): Promise<void>resetPassword(dto: ResetPasswordDto): Promise<void>me(): Promise<MeResponse>- Get current user profileupdateProfile(dto: UpdateProfileDto): Promise<void>
searchItems(query?: string, filters?: object): Promise<SearchItemsResponse>createItem(dto: CreateItemDto): Promise<CreateItemResponse>updateItem(id: string, dto: UpdateItemDto): Promise<UpdateItemResponse>deleteItem(id: string): Promise<void>myListedItems(): Promise<MyListedItemsResponse>myCollectedItems(): Promise<MyCollectedItemsResponse>getItemByQr(itemId: string): Promise<QrItemView>
createClaim(dto: CreateClaimDto): Promise<CreateClaimResponse>approveClaim(claimId: string): Promise<ApproveClaimResponse>rejectClaim(claimId: string): Promise<void>collectItem(dto: CollectItemDto): Promise<CollectItemResponse>
nearbyShops(lat: number, lng: number): Promise<NearbyShop[]>createShop(dto: CreateShopDto): Promise<ShopDto>listMyShopItems(shopId: string): Promise<ListMyShopItemsResponse>
scanDropin(dto: DropoffScanDto): Promise<DropoffInResult>scanClaimout(dto: ShopScanDto): Promise<ClaimOutResult>scanItemQr(itemId: string): Promise<QrScanAck>
getUserImpact(): Promise<ImpactMetrics>
- No client-side rate limiting implemented
- Backend may enforce rate limits (check API responses for
429status)
- Check if tokens exist and are not expired
- Add
Authorizationheader with access token - Make fetch request to
${API_BASE_URL}${endpoint} - Handle response:
- Success: Parse JSON and return data
401: Attempt token refresh, retry once- Other errors: Throw
ApiError
Most UI state is managed locally with useState:
- Form inputs
- Modal open/close states
- Tab selections
- Filter selections
Persistent state in localStorage via src/lib/kvStore.ts:
auth.tokens- JWT access and refresh tokensauth.tokens.meta- Last activity timestampcurrent-user- Current consumer user profilepartner-user- Current partner user profileonboarding-dismissals- Dismissed onboarding promptsshow-demo-guide- Demo guide visibility preference- Custom keys via
useKV(key, defaultValue)
// Low-level functions
kvGet<T>(key: string): Promise<T | undefined>
kvSet<T>(key: string, value: T): Promise<void>
kvDelete(key: string): Promise<void>
// React hook (reactive)
const [value, setValue] = useKV<T>(key, defaultValue)- Data is NOT encrypted (avoid storing sensitive data)
- Data persists across sessions (until logout or browser clear)
- Storage quota: ~5-10MB depending on browser
- Changes don't sync across tabs (use
storageevent listener if needed)
Real-time state managed by custom hooks:
useMessaging()- Chat rooms and messagesuseNotifications()- Notifications list and countusePresence()- User online/offline status
Global loading indicators via src/lib/loadingStore.ts:
startLoading('unique-key')
// ... async operation
finishLoading('unique-key')
// Component usage
const isLoading = useLoadingStore((s) => s.loadingKeys.has('unique-key'))- Email verification enforcement - Users must verify email before accessing app
- Password strength validation - Enforced strong passwords (min length, mixed case, number, symbol)
- Input sanitization - HTML and control characters stripped from user input
- File upload validation - Image type and size validation implemented
- No rate limiting - Forms and API calls can be spammed
- No CAPTCHA - Bots can create accounts
- No two-factor authentication - Only email/password auth
- No CSRF protection - API calls don't include CSRF tokens
- LocalStorage not encrypted - Tokens and user data visible in dev tools
- No account lockout - Unlimited login attempts allowed
- Console logging - Errors logged to console (visible in production)
- No XSS protection beyond sanitization - Should use Content Security Policy
- Issue: Requires internet connection to load map tiles
- Workaround: Show error message if tiles fail to load
- Note: Map uses OpenStreetMap (free, no API key needed)
- Issue: Large images (>2MB) may timeout or fail
- Workaround: Client-side compression before upload (not implemented)
- Best Practice: Use HTTP upload endpoint for files >1MB (not WebSocket)
- Issue: WebSocket connections close when app is backgrounded on mobile
- Workaround: Auto-reconnect on visibility change (implemented)
- Note: Some notifications may be missed if app closed
- Issue: Camera access blocked on HTTP (requires HTTPS)
- Workaround: Use
localhostfor dev (allowed by browsers) - Production: Must deploy on HTTPS
- Issue: No debouncing on search input (API called on every keystroke)
- Impact: Unnecessary API calls, potential rate limiting
- Workaround: Add debounce hook (not implemented)
- Issue: App requires internet connection (no service worker)
- Impact: Breaks completely when offline
- Future: Implement PWA with offline caching
- Tested: Chrome 90+, Firefox 88+, Safari 14+
- Not Tested: Edge, Opera, mobile browsers extensively
- Known Issue: Safari sometimes blocks localStorage in private mode
- Issue: Some modals are hard to use on small screens (<375px)
- Workaround: Use Sheet component instead of Dialog on mobile
- Issue: Refresh token logic is basic (retries once, no queue)
- Impact: Concurrent requests may fail during refresh
- Workaround: Retry failed requests manually
- Issue: Partner portal is less polished than consumer app
- Features: Some features are stubs or incomplete
- Testing: Less tested than consumer app
- Large Lists: Item lists not virtualized (lag with >100 items)
- Image Loading: No lazy loading (all images load immediately)
- Bundle Size: ~500KB gzipped (mainly Radix UI and Leaflet)
- Initial Load: ~2-3s on 3G (can be optimized with code splitting)
- Components:
src/components/auth/ - API Calls:
src/lib/api/client.ts-register(),login(),verify() - Token Management:
src/lib/api/client.ts-getTokens(),setTokens() - Auth Dialog:
src/components/auth/AuthDialog.tsx
- Listing Form:
src/components/ItemListingForm.tsx(large file, 1400+ lines) - Browse UI:
src/components/ItemListing.tsx - My Listings:
src/components/MyListingsView.tsx - API:
src/lib/api/client.ts-createItem(),searchItems(), etc. - Types:
src/types/listings.ts
- UI:
src/components/messaging/MessageCenter.tsx - Socket Logic:
src/lib/messaging/socket.ts - Hook:
src/hooks/useMessaging.ts - Backend Docs:
backend_docs/messaging_websocket.md
- UI:
src/components/NotificationList.tsx - Socket Logic:
src/lib/notifications/socket.ts - Hook:
src/hooks/useNotifications.ts - Backend Docs:
backend_docs/notification_websocket.md
- Generation:
src/components/QRCode.tsx - Consumer Scanning:
src/components/QuickClaimScanner.tsx - Partner Scanning:
src/components/ShopScanner.tsx - API:
src/lib/api/client.ts-getItemByQr(),scanDropin(),scanClaimout()
- Tailwind Config:
tailwind.config.js - Global Styles:
src/main.css,src/index.css - Theme Toggle:
src/components/ThemeToggle.tsx - Theme Hook:
src/hooks/useThemeMode.ts - UI Components:
src/components/ui/
- Map Component:
src/components/DropOffMap.tsx - Location Selector:
src/components/LocationSelector.tsx - Drop-off Locations:
src/components/dropOffLocations.ts(static data) - Map Loader:
src/lib/loadLeaflet.ts
import { apiFunction } from '@/lib/api'
import { startLoading, finishLoading } from '@/lib/loadingStore'
import { toast } from 'sonner'
async function handleAction() {
startLoading('action-key')
try {
const result = await apiFunction(params)
toast.success('Success!')
} catch (error) {
toast.error(error.message || 'Failed')
} finally {
finishLoading('action-key')
}
}import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
field: z.string().min(1, 'Required')
})
const form = useForm({
resolver: zodResolver(schema),
defaultValues: { field: '' }
})
<form onSubmit={form.handleSubmit(onSubmit)}>
<Input {...form.register('field')} />
{form.formState.errors.field && <span>{form.formState.errors.field.message}</span>}
</form>import { messageSocket } from '@/lib/messaging/socket'
useEffect(() => {
const handler = (data) => {
// Handle event
}
messageSocket.on('event:name', handler)
return () => {
messageSocket.off('event:name', handler)
}
}, [])import { useKV } from '@/hooks/useKV'
const [value, setValue] = useKV<string>('my-key', 'default')
// Update
setValue('new-value')
// Value persists across page reloadsLarge files that may be intimidating:
ItemListingForm.tsx- 1,400 lines (multi-step form logic)MyListingsView.tsx- 1,500 lines (complex claim management)ItemListing.tsx- 1,000 lines (search, filters, detail view)App.tsx- 800 lines (main orchestration)api/client.ts- 600 lines (all API functions)
Small, focused files:
- Most hooks: 100-300 lines
- UI components: 50-200 lines
- Utility files: 50-150 lines
- Clone repo and install dependencies
- Set up
.envfile (see Environment Variables) - Ensure backend API is running
- Run
npm run devand openhttp://localhost:5173 - Create a test account and explore the app
- Read
PRD.mdfor product requirements - Read
backend_docs/for API documentation
- Pull latest changes:
git pull - Install new dependencies:
npm install - Start dev server:
npm run dev - Make changes (hot reload active)
- Test changes in browser
- Run linter:
npm run lint - Commit and push:
git add . && git commit -m "..." && git push
- Unit Tests: Vitest for utility functions (run:
npm test) - Manual Testing: Use app in browser (most critical)
- API Testing: Use backend's built-in test tools (see backend docs)
- Browser Testing: Test on Chrome, Firefox, Safari
- Mobile Testing: Use browser dev tools mobile emulation
- Open browser console (F12)
- Check Network tab for failed requests
- Look at request/response headers and body
- Verify
VITE_API_BASE_URLis correct - Check backend server logs
- Check console for socket connection errors
- Verify JWT token is valid (not expired)
- Test with backend's built-in WebSocket tester (see backend docs)
- Check network tab for WebSocket frames
- Check if component is using the right state hook
- Verify KV store key names are correct
- Look for missing dependencies in
useEffect() - Use React DevTools to inspect component state
- Check Tailwind class names are correct
- Verify theme colors in
tailwind.config.js - Use browser inspector to see computed styles
- Check for CSS specificity conflicts
- Check browser console for Cloudinary errors
- Verify env variables are set correctly
- Check image file size (<2MB recommended)
- Test with different image formats (JPEG, PNG)
- Create component in
src/components/YourPage.tsx - Add tab or route in
App.tsxor router - Update navigation UI to access new page
- Add types if needed
- Add type definitions in
src/lib/api/types.ts - Add function in
src/lib/api/client.ts - Export function in
src/lib/api/index.ts - Use in component with error handling
- Create file in
src/components/YourComponent.tsx - Export from
src/components/index.ts(if shared) - Import and use in parent component
- Create file in
src/hooks/useYourHook.ts - Export from
src/hooks/index.ts - Use in components
# Check for updates
npm outdated
# Update specific package
npm update package-name
# Update all (careful!)
npm update
# After updates, test thoroughly
npm run build
npm run lint
npm test- All environment variables set in production
- Backend API URL points to production server
- Cloudinary configured for production folder
- HTTPS enabled (required for camera/location access)
- Domain configured and SSL certificate valid
- Build runs without errors:
npm run build - Linter passes:
npm run lint - All tests pass:
npm test - Browser testing completed (Chrome, Firefox, Safari)
- Mobile testing completed (iOS, Android)
# Build optimized bundle
npm run build
# Output will be in dist/ directory
# Contains:
# - index.html (entry point)
# - assets/ (JS, CSS, images)Deploy dist/ folder to:
- Vercel:
vercel --prod - Netlify:
netlify deploy --prod - GitHub Pages: Push
dist/togh-pagesbranch - AWS S3 + CloudFront: Upload to S3, serve via CDN
Use a simple static server:
npm install -g serve
serve -s dist -p 3000FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]Create .env.production:
VITE_API_BASE_URL=https://api.trucycle.com
VITE_CLOUDINARY_CLOUD_NAME=dxxxxxxxxxxxx
VITE_CLOUDINARY_UPLOAD_PRESET=trucycle_prod
VITE_CLOUDINARY_FOLDER=production/trucycle/itemsConsider adding:
- Error Tracking: Sentry, Rollbar, Bugsnag
- Analytics: Google Analytics, Mixpanel, Amplitude
- Performance: Lighthouse CI, Web Vitals
- Uptime: Pingdom, UptimeRobot
- Review error logs and user feedback
- Check backend API health
- Monitor Cloudinary storage usage
- Review and moderate user content
- Update dependencies:
npm update - Run security audit:
npm audit - Check browser compatibility with new releases
- Review and optimize Cloudinary folder structure
- Major dependency updates (React, Vite, etc.)
- Performance audit and optimization
- Security review
- Backup and disaster recovery test
- Start Here: Read this README thoroughly
- Understand Product: Read
PRD.mdfor requirements - Understand Backend: Read
backend_docs/for API docs - Code Walkthrough: Start with
App.tsx, then explore components - Common Tasks: See Common Tasks section
- Ask Questions: Document maintainer: [Add contact information]
β οΈ Note: Replace these placeholders with actual contact information before handover
- Backend API: [Backend team contact - Add name and email]
- Cloudinary: [Account owner - Add name and email]
- Domain/Hosting: [DevOps contact - Add name and email]
- Product Owner: [Product contact - Add name and email]
src/App.tsx- Main app orchestrationsrc/lib/api/client.ts- All API communicationsrc/lib/messaging/socket.ts- Messaging WebSocketsrc/lib/notifications/socket.ts- Notification WebSocketsrc/components/ItemListingForm.tsx- Item creationsrc/components/MyListingsView.tsx- Claim management
- Cloudinary: Image hosting and optimization
- OpenStreetMap: Map tiles (no account needed)
- Backend API: Custom Node.js/Express API (separate repo)
β οΈ Note: All credentials should be stored in a secure password manager
- Cloudinary: [Add link to password manager entry]
- Domain Registrar: [Add link to password manager entry]
- Hosting Service: [Add link to password manager entry]
- Backend Database: [Backend team manages - contact backend team for access]
- PWA Support: Service worker for offline access
- Push Notifications: Web push for real-time alerts
- Advanced Search: Elasticsearch for better search
- Payment Integration: Stripe for paid exchanges
- Admin Dashboard: Moderation and analytics
- AI Features: Image recognition, fraud detection
- Mobile Apps: React Native or native iOS/Android
- Multi-language: i18n support for internationalization
- Accessibility: WCAG 2.1 AA compliance
- Testing: Comprehensive unit and E2E test suite
This project is licensed under the MIT License.
Copyright (c) 2025 TruCycle
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
For questions, issues, or contributions:
- GitHub Issues: Repository Issues
- Email: akinyemisamuel170@gmail.com
- Documentation: This README and files in
backend_docs/
Last Updated: December 2025
Version: 1.0.0
Maintainer: Samuel Akinyemi