pstzx

Running Start Tracking

Component

About

Stride is a running app that tracks every route, shows your distance, time, and pace live, and helps you build weekly goals that fit your level, from your first 5K to marathon prep.

Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Running Start Tracking to your project and fits it to your theme.

Code

How Running Start Tracking is used: the demo's code. The component's own source comes with a free account.

Usage.tsx

import React, { useEffect, useRef, useState } from "react";

const PURPLE = "#5B21B6"; // deep violet
const BLUE = "#1D4ED8"; // royal blue

const css = `
@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,600;12..96,800&family=Fraunces:ital,opsz,wght@1,9..144,500&display=swap');

.rl { --p: ${PURPLE}; --b: ${BLUE}; --g: linear-gradient(120deg, #7C3AED 0%, #4F46E5 50%, #2563EB 100%); --gd: linear-gradient(120deg, var(--p) 0%, #3730A3 55%, var(--b) 100%); --w: #0E0B1F; --k: #FCFBFF; background: #FCFBFF; color: var(--w);
  font-family: 'Bricolage Grotesque', system-ui, sans-serif; min-height: 100vh; position: relative; overflow: hidden; }
.rl *, .rl *::before, .rl *::after { box-sizing: border-box; }

/* ---------- background rute ---------- */
.rl-bg { background: #FCFBFF; position: fixed; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
.rl-route { fill: none; stroke-linecap: round; stroke-linejoin: round; }
.rl-ghost { stroke: #312E81; stroke-opacity: .32; stroke-width: 2; stroke-dasharray: 3 9; }
.rl-main-line { stroke-width: 5; }
.rl-trace { stroke: url(#rlg); stroke-width: 3; stroke-dasharray: 1; stroke-dashoffset: 1;
  animation: rl-draw 9s cubic-bezier(.6,.05,.3,1) infinite; }
.rl-trace.main { animation: none; stroke-dashoffset: 1; filter: drop-shadow(0 0 7px rgba(79,70,229,.5)); }
.rl-runner { filter: drop-shadow(0 0 9px rgba(79,70,229,.75)); }
.rl-trace.b { stroke: #1E1B4B; stroke-opacity: .45; stroke-width: 1.5; animation-delay: 1.6s; animation-duration: 11s; }
.rl-trace.c { stroke: url(#rlg); stroke-opacity: .5; stroke-width: 1.5; animation-delay: 3.2s; animation-duration: 10s; }
@keyframes rl-draw {
  0% { stroke-dashoffset: 1; opacity: 1; }
  70% { stroke-dashoffset: 0; opacity: 1; }
  90% { stroke-dashoffset: 0; opacity: 1; }
  100% { stroke-dashoffset: 0; opacity: 0; }
}
.rl-pin { fill: url(#rlg); }
.rl-ring { fill: none; stroke: url(#rlg); stroke-width: 2; transform-box: fill-box; transform-origin: center;
  animation: rl-ping 2.4s ease-out infinite; }
@keyframes rl-ping { from { transform: scale(1); opacity: .9; } to { transform: scale(4.5); opacity: 0; } }
.rl-hud { position: fixed; right: 28px; top: 50%; transform: translateY(-50%); z-index: 5; display: flex; flex-direction: column; background: rgba(255,255,255,.72);
  border: 1px solid rgba(49,46,129,.14); border-left: 3px solid; border-image: linear-gradient(180deg, #7C3AED, #2563EB) 1;
  backdrop-filter: blur(14px) saturate(1.4); box-shadow: 0 24px 60px -24px rgba(67,56,202,.45), 0 2px 0 rgba(255,255,255,.8) inset; }
.rl-hud div { padding: 16px 24px; min-width: 150px; }
.rl-hud div + div { border-top: 1px solid rgba(49,46,129,.12); }
.rl-hud b { display: block; font-size: 32px; font-weight: 800; letter-spacing: -.03em; font-variant-numeric: tabular-nums; line-height: 1.1; }
.rl-hud small { font-size: 12px; color: #6B6985; letter-spacing: .01em; }
.rl-hud .on b { background: var(--g); -webkit-background-clip: text; background-clip: text; color: transparent; }
.rl-flag { paint-order: stroke; stroke: #FCFBFF; stroke-width: 5px; stroke-linejoin: round; font-size: 13px; fill: #0E0B1F; font-family: 'Bricolage Grotesque', system-ui, sans-serif; font-weight: 600; }
.rl-vignette { position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background: radial-gradient(circle at 90% 8%, rgba(79,70,229,.18), transparent 40%),
    radial-gradient(circle at 6% 94%, rgba(124,58,237,.14), transparent 38%),
    linear-gradient(90deg, rgba(252,251,255,.92) 0%, rgba(252,251,255,.55) 45%, rgba(252,251,255,.2) 100%); }

/* ---------- layout ---------- */
.rl-main { position: relative; z-index: 2; }
.rl-wrap { max-width: 1160px; margin: 0 auto; padding: 0 24px; }

.rl-nav { display: flex; align-items: center; justify-content: space-between; padding: 24px 0; border-bottom: 1px solid rgba(49,46,129,.1); }
.rl-logo { font-weight: 800; font-size: 22px; letter-spacing: -.03em; display: flex; align-items: center; gap: 10px; }
.rl-logo i { width: 14px; height: 14px; border-radius: 50%; background: var(--g); display: inline-block; box-shadow: 0 0 0 4px rgba(79,70,229,.14); }
.rl-links { display: flex; gap: 28px; align-items: center; font-size: 15px; }
.rl-links a { color: #0E0B1F; text-decoration: none; opacity: .75; }
.rl-links a:hover { opacity: 1; }

.rl-btn { display: inline-block; font: inherit; font-weight: 600; font-size: 16px; text-decoration: none; cursor: pointer;
  padding: 15px 28px; border-radius: 999px; border: 2px solid transparent; background: var(--gd); color: #fff; box-shadow: 0 14px 30px -12px rgba(67,56,202,.7), inset 0 1px 0 rgba(255,255,255,.28); transition: background .25s, border-color .25s, color .25s, box-shadow .25s, transform .25s; }
.rl-btn:hover { background: rgba(255,255,255,.7); border-color: #4F46E5; color: #3730A3; box-shadow: 0 14px 30px -14px rgba(67,56,202,.45); transform: translateY(-1px); }
.rl-btn.light { background: #fff; color: #0E0B1F; border-color: rgba(49,46,129,.22); box-shadow: 0 10px 24px -14px rgba(67,56,202,.4); }
.rl-btn.light:hover { background: #fff; border-color: #4F46E5; color: #3730A3; }
.rl-btn.ghost { background: transparent; color: #0E0B1F; border-color: rgba(30,27,75,.35); box-shadow: none; }
.rl-btn.ghost:hover { border-color: #1E1B4B; color: #0E0B1F; background: rgba(30,27,75,.05); }
.rl a:focus-visible, .rl button:focus-visible { outline: 3px solid #4F46E5; outline-offset: 3px; }

.rl-hero { min-height: calc(100vh - 100px); display: flex; align-items: center; padding-bottom: 40px; }
.rl-hero h1 { font-size: clamp(44px, 8.5vw, 116px); line-height: .92; letter-spacing: -.045em; font-weight: 800; margin: 0 0 28px; max-width: 11ch; }
.rl-hero h1 span { font-family: 'Fraunces', Georgia, serif; font-style: italic; font-weight: 500; letter-spacing: -.035em; padding-right: .06em; background: var(--g); -webkit-background-clip: text; background-clip: text; color: transparent; }
.rl-hero p { font-size: clamp(17px, 2vw, 21px); line-height: 1.55; max-width: 46ch; margin: 0 0 36px; color: #4A4863; }
.rl-cta { display: flex; gap: 14px; flex-wrap: wrap; }

.rl-section { padding: 90px 0; }
.rl-section h2 { font-size: clamp(32px, 5vw, 60px); letter-spacing: -.035em; line-height: 1; margin: 0 0 48px; max-width: 16ch; font-weight: 800; }

.rl-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; border-top: 2px solid; border-image: linear-gradient(90deg, #8B5CF6, #3B82F6) 1; }
.rl-item { padding: 28px 28px 36px 0; }
.rl-item + .rl-item { padding-left: 28px; border-left: 1px solid rgba(0,0,0,.14); }
.rl-item h3 { font-size: 24px; margin: 0 0 10px; letter-spacing: -.02em; }
.rl-item p { margin: 0; line-height: 1.6; color: rgba(0,0,0,.72); max-width: 34ch; }

.rl-band { background: var(--gd); color: #fff; padding: 80px 0; position: relative; z-index: 2; }
.rl-band .rl-wrap { display: flex; justify-content: space-between; align-items: center; gap: 32px; flex-wrap: wrap; }
.rl-band h2 { font-size: clamp(32px, 5vw, 64px); letter-spacing: -.04em; line-height: 1; margin: 0; max-width: 15ch; font-weight: 800; }
.rl-band .rl-btn { background: #000; color: #fff; border-color: #000; }
.rl-band .rl-btn:hover { background: transparent; border-color: #fff; color: #fff; }

.rl-foot { position: relative; z-index: 2; background: #fff; padding: 36px 0; font-size: 14px; color: rgba(0,0,0,.55); }
.rl-foot .rl-wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px; }

@media (max-width: 820px) {
  .rl-links a:not(.rl-btn) { display: none; }
  .rl-hud { left: 12px; right: 12px; bottom: 12px; top: auto; transform: none; flex-direction: row; border-left-width: 1px; border-image: none; border-top: 3px solid #6366F1; }
  .rl-hud div { flex: 1; min-width: 0; padding: 10px 12px; }
  .rl-hud div + div { border-top: 0; border-left: 1px solid rgba(0,0,0,.14); }
  .rl-hud b { font-size: 20px; }
  .rl-grid { grid-template-columns: 1fr; }
  .rl-item, .rl-item + .rl-item { padding: 24px 0; border-left: 0; }
  .rl-item + .rl-item { border-top: 1px solid rgba(0,0,0,.14); }
}
@media (prefers-reduced-motion: reduce) {
  .rl-trace { animation: none; stroke-dashoffset: 0; }
  .rl-trace.main { stroke-dashoffset: 0; }
  .rl-ring { animation: none; opacity: .5; }
}
`;

// ---------- peta kota: grid jalan, blok bangunan, taman, sungai ----------
const GX = (c: number) => 90 + 126 * c; // 11 jalan vertikal (c=0..10)
const GY = (r: number) => 130 + 134 * r; // jalan horizontal (r=0..5)
type Pt = [number, number];
const toPts = (l: [number, number][]): Pt[] => l.map(([c, r]) => [GX(c), GY(r)]);

// jalur dengan tikungan membulat mengikuti jalan
function rounded(pts: Pt[], rad = 26): string {
  let d = `M ${pts[0][0]} ${pts[0][1]}`;
  for (let i = 1; i < pts.length - 1; i++) {
    const [px, py] = pts[i - 1], [cx, cy] = pts[i], [nx, ny] = pts[i + 1];
    const l1 = Math.hypot(cx - px, cy - py), l2 = Math.hypot(nx - cx, ny - cy);
    const r1 = Math.min(rad, l1 / 2), r2 = Math.min(rad, l2 / 2);
    const ax = cx - ((cx - px) / l1) * r1, ay = cy - ((cy - py) / l1) * r1;
    const bx = cx + ((nx - cx) / l2) * r2, by = cy + ((ny - cy) / l2) * r2;
    d += ` L ${ax} ${ay} Q ${cx} ${cy} ${bx} ${by}`;
  }
  const last = pts[pts.length - 1];
  return d + ` L ${last[0]} ${last[1]}`;
}

// Start (kiri bawah) -> tujuan (kanan atas), lewat jalan kota
const MAIN = rounded(toPts([[0,5],[0,4],[2,4],[2,3],[4,3],[4,2],[7,2],[7,1],[9,1],[9,0],[10,0]]));
const ALT_B = rounded(toPts([[0,5],[3,5],[3,4],[5,4],[5,3],[8,3],[8,2],[10,2],[10,0]]));
const ALT_C = rounded(toPts([[0,5],[0,3],[1,3],[1,1],[3,1],[3,0],[10,0]]));

function rand(seed: number) {
  return () => {
    seed |= 0; seed = (seed + 0x6d2b79f5) | 0;
    let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

function buildCity() {
  const rnd = rand(7);
  const items: React.ReactNode[] = [];
  let k = 0;
  let parkLabel = false;
  for (let c = -1; c <= 10; c++) {
    for (let r = -1; r <= 5; r++) {
      const x0 = GX(c) + 14, y0 = GY(r) + 14, w = 98, h = 106;
      if (rnd() < 0.11) {
        // taman kota
        items.push(
          <g key={k++}>
            <rect x={x0} y={y0} width={w} height={h} rx="10" fill="url(#rlg)" fillOpacity=".13" />
            {[0, 1, 2, 3, 4].map((i) => (
              <circle key={i} cx={x0 + 16 + rnd() * (w - 32)} cy={y0 + 16 + rnd() * (h - 32)} r={5 + rnd() * 4} fill="url(#rlg)" fillOpacity=".4" />
            ))}
            {!parkLabel && c > 1 && r > 0 && (parkLabel = true) && (
              <text className="rl-flag" x={x0 + w / 2} y={y0 + h / 2 + 4} textAnchor="middle" style={{ fontSize: 11 }}>City Park</text>
            )}
          </g>
        );
        continue;
      }
      // gedung: 2x2 petak dengan ukuran acak
      for (let i = 0; i < 2; i++) {
        for (let j = 0; j < 2; j++) {
          if (rnd() < 0.22) continue;
          const bw = w / 2 - 4, bh = h / 2 - 4;
          const inset = rnd() * 9;
          const lit = rnd() < 0.09;
          items.push(
            <rect
              key={k++}
              x={x0 + i * (bw + 8) + inset / 2}
              y={y0 + j * (bh + 8) + inset / 2}
              width={bw - inset}
              height={bh - inset}
              rx="2"
              fill={lit ? "url(#rlg)" : "#1E1B4B"}
              fillOpacity={lit ? 0.26 : 0.035 + rnd() * 0.06}
            />
          );
        }
      }
    }
  }
  return (
    <g>
      {/* sungai */}
      <path d="M -60 330 C 260 460, 520 130, 780 330 S 1220 580, 1520 470" fill="none" stroke="#2563EB" strokeOpacity=".08" strokeWidth="54" strokeLinecap="round" />
      <path d="M -60 330 C 260 460, 520 130, 780 330 S 1220 580, 1520 470" fill="none" stroke="#4F46E5" strokeOpacity=".3" strokeWidth="1.5" strokeDasharray="2 14" strokeLinecap="round" />
      {items}
      {/* marka tengah jalan */}
      {Array.from({ length: 13 }, (_, i) => (
        <line key={"v" + i} x1={GX(i - 1) } y1="-20" x2={GX(i - 1)} y2="920" stroke="#312E81" strokeOpacity=".12" strokeDasharray="6 12" />
      ))}
      {Array.from({ length: 8 }, (_, i) => (
        <line key={"h" + i} x1="-20" y1={GY(i - 1)} x2="1460" y2={GY(i - 1)} stroke="#312E81" strokeOpacity=".12" strokeDasharray="6 12" />
      ))}
    </g>
  );
}
const CITY = buildCity();

const TOTAL_KM = 10;
const TOTAL_SEC = 55 * 60; // 10 km dengan pace 5:30 /km
const RUN_MS = 14000; // durasi animasi lari
const HOLD_MS = 3000; // jeda di titik tujuan

const pad = (n: number) => String(n).padStart(2, "0");
const fmtTime = (sec: number) =>
  `${pad(Math.floor(sec / 3600))}:${pad(Math.floor((sec % 3600) / 60))}:${pad(Math.floor(sec % 60))}`;
const fmtPace = (secPerKm: number) =>
  `${Math.floor(secPerKm / 60)}:${pad(Math.round(secPerKm % 60))}`;

export default function RunLanding() {
  const mainRef = useRef<SVGPathElement>(null);
  const traceRef = useRef<SVGPathElement>(null);
  const dotRef = useRef<SVGCircleElement>(null);
  const [progress, setProgress] = useState(0);

  useEffect(() => {
    const prev = [document.documentElement.style.background, document.body.style.background, document.body.style.margin];
    document.documentElement.style.background = "#FCFBFF";
    document.body.style.background = "#FCFBFF";
    document.body.style.margin = "0";
    return () => {
      [document.documentElement.style.background, document.body.style.background, document.body.style.margin] = prev;
    };
  }, []);

  useEffect(() => {
    const path = mainRef.current;
    const trace = traceRef.current;
    const dot = dotRef.current;
    if (!path || !trace || !dot) return;
    const len = path.getTotalLength();
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let raf = 0;
    let start = performance.now();

    const apply = (p: number) => {
      const pt = path.getPointAtLength(len * p);
      dot.setAttribute("cx", String(pt.x));
      dot.setAttribute("cy", String(pt.y));
      trace.style.strokeDashoffset = String(1 - p);
      setProgress(p);
    };

    if (reduce) {
      apply(1);
      return;
    }
    const tick = (now: number) => {
      const t = (now - start) % (RUN_MS + HOLD_MS);
      // easing ringan agar pace terlihat natural
      const raw = Math.min(t / RUN_MS, 1);
      apply(raw);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, []);

  const km = progress * TOTAL_KM;
  const sec = progress * TOTAL_SEC;
  const finished = progress >= 1;

  return (
    <div className="rl">
      <style>{css}</style>

      {/* Background: rute lari dari titik awal ke tujuan */}
      <svg
        className="rl-bg"
        viewBox="0 0 1440 900"
        preserveAspectRatio="xMidYMid slice"
        aria-hidden="true"
      >
        <defs>
          <linearGradient id="rlg" gradientUnits="userSpaceOnUse" x1="0" y1="900" x2="1440" y2="0">
            <stop offset="0" stopColor="#6D28D9" />
            <stop offset="1" stopColor="#2563EB" />
          </linearGradient>
        </defs>
        {CITY}
        <path ref={mainRef} id="rl-main" className="rl-route rl-ghost" d={MAIN} />
        <path className="rl-route rl-ghost" d={ALT_B} />
        <path className="rl-route rl-ghost" d={ALT_C} />

        <path className="rl-route rl-trace c" pathLength={1} d={ALT_C} />
        <path className="rl-route rl-trace b" pathLength={1} d={ALT_B} />
        <path ref={traceRef} className="rl-route rl-trace main rl-main-line" pathLength={1} d={MAIN} />

        {/* Titik awal */}
        <circle cx="90" cy="800" r="9" fill="#0E0B1F" />
        <circle cx="90" cy="800" r="18" fill="none" stroke="#312E81" strokeOpacity=".4" />
        <text className="rl-flag" x="116" y="806">Start · 0 km</text>

        {/* Tujuan */}
        <circle className="rl-ring" cx="1350" cy="130" r="9" />
        <circle className="rl-pin" cx="1350" cy="130" r="9" />
        <text className="rl-flag" x="1350" y="108" textAnchor="end">Finish · {TOTAL_KM} km</text>

        {/* Pelari: posisi mengikuti rute utama */}
        <circle ref={dotRef} className="rl-runner" cx="90" cy="800" r="8" fill="#0E0B1F" stroke="url(#rlg)" strokeWidth="4" />
      </svg>
      <div className="rl-vignette" />

      <div className={"rl-hud" + (finished ? " on" : "")} role="status" aria-label="Running simulation">
        <div><b>{km.toFixed(2)}</b><small>km of {TOTAL_KM} km</small></div>
        <div><b>{fmtTime(sec)}</b><small>elapsed time</small></div>
        <div><b>{fmtPace(TOTAL_SEC / TOTAL_KM)}</b><small>pace /km</small></div>
      </div>

      <main className="rl-main">
        <div className="rl-wrap">
          <nav className="rl-nav" aria-label="Main">
            <div className="rl-logo"><i /> Stride</div>
            <div className="rl-links">
              <a href="#" className="rl-btn light" style={{ padding: "10px 20px", fontSize: 15 }}>
                Get the app
              </a>
            </div>
          </nav>

          <header className="rl-hero">
            <div>
              <h1>
                From the start line to the <span>finish line.</span>
              </h1>
              <p>
                Stride tracks every route you run, calculates pace and distance
                live, and helps you build weekly goals that actually make sense.
              </p>
              <div className="rl-cta">
                <a className="rl-btn" href="#">Start running free</a>
              </div>
            </div>
          </header>
        </div>
      </main>
    </div>
  );
}

More by pstzx

View all
  • Contur Land Map

    Contur Land Map

    9
  • hole gradient

    hole gradient

    1
  • Button New & Search

    Button New & Search

  • Arena Games Glass

    Arena Games Glass

  • Vortex Game online

    Vortex Game online