A production-grade React 18 + TypeScript single-page application delivering an interactive referral network intelligence dashboard with graph-theoretic visualisations, sports props analytics, multi-provider OAuth authentication, spring-physics animation orchestration, and a composable Recharts data visualisation layer β built for the Mercor Challenge and deployed live at statyx.io.
| Resource | URL |
|---|---|
| π Live Platform | statyx.io |
| π¬ Live Demo Video | Watch on Google Drive |
| π Backend Repo | github.com/vk93102/statyx-Backend |
| βοΈ Frontend Repo | github.com/vk93102/statyx-frontend |
- Demo Walkthrough
- Project Overview
- Frontend Architecture
- Component Hierarchy
- Tech Stack Deep Dive
- State Management Architecture
- Graph Algorithm Engine
- Data Visualisation Layer
- Animation System
- Authentication Flow (Frontend)
- Backend Integration
- Performance Architecture
- Directory Structure
- Design System
- Build Pipeline
- Local Setup
- Environment Variables
The demo covers the complete end-to-end user journey across both the frontend SPA and its integration with the Django REST backend. Here is a detailed breakdown of what is demonstrated:
What you see: The application opens to a polished sign-in/sign-up surface with three distinct authentication pathways rendered with Framer Motion entrance animations and staggered micro-interaction reveals.
Technical detail:
- Google OAuth 2.0 flow β Frontend triggers Google Sign-In popup, receives ID token (JWT), POSTs to
POST /api/auth/google/on the Django backend. Backend verifies the JWT signature against Google's JWKS endpoint, extracts thesubclaim as stable identity, upserts the user record in PostgreSQL, and returns a Django session cookie (HttpOnly; Secure; SameSite=Lax). - LinkedIn OAuth 2.0 PKCE flow β Frontend redirects to LinkedIn authorization endpoint with
scope=openid profile email. LinkedIn issues an auth code, the callback page exchanges it with the Django backend viaPOST /api/auth/linkedin/, which fetches the user's professional profile from LinkedIn/v2/meand upserts onlinkedin_id. - Twilio SMS OTP flow β User enters phone number, triggering
POST /api/auth/send-otp/which dispatches an SMS via the Twilio Verify API (twilio.verify.v2.services.verifications.create()). User enters the 6-digit code,POST /api/auth/verify-otp/callsverification_checks.create()β onstatus == "approved", a Django session is established.
What to notice: Smooth Framer Motion spring-physics transitions between auth steps, loading spinner states during API round-trips, error boundary handling for failed OAuth callbacks.
What you see: The primary dashboard surface renders an animated KPI card grid showing total network size, direct referral counts, indirect reach, and top-line growth metrics β all driven by graph traversal computations.
Technical detail:
- Network reach computation runs client-side via
utils/graphAlgorithms.jsβ a BFS traversal from each root node through the adjacency list representation of the referral DAG. Time complexity: O(V + E) where V = users, E = referral edges. - Counter animations use Framer Motion's
useMotionValue+useTransformhooks with spring physics configuration (stiffness: 100, damping: 30) β numbers count up from 0 to their final value on mount, with configurable duration driven by value magnitude. - KPI cards implement staggered
AnimatePresencewithinitial={{ opacity: 0, y: 20 }}βanimate={{ opacity: 1, y: 0 }}withdelay: index * 0.08creating a cascading reveal on page load. - Data is fetched via RTK Query
networkApiendpoints β automatically cached with a 5-minute TTL, deduplicated across concurrent renders, and refetched on window focus.
What to notice: The cascade animation on load, real-time metric cards, responsive grid collapsing on mobile viewport.
What you see: A ranked leaderboard of the most influential users in the referral network, with unique reach scores, flow centrality values, and comparative bar visualisations.
Technical detail:
- Greedy submodular maximisation algorithm (
greedyInfluencers()ingraphAlgorithms.js) selects the top-k users maximising total unique network reach with minimum audience overlap. Approximation guarantee: (1 - 1/e) β 63% of optimal β the standard monotone submodular function maximisation bound. Time complexity: O(k Γ VΒ²). - Flow centrality (
flowCentrality()) computes all-pairs shortest paths via Floyd-Warshall β O(VΒ³) β to quantify how much network flow passes through each node. High centrality = structural bottleneck = high-value influencer. - Influencer cards render with Recharts
<BarChart>showing reach comparison, with animated bar entry usinganimationDuration={800}andanimationEasing="ease-out". - Rankings use a custom comparator prioritising
uniqueReachdescending, withflowCentralityas a tiebreaker.
What to notice: The bar charts animating in, the reach vs. centrality distinction, the ranked table with sortable columns.
What you see: An interactive simulation control panel where users configure referral rate, conversion probability, days to simulate, and initial seeding β producing a live Recharts Line chart of projected network growth.
Technical detail:
- Discrete-time growth simulation (
simulateGrowth()) models network expansion as:users_at_day_t = users_at_day_t-1 + (active_referrers Γ referral_rate Γ conversion_prob). Each day's referrer count uses the prior day's active user count weighted by an adoption probability function (monotonically increasing S-curve). Time complexity: O(days Γ active_referrers). - Adoption modelling uses a logistic growth function:
P(t) = 1 / (1 + e^(-k(t - tβ)))wherekcontrols steepness andtβis the inflection point β modelling the realistic S-curve of viral product adoption. - Binary search for target achievement (
binarySearchTarget()) β given a target user count T, binary searches over the days axis to find the minimum days D* such thatsimulate(days=D*).users >= T. Time complexity: O(log(max_days) Γ simulation_cost). - The Recharts
<LineChart>updates reactively on every slider change viauseCallback-memoised simulation re-runs, withanimationDuration={300}for smooth chart transitions.
What to notice: Real-time chart update on slider drag, the S-curve shape of realistic growth, the target-day binary search marker on the chart.
What you see: A scenario comparison table and bar chart showing the minimum bonus value required to achieve different user acquisition targets, computed via binary search over the simulation function.
Technical detail:
- Bonus optimisation (
optimiseBonus()) wraps the growth simulation in a binary search over the bonus range[0, MAX_BONUS]. For each candidate bonusmid, runs the full simulation and checks ifresult.users >= target. Converges in O(log(MAX_BONUS)) iterations, each costing one simulation run. - Multiple target scenarios are computed in parallel using JavaScript's event loop β each scenario's binary search is initiated as a separate synchronous computation, results collected and rendered as a comparative bar chart.
- ROI analysis computes
cost_per_acquired_user = optimal_bonus Γ projected_acquireesfor each scenario, surfacing the most capital-efficient bonus tier.
What to notice: The comparison across different target tiers, the ROI efficiency metric, bar chart scenario comparison.
What you see: A date-scoped fixture list for NBA, NFL, and Soccer (BETA), with per-fixture player prop cards showing hit rates, odds comparison, EV scores, and matchup grades β all fetched in real time from the Django backend.
Technical detail:
- RTK Query
fixturesApifetchesGET /api/nba/fixtures/?date=YYYY-MM-DDβ the selected date flows through React Router's URL search params, making the fixture view deep-linkable and browser-history-aware. - Prop cards display
hit_rate_l5,hit_rate_l10,hit_rate_seasonfrom the backend's pre-computed splits, along withev_score(Expected Value %) andmatchup_grade(A+ β F) served directly from the DjangoNBAPlayerPropmodel's serialized representation. - EV Badge component classifies
ev_scoreinto visual tiers: > +8% β π’ green, +4β8% β π‘ yellow, +1β4% β βͺ grey, negative β π΄ hidden. - Odds table renders multi-book over/under comparison with inline vig (juice) calculation:
vig = P_over_implied + P_under_implied - 1.0. - Date navigation uses
date-fnsfor ISO 8601 date arithmetic, with prev/next day controls updating the RTK Query cache key and triggering a background refetch.
What to notice: The date selector updating all fixtures, prop card EV badge colouring, hit rate trend across L5/L10/season windows, matchup grade badge.
This repository is the client-side application of the Statyx platform β a React 18 SPA consuming the statyx-Backend Django REST API over CORS-permitted HTTPS.
The frontend's architectural responsibility is split into three orthogonal domains:
1. Graph Analytics Computation & Visualisation β All referral network graph algorithms (BFS, DFS, greedy selection, Floyd-Warshall centrality, discrete-time simulation, binary search optimisation) execute in the browser, in utils/graphAlgorithms.js, with results piped directly into Recharts visualisations via React state β zero server round-trips for computation, only for data persistence.
2. Sports Props Research UI β A server-state-driven interface consuming the Django props API via RTK Query, with automatic caching, background refetching, and optimistic updates. Presents pre-computed EV scores, hit rates, matchup grades, and multi-book odds without client-side analytics computation.
3. Multi-Provider Authentication Shell β OAuth 2.0 callback handling, Twilio OTP verification flow, and session-cookie-based auth state management β bridging three identity providers through a unified Django session backend.
Browser (statyx.io)
β
βββ index.html β Vite entry point, single DOM mount target
β
βββ main.jsx β React.createRoot() + Redux Provider
β
βββ App.jsx β React Router v6 BrowserRouter
β
βββ /auth/* β Public routes (no session required)
β βββ /sign-in β <SignIn />
β βββ /sign-up β <SignUp />
β βββ /verify-otp β <OTPVerify />
β βββ /auth/*/callback β <OAuthCallback />
β
βββ /app/* β Protected routes (session required)
β
βββ Layout wrapper β Sidebar nav + dark sidebar dock
β
βββ /app/overview β <Overview /> Network KPIs
βββ /app/influencers β <Influencers /> Ranked table
βββ /app/simulation β <Simulation /> Growth sim
βββ /app/optimization β <Optimization /> Bonus optimiser
βββ /app/nba β <NBAProps /> NBA fixtures + props
βββ /app/nfl β <NFLProps /> NFL fixtures + props
βββ /app/soccer β <SoccerProps /> Soccer fixtures
User Interaction
β
βΌ
React Component
β
βββ Local state (useState / useReducer)
β βββ UI-only: modal open, input value, tab selection
β
βββ Redux slice dispatch (createSlice)
β βββ Cross-component: auth state, selected date, active sport
β
βββ RTK Query hook (useGetFixturesQuery / useGetPropsQuery)
βββ Server state: fixtures, props, network graph data
β
βββ Cache HIT β renders immediately
βββ Cache MISS β fetch β normalise β cache β render
β
βΌ
Django REST API
(statyx-Backend on Render.com)
β
βΌ
PostgreSQL 15
App.jsx
βββ AuthGuard (HOC) Session validation wrapper
β βββ ProtectedRoute Redirects unauthenticated users
β
βββ Sidebar Dark nav panel
β βββ NavLink Γ 7 Sport + dashboard links
β βββ FloatingDock Collapsed action controls
β
βββ Dashboard Pages
β β
β βββ Overview
β β βββ KPICard Γ 4 Animated metric cards (Framer Motion)
β β βββ ReachChart Recharts BarChart β reach distribution
β β βββ GrowthTrendLine Recharts LineChart β historical growth
β β
β βββ Influencers
β β βββ InfluencerTable Sortable ranked table
β β β βββ InfluencerRow Γ n Per-user row with reach + centrality
β β βββ ReachBarChart Recharts BarChart β comparative reach
β β
β βββ Simulation
β β βββ SimControls Slider panel (rate, prob, days)
β β βββ GrowthLineChart Recharts LineChart β projected growth
β β βββ TargetMarker Binary-search result annotation
β β
β βββ Optimization
β βββ ScenarioTable Target tier comparison table
β βββ BonusBarChart Recharts BarChart β bonus per scenario
β βββ ROIMetricCard Cost-per-acquisition metric
β
βββ Sports Pages
βββ NBAProps / NFLProps / SoccerProps
β βββ DateSelector ISO 8601 date nav (date-fns)
β βββ FixtureList Fetched via RTK Query
β β βββ FixtureCard Γ n
β β βββ PropList
β β βββ PropCard Γ n
β β βββ PlayerInfo
β β βββ HitRateBar (L5 / L10 / season)
β β βββ OddsTable (multi-book over/under + vig)
β β βββ EVBadge (classified EV%)
β βββ LoadingSkeletons Framer Motion skeleton screens
β
βββ Shared
βββ PropCard
βββ OddsTable
βββ EVBadge
βββ MatchupGradeBadge
React 18 with concurrent mode features β useTransition for deferring non-urgent state updates during simulation re-renders, Suspense boundaries wrapping RTK Query data-fetching components for declarative loading states, and React.memo + useMemo for referential equality optimisation on expensive chart re-renders.
TypeScript 5 (strict mode) β tsconfig.json enables "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true. All API response shapes are typed via discriminated union interfaces. All component props are typed with no implicit any. Graph algorithm return types are fully annotated.
Vite 5 with SWC (@vitejs/plugin-react-swc) β Rust-native TypeScript/JSX transpiler replacing Babel. SWC is ~20Γ faster than Babel for large codebases. Vite's native ESM dev server eliminates the bundling step entirely during development β each module is served as a raw ES module, enabling true sub-10ms HMR for individual component edits.
Production build produces tree-shaken ES module bundles with automatic code-splitting at the route boundary β each dashboard tab is a separate dynamic import chunk, ensuring the initial page load only downloads the authentication shell (~40KB gzip) with remaining chunks loaded on-demand.
ESLint with strict configuration (eslint.config.js) β enforces consistent import ordering, no unused variables, exhaustive dependency arrays in useEffect/useCallback/useMemo, and React-specific rules (no missing keys, no direct state mutation).
Redux Toolkit (@reduxjs/toolkit) β The global state store uses configureStore with three slices:
authSlice β {user, sessionStatus, oauthProvider, loadingState}
networkSlice β {nodes, edges, computedMetrics, selectedSimConfig}
uiSlice β {selectedDate, activeSport, sidebarCollapsed}
All slices use Immer-powered reducers (built into RTK) β mutable-style state updates that produce immutable state via structural sharing, eliminating accidental mutation bugs.
RTK Query β Eliminates manual useEffect + fetch + loading/error state boilerplate. Each API endpoint definition auto-generates typed React hooks:
// Auto-generated hooks from endpoint definitions:
useGetNBAFixturesQuery({ date }) // fixtures list
useGetPlayerPropsQuery({ fixtureId }) // props per fixture
useGetNetworkInfluencersQuery() // influencer rankings
useSendOTPMutation() // OTP dispatch
useVerifyOTPMutation() // OTP verificationCache entries are keyed by serialised argument β useGetNBAFixturesQuery({ date: "2026-03-18" }) and useGetNBAFixturesQuery({ date: "2026-03-17" }) maintain independent cache entries, enabling instant back-navigation without refetching.
React Router v6 with createBrowserRouter + nested route definitions. Protected routes are wrapped in an <AuthGuard> component that reads authSlice.sessionStatus from the Redux store β unauthenticated users are redirected to /auth/sign-in with the intended destination preserved in location.state for post-login redirect.
URL search params (useSearchParams) drive the fixture date β ?date=2026-03-18 β making every fixture view deep-linkable, shareable, and browser-history-navigable.
Recharts 2.x β All four chart types in use:
| Chart | Location | Data |
|---|---|---|
<BarChart> |
Influencers tab | Unique reach per influencer |
<LineChart> |
Simulation tab | Projected daily user count |
<BarChart> |
Optimisation tab | Bonus cost per scenario |
<PieChart> |
Overview tab | Network composition breakdown |
All charts are <ResponsiveContainer width="100%" height={300}> wrapped β automatically adapts to parent container width. Animated on mount via isAnimationActive={true} with animationDuration={800} and animationEasing="ease-out".
Custom <Tooltip> formatters apply locale-aware number formatting and percentage display. Custom <Legend> with icon and label styling matching the Tailwind design tokens.
Framer Motion β Declarative animation library with a React-idiomatic API. Three animation patterns in use:
1. Mount/unmount transitions via AnimatePresence:
<AnimatePresence mode="wait">
<motion.div
key={activeTab}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }}
/>
</AnimatePresence>2. Staggered list reveals via variants + staggerChildren:
const container = {
hidden: {},
show: { transition: { staggerChildren: 0.08 } }
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
};3. Spring-physics counter animations via useMotionValue + animate:
const count = useMotionValue(0);
useEffect(() => {
animate(count, targetValue, {
duration: 1.2,
ease: "easeOut"
});
}, [targetValue]);Skeleton loading screens β motion.div elements with animate={{ opacity: [0.5, 1, 0.5] }} and transition={{ repeat: Infinity, duration: 1.5 }} create pulsing placeholder shimmer effects while RTK Query fetches resolve.
Lucide React (v0.383.0) β Tree-shakeable SVG icon library. Only icons explicitly imported are included in the production bundle β zero unused icon weight. Used across navigation links, sport badges, action buttons, and status indicators.
interface RootState {
auth: {
user: User | null;
sessionStatus: 'idle' | 'loading' | 'authenticated' | 'unauthenticated';
oauthProvider: 'google' | 'linkedin' | 'otp' | null;
error: string | null;
};
network: {
nodes: NetworkNode[];
edges: NetworkEdge[];
adjacencyList: Record<string, string[]>;
computedMetrics: {
influencers: RankedInfluencer[];
centralityScores: Record<string, number>;
totalReach: number;
} | null;
simConfig: SimulationConfig;
simResult: SimulationResult | null;
};
ui: {
selectedDate: string; // ISO 8601 β "2026-03-18"
activeSport: Sport;
sidebarCollapsed: boolean;
};
}// src/store/api.js
const statyxApi = createApi({
reducerPath: 'statyxApi',
baseQuery: fetchBaseQuery({
baseUrl: import.meta.env.VITE_API_URL,
credentials: 'include', // sends session cookie with every request
}),
tagTypes: ['Fixtures', 'Props', 'Network', 'User'],
endpoints: (builder) => ({
getNBAFixtures: builder.query({
query: ({ date }) => `/api/nba/fixtures/?date=${date}`,
providesTags: ['Fixtures'],
}),
getPlayerProps: builder.query({
query: ({ fixtureId }) => `/api/nba/fixtures/${fixtureId}/props/`,
providesTags: (result, error, { fixtureId }) => [
{ type: 'Props', id: fixtureId }
],
}),
sendOTP: builder.mutation({
query: ({ phone }) => ({
url: '/api/auth/send-otp/',
method: 'POST',
body: { phone },
}),
}),
verifyOTP: builder.mutation({
query: ({ phone, code }) => ({
url: '/api/auth/verify-otp/',
method: 'POST',
body: { phone, code },
}),
invalidatesTags: ['User'], // refetch user profile on login
}),
}),
});All graph algorithms live in src/utils/graphAlgorithms.js and execute synchronously in the browser's main thread. For networks up to ~1,000 users, all operations complete well within a single frame budget (16ms).
The referral network is stored as an adjacency list β a JavaScript Map<nodeId, nodeId[]> β enabling O(1) neighbour lookup for BFS/DFS traversals. Built once on data load, updated incrementally on new edge insertion.
function getReachBFS(rootId, adjacencyList) {
const visited = new Set();
const queue = [rootId];
visited.add(rootId);
while (queue.length > 0) {
const node = queue.shift();
for (const neighbour of (adjacencyList.get(node) ?? [])) {
if (!visited.has(neighbour)) {
visited.add(neighbour);
queue.push(neighbour);
}
}
}
return visited.size - 1; // exclude root; count of indirect referrals
}function wouldCreateCycle(fromId, toId, adjacencyList) {
// DFS from toId β if we can reach fromId, adding this edge creates a cycle
const visited = new Set();
const stack = [toId];
while (stack.length > 0) {
const node = stack.pop();
if (node === fromId) return true;
if (!visited.has(node)) {
visited.add(node);
for (const n of (adjacencyList.get(node) ?? [])) stack.push(n);
}
}
return false;
}function greedyInfluencers(k, allNodes, adjacencyList) {
const selected = [];
const covered = new Set();
for (let i = 0; i < k; i++) {
let bestNode = null, bestGain = -1;
for (const node of allNodes) {
if (selected.includes(node)) continue;
const reach = getBFSSet(node, adjacencyList);
const marginalGain = [...reach].filter(n => !covered.has(n)).length;
if (marginalGain > bestGain) {
bestGain = marginalGain;
bestNode = node;
}
}
if (!bestNode) break;
const bestReach = getBFSSet(bestNode, adjacencyList);
bestReach.forEach(n => covered.add(n));
selected.push(bestNode);
}
return selected;
// Approximation: (1 - 1/e) β 63% of optimal solution
}function computeFlowCentrality(nodes, adjacencyList) {
const n = nodes.length;
const idx = Object.fromEntries(nodes.map((id, i) => [id, i]));
const dist = Array.from({ length: n }, (_, i) =>
Array.from({ length: n }, (_, j) => (i === j ? 0 : Infinity))
);
// Initialise edges
for (const [from, neighbours] of adjacencyList) {
for (const to of neighbours) dist[idx[from]][idx[to]] = 1;
}
// Floyd-Warshall relaxation
for (let k = 0; k < n; k++)
for (let i = 0; i < n; i++)
for (let j = 0; j < n; j++)
if (dist[i][k] + dist[k][j] < dist[i][j])
dist[i][j] = dist[i][k] + dist[k][j];
// Centrality = fraction of shortest paths passing through each node
const centrality = {};
for (const node of nodes) {
let score = 0;
const k = idx[node];
for (let i = 0; i < n; i++)
for (let j = 0; j < n; j++)
if (i !== k && j !== k && dist[i][j] === dist[i][k] + dist[k][j])
score++;
centrality[node] = score;
}
return centrality;
}function optimiseBonus(targetUsers, days, simulateFn) {
let lo = 0, hi = MAX_BONUS;
while (lo < hi) {
const mid = Math.floor((lo + hi) / 2);
const result = simulateFn({ bonus: mid, days });
if (result.totalUsers >= targetUsers) hi = mid;
else lo = mid + 1;
}
return lo; // minimum bonus achieving targetUsers in `days` days
}All charts use Recharts' composable declarative API β each visual is assembled from atomic primitives (<XAxis>, <YAxis>, <Tooltip>, <Legend>, <Bar>, <Line>, <Cell>) rather than a monolithic config object. This enables per-series conditional styling, custom tooltip content, and responsive container sizing.
const CustomTooltip = ({ active, payload, label }) => {
if (!active || !payload?.length) return null;
return (
<div className="bg-gray-900 border border-gray-700 rounded-lg p-3 shadow-xl">
<p className="text-gray-400 text-xs mb-1">{label}</p>
{payload.map((entry) => (
<p key={entry.name} style={{ color: entry.color }} className="text-sm font-semibold">
{entry.name}: {entry.value.toLocaleString()}
</p>
))}
</div>
);
};Every chart is wrapped in <ResponsiveContainer width="100%" height={chartHeight}> where chartHeight is derived from a useWindowSize hook β ensuring pixel-perfect rendering across breakpoints without fixed pixel heights that break on mobile viewports.
IDLE
β
βββ [user visits protected route] β check session cookie
β β
β βββ cookie valid β AUTHENTICATED β render app
β βββ cookie absent β UNAUTHENTICATED β redirect /auth/sign-in
β
βββ [user initiates OTP] β LOADING
β β
β βββ OTP sent β OTP_PENDING β render OTPVerify
β β β
β β βββ code correct β AUTHENTICATED
β β βββ code wrong β OTP_PENDING (error shown)
β βββ send failed β UNAUTHENTICATED (error shown)
β
βββ [user clicks Google] β OAUTH_REDIRECTING β popup opens
β β
β βββ token returned β POST /api/auth/google/ β AUTHENTICATED
β
βββ [user clicks LinkedIn] β OAUTH_REDIRECTING β redirect
β
βββ callback page β POST /api/auth/linkedin/ β AUTHENTICATED
// src/components/auth/OAuthCallback.jsx
export function OAuthCallback() {
const dispatch = useDispatch();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
useEffect(() => {
const code = searchParams.get('code');
const provider = detectProvider(); // linkedin | google from URL pattern
if (!code) {
dispatch(authSlice.actions.setError('OAuth callback missing code'));
navigate('/auth/sign-in');
return;
}
dispatch(exchangeOAuthCode({ provider, code }))
.unwrap()
.then(() => navigate('/app/overview'))
.catch(() => navigate('/auth/sign-in'));
}, []);
return <LoadingSpinner label="Completing sign-in..." />;
}The frontend communicates exclusively with the statyx-Backend Django REST API. All requests include credentials: 'include' to send the session cookie cross-origin β enabled by django-cors-headers on the backend with CORS_ALLOW_CREDENTIALS=True and CORS_ALLOWED_ORIGINS explicitly listing the frontend origin.
// vite.config.js β development proxy (avoids CORS preflight in dev)
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
}
}
});In production, VITE_API_URL is set to the Render.com backend URL β https://statyx-backend.onrender.com β and RTK Query's fetchBaseQuery.baseUrl points there directly, with CORS handled by the backend's django-cors-headers middleware.
Component mounts
β
βΌ
useGetNBAFixturesQuery({ date }) called
β
βββ Cache entry exists + fresh?
β βββ YES β return cached data immediately, no network request
β
βββ Cache miss or stale?
βββ Dispatch internal fetch action
β
βΌ
fetchBaseQuery
β
βββ Build URL: VITE_API_URL + /api/nba/fixtures/?date=2026-03-18
βββ Attach session cookie (credentials: 'include')
βββ Set Content-Type: application/json
β
βΌ
Django backend (Render.com)
βββ django-cors-headers validates origin
βββ Session middleware authenticates request
βββ NBAFixtureViewSet.list() queries PostgreSQL
βββ DRF serializer returns JSON
β
βΌ
RTK Query normalises response
βββ Stores in Redux cache keyed by { date }
βββ Sets TTL (5 min default)
βββ Re-renders subscribed components
Auth
POST /api/auth/send-otp/ Twilio OTP dispatch
POST /api/auth/verify-otp/ OTP verification β session
POST /api/auth/google/ Google ID token β session
POST /api/auth/linkedin/ LinkedIn code β session
POST /api/auth/logout/ Session teardown
GET /api/auth/me/ Current user profile
POST /api/auth/avatar/ Avatar upload (Pillow)
NBA
GET /api/nba/fixtures/?date= Fixture list
GET /api/nba/fixtures/{id}/props/ Props per fixture (EV, hit rates, odds)
GET /api/nba/players/{id}/splits/ Historical splits
NFL / Football (BETA)
GET /api/football/fixtures/?date=
GET /api/football/fixtures/{id}/props/
Network Graph
POST /api/network/add-referral/ Add edge (cycle-safe)
GET /api/network/reach/{id}/ BFS reach count
GET /api/network/influencers/ Greedy top-k
POST /api/network/simulate/ Growth simulation
POST /api/network/optimise-bonus/ Min viable bonus
GET /api/network/centrality/ Flow centrality scores
Vite automatically splits the bundle at dynamic import() boundaries. Each major route is a separate chunk:
// React Router route definitions use lazy loading
const Overview = lazy(() => import('./components/dashboard/Overview'));
const Influencers = lazy(() => import('./components/dashboard/Influencers'));
const Simulation = lazy(() => import('./components/dashboard/Simulation'));
const NBAProps = lazy(() => import('./components/sports/NBAProps'));This ensures the initial page load only ships the authentication shell. Dashboard chunks download in the background after sign-in.
| Hook | Applied Where | Reason |
|---|---|---|
React.memo |
PropCard, InfluencerRow, OddsTable |
Prevents re-render when parent re-renders but props unchanged |
useMemo |
Graph computation results | Floyd-Warshall O(VΒ³) is expensive β recompute only when adjacency list changes |
useCallback |
Simulation re-run handler | Stable reference prevents child re-renders on every slider move |
useMemo |
Chart data transformation | Recharts data arrays recreated only when raw data changes |
// Polling for live game data during active game windows
useGetNBAFixturesQuery(
{ date: selectedDate },
{
pollingInterval: isGameDay ? 30_000 : 0, // 30s polling on game days only
refetchOnFocus: true, // refetch when tab regains focus
refetchOnMountOrArgChange: 300, // refetch if >5 min old
}
);| Metric | Target | Implementation |
|---|---|---|
| Initial bundle size | < 200KB gzip | Route-level code splitting + tree-shaking |
| First Contentful Paint | < 1.5s | Vite static SPA β no SSR hydration overhead |
| Time to Interactive | < 2s | Auth shell loads immediately; dashboard lazy-loads |
| Animation frame rate | 60 FPS | Framer Motion GPU-composited transforms only |
| RTK Query cache hit | > 80% same-session | 5-min TTL + arg-keyed cache entries |
statyx-frontend/
β
βββ public/
β βββ favicon.svg
β βββ og-image.png
β
βββ src/
β β
β βββ main.jsx Vite entry β ReactDOM.createRoot + Redux Provider
β βββ App.jsx BrowserRouter + route tree + AuthGuard
β β
β βββ store/
β β βββ index.js configureStore β combines all slices + RTK Query
β β βββ authSlice.js User identity, session status, OAuth state
β β βββ networkSlice.js Graph data, computed metrics, sim config
β β βββ uiSlice.js Date selection, active sport, UI prefs
β β βββ api.js RTK Query createApi β all endpoint definitions
β β
β βββ components/
β β β
β β βββ auth/
β β β βββ SignIn.jsx Email/password + OAuth provider buttons
β β β βββ SignUp.jsx Registration form + phone input + OTP trigger
β β β βββ OTPVerify.jsx 6-digit code input with countdown timer
β β β βββ OAuthCallback.jsx LinkedIn + Google redirect code exchange
β β β
β β βββ dashboard/
β β β βββ Overview.jsx KPI grid + reach chart + growth trend
β β β βββ Influencers.jsx Ranked table + flow centrality + bar chart
β β β βββ Simulation.jsx Sim controls + line chart + target marker
β β β βββ Optimization.jsx Bonus scenarios + bar chart + ROI metric
β β β
β β βββ sports/
β β β βββ NBAProps.jsx Date nav + fixture list + prop cards
β β β βββ NFLProps.jsx NFL fixture list (BETA badge)
β β β βββ SoccerProps.jsx Soccer fixture list (BETA badge)
β β β
β β βββ shared/
β β βββ PropCard.jsx Player prop card β hit rates + EV + matchup
β β βββ OddsTable.jsx Multi-book over/under comparison + vig calc
β β βββ EVBadge.jsx EV% tier classification badge
β β βββ MatchupGradeBadge.jsx A+βF grade badge with colour scale
β β βββ HitRateBar.jsx Visual hit-rate progress bar (L5/L10/season)
β β βββ LoadingSkeletons.jsx Framer Motion shimmer placeholders
β β βββ Sidebar.jsx Navigation panel + floating dock
β β
β βββ utils/
β β βββ graphAlgorithms.js BFS, DFS, greedy, Floyd-Warshall, binary search
β β βββ evCalculator.js EV% formula, implied probability, vig
β β βββ simulationEngine.js Discrete-time growth model, S-curve adoption
β β βββ formatters.js Date (date-fns), odds, percentages, currency
β β
β βββ assets/
β βββ fonts/
β βββ images/
β
βββ index.html Single HTML shell β Vite entry point
βββ vite.config.js Vite + SWC plugin + dev proxy config
βββ tsconfig.json TypeScript strict mode config
βββ eslint.config.js ESLint flat config β React + TS rules
βββ package.json Dependencies + scripts
βββ package-lock.json Lockfile β deterministic installs
- Dark-first palette β
bg-gray-950base,bg-gray-900surface,bg-gray-800elevated surface - Accent colour β Cyan (
#00d4ff) for primary actions, active states, and positive EV indicators - Typography scale β Display headings use
font-boldattext-2xl+; body copy attext-smwithtext-gray-400secondary - Glassmorphism cards β
bg-white/5 backdrop-blur-sm border border-white/10for elevated content surfaces - Spacing system β Tailwind's 4px base unit, consistent
gap-4,p-6,rounded-xlthroughout
| Breakpoint | Width | Layout Change |
|---|---|---|
sm |
640px | Single-column stack |
md |
768px | 2-column grid |
lg |
1024px | Sidebar visible, 3-column |
xl |
1280px | Full 4-column KPI grid |
All Framer Motion transitions use a shared duration/easing token:
export const TRANSITION_FAST = { duration: 0.15, ease: 'easeOut' };
export const TRANSITION_MED = { duration: 0.25, ease: 'easeOut' };
export const TRANSITION_SLOW = { duration: 0.4, ease: [0.4, 0, 0.2, 1] };
export const SPRING_BOUNCY = { type: 'spring', stiffness: 400, damping: 25 };
export const SPRING_SMOOTH = { type: 'spring', stiffness: 100, damping: 30 };npm run dev
# β Vite ESM dev server on http://localhost:5173
# β SWC transpilation (no Babel)
# β Hot Module Replacement: component edits reflect < 50ms
# β Django API proxied via vite.config.js server.proxynpm run build
# β TypeScript compilation (tsc --noEmit β type check only)
# β Vite + Rollup bundle:
# dist/index.html β Shell
# dist/assets/index-[hash].js β Auth shell chunk
# dist/assets/Overview-[hash].js β Route chunk (lazy)
# dist/assets/NBAProps-[hash].js β Route chunk (lazy)
# ... one chunk per lazily-imported route
# β Brotli + GZip compression of all JS/CSS assets
# β Asset fingerprinting (content hash in filename) for immutable CDN cachingnpm run typecheck
# β tsc --noEmit --strict
# β Validates all TypeScript across the codebase
# β Must pass with zero errors before any PR merge# 1. Clone the repository
git clone https://github.com/vk93102/statyx-frontend.git
cd statyx-frontend
# 2. Install dependencies (deterministic via lockfile)
npm install
# 3. Configure environment
cp .env.example .env.local
# Set VITE_API_URL to your local Django backend:
# VITE_API_URL=http://localhost:8000
# (or leave blank to use the vite.config.js proxy)
# 4. Start development server
npm run dev
# β http://localhost:5173
# 5. Run type check
npm run typecheck
# 6. Run linter
npx eslint src/
# 7. Production build (output to dist/)
npm run build
# 8. Preview production build locally
npm run preview
# β http://localhost:4173
β οΈ The frontend requires the statyx-Backend Django server running onhttp://localhost:8000for all API calls to resolve. Follow the backend setup instructions in that repo first.
# Required β Django backend URL
VITE_API_URL=http://localhost:8000
# Optional β override in production deployment
# VITE_API_URL=https://statyx-backend.onrender.comThe VITE_ prefix is required by Vite β only variables prefixed VITE_ are exposed to client-side code via import.meta.env.VITE_*. Unprefixed variables remain server-only and are never bundled into the browser payload.
- Fork github.com/vk93102/statyx-frontend
- Create a feature branch:
git checkout -b feature/your-feature - Ensure
npm run typecheckpasses with zero TypeScript errors - Ensure
npx eslint src/passes with zero ESLint errors - Follow Conventional Commits:
feat:,fix:,perf:,refactor:,chore: - Open a Pull Request β describe what the change does and why
- No
anytype β all values must be explicitly typed - All
useEffectdependency arrays must be exhaustive (enforced by ESLint) - All new components must be wrapped in
React.memoif they receive stable props - Chart components must use
<ResponsiveContainer>β no fixed pixel widths - All Framer Motion animations must use shared transition tokens from
utils/tokens.js
Β© Statyx. All rights reserved.
Statyx Frontend
React 18 Β· TypeScript 5 Β· Vite 5 + SWC Β· Redux Toolkit Β· RTK Query
Recharts Β· Framer Motion Β· Tailwind CSS Β· Lucide React
Live Platform Β·
Frontend Repo Β·
Backend Repo Β·
Demo Video