feat(ProCanvas): retro game overhaul with log actions, challenges & achievements modals

- Complete ProCanvas redesign with retro sports game aesthetic
- Card-game style photo frame with tilt, shimmer, corner star accents
- Daily Missions card with weekly challenge + individual quest progress bars
- Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting)
- Each log action opens themed modal with relevant input fields
- Challenges modal with 6 active challenges and progress tracking
- Achievements modal with all badges, unlock status, descriptions
- Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals
- Rewards & Checkpoints with named stages (Daily Grind to Legend Run)
- Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings)
- Hit The Map button with smooth pulsing glow animation
- Grid pattern overlay in Leaderboard card
- Light mode support via dark: Tailwind variants throughout
- Top 3 badges displayed on profile card
- Fixed dropdown option visibility in dark mode
This commit is contained in:
Satyam
2026-02-26 01:36:36 +05:30
parent 3d215f6db3
commit 7694788387
14 changed files with 6795 additions and 931 deletions
+440
View File
@@ -0,0 +1,440 @@
# I0 — Integration Overview
**Module:** I0 — Integration Team Foundation
**Companion backend doc:** `docs/backend/00_project_overview.md` (B0)
**Status:** Integration Team reads this first, before touching any frontend code.
---
## What This Module Covers
This document establishes the shared foundation every integration module (`I1``I8`) will build on:
1. API client setup — Axios instance, base URL, default headers
2. Auth token strategy — httpOnly cookies, no localStorage
3. Global 401 interceptor — automatic token refresh + request retry
4. Environment variable setup
5. Loading and error state conventions
6. The mock-to-real migration pattern (how to replace mockStore piece by piece)
7. Role-redirect logic (how to stay in sync with `Login.jsx`)
Read this document fully before working on any `I1``I8` module.
---
## 1. Environment Variables
The frontend uses Vite. All client-side environment variables must be prefixed with `VITE_`.
**Add to `.env.local` (never commit this file):**
```env
# Backend API
VITE_API_URL=http://localhost:8000/api/v1
# External (already present — do not change)
VITE_GROQ_API_KEY=your_groq_key_here
VITE_OPENWEATHER_API_KEY=your_openweather_key_here
```
**Add to `.env.example` (commit this as a template):**
```env
# Backend API base URL
VITE_API_URL=http://localhost:8000/api/v1
```
**Access in code:**
```js
// src/config/env.js (add alongside existing exports)
export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8000/api/v1';
```
> The `?? fallback` prevents crashes during local dev if `.env.local` is missing. In production on Vercel, `VITE_API_URL` must be set as an environment variable in the Vercel dashboard.
---
## 2. API Client Setup
Create one shared Axios instance. Every integration module imports from this file — never call `axios.create()` again elsewhere.
**File to create:** `src/api/client.js`
```js
import axios from 'axios';
import { API_URL } from '../config/env';
const apiClient = axios.create({
baseURL: API_URL,
withCredentials: true, // sends httpOnly cookies automatically
headers: {
'Content-Type': 'application/json',
},
});
export default apiClient;
```
### Why `withCredentials: true`?
The backend sets the access token and refresh token as `httpOnly` cookies (see B1). The browser will not include these cookies on cross-origin requests unless `withCredentials: true` is set. This is the only auth mechanism — there is no `Authorization: Bearer` header and no `localStorage`.
### What NOT to do
```js
// ❌ Never do this
localStorage.setItem('access_token', token);
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
// ✅ Correct — the cookie is set by the backend, browser sends it automatically
```
---
## 3. Global 401 Interceptor — Token Refresh and Retry
When the access token expires, the backend returns `401`. The client must silently refresh the token and retry the original request. This logic lives in one place: `src/api/interceptors.js`.
**File to create:** `src/api/interceptors.js`
```js
import apiClient from './client';
let isRefreshing = false;
let failedQueue = []; // requests that arrived while refresh was in-flight
function processQueue(error, token = null) {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error);
} else {
resolve(token);
}
});
failedQueue = [];
}
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Only handle 401s that haven't already been retried
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
// If a refresh is already in progress, queue this request
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(() => apiClient(originalRequest));
}
originalRequest._retry = true;
isRefreshing = true;
try {
// POST /auth/refresh — backend rotates the cookie automatically
await apiClient.post('/auth/refresh');
processQueue(null);
return apiClient(originalRequest); // retry original request
} catch (refreshError) {
processQueue(refreshError);
// Refresh failed → force logout
window.dispatchEvent(new CustomEvent('auth:logout-required'));
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
}
);
export default apiClient;
```
**Register interceptors in your app entry point.** Import this file once at the top of `src/main.jsx` (or `src/App.jsx`):
```js
// src/main.jsx — add this import at the top
import './api/interceptors';
```
> The `failedQueue` pattern prevents a thunderstorm of refresh requests when multiple concurrent API calls all get 401 at once. Only one refresh call is made; all others wait in the queue and retry together.
### Listening for the forced-logout event
In `AuthContext.jsx`, add a listener for the custom event dispatched when refresh fails:
```js
// Inside AuthProvider useEffect
useEffect(() => {
const handleForceLogout = () => {
logout(); // existing logout function
};
window.addEventListener('auth:logout-required', handleForceLogout);
return () => window.removeEventListener('auth:logout-required', handleForceLogout);
}, []);
```
---
## 4. API Module Files
Each integration module (`I1``I8`) will create one `src/api/*.js` file. These are thin wrappers around `apiClient` — no business logic, no state. Example structure:
```
src/api/
├── client.js ← Axios instance (shared)
├── interceptors.js ← 401 handler + refresh (imported once in main.jsx)
├── auth.js ← I1: login, logout, refresh, getMe
├── properties.js ← I3: getProperties, getProperty, assignAgent, updateStatus
├── users.js ← I4: getUsers, getUser, updateUser
├── meetings.js ← I5: getMeetings, createMeeting, updateMeeting, requestChange
├── vendors.js ← I6: getVendors, getVendor, uploadComplianceDoc
├── invoices.js ← I7: getInvoices, createInvoice, approveInvoice
└── chatbot.js ← I8: sendMessage (streaming)
```
**Convention for every API file:**
```js
// src/api/properties.js — example pattern
import apiClient from './client';
export async function getProperties(params = {}) {
const { data } = await apiClient.get('/properties', { params });
return data; // { data: [...], meta: { total, page, page_size } }
}
export async function getProperty(id) {
const { data } = await apiClient.get(`/properties/${id}`);
return data;
}
export async function assignAgent(propertyId, agentId) {
const { data } = await apiClient.patch(`/properties/${propertyId}`, {
assigned_agent_id: agentId,
});
return data;
}
```
Always destructure `{ data }` from the Axios response — the actual payload is in `response.data`, not `response` itself.
---
## 5. Loading and Error State Conventions
Every component that fetches data should follow this pattern. Use React's built-in `useState` + `useEffect` — no new libraries needed at this stage.
### Standard fetch pattern
```js
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
setError(null);
const result = await getProperties({ status: 'Hot Lead' });
if (!cancelled) setData(result.data);
} catch (err) {
if (!cancelled) setError(err.response?.data?.detail ?? 'Failed to load data');
} finally {
if (!cancelled) setLoading(false);
}
}
fetchData();
return () => { cancelled = true; };
}, []);
```
The `cancelled` flag prevents state updates on unmounted components (React strict mode issue).
### Error shape from the backend
```js
// All backend errors follow this shape (from B0):
// { detail: "Human-readable message", code: "MACHINE_CODE" }
// Access in catch block:
const message = err.response?.data?.detail ?? 'An unexpected error occurred';
const code = err.response?.data?.code; // e.g. "PERMISSION_DENIED"
// HTTP status codes:
// 400 — Validation error (bad request body)
// 401 — Not authenticated / token expired
// 403 — Authenticated but forbidden (wrong role)
// 404 — Resource not found
// 422 — FastAPI schema validation failed
// 429 — Rate limited
// 500 — Server error
```
### Loading UI
Use the existing `LoadingSpinner` component (or the role-appropriate skeleton) that is already present in the codebase. Do not add new spinner libraries.
---
## 6. Mock-to-Real Migration Pattern
The frontend currently runs entirely off `src/data/mockStore.jsx` through `MockStoreProvider`. The migration approach is **module by module** — never rip out the whole mock layer at once.
### The three phases of each module swap
**Phase 1 — Feature-flagged (default OFF)**
Each integration module starts with a feature flag so the mock data remains the fallback during development:
```js
// src/config/env.js — add this
export const FEATURE_REAL_API = import.meta.env.VITE_FEATURE_REAL_API === 'true';
```
```env
# .env.local — set to true when you're ready to test a module
VITE_FEATURE_REAL_API=false
```
**Phase 2 — Swap one data source at a time**
For example, when doing I3 (Properties), update only the properties data hook:
```js
// src/hooks/useProperties.js — new file created by I3
import { useState, useEffect } from 'react';
import { FEATURE_REAL_API } from '../config/env';
import { getProperties } from '../api/properties';
import { useMockStore } from '../context/MockStoreContext';
export function useProperties(filters = {}) {
const mockStore = useMockStore();
if (!FEATURE_REAL_API) {
// Return mock data in the same shape as the real API
return {
data: mockStore.properties.filter(/* apply filters */),
loading: false,
error: null,
};
}
// Real API path (only when flag is true)
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ... useEffect fetch ...
return { data, loading, error };
}
```
> Each integration module's spec (I1I8) will define the exact hook shape and what filters to support.
**Phase 3 — Remove the mock branch**
Once QA confirms the real API works correctly, delete the mock branch and the feature flag check. The `MockStoreProvider` can be removed only when all modules are migrated (I1 → I8 all complete).
### Shape matching rule
The real API response shape must always match the mock data shape that components already consume. If the backend returns a field name differently (e.g., `agent_id` vs `agentId`), transform it in the API layer (`src/api/*.js`), not in the component.
```js
// src/api/properties.js — transform snake_case to camelCase if needed
export async function getProperties(params = {}) {
const { data } = await apiClient.get('/properties', { params });
return {
...data,
data: data.data.map((p) => ({
...p,
agentId: p.assigned_agent_id, // normalize for components
marketValue: p.estimated_market_value,
})),
};
}
```
---
## 7. Role → Redirect Map
The backend returns a `role` field in the `/auth/me` and `/auth/login` responses. The frontend uses this to redirect users to their portal. This map must stay in sync with the switch statement in `src/pages/Login.jsx`.
| Role | Redirect path |
|------|--------------|
| `OWNER` | `/owner/snapshot` |
| `ADMIN` | `/admin/dashboard` |
| `FIELD_AGENT` | `/emp/fa/dashboard` |
| `CONTRACTOR` | `/contractor/dashboard` |
| `SUBCONTRACTOR` | `/subcontractor/dashboard` |
| `VENDOR` | `/vendor/dashboard` |
| `CUSTOMER` | `/portal/profile` |
These exact strings are used in `src/context/AuthContext.jsx` (the `ROLE_ROUTES` map) after I1 replaces the login flow.
---
## 8. Dependency Installation
Only one new package is needed: Axios. Everything else (React Query, SWR, etc.) is explicitly out of scope for the integration phase — keep the state management approach simple and consistent with the existing codebase.
```bash
npm install axios
```
Verify it was added to `package.json` `dependencies` (not `devDependencies`).
---
## 9. File Creation Checklist for I0
Before moving to I1, confirm all of the following are created or updated:
| File | Action |
|------|--------|
| `.env.local` | Add `VITE_API_URL=http://localhost:8000/api/v1` |
| `.env.example` | Add `VITE_API_URL=` (no value) |
| `src/config/env.js` | Export `API_URL` and `FEATURE_REAL_API` |
| `src/api/client.js` | Create Axios instance with `withCredentials: true` |
| `src/api/interceptors.js` | Create 401 interceptor with queue + retry logic |
| `src/main.jsx` | Add `import './api/interceptors'` at top |
| `src/context/AuthContext.jsx` | Add `auth:logout-required` event listener |
| `package.json` | `axios` present in `dependencies` |
---
## 10. What Each Integration Module Builds On Top of This
| Module | What it adds |
|--------|-------------|
| **I1** — Auth Integration | Replaces `AuthContext` login/logout with real `/auth/login`, `/auth/logout`, `/auth/me` calls |
| **I2** — Data Layer Migration | Wraps `MockStoreProvider` in a feature-flagged `ApiProvider`; establishes hook conventions |
| **I3** — Properties | Hooks for map + property CRUD; geospatial filter params; lead status updates |
| **I4** — Users & People | Hooks for `PeopleDirectory`, `VendorDirectory`; masked field handling |
| **I5** — Meetings | Hooks for `AdminSchedule`, meeting status transitions, change request flow |
| **I6** — Vendors & Compliance | Hooks for vendor CRUD, COI upload, compliance badge logic |
| **I7** — Financial | Hooks for invoice tables, AR/AP views, payout approval flow |
| **I8** — Chatbot | Streaming response from `/chatbot/message`; replaces direct Groq SDK calls |
---
## 11. Key Constraints — Do Not Violate
1. **Never store tokens in `localStorage` or `sessionStorage`.** The httpOnly cookie is set by the backend. The frontend never touches it directly.
2. **Never call `axios.create()` outside of `src/api/client.js`.** All API calls go through the shared instance so the interceptor works.
3. **Never modify `src/data/mockStore.jsx` during integration.** Mock data is the reference — it must remain intact as a fallback.
4. **Never remove `MockStoreProvider` from `main.jsx` until I2 is fully complete** and all feature flags have been validated.
5. **Money values arrive in cents from the API.** Divide by 100 before display. Never send or store display-formatted dollar values back to the API.
6. **All timestamps are ISO 8601 UTC.** Use `new Date(isoString).toLocaleDateString()` or the existing date util for display formatting — do not use raw strings.
---
*Document maintained by Integration Team — mark complete in `docs/README.md` when all files in Section 9 checklist are created and verified.*