<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: dheerajraj2103-wq</title>
    <description>The latest articles on DEV Community by dheerajraj2103-wq (@dheerajraj2103wq).</description>
    <link>https://dev.to/dheerajraj2103wq</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4004834%2F431c324f-17db-4a50-bb26-13ab1d396b9d.png</url>
      <title>DEV Community: dheerajraj2103-wq</title>
      <link>https://dev.to/dheerajraj2103wq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dheerajraj2103wq"/>
    <language>en</language>
    <item>
      <title>Building Surf Rush: A Full-Featured Telegram Mini App Game with React, TypeScript &amp; HTML5 Canvas</title>
      <dc:creator>dheerajraj2103-wq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 15:16:19 +0000</pubDate>
      <link>https://dev.to/dheerajraj2103wq/building-surf-rush-a-full-featured-telegram-mini-app-game-with-react-typescript-html5-canvas-3n7c</link>
      <guid>https://dev.to/dheerajraj2103wq/building-surf-rush-a-full-featured-telegram-mini-app-game-with-react-typescript-html5-canvas-3n7c</guid>
      <description>&lt;h1&gt;
  
  
  Building Surf Rush: A Full-Featured Telegram Mini App Game with React, TypeScript &amp;amp; HTML5 Canvas
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;How I engineered a real-time endless runner with XP progression, power-ups, daily missions, and a reward store — entirely inside Telegram.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The Telegram Mini App ecosystem has quietly become one of the most exciting frontiers in web development. With over 900 million monthly active users and a built-in distribution channel that requires zero app store approval, it offers a rare opportunity: ship a polished, interactive experience directly inside a chat app that people already have open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surf Rush&lt;/strong&gt; is my answer to that opportunity — a feature-rich, mobile-first endless runner game built as a Telegram Mini App using React, TypeScript, and HTML5 Canvas. Players surf through dynamically generated waves, dodge obstacles, collect coins, level up, complete daily missions, unlock power-ups, and compete on a global leaderboard — all without leaving Telegram.&lt;/p&gt;

&lt;p&gt;This article is a deep technical walkthrough of how I built it: the architecture decisions, the bugs that nearly broke me, the solutions that saved the project, and the lessons I'm taking forward into every future build.&lt;/p&gt;

&lt;p&gt;Whether you're a &lt;strong&gt;React developer&lt;/strong&gt; curious about game loops, a &lt;strong&gt;Telegram Mini App developer&lt;/strong&gt; looking for real-world patterns, a &lt;strong&gt;Web3 enthusiast&lt;/strong&gt; interested in on-chain gaming foundations, or a recruiter evaluating full-stack engineering depth — this is the story of Surf Rush, told honestly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built Surf Rush
&lt;/h2&gt;

&lt;p&gt;I wanted to build something that sat at the intersection of three things I care about: &lt;strong&gt;real-time interactivity&lt;/strong&gt;, &lt;strong&gt;progressive user engagement systems&lt;/strong&gt;, and &lt;strong&gt;social-native distribution&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Most portfolio projects live on a static URL that nobody visits. Telegram Mini Apps flip that equation — the distribution channel is the product. A game that lives inside Telegram gets shared inside Telegram, which means organic virality is a feature of the platform, not an afterthought.&lt;/p&gt;

&lt;p&gt;From a technical standpoint, I wanted to answer questions I'd been asking myself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can a React app manage a 60fps game loop without stuttering?&lt;/li&gt;
&lt;li&gt;How do you design XP, leveling, and daily missions that actually feel rewarding?&lt;/li&gt;
&lt;li&gt;What does mobile-first game UX look like when you're constrained to a 390px viewport inside a chat app?&lt;/li&gt;
&lt;li&gt;How do Telegram's Web App APIs behave in production, and where do they break?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Surf Rush is my empirical answer to all of those questions.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Overview
&lt;/h2&gt;

&lt;p&gt;Surf Rush is an endless runner where the player controls a surfer navigating an ocean course. The core gameplay loop is simple: survive as long as possible, collect coins, and avoid obstacles. But layered on top of that loop is a full progression system designed to keep players coming back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core gameplay objectives:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Survive increasingly fast waves and obstacle patterns&lt;/li&gt;
&lt;li&gt;Collect coins scattered across the course&lt;/li&gt;
&lt;li&gt;Activate power-ups (Shield, Magnet) to extend runs&lt;/li&gt;
&lt;li&gt;Accumulate XP to level up and unlock cosmetic rewards&lt;/li&gt;
&lt;li&gt;Complete daily missions for bonus coin rewards&lt;/li&gt;
&lt;li&gt;Climb the leaderboard to earn social bragging rights&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The game is fully playable in a browser at &lt;a href="https://surf-rush-game.vercel.app/" rel="noopener noreferrer"&gt;surf-rush-game.vercel.app&lt;/a&gt; and is designed to launch natively inside Telegram as a Mini App.&lt;/p&gt;




&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Endless Runner Gameplay
&lt;/h3&gt;

&lt;p&gt;The core game loop runs on an HTML5 Canvas element. The world scrolls horizontally at an accelerating speed — the longer a player survives, the faster the course becomes. Obstacles are procedurally placed using a weighted randomization system that ensures fair but escalating difficulty. A collision detection system checks bounding boxes on every frame tick.&lt;/p&gt;

&lt;h3&gt;
  
  
  Coin System
&lt;/h3&gt;

&lt;p&gt;Coins appear along the course in randomized clusters. Each coin collected increments the player's session total and contributes to their all-time wallet balance. The coin balance persists between sessions and is used as currency in the Reward Store. Coins are rendered as canvas sprites with a simple bobbing animation to make them visually distinct from the background.&lt;/p&gt;

&lt;h3&gt;
  
  
  XP and Level Progression
&lt;/h3&gt;

&lt;p&gt;Every run awards XP based on distance traveled and coins collected. XP accumulates across sessions, and players level up when they cross predefined thresholds. Each level-up triggers a visual celebration animation and unlocks access to new items in the Reward Store. The progression curve is designed to feel rewarding in the early game while maintaining long-term depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Daily Missions
&lt;/h3&gt;

&lt;p&gt;Three daily missions regenerate every 24 hours. Examples include: &lt;em&gt;"Collect 50 coins in a single run"&lt;/em&gt;, &lt;em&gt;"Survive for 90 seconds"&lt;/em&gt;, or &lt;em&gt;"Use a power-up 3 times."&lt;/em&gt; Completing missions awards bonus coins and XP. Mission state persists in &lt;code&gt;localStorage&lt;/code&gt; with a daily timestamp check to handle resets.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Daily mission reset logic&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;checkDailyReset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastReset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lastReset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;last&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Reward Store
&lt;/h3&gt;

&lt;p&gt;The Reward Store is a shop interface where players spend accumulated coins on cosmetic upgrades — board skins, trail effects, and character variants. Purchased items persist in &lt;code&gt;localStorage&lt;/code&gt; and are applied immediately to the game renderer. The store creates a long-term economy that gives coins meaning beyond the immediate run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Leaderboard
&lt;/h3&gt;

&lt;p&gt;The leaderboard ranks players by their all-time high score. In the current implementation, scores are stored locally and displayed in a ranked list UI. The architecture is designed to swap the local store for a cloud backend (Supabase or Firebase) without touching the leaderboard UI component.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shield Power-Up
&lt;/h3&gt;

&lt;p&gt;The Shield grants the player one free collision — the next obstacle hit is absorbed without ending the run. Visually, a glowing aura renders around the player sprite on the canvas. The shield state is tracked as a boolean in the game state object and consumed on the first valid collision event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;GameState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;coins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;shieldActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;magnetActive&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Magnet Power-Up
&lt;/h3&gt;

&lt;p&gt;The Magnet automatically draws nearby coins toward the player for a timed duration (8 seconds). This required adding a proximity check inside the coin update loop — each coin's distance to the player is calculated on every frame, and coins within the magnet radius are pulled toward the player position using linear interpolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Magnet attraction logic inside game loop&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;magnetActive&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;coins&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dist&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MAGNET_RADIUS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MAGNET_PULL_STRENGTH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;coin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;dy&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;MAGNET_PULL_STRENGTH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Telegram Sharing
&lt;/h3&gt;

&lt;p&gt;After each run, players can share their score directly to a Telegram chat using the Web App's native share API. This was one of the trickiest features to implement correctly — more on that in the challenges section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mobile Responsiveness
&lt;/h3&gt;

&lt;p&gt;The game canvas scales dynamically to the device viewport using a &lt;code&gt;ResizeObserver&lt;/code&gt;. All UI panels (leaderboard, store, missions) are built with CSS Flexbox and clamp-based typography to remain readable at any screen size.&lt;/p&gt;

&lt;h3&gt;
  
  
  UI/UX Improvements
&lt;/h3&gt;

&lt;p&gt;Throughout development I iterated on the game's feel: adding particle effects on coin collection, a screen-shake effect on collision, smooth easing on menu transitions, and a tutorial overlay for first-time players. These details transform a functional prototype into something that feels &lt;em&gt;made&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Technology Stack
&lt;/h2&gt;

&lt;h3&gt;
  
  
  React
&lt;/h3&gt;

&lt;p&gt;React was the natural choice for the menu system, HUD, and all non-canvas UI. Its component model maps cleanly to the game's distinct screens (main menu, active game, leaderboard, store), and the hook system (&lt;code&gt;useState&lt;/code&gt;, &lt;code&gt;useEffect&lt;/code&gt;, &lt;code&gt;useRef&lt;/code&gt;) provides clean integration points with the imperative Canvas API.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript
&lt;/h3&gt;

&lt;p&gt;TypeScript was non-negotiable on a project of this complexity. The game state object, player data model, power-up types, and mission definitions all benefit from strict typing. Catching a type mismatch at compile time rather than discovering a &lt;code&gt;undefined is not a function&lt;/code&gt; crash mid-run is the difference between professional and amateur code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vite
&lt;/h3&gt;

&lt;p&gt;Vite's near-instant HMR made iterating on both game logic and UI fast enough to maintain flow state. With &lt;code&gt;tsc --noEmit&lt;/code&gt; running in watch mode alongside Vite, the feedback loop was tight: save, see the result, catch the type error, fix it — all in under two seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTML5 Canvas
&lt;/h3&gt;

&lt;p&gt;The game loop runs entirely on a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element via the 2D rendering context. Using Canvas rather than a DOM-based approach gives direct control over every pixel on every frame, which is essential for smooth animation at 60fps. React manages the canvas lifecycle through a &lt;code&gt;useRef&lt;/code&gt;, while all rendering logic lives in vanilla TypeScript functions outside the React render cycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS3
&lt;/h3&gt;

&lt;p&gt;The menu system uses CSS Grid and Flexbox for layout, CSS custom properties for theming, and &lt;code&gt;@keyframes&lt;/code&gt; animations for transitions. Keeping the game renderer in Canvas and the UI in CSS means each layer uses the right tool for its job.&lt;/p&gt;

&lt;h3&gt;
  
  
  Telegram Mini Apps
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;@twa-dev/sdk&lt;/code&gt; package provides typed access to the Telegram Web App JavaScript API. I use it for: detecting the user's Telegram identity, triggering the native share sheet, controlling the back button behavior, and adapting the color scheme to match the user's Telegram theme.&lt;/p&gt;




&lt;h2&gt;
  
  
  Game Architecture
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Folder Structure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;surf-rush/
├── public/
│   └── assets/          # Sprites, audio, icons
├── src/
│   ├── components/      # React UI components
│   │   ├── Game/        # Canvas wrapper + HUD
│   │   ├── Leaderboard/
│   │   ├── Store/
│   │   ├── Missions/
│   │   └── Shared/      # Buttons, modals, transitions
│   ├── engine/          # Pure game logic (no React)
│   │   ├── gameLoop.ts
│   │   ├── physics.ts
│   │   ├── renderer.ts
│   │   ├── collisions.ts
│   │   └── entities.ts
│   ├── hooks/           # Custom React hooks
│   │   ├── useGameState.ts
│   │   ├── usePlayerData.ts
│   │   └── useTelegram.ts
│   ├── store/           # State management
│   │   └── playerStore.ts
│   ├── types/           # Shared TypeScript interfaces
│   └── utils/           # Helpers, constants, formatters
├── index.html
├── vite.config.ts
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Component Hierarchy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;App&amp;gt;
├── &amp;lt;TelegramProvider&amp;gt;     // Initializes Telegram Web App SDK
└── &amp;lt;Router&amp;gt;
    ├── &amp;lt;MainMenu&amp;gt;
    ├── &amp;lt;Game&amp;gt;
    │   ├── &amp;lt;GameCanvas&amp;gt;   // useRef → Canvas element
    │   ├── &amp;lt;HUD&amp;gt;          // Score, coins, power-up timers
    │   └── &amp;lt;GameOver&amp;gt;     // Post-run stats + share button
    ├── &amp;lt;Leaderboard&amp;gt;
    ├── &amp;lt;RewardStore&amp;gt;
    └── &amp;lt;Missions&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Game Loop
&lt;/h3&gt;

&lt;p&gt;The game loop uses &lt;code&gt;requestAnimationFrame&lt;/code&gt; to target 60fps. The loop is started and stopped using a &lt;code&gt;useEffect&lt;/code&gt; in the &lt;code&gt;GameCanvas&lt;/code&gt; component, with the cancellation token stored in a &lt;code&gt;useRef&lt;/code&gt; to prevent memory leaks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;animationRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startLoop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;updateGameState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// Physics, collision, entity updates&lt;/span&gt;
    &lt;span class="nf"&gt;renderFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Canvas draw calls&lt;/span&gt;
    &lt;span class="nx"&gt;animationRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;animationRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Cleanup on unmount&lt;/span&gt;
&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;startLoop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;cancelAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;animationRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;updateGameState&lt;/code&gt; function is a pure function that takes the current state and timestamp, applies physics, checks collisions, updates entity positions, and returns the next state. This functional approach made the game loop straightforward to test and debug.&lt;/p&gt;




&lt;h2&gt;
  
  
  Major Challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Blank Screen Bug
&lt;/h3&gt;

&lt;p&gt;Early in development, the game canvas would occasionally render as a blank white rectangle. The game loop was running — I could confirm via console logs — but nothing appeared on screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; The Canvas &lt;code&gt;2d&lt;/code&gt; rendering context was being retrieved before the DOM had finished mounting. The &lt;code&gt;useRef&lt;/code&gt; returned the element reference, but the element's dimensions were &lt;code&gt;0 × 0&lt;/code&gt; at the time &lt;code&gt;getContext('2d')&lt;/code&gt; was called.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; I moved context initialization into a &lt;code&gt;useLayoutEffect&lt;/code&gt; (which runs synchronously after DOM mutations, before paint) and added an explicit dimension check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useLayoutEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvasRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetWidth&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;offsetHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ctxRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;startLoop&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Telegram Share Bug
&lt;/h3&gt;

&lt;p&gt;Clicking the share button after a run would occasionally do nothing — no dialog, no error, just silence. This only happened on certain Telegram clients and couldn't be reproduced consistently in the browser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; The Telegram Web App SDK's &lt;code&gt;showShareScreen&lt;/code&gt; method requires the Mini App to be fully initialized before it can be called. On slower devices, the share button was sometimes tapped before &lt;code&gt;window.Telegram.WebApp.ready()&lt;/code&gt; had resolved.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; I wrapped all Telegram API calls in a readiness guard and added an initialization promise that the rest of the app awaits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// useTelegram.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setIsReady&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Telegram&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;WebApp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;setIsReady&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shareScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isReady&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Telegram&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;WebApp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;tg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openTelegramLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://t.me/share/url?url=https://surf-rush-game.vercel.app&amp;amp;text=I scored &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in Surf Rush! 🏄‍♂️`&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Mobile Responsiveness
&lt;/h3&gt;

&lt;p&gt;The initial build looked perfect on desktop Chrome DevTools' mobile simulator — and broken on real devices. Text overflowed, buttons were cut off by the iOS safe area, and the canvas didn't fill the viewport correctly on Android.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; A three-part fix. First, I added a &lt;code&gt;ResizeObserver&lt;/code&gt; to the canvas element to keep its internal resolution in sync with its CSS size. Second, I added &lt;code&gt;env(safe-area-inset-bottom)&lt;/code&gt; padding to the bottom navigation bar to avoid the iPhone home indicator. Third, I replaced all fixed pixel values in the UI with &lt;code&gt;clamp()&lt;/code&gt; expressions and viewport units.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.game-hud&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;padding-bottom&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;safe-area-inset-bottom&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3vw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Power-Up Implementation
&lt;/h3&gt;

&lt;p&gt;Implementing the Magnet required running proximity calculations on every coin for every frame — potentially hundreds of calculations at 60fps. On low-end mobile devices, this caused visible frame drops.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; I added a spatial partitioning optimization using a simple grid bucketing approach: coins are sorted into a coarse grid on each frame, and proximity checks only compare the player against coins in adjacent grid cells. This reduced the number of distance calculations by roughly 80% in dense coin layouts.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Performance Optimization
&lt;/h3&gt;

&lt;p&gt;Beyond the Magnet issue, I found that re-rendering the entire canvas background on every frame — ocean, horizon, clouds — was expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; I switched to a layered canvas approach using two overlapping canvas elements. The background layer (ocean, sky, clouds) renders at 10fps using a throttled &lt;code&gt;setInterval&lt;/code&gt;. The foreground layer (player, obstacles, coins, particles) continues at 60fps. This halved the render budget on mid-range devices.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. UI Polishing
&lt;/h3&gt;

&lt;p&gt;Late in development, the menus felt functional but cold. Transitions between screens were instant jumps, button presses had no tactile feedback, and the game-over screen felt abrupt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; I added CSS transition classes using a simple state machine for screen transitions, a &lt;code&gt;scale&lt;/code&gt; transform keyframe animation on button press (the "squish" effect), and a post-run animation sequence that counts the score up digit by digit before showing the full game-over panel.&lt;/p&gt;




&lt;h2&gt;
  
  
  User Experience Improvements
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Tutorial Overlay:&lt;/strong&gt; First-time players see a semi-transparent tutorial panel over the canvas that demonstrates swipe controls with animated arrow indicators. It dismisses on first interaction and never appears again, using a &lt;code&gt;localStorage&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Onboarding Flow:&lt;/strong&gt; The main menu detects whether the player is new (no saved data) and shows a brief welcome screen with the game's objective before dropping them into gameplay.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Animations:&lt;/strong&gt; Coin collection triggers a floating &lt;code&gt;+1&lt;/code&gt; particle. Level-up triggers a full-screen flash with the new level number. Power-up activation plays a brief scale-in animation on the HUD icon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Responsive Canvas:&lt;/strong&gt; The canvas maintains a locked aspect ratio using a &lt;code&gt;preserveAspectRatio&lt;/code&gt;-style calculation, preventing the game world from stretching on ultrawide or narrow screens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Haptic Feedback:&lt;/strong&gt; On Telegram Mobile, &lt;code&gt;window.Telegram.WebApp.HapticFeedback.impactOccurred('medium')&lt;/code&gt; fires on collision and level-up, adding physical reinforcement to key moments.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Separate your game engine from your framework.&lt;/strong&gt; Putting physics and rendering logic inside React components is a mistake. React's rendering model and a game loop's update model are fundamentally at odds. The best decision I made was early: all game logic lives in &lt;code&gt;src/engine/&lt;/code&gt;, and React only manages the lifecycle (mount/unmount the loop) and the UI layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Test on real devices early.&lt;/strong&gt; The mobile bugs I encountered would have been caught in week one if I'd been testing on a physical iPhone and Android device from the start. Simulators lie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Telegram's SDK quirks are real.&lt;/strong&gt; The documentation is thin and sometimes contradictory. Building a &lt;code&gt;useTelegram&lt;/code&gt; hook that centralizes all SDK interactions — and guards every call behind a readiness check — is the right pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Progressive enhancement beats feature completeness.&lt;/strong&gt; The version of Surf Rush that went live was missing cloud leaderboards and achievements. But it was polished, fast, and fun. Shipping a solid core and iterating beats holding back for a perfect v1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Performance is a feature on mobile.&lt;/strong&gt; A game that runs at 45fps on a mid-range Android is a worse game than one that runs at 60fps. Budget your rendering work early and measure on real hardware.&lt;/p&gt;




&lt;h2&gt;
  
  
  Future Improvements
&lt;/h2&gt;

&lt;p&gt;Surf Rush's architecture was built with extensibility in mind. The roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multiplayer mode&lt;/strong&gt; — real-time ghost racing where players see each other's recorded runs as transparent "ghosts" on the same course&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blockchain rewards&lt;/strong&gt; — integrating TON (Telegram's native blockchain) to convert in-game coins to on-chain tokens at the end of each session&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NFT cosmetics&lt;/strong&gt; — board skins and character variants minted as NFTs that players own across games&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud leaderboard&lt;/strong&gt; — replacing the local leaderboard with a Supabase backend for global ranking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Achievements system&lt;/strong&gt; — persistent badges for milestone events (first level-up, 1000 coins collected, 10-day streak)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seasonal events&lt;/strong&gt; — time-limited course themes (winter, Halloween) with exclusive cosmetic drops&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Analytics dashboard&lt;/strong&gt; — session length, retention cohorts, and mission completion rates to inform future design decisions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Web3 integration path is particularly compelling given Telegram's existing relationship with TON blockchain — it's one of the few gaming contexts where blockchain rewards feel native rather than bolted on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Live Demo &amp;amp; Repository
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://surf-rush-game.vercel.app/" rel="noopener noreferrer"&gt;https://surf-rush-game.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/your-username/surf-rush" rel="noopener noreferrer"&gt;https://github.com/your-username/surf-rush&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Clone the repo, run &lt;code&gt;npm install &amp;amp;&amp;amp; npm run dev&lt;/code&gt;, and the game will open at &lt;code&gt;localhost:5173&lt;/code&gt;. To test Telegram-specific features, use the &lt;a href="https://core.telegram.org/bots/webapps" rel="noopener noreferrer"&gt;Telegram Bot API&lt;/a&gt; to register a Mini App pointing to your local ngrok tunnel.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Surf Rush started as a question — &lt;em&gt;can you build a genuinely fun, feature-complete game inside Telegram using only web technologies?&lt;/em&gt; — and became the most technically satisfying project I've shipped.&lt;/p&gt;

&lt;p&gt;The combination of React, TypeScript, and HTML5 Canvas proved to be a powerful and underrated game development stack. The Telegram Mini App platform provided distribution and social features that would take months to build independently. And the discipline of shipping a real product — with real bugs, real performance constraints, and real users — taught me more than any tutorial could.&lt;/p&gt;

&lt;p&gt;If you're a developer looking to break into Telegram Mini Apps, or a React developer curious about game development, I hope this walkthrough gives you a clear and honest picture of what that work actually looks like.&lt;/p&gt;

&lt;p&gt;The code is open source. Fork it, break it, improve it, or use it as a reference for your own Telegram Mini App game. And if you find a bug — open an issue. That's how good software gets better.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Keywords: Telegram Mini App, React Game Development, TypeScript Game, Web3 Internship Project, HTML5 Canvas Game, Endless Runner, Telegram Web App SDK, React TypeScript Vite&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>react</category>
      <category>showdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Building Surf Rush: A Telegram Mini App Endless Runner with React &amp; TypeScript</title>
      <dc:creator>dheerajraj2103-wq</dc:creator>
      <pubDate>Sat, 27 Jun 2026 05:08:07 +0000</pubDate>
      <link>https://dev.to/dheerajraj2103wq/building-surf-rush-a-telegram-mini-app-endless-runner-with-react-typescript-4opg</link>
      <guid>https://dev.to/dheerajraj2103wq/building-surf-rush-a-telegram-mini-app-endless-runner-with-react-typescript-4opg</guid>
      <description></description>
    </item>
  </channel>
</rss>
