Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
Western Reserve Conference Fantasy Football League
Translation pending
GamesJavaScript · TypeScriptGitHub @shawngidleyOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Western Reserve Conference Fantasy Football website — a multi-page application with live NFL stats via Tank01 API, Supabase backend, dynasty keeper system, live draft board with countdown timer and theme songs, FAAB waiver system, and a full suite of league management tools including standings, live scoring, lineup management,... · Built with Manus
Taken from the repository README
Loading the full README. The saved excerpt is shown below.
Pure React 19 + Tailwind 4 template with shadcn/ui baked in. Use this README as the checklist for shipping static experiences.
Note: This template includes a minimal shared/ and server/ directory with placeholder types to support imported templates. These are just compatibility placeholders - web-static remains a true static-only template without API functionality.
Stack Overview
• Client-only routing powered by React + Wouter.
• Design tokens live entirely in client/src/index.css—keep that file intact.Read the full README ↗Pure React 19 + Tailwind 4 template with shadcn/ui baked in. Use this README as the checklist for shipping static experiences.
Note: This template includes a minimal
shared/andserver/directory with placeholder types to support imported templates. These are just compatibility placeholders - web-static remains a true static-only template without API functionality.
client/src/index.css—keep that file intact.client/
public/ ← Small configuration files ONLY (favicon.ico, robots.txt). DO NOT put images/media here.
src/
pages/ ← Page-level components
components/ ← Reusable UI & shadcn/ui
contexts/ ← React contexts
hooks/ ← Custom React hooks
lib/ ← Utility helpers
App.tsx ← Routes & top-level layout
main.tsx ← React entry point
index.css ← global style
server/ ← Placeholder for imported template compatibility
shared/ ← Placeholder for imported template compatibility
const.ts ← Shared constants
DO NOT store images, videos, or large assets in client/public/ or client/src/assets/. Local media files will cause deployment timeouts.
Required workflow:
manus-upload-file --webdev path/to/image.png<img src="/manus-storage/image_a1b2c3d4.png" />/home/ubuntu/webdev-static-assets/ (outside the project directory)Only small configuration files like favicon.ico, robots.txt, and manifest.json belong in client/public/.
Files in client/public are available at the root of your site—reference them with absolute paths (/robots.txt, etc.) from HTML templates, JSX, or meta tags.
client/src/index.css for global theming and add needed font using google font cdn in client/index.html.client/src/pages/. Keep sections modular so they can be reused across routes.client/src/components/—extend shadcn/ui when needed instead of duplicating markup.useEffect if the site needs dynamic content from public APIs.UI & Styling:
@/components/ui/* (e.g., button, card, dialog).variant, size, etc. where available.@layer base rules in client/src/index.css. Utilities like border-border and font-sans depend on them.components/ for reuse instead of copy‑paste.client/src/index.css instead of hard‑coding to keep global consistency.React Best Practices:
useEffectCustomized Defaults: This template customizes some Tailwind/shadcn defaults for simplified usage:
.container is customized to auto-center and add responsive padding (see index.css). Use directly without mx-auto/px-*. For custom widths, use max-w-* with mx-auto px-4..flex is customized to have min-width:0 and min-height:0 by defaultbutton variant outline uses transparent background (not bg-background). Add bg color class manually if needed.When generating frontend UI, avoid generic patterns that lack visual distinction:
Bake motion taste in from the first line of code. Snappy, physically intuitive interactions are not a polish pass — they are part of the initial build.
--ease-out: cubic-bezier(0.23, 1, 0.32, 1);. For moving/morphing use --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1);. NEVER use ease-in for UI animations — it feels sluggish.transform: scale(0.97) on :active with a ~160ms ease-out transition so the UI confirms it heard the user.scale(0) — nothing in the real world appears from nothing. Start from scale(0.95) combined with opacity: 0.transform-origin: var(--radix-popover-content-transform-origin)). Modals are the exception and stay centered.transform and opacity for motion — they run on the GPU and skip layout/paint. Avoid animating width, height, padding, margin, top/left unless absolutely necessary.prefers-reduced-motion: gate non-essential motion behind @media (prefers-reduced-motion: no-preference).Before implementing UI features, check if these components already exist:
Maps:
client/src/components/Map.tsx - Google Maps integration with proxy authentication. Provides MapView component with onMapReady callback for initializing Google Maps services (Places, Geocoder, Directions, Drawing, etc.). All map functionality works directly in the browser.When implementing features that match these categories, MUST evaluate the component first to decide whether to use or customize it.
CRITICAL: The Manus proxy provides FULL access to ALL Google Maps features - including advanced drawing, heatmaps, Street View, all layers, Places API, etc. Do NOT ask users for Google Map API keys - authentication is automatic.
Implementation:
client/src/components/Map.tsx and initialize ANY Google Maps service (geocoding, directions, places, drawing, visualization, geometry, etc.) in the onMapReady callback. ALL Google Maps JavaScript API features work directly in the browser.NEVER use external map libraries or request API keys from users - the Manus proxy handles everything automatically with no feature limitations.
package.json
{
"name": "{{project_name}}",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"scripts": {
"dev": "vite --host",
"build": "vite build && esbuild server/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist",
"start": "NODE_ENV=production node dist/index.js",
"preview": "vite preview --host",
"check": "tsc --noEmit",
"format": "prettier --write ."
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.7",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.7",
"@radix-ui/react-menubar": "^1.1.16",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"axios": "^1.12.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"embla-carousel-react": "^8.6.0",
"express": "^4.21.2",
"framer-motion": "^12.23.22",
"input-otp": "^1.4.2",
"lucide-react": "^0.453.0",
"nanoid": "^5.1.5",
"next-themes": "^0.4.6",
"react": "^19.2.1",
"react-day-picker": "^9.11.1",
"react-dom": "^19.2.1",
"react-hook-form": "^7.64.0",
"react-resizable-panels": "^3.0.6",
"recharts": "^2.15.2",
"sonner": "^2.0.7",
"streamdown": "^1.4.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"wouter": "^3.3.5",
"zod": "^4.1.12"
},
"devDependencies": {
"@builder.io/vite-plugin-jsx-loc": "^0.1.1",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.1.3",
"@types/express": "4.17.21",
"@types/google.maps": "^3.58.1",
"@types/node": "^24.7.0",
"@types/react": "^19.2.1",
"@types/react-dom": "^19.2.1",
"@vitejs/plugin-react": "^5.0.4",
"add": "^2.0.6",
"autoprefixer": "^10.4.20",
"esbuild": "^0.25.0",
"pnpm": "^10.15.1",
"postcss": "^8.4.47",
"prettier": "^3.6.2",
"tailwindcss": "^4.1.14",
"tsx": "^4.19.1",
"tw-animate-css": "^1.4.0",
"typescript": "5.6.3",
"vite": "^7.1.7",
"vite-plugin-manus-runtime": "^0.0.58",
"vitest": "^2.1.4"
},
"packageManager": "[email protected]+sha512.c753b6c3ad7afa13af388fa6d808035a008e30ea9993f58c6663e2bc5ff21679aa834db094987129aa4d488b86df57f7b634981b2f827cdcacc698cc0cfb88af",
"pnpm": {
"patchedDependencies": {
"[email protected]": "patches/[email protected]"
},
"overrides": {
"tailwindcss>nanoid": "3.3.7"
}
}
}
client/src/App.tsx
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
import { Route, Switch } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
import Home from "./pages/Home";
function Router() {
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/404"} component={NotFound} />
{/* Final fallback route */}
<Route component={NotFound} />
</Switch>
);
}
// NOTE: About Theme
// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
// to keep consistent foreground/background color across components
// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
function App() {
return (
<ErrorBoundary>
<ThemeProvider
defaultTheme="light"
// switchable
>
<TooltipProvider>
<Toaster />
<Router />
</TooltipProvider>
</ThemeProvider>
</ErrorBoundary>
);
}
export default App;
client/src/pages/Home.tsx
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { Streamdown } from 'streamdown';
/**
* All content in this page are only for example, replace with your own feature implementation
* When building pages, remember your instructions in Frontend Best Practices, Design Guide and Common Pitfalls
*/
export default function Home() {
// If theme is switchable in App.tsx, we can implement theme toggling like this:
// const { theme, toggleTheme } = useTheme();
return (
<div className="min-h-screen flex flex-col">
<main>
{/* Example: lucide-react for icons */}
<Loader2 className="animate-spin" />
Example Page
{/* Example: Streamdown for markdown rendering */}
<Streamdown>Any **markdown** content</Streamdown>
<Button variant="default">Example Button</Button>
</main>
</div>
);
}
client/src/index.css
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--primary: var(--color-blue-700);
--primary-foreground: var(--color-blue-50);
--sidebar-primary: var(--color-blue-600);
--sidebar-primary-foreground: var(--color-blue-50);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--radius: 0.65rem;
--background: oklch(1 0 0);
--foreground: oklch(0.235 0.015 65);
--card: oklch(1 0 0);
--card-foreground: oklch(0.235 0.015 65);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.235 0.015 65);
--secondary: oklch(0.98 0.001 286.375);
--secondary-foreground: oklch(0.4 0.015 65);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.141 0.005 285.823);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.623 0.214 259.815);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.235 0.015 65);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.141 0.005 285.823);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.623 0.214 259.815);
}
.dark {
--primary: var(--color-blue-700);
--primary-foreground: var(--color-blue-50);
--sidebar-primary: var(--color-blue-500);
--sidebar-primary-foreground: var(--color-blue-50);
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.85 0.005 65);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.85 0.005 65);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.85 0.005 65);
--secondary: oklch(0.24 0.006 286.033);
--secondary-foreground: oklch(0.7 0.005 65);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.92 0.005 65);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.488 0.243 264.376);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.85 0.005 65);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.488 0.243 264.376);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
button:not(:disabled),
[role="button"]:not([aria-disabled="true"]),
[type="button"]:not(:disabled),
[type="submit"]:not(:disabled),
[type="reset"]:not(:disabled),
a[href],
select:not(:disabled),
input[type="checkbox"]:not(:disabled),
input[type="radio"]:not(:disabled) {
@apply cursor-pointer;
}
}
@layer components {
/**
* Custom container utility that centers content and adds responsive padding.
*
* This overrides Tailwind's default container behavior to:
* - Auto-center content (mx-auto)
* - Add responsive horizontal padding
* - Set max-width for large screens
*
* Usage: <div className="container">...</div>
*
* For custom widths, use max-w-* utilities directly:
* <div className="max-w-6xl mx-auto px-4">...</div>
*/
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem; /* 16px - mobile padding */
padding-right: 1rem;
}
.flex {
min-height: 0;
min-width: 0;
}
@media (min-width: 640px) {
.container {
padding-left: 1.5rem; /* 24px - tablet padding */
padding-right: 1.5rem;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 2rem; /* 32px - desktop padding */
padding-right: 2rem;
max-width: 1280px; /* Standard content width */
}
}
}
client/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>{{project_title}}</title>
<!-- THIS IS THE START OF A COMMENT BLOCK, BLOCK TO BE DELETED: Google Fonts here, example:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
THIS IS THE END OF A COMMENT BLOCK, BLOCK TO BE DELETED -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<script
defer
src="%VITE_ANALYTICS_ENDPOINT%/umami"
data-website-id="%VITE_ANALYTICS_WEBSITE_ID%"></script>
</body>
</html>
server/index.ts
import express from "express";
import { createServer } from "http";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function startServer() {
const app = express();
const server = createServer(app);
// Serve static files from dist/public in production
const staticPath =
process.env.NODE_ENV === "production"
? path.resolve(__dirname, "public")
: path.resolve(__dirname, "..", "dist", "public");
app.use(express.static(staticPath));
// Handle client-side routing - serve index.html for all routes
app.get("*", (_req, res) => {
res.sendFile(path.join(staticPath, "index.html"));
});
const port = process.env.PORT || 3000;
server.listen(port, () => {
console.log(`Server running on http://localhost:${port}/`);
});
}
startServer().catch(console.error);
Problem: Background image/color bleeds through between two wave shapes, creating a "sandwich" artifact.
Root cause: The SVG path fills the wrong side of the wave (transparent where it should be solid), and position: absolute inside overflow: hidden prevents seamless section joins. Using rotate-180 to flip a divider reverses fill direction and color mapping, causing subtle mismatches.
Fix:
position="top"), one fills below (position="bottom") — never rely on CSS rotation to derive one from the other.position: absolute inside overflow: hidden.color prop must match the destination section's background.// ❌ Absolute-positioned divider inside overflow:hidden — transparent area exposes hero bg
<section className="relative overflow-hidden">
<div className="absolute bottom-0"><WaveDivider color="white" /></div>
</section>
// ✅ Block-flow divider pulled up over hero with negative margin
<section>{/* hero */}</section>
<div className="relative z-10 -mt-16">
<WaveDivider position="bottom" color="oklch(0.97 0.005 80)" />
</div>
<section className="bg-mist" style={{ marginTop: "-1px" }}>{/* next */}</section>
Problem: Text is invisible because it lacks contrast against its actual rendered background — not the CSS parent's background-color, but what the user physically sees behind the text.
Three common traps:
bg-transparent — looks fine over a dark hero, invisible when white content scrolls behind it.Fix: Before shipping, ask: "What will the user actually see behind this text?" If the answer is variable (scroll, images, transparency), add a solid or semi-transparent background to guarantee contrast.
// ❌ Transparent nav — contrast depends on scroll position
<header className="fixed bg-transparent text-white">...</header>
// ✅ Nav transitions to opaque on scroll
<header className={scrolled ? "bg-white/90 backdrop-blur-xl text-gray-900" : "bg-transparent text-white"}>...</header>
// ❌ Text over unknown-brightness image
<h1 className="absolute bottom-4 text-white">Title</h1>
// ✅ Gradient overlay guarantees contrast
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent" />
<h1 className="absolute bottom-4 text-white">Title</h1>
Anti-pattern: Creating new objects/arrays in render that are used as query inputs
// ❌ Bad: New Date() creates new reference every render → infinite queries
const { data } = trpc.items.getByDate.useQuery({
date: new Date(), // ← New object every render!
});
// ❌ Bad: Array/object literals in query input
const { data } = trpc.items.getByIds.useQuery({
ids: [1, 2, 3], // ← New array reference every render!
});
Correct approach: Stabilize references with useState/useMemo
// ✅ Good: Initialize once with useState
const [date] = useState(() => new Date());
const { data } = trpc.items.getByDate.useQuery({ date });
// ✅ Good: Memoize complex inputs
const ids = useMemo(() => [1, 2, 3], []);
const { data } = trpc.items.getByIds.useQuery({ ids });
Why this happens: TRPC queries trigger when input references change. Objects/arrays created in render have new references each time, causing infinite re-fetches.
Problem: Creating nested routes without escape routes—no header nav, no sidebar, no back button.
Root cause: Implementing individual pages before establishing global layout structure.
Solution: Define layout wrapper in App.tsx first, then build pages inside it. For admin tools use DashboardLayout; for detail pages add back button with router.back().
Root cause: Semantic colors (bg-background, text-foreground) are CSS variables that resolve based on ThemeProvider's active theme. Mismatches cause invisible text.
Two critical rules:
defaultTheme="dark" in App.tsx, ensure .dark {} in index.css has dark background + light foreground valuesbg-{semantic}, MUST also use text-{semantic}-foreground (not automatic - text inherits from parent otherwise)Quick reference:
// ✅ Theme + CSS alignment
<ThemeProvider defaultTheme="dark"> {/* Must match .dark in index.css */}
<div className="bg-background text-foreground">...</div>
</ThemeProvider>
// ✅ Required class pairs
<div className="bg-popover text-popover-foreground">...</div>
<div className="bg-card text-card-foreground">...</div>
<div className="bg-accent text-accent-foreground">...</div>
Problem: Wrapping <a> tags inside another <a> or wouter's <Link> creates nested anchors and runtime errors.
Solution: Pass children directly to Link—it already renders an <a> internally.
// ❌ Bad: <Link><a>...</a></Link> or <a><a>...</a></a>
// ✅ Good: <Link>...</Link> or just <a>...</a>
Select.Item valuesRule: Every <Select.Item> must have a non-empty value prop—never "", undefined, or omitted.
Rule: Use sonner for toasts; do not add react-toastify or @radix-ui/react-toast
Rule: If you put placeholder components for App.tsx routes, you MUST replace them with actual components after your implementation.