From 733ab6d1e35611f4765c24440f45903b578b919a Mon Sep 17 00:00:00 2001 From: Leo dev Date: Mon, 2 Feb 2026 00:03:02 +0100 Subject: [PATCH] Some small progress on landing page --- package-lock.json | 38 ++++- package.json | 1 + src/app/page.tsx | 209 +++++++++++++++++++------- src/components/app/Footer.tsx | 84 +++++++++++ src/components/app/LeaderboardRow.tsx | 73 +++++++++ src/components/app/NavLink.tsx | 29 ++++ src/components/app/Navbar.tsx | 71 +++++++++ src/components/app/ProblemCard.tsx | 81 ++++++++++ src/components/app/SplitRow.tsx | 66 ++++++++ src/components/app/StatCard.tsx | 35 +++++ src/components/ui/button.tsx | 64 ++++++++ 11 files changed, 691 insertions(+), 60 deletions(-) create mode 100644 src/components/app/Footer.tsx create mode 100644 src/components/app/LeaderboardRow.tsx create mode 100644 src/components/app/NavLink.tsx create mode 100644 src/components/app/Navbar.tsx create mode 100644 src/components/app/ProblemCard.tsx create mode 100644 src/components/app/SplitRow.tsx create mode 100644 src/components/app/StatCard.tsx create mode 100644 src/components/ui/button.tsx diff --git a/package-lock.json b/package-lock.json index da5df19..516da5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "dev-run", "version": "0.1.0", "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", @@ -699,6 +700,39 @@ "node": ">= 10" } }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -993,7 +1027,7 @@ "version": "19.2.10", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1069,7 +1103,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/detect-libc": { diff --git a/package.json b/package.json index eaf4bf3..40e4903 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "start": "next start" }, "dependencies": { + "@radix-ui/react-slot": "^1.2.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.563.0", diff --git a/src/app/page.tsx b/src/app/page.tsx index 295f8fd..3b029d4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,65 +1,158 @@ -import Image from "next/image"; +import { Timer, Zap, Trophy, Users, ArrowRight, Play } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; + +const Landing = () => { + const features = [ + { + icon: Timer, + title: "Race the Clock", + description: "Built-in timer with splits tracking. Every millisecond counts in your pursuit of the perfect run.", + }, + { + icon: Trophy, + title: "Compete Globally", + description: "Weekly and all-time leaderboards. Claim your spot among the fastest runners worldwide.", + }, + { + icon: Zap, + title: "Quick Runs", + description: "No signup walls. Jump into a run instantly and start grinding for that PB.", + }, + { + icon: Users, + title: "Community First", + description: "Share your runs, compare splits, and celebrate clean runs with fellow speedrunners.", + }, + ]; + + const stats = [ + { value: "12K+", label: "Runners" }, + { value: "847K", label: "Total Runs" }, + { value: "156", label: "Problems" }, + { value: "2:34", label: "World Record" }, + ]; -export default function Home() { return ( -
-
- Next.js logo + {/* Hero Section */} +
+ {/* Background gradient */} +
-
-

- To get started, edit the page.tsx file. -

-

- Looking for a starting point or more instructions? Head over to{" "} - - Templates - {" "} - or the{" "} - - Learning - {" "} - center. -

+ +
+
+ {/* Badge */} +
+ + The speedrunning platform for developers +
+ + {/* Headline */} +

+ Code Fast.{" "} + Run Faster. +

+ + {/* Subheadline */} +

+ DevRun is competitive programming reimagined. Race the clock through algorithmic challenges, + track your splits, and compete for the fastest runs on the global leaderboard. +

+ + {/* CTA Buttons */} +
+ + + + + + +
+
+ + {/* Stats */} +
+ {stats.map((stat, i) => ( +
+

{stat.value}

+

{stat.label}

+
+ ))} +
-
+ + {/* Features Section */} +
+
+
+

+ Built for the grind +

+

+ Everything you need to optimize your runs and climb the ranks. +

+
+ +
+ {features.map((feature, i) => ( +
+
+ +
+

{feature.title}

+

{feature.description}

+
+ ))} +
-
+ + + {/* CTA Section */} +
+
+ +
+
+

+ Ready to chase your PB? +

+

+ Join thousands of runners pushing the limits of competitive programming. +

+ + + +
+
+
); -} +}; + +export default Landing; diff --git a/src/components/app/Footer.tsx b/src/components/app/Footer.tsx new file mode 100644 index 0000000..4362732 --- /dev/null +++ b/src/components/app/Footer.tsx @@ -0,0 +1,84 @@ +import { Timer, Github, Twitter, MessageCircle } from "lucide-react"; + +const Footer = () => { + return ( + + ); +}; + +export default Footer; diff --git a/src/components/app/LeaderboardRow.tsx b/src/components/app/LeaderboardRow.tsx new file mode 100644 index 0000000..59189ed --- /dev/null +++ b/src/components/app/LeaderboardRow.tsx @@ -0,0 +1,73 @@ +import { Trophy } from "lucide-react"; + +interface LeaderboardRowProps { + rank: number; + username: string; + pb: string; + runsCount: number; + category: string; + index?: number; +} + +const LeaderboardRow = ({ rank, username, pb, runsCount, category, index = 0 }: LeaderboardRowProps) => { + const getRankStyle = (rank: number) => { + if (rank === 1) return "rank-gold"; + if (rank === 2) return "rank-silver"; + if (rank === 3) return "rank-bronze"; + return "bg-secondary text-muted-foreground"; + }; + + const getRankGlow = (rank: number) => { + if (rank === 1) return "glow-gold"; + if (rank === 2) return "glow-silver"; + if (rank === 3) return "glow-bronze"; + return ""; + }; + + const isTopThree = rank <= 3; + + return ( + + {/* Rank */} + +
+ {rank <= 3 ? : rank} +
+ + + {/* Username */} + +
+
+ {username.charAt(0).toUpperCase()} +
+ + {username} + +
+ + + {/* PB */} + + {pb} + + + {/* Runs */} + + {runsCount} + + + {/* Category */} + + + {category} + + + + ); +}; + +export default LeaderboardRow; diff --git a/src/components/app/NavLink.tsx b/src/components/app/NavLink.tsx new file mode 100644 index 0000000..39daaa8 --- /dev/null +++ b/src/components/app/NavLink.tsx @@ -0,0 +1,29 @@ +import Link, { LinkProps } from "next/link"; +import { forwardRef } from "react"; +import { cn } from "@/lib/utils"; +import { usePathname } from "next/navigation"; + +interface NavLinkCompatProps extends Omit { + className?: string; + activeClassName?: string; + pendingClassName?: string; +} + +const NavLink = forwardRef( + ({ className, activeClassName, pendingClassName, href, ...props }, ref) => { + const pathname = usePathname(); + const isActive = pathname === href; + const isPending = false; + + return ( + + ); + } +); + +NavLink.displayName = "NavLink"; diff --git a/src/components/app/Navbar.tsx b/src/components/app/Navbar.tsx new file mode 100644 index 0000000..2fe0fe0 --- /dev/null +++ b/src/components/app/Navbar.tsx @@ -0,0 +1,71 @@ +import Link from "next/link"; +import { Timer, Trophy, User, Zap } from "lucide-react"; +import { usePathname } from "next/navigation"; + +const Navbar = () => { + const pathname = usePathname(); + + const navLinks = [ + { to: "/problems", label: "Problems", icon: Zap }, + { to: "/run", label: "Run", icon: Timer }, + { to: "/leaderboards", label: "Leaderboards", icon: Trophy }, + { to: "/profile", label: "Profile", icon: User }, + ]; + + const isActive = (path: string) => pathname === path; + + return ( + + ); +}; + +export default Navbar; diff --git a/src/components/app/ProblemCard.tsx b/src/components/app/ProblemCard.tsx new file mode 100644 index 0000000..60481d2 --- /dev/null +++ b/src/components/app/ProblemCard.tsx @@ -0,0 +1,81 @@ +import { Clock, Zap, Users } from "lucide-react"; + +interface ProblemCardProps { + name: string; + difficulty: "easy" | "medium" | "hard"; + tags: string[]; + bestTime?: string; + attempts: number; + index?: number; +} + +const ProblemCard = ({ name, difficulty, tags, bestTime, attempts, index = 0 }: ProblemCardProps) => { + const difficultyStyles = { + easy: "diff-easy", + medium: "diff-medium", + hard: "diff-hard", + }; + + const difficultyLabels = { + easy: "Easy", + medium: "Medium", + hard: "Hard", + }; + + return ( +
+ {/* Glow effect on hover */} +
+ +
+ {/* Header */} +
+

+ {name} +

+ + {difficultyLabels[difficulty]} + +
+ + {/* Tags */} +
+ {tags.map((tag) => ( + + {tag} + + ))} +
+ + {/* Stats */} +
+ {bestTime && ( +
+ + {bestTime} +
+ )} +
+ + {attempts} runs +
+
+ + {/* Quick run button - appears on hover */} +
+ +
+
+
+ ); +}; + +export default ProblemCard; diff --git a/src/components/app/SplitRow.tsx b/src/components/app/SplitRow.tsx new file mode 100644 index 0000000..9762457 --- /dev/null +++ b/src/components/app/SplitRow.tsx @@ -0,0 +1,66 @@ +import { Check, Minus } from "lucide-react"; + +interface SplitRowProps { + problemName: string; + splitTime: string; + delta?: string; + isCompleted: boolean; + isCurrent?: boolean; + pbTime?: string; +} + +const SplitRow = ({ problemName, splitTime, delta, isCompleted, isCurrent, pbTime }: SplitRowProps) => { + const getDeltaColor = (delta?: string) => { + if (!delta) return "text-muted-foreground"; + if (delta.startsWith("-")) return "text-emerald-400"; + if (delta.startsWith("+")) return "text-red-400"; + return "text-muted-foreground"; + }; + + return ( +
+
+ {/* Status indicator */} +
+ {isCompleted ? : } +
+ + + {problemName} + +
+ +
+ {/* Delta */} + {isCompleted && delta && ( + + {delta} + + )} + + {/* Split time */} + + {isCompleted ? splitTime : pbTime || '--:--'} + +
+
+ ); +}; + +export default SplitRow; diff --git a/src/components/app/StatCard.tsx b/src/components/app/StatCard.tsx new file mode 100644 index 0000000..ada35c5 --- /dev/null +++ b/src/components/app/StatCard.tsx @@ -0,0 +1,35 @@ +import { LucideIcon } from "lucide-react"; + +interface StatCardProps { + label: string; + value: string; + icon: LucideIcon; + trend?: string; + index?: number; +} + +const StatCard = ({ label, value, icon: Icon, trend, index = 0 }: StatCardProps) => { + return ( +
+
+
+ +
+ {trend && ( + + {trend} + + )} +
+

{value}

+

{label}

+
+ ); +}; + +export default StatCard; diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..915ea2a --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot : "button" + + return ( + + ) +} + +export { Button, buttonVariants }