diff --git a/.github/repository.yml b/.github/repository.yml new file mode 100644 index 0000000..b52b739 --- /dev/null +++ b/.github/repository.yml @@ -0,0 +1,19 @@ +name: "Typecade — Free Typing Speed Test & WPM Tracker" +description: "A minimalist typing platform with real-time multiplayer races, a 5-module touch-typing curriculum, and global WPM leaderboards. Built with Next.js 16, Supabase, Tailwind v4." +topics: + - typing-test + - wpm + - typing-speed + - multiplayer + - realtime + - nextjs + - tailwindcss + - supabase + - typescript + - framer-motion + - educational + - productivity + - gamification + - zustand +homepage: "https://typecade.com" +social_preview: "public/opengraph-image.png" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..609cead --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: + push: + branches: [master, staging] + pull_request: + +permissions: + contents: read + +jobs: + validate: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint -- --ignore-pattern test.js + - run: npx tsc --noEmit + - run: npm test + - run: npm run build + - run: npm run build:worker diff --git a/ANALYTICS.md b/ANALYTICS.md new file mode 100644 index 0000000..59cef9e --- /dev/null +++ b/ANALYTICS.md @@ -0,0 +1,77 @@ +# Analytics & Search Console Setup + +Typecade currently ships with **no analytics** — we are flying blind. This +guide walks through wiring up privacy-friendly analytics and submitting the +site to search engines so we can measure and grow. + +## Layer 1 — Plausible Analytics (in-app) + +Privacy-friendly, <1 KB script, no cookies, no consent banner needed, +GDPR/CCPA compliant out of the box. Free for sites under 10K monthly +pageviews. + +1. Sign up at https://plausible.io (or self-host with their docker image). +2. Add a site with domain `typecade.com`. +3. Optional: in `.env.local` set + ``` + NEXT_PUBLIC_PLAUSIBLE_DOMAIN=typecade.com + ``` + (or your self-hosted API host via `NEXT_PUBLIC_PLAUSIBLE_API_HOST`). +4. The `` component in `app/layout.tsx` loads the + script automatically. No code change needed beyond step 3. +5. Verify: open the site in an incognito window — the Plausible dashboard + "Realtime" view should show a visitor within ~5 seconds. + +## Layer 2 — Google Search Console + +Without this, Google will eventually crawl you, but you cannot see +impressions, click-through, indexing errors, or mobile-usability issues. + +1. Sign in at https://search.google.com/search-console with the Google + account that owns typecade.com. +2. **Add property → URL prefix** → enter `https://typecade.com`. +3. **Verification → HTML tag** — copy the `content="..."` value (a long + random string). +4. Add it to `.env.local`: + ``` + NEXT_PUBLIC_GOOGLE_SITE_VERIFICATION=paste-the-content-here + ``` +5. Deploy. Verify in Search Console. +6. Once verified, **Sitemaps → Add sitemap** → submit `https://typecade.com/sitemap.xml`. +7. **URL Inspection → paste `https://typecade.com`** → **Request indexing**. + Repeat for `/arena`, `/learn`, `/board`, `/about`, and at least 3 learn + lessons. + +## Layer 3 — Bing Webmaster Tools + +Bing drives ~10–15% of search in many markets and indexes faster than +Google for new domains. Worth 5 minutes. + +1. Sign in at https://www.bing.com/webmasters with a Microsoft account. +2. **Add site** → `https://typecade.com`. +3. **Verify → HTML meta tag** — copy the `content="..."` value. +4. Add to `.env.local`: + ``` + NEXT_PUBLIC_BING_SITE_VERIFICATION=paste-the-content-here + ``` +5. Deploy, verify, then submit the sitemap and request indexing. + +## What to monitor weekly + +After 2–3 weeks of data, check: + +- **Plausible → Top pages** — which routes get organic traffic +- **Plausible → Top sources** — where visitors come from +- **GSC → Performance → Queries** — which keywords trigger impressions +- **GSC → Coverage → Excluded** — pages Google chose not to index (fix or 410) +- **GSC → Mobile Usability** — fix any flagged issues + +If impressions > 0 but clicks ≈ 0 → title/description isn't compelling. +If impressions = 0 for everything → indexing is the bottleneck. + +## Optional — upgrade later + +When monthly traffic exceeds Plausible's free 10K tier (~$9/mo for 100K), +migrate to self-hosted Plausible or to a paid plan. GA4 is *not* +recommended for an indie product — the consent banner friction costs more +than the data quality difference at our scale. \ No newline at end of file diff --git a/LAUNCH.md b/LAUNCH.md new file mode 100644 index 0000000..6ee214b --- /dev/null +++ b/LAUNCH.md @@ -0,0 +1,151 @@ +# Launch playbook + +This is a checklist for getting Typecade in front of people. We are +optimising for **qualified traffic** (people who actually try the app), +not vanity metrics. The order matters — do them roughly in sequence. + +## 0. Pre-flight (do this BEFORE anything else) + +- [ ] Production deploy is up at https://typecade.com +- [ ] All routes return 200 (`/` `/arena` `/learn` `/board` `/about` `/id` `/id/about`) +- [ ] Plausible is tracking pageviews (check dashboard Realtime tab) +- [ ] Google Search Console verified + sitemap submitted + 6 key URLs requested for indexing +- [ ] Bing Webmaster Tools verified + sitemap submitted +- [ ] Supabase keep-alive migration applied (`supabase/README.md`) +- [ ] UptimeRobot monitor live +- [ ] No 404s, no console errors on the homepage +- [ ] Tested in incognito: typing → result → share → click shared link → works end-to-end + +## 1. GitHub repo polish (1 hour) + +The repo's GitHub presence is one of the first things devs look at. Do all of this in one sitting. + +- [ ] Set repository description (one-liner from `.github/repository.yml`) +- [ ] Set website URL to `https://typecade.com` +- [ ] Add topics: `typing-test`, `wpm`, `multiplayer`, `nextjs`, `tailwindcss`, `supabase`, `typescript`, `framer-motion`, `zustand` +- [ ] Pin this repo to your GitHub profile +- [ ] Add 3-5 useful Issues with the `good first issue` label — gives visitors something to click +- [ ] Turn on Discussions +- [ ] Add a project board (optional) + +## 2. Product Hunt launch (when ready, time it carefully) + +Product Hunt rewards launches that hit the front page. The algorithm +favours engagement in the first 24h. + +**Best launch windows:** Tuesday–Thursday, 12:01 AM Pacific. Avoid US +holidays and major tech announcements. + +**Pre-launch (1 week before):** +- [ ] Reserve the launch date on Product Hunt ("Coming Soon" page) +- [ ] Post a teaser on Twitter/X with a 15-second screen recording +- [ ] Email 10-20 friendly dev/typing people asking them to commit to upvoting on launch day + +**Launch day:** +- [ ] Publish the Product Hunt page at 12:01 AM Pacific +- [ ] Title: "Typecade — Free minimalist typing test with multiplayer races" +- [ ] Tagline (60 chars): "Minimalist typing test with multiplayer races, no signup." +- [ ] First comment (from maker account): brief story, what makes it different, ask for feedback +- [ ] Reply to EVERY comment within the first 4 hours +- [ ] Share the PH link on Twitter, LinkedIn, indiehackers + +**Topics:** `Productivity`, `Education`, `Developer Tools`, `Games` + +## 3. Hacker News (Show HN) + +HN is harder and more skeptical, but the upside is real if your post +gets traction. + +- [ ] Write the post from a personal angle: "Show HN: I built a typing test because I was tired of [specific pain]" +- [ ] Include 2-3 screenshots and a screen recording link +- [ ] Be ready to defend technical choices in comments for 6+ hours +- [ ] Do NOT cross-post from PH same day (HN hates that) +- [ ] Best window: Tuesday/Wednesday 8-10 AM Eastern + +## 4. Reddit + +Authenticity matters more than reach. Spam gets you banned. + +**Subreddits worth one targeted post each:** +- r/typing (1.2k members, very engaged) +- r/InternetIsBeautiful (showcase, gets upvotes for craft) +- r/webdev (technical angle — "How I built X") +- r/nextjs (framework-specific) +- r/Indonesia (if you're Indonesian or have Indonesian content — `/id` page is perfect) +- r/productivity (general appeal) + +**Rules of thumb:** +- Wait until you have something interesting to show, not "I just launched" +- Don't post to more than 2-3 subreddits in a week +- Reply to every comment + +## 5. Indie Hackers + +- [ ] Post in the "Milestones" channel when you hit 100 / 1k / 10k tests taken +- [ ] Monthly progress update in "Building a typing app" thread +- [ ] Search for "typing test" threads and answer helpfully + +## 6. Directory submissions (one-time) + +Each takes 5 minutes, links help SEO and discoverability: + +- [ ] alternativeto.net (page for Typecade as alternative to Monkeytype) +- [ ] saashub.com +- [ ] producthunt.com (separate from launch — just a listing) +- [ ] betaList.com (if still in beta) +- [ ] crunchbase.com (basic company profile) + +## 7. Twitter / X + +Build in public. It compounds. + +- [ ] Set up `@typecade` account with the logo and the bio: "Free typing test with multiplayer races. Built by @yourhandle." +- [ ] Post once/week: dev progress, user milestones, typing tips +- [ ] Engage with @monkeytyper, @keybrcom, @typingcom — not promotion, real discussion +- [ ] Every major change → tweet a 15s screen recording + +## 8. SEO compounding (long game) + +SEO doesn't spike, it compounds. Keep doing these even after launch: + +- [ ] Publish 1 blog post per week targeting long-tail keywords: + - "How to type faster" (info intent) + - "Best typing test for programmers" (comparison intent) + - "Touch typing lessons" (learn intent) + - "Tes mengetik online" (Indonesian) + - Use the Learn module structure for content ideas +- [ ] Submit any new routes to GSC for indexing +- [ ] Check Search Console → Performance weekly; iterate on title/description for pages with impressions but low CTR + +## 9. Track everything + +Set up a weekly 15-minute review: + +1. Plausible dashboard: traffic sources, top pages, top sources +2. GSC: clicks, impressions, average position by query +3. Supabase: total tests taken (this is the metric that matters) +4. Product Hunt page (if launched): upvotes, comments + +If a single traffic source is >70% of total visits within the first month, +diversify. You don't want to depend on one channel. + +## 10. Common pitfalls + +- **Don't launch before the empty-state CTAs work.** First-time visitors + will judge Typecade in 5 seconds. A "No active arenas" screen with + zero context loses them. +- **Don't over-promise multiplayer.** If there are 0 active rooms, the + page should invite the visitor to host one, not pretend people are + playing. +- **Don't add features to fix growth.** Add analytics + the basics + first. 80% of growth comes from distribution, not product. +- **Don't hide the typing area.** The product IS the page. SEO + + design + share loops get people there; the typing area gets them + to stay. + +--- + +If you follow this for 90 days consistently, you should be at 1k+ +DAUs and a non-zero organic search position for "typing test". + +— typecade team \ No newline at end of file diff --git a/README.md b/README.md index 6e3d13e..bc18c4b 100644 --- a/README.md +++ b/README.md @@ -106,9 +106,17 @@ Supabase-backed screens also read these public variables: ```dotenv NEXT_PUBLIC_SUPABASE_URL=your-project-url -NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key +NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key +# Server-only; never expose this in a NEXT_PUBLIC_ variable. +SUPABASE_SERVICE_ROLE_KEY=your-service-role-key +NEXT_PUBLIC_OVERDRIVE_COMPETITIVE=false +OVERDRIVE_DAILY_SECRET=server-only-daily-secret ``` +Apply the checked-in SQL migrations in `supabase/migrations/` before enabling +competitive mode. Keep `NEXT_PUBLIC_OVERDRIVE_COMPETITIVE=false` until the +server submission and replay verification gates are enabled. + Start the development server: ```bash diff --git a/app/(main)/about/page.tsx b/app/(main)/about/page.tsx index 672d80d..3a826d5 100644 --- a/app/(main)/about/page.tsx +++ b/app/(main)/about/page.tsx @@ -1,26 +1,23 @@ -import type { Metadata } from 'next'; -import Link from 'next/link'; -import { Button } from '@/components/ui/button'; -import { Keyboard, Shield, Trophy, Zap, BarChart3, GraduationCap, Users } from 'lucide-react'; +import type { Metadata } from "next"; +import Link from "next/link"; export const metadata: Metadata = { - title: 'About Typecade | The Professional Touch Typing Performance Platform', - description: 'Typecade is a minimalist, high-performance typing platform. Learn our methodology for WPM calculation, explore our structured curriculum, and discover how we help typists reach 100+ WPM through precision practice.', - keywords: ['about typecade', 'typing test methodology', 'wpm calculation standard', 'touch typing platform', 'typing speed metrics', 'pro typist tools'], + title: "About — Typecade", + description: "Typecade is a focused typing practice. Less is more.", alternates: { - canonical: 'https://typecade.com/about', + canonical: "https://typecade.com/about", }, openGraph: { - title: 'About Typecade | High-Performance Typing Practice', - description: 'Discover the methodology and features behind the internet\'s most minimalist typing platform.', - type: 'website', - url: 'https://typecade.com/about', + title: "About — Typecade", + description: "Typecade is a focused typing practice. Less is more.", + type: "website", + url: "https://typecade.com/about", images: [ { url: "/opengraph-image.png", width: 1200, height: 630, - alt: "Typecade Platform Overview", + alt: "About — Typecade", }, ], }, @@ -28,43 +25,40 @@ export const metadata: Metadata = { export default function AboutPage() { const jsonLd = { - '@context': 'https://schema.org', - '@graph': [ + "@context": "https://schema.org", + "@graph": [ { - '@type': 'WebPage', - '@id': 'https://typecade.com/about#webpage', - url: 'https://typecade.com/about', - name: 'About Typecade', - description: 'The methodology, technology, and philosophy behind the Typecade typing platform.', - isPartOf: { '@id': 'https://typecade.com/#website' }, + "@type": "WebPage", + "@id": "https://typecade.com/about#webpage", + url: "https://typecade.com/about", + name: "About — Typecade", + isPartOf: { "@id": "https://typecade.com/#website" }, }, { - '@type': 'Organization', - '@id': 'https://typecade.com/#organization', - name: 'Typecade', - url: 'https://typecade.com', - logo: 'https://typecade.com/typecade-logo.png', - sameAs: [ - 'https://github.com/irham3/typecade' - ], + "@type": "Organization", + "@id": "https://typecade.com/#organization", + name: "Typecade", + url: "https://typecade.com", + logo: "https://typecade.com/typecade-logo.png", + sameAs: ["https://github.com/irham3/typecade"], }, { - '@type': 'FAQPage', + "@type": "FAQPage", mainEntity: [ { - '@type': 'Question', - name: 'How is WPM calculated on Typecade?', + "@type": "Question", + name: "How is WPM calculated?", acceptedAnswer: { - '@type': 'Answer', - text: 'Typecade follows the global standard where one "word" equals exactly five keystrokes, including spaces. We calculate raw speed and then adjust for accuracy to ensure your performance metrics are realistic and comparable to professional standards.', + "@type": "Answer", + text: "Every five keystrokes count as one word. Spaces and punctuation included. This is the universal standard used by typing tools since the 1980s, so your scores line up with what you'd see anywhere else.", }, }, { - '@type': 'Question', - name: 'Can I use Typecade for free?', + "@type": "Question", + name: "Is it free?", acceptedAnswer: { - '@type': 'Answer', - text: 'Yes. Typecade is a free-to-use platform. All modules, including the Multiplayer Arena and the Learn curriculum, are accessible to all users without cost. Creating an account allows for persistent progress tracking and leaderboard positioning.', + "@type": "Answer", + text: "Yes. Every feature is free. Sign in if you want your results saved across devices; skip it if you don't.", }, }, ], @@ -79,168 +73,108 @@ export default function AboutPage() { dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> -
- {/* Hero / Intro */} -
-
- - Performance Focused -
-

- Precision-Engineered
- Typing Performance. -

-

- Typecade is more than just a speed test. It is a high-performance environment designed for typists who value precision, minimalist design, and deep technical metrics. Our platform removes the clutter of modern web interfaces to let you focus on what matters: your muscle memory. -

-
- - {/* The Tech / Methodology */} -
-
-
- -

The Standard Methodology

-
-

- To ensure global comparability, Typecade adheres to the standardized "5-character word" rule. Instead of counting individual dictionary words, we treat every 5 keystrokes (including spaces and punctuation) as one word of progress. -

-

- This eliminates the variance between simple and complex texts, providing a baseline metric that is consistent across all modes, from 15-second sprints to long-form quote practice. -

-
-
- 5KPH - Standard Word -
-
- 98%⁺ - Target Accuracy -
-
-
-
-
-

Accuracy vs Speed

-

- Our engine prioritizes accuracy above all. In our scoring algorithm, uncorrected errors are penalized. We recommend typists aim for near-perfect accuracy before attempting to break the 100 WPM barrier. -

-
    -
  • - - Real-time error highlighting -
  • -
  • - - Corrective backspace settings -
  • -
  • - - Detailed per-key accuracy tracking -
  • -
-
-
-
+
+ {/* Quiet header — no badge, no chip, no marketing accent. */} +

+ Less is
+ more. +

- {/* Features Grid */} -
-
-

Engineered for Mastery

-

Everything you need to reach elite typing speeds, built with a performance-first philosophy.

-
+

+ A typing test that gets out of the way. No progress to chase, no + streaks to maintain. Just you and the words. +

-
- {[ - { - icon: , - title: "Diverse Formats", - desc: "Switch between Time, Words, Quote, and Custom text to train different aspects of your typing rhythm." - }, - { - icon: , - title: "Guided Path", - desc: "Our 5-module curriculum takes you from home-row basics to complex integration with symbols and numbers." - }, - { - icon: , - title: "Real-time Arena", - desc: "Low-latency multiplayer races with global matchmaking and private lobbies for competitive group practice." - }, - { - icon: , - title: "Performance Board", - desc: "Track your rank globally. Our leaderboards capture top WPM scores across all standard test durations." - } - ].map((feature, i) => ( -
-
- {feature.icon} -
-

{feature.title}

-

{feature.desc}

-
- ))} + {/* The methodology — minimal, no decoration. */} +
+
+ 01 +

+ One rule. +

+

+ Five keystrokes is one word. Spaces and punctuation count. Same + rule every typing tool has used since the 1980s — adopted here + so the score you see on Typecade matches what you'd see anywhere + else. +

+

+ Accuracy is weighted over speed. An uncorrected mistake costs + more than the keystroke that caused it. We tell you this once + and then we trust you to use it well. +

- {/* Typing Philosophy */} -
-
-

- "The keyboard is the primary interface between
the human mind and the digital world." + {/* What's here — three items, no icons, no card backgrounds. */} +
+
+ 02 +

+ What's here.

-
-

- At Typecade, we believe that touch typing is more than just a data entry skill — it's a creative velocity. When you master your keyboard, the friction between thought and code disappears. -

-

- Our goal is to make that mastery accessible to everyone through a platform that is fast, beautiful, and fundamentally useful. No distractions, no gamification noise. Just pure performance. -

-
-
- {/* Final CTA Card */} -
-
-
-

- Ready to scale your speed? -

-

- Join our global community of typists. Create a profile to track your growth, or jump straight into a race and test your limits. -

-
+
    +
  • + + Practice + +

    + Time, words, quotes, or your own text. Pick the format and + the duration. The page remembers your last choice. +

    +
  • +
  • + + Arena + +

    + Real-time races against other typists. WPM bars update as + you go. No lag, no waiting rooms. +

    +
  • +
  • + + Learn + +

    + A five-module curriculum from home row to total fluency. + Short, focused lessons. No video. +

    +
  • +
  • + + Board + +

    + Top scores across all modes and durations. Sign in only if + you want your results remembered. +

    +
  • +
+
-
- - -
+ {/* What's not here — restraint as a feature. */} +
+
+ 03 +

+ What's not here. +

+

+ No avatars, no levels, no daily quests, no achievements, no + "premium" tier. No notifications. No email unless you ask for it. + The page is the product. +

+ + {/* Sign-off, single line, no CTA button. */} +

+ made by a typist, for typists. +

); -} - +} \ No newline at end of file diff --git a/app/(main)/arena/opengraph-image.tsx b/app/(main)/arena/opengraph-image.tsx new file mode 100644 index 0000000..49e2bc4 --- /dev/null +++ b/app/(main)/arena/opengraph-image.tsx @@ -0,0 +1,137 @@ +import { ImageResponse } from "next/og"; + +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; +export const alt = "Typecade Arena — Real-time multiplayer typing races"; +export const dynamic = "force-static"; + +const FONT_STACK = + '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'; +const MONO_STACK = + '"JetBrains Mono", "SF Mono", "Cascadia Code", Consolas, monospace'; + +export default async function Image() { + return new ImageResponse( + ( +
+
+
+ ⚡ +
+
+ Typecade Arena +
+
+ +
+ Race typists in real time. +
+ +
+ Host a room or join one. Live WPM bars, no lag, no signup. +
+ + {/* Race visualization */} +
+ {[ + { name: "neon_fox_92", wpm: 112, pct: 78, color: "#F43F5E" }, + { name: "you", wpm: 92, pct: 64, color: "#6366F1" }, + { name: "byte_rider", wpm: 87, pct: 60, color: "#9CA3AF" }, + ].map((row) => ( +
+
+ {row.name} +
+
+
+
+
+ {row.wpm} +
+
+ ))} +
+
+ ), + size + ); +} \ No newline at end of file diff --git a/app/(main)/board/opengraph-image.tsx b/app/(main)/board/opengraph-image.tsx new file mode 100644 index 0000000..0c1c1cf --- /dev/null +++ b/app/(main)/board/opengraph-image.tsx @@ -0,0 +1,146 @@ +import { ImageResponse } from "next/og"; + +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; +export const alt = "Typecade Board - Global typing test leaderboards"; +export const dynamic = "force-static"; + +const FONT_STACK = + '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'; +const MONO_STACK = + '"JetBrains Mono", "SF Mono", "Cascadia Code", Consolas, monospace'; + +export default async function Image() { + return new ImageResponse( + ( +
+
+
+ * +
+
+ Typecade Board +
+
+ +
+ See where you rank. +
+ +
+ Global leaderboards across all test modes and durations. + Filter by today, this week, or all-time. +
+ +
+ {[ + { rank: 1, name: "thunderkey_", wpm: 184, acc: 99, medal: "1st" }, + { rank: 2, name: "neon_fox_92", wpm: 171, acc: 98, medal: "2nd" }, + { rank: 3, name: "byte_rider", wpm: 168, acc: 97, medal: "3rd" }, + { rank: 4, name: "you", wpm: 142, acc: 96, medal: "" }, + ].map((row) => ( +
+
+ {row.medal || "Run"} #{row.rank} +
+
+ {row.name} +
+
+ {row.wpm} wpm +
+
+ {row.acc}% +
+
+ ))} +
+
+ ), + size, + ); +} diff --git a/app/(main)/learn/opengraph-image.tsx b/app/(main)/learn/opengraph-image.tsx new file mode 100644 index 0000000..bf09167 --- /dev/null +++ b/app/(main)/learn/opengraph-image.tsx @@ -0,0 +1,122 @@ +import { ImageResponse } from "next/og"; + +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; +export const alt = "Typecade Learn — 5-module touch typing curriculum"; +export const dynamic = "force-static"; + +const FONT_STACK = + '"Inter", "SF Pro Display", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif'; +const MONO_STACK = + '"JetBrains Mono", "SF Mono", "Cascadia Code", Consolas, monospace'; + +export default async function Image() { + return new ImageResponse( + ( +
+
+
+ L +
+
+ Typecade Learn +
+
+ +
+ Master the keyboard, +
+ one row at a time. +
+ +
+ A 5-module curriculum from home row to total fluency. Free, + structured, and progressive. +
+ + {/* Module pills */} +
+ {[ + "1 · Home Row", + "2 · Top Row", + "3 · Bottom Row", + "4 · Shift Key", + "5 · Numbers & Mastery", + ].map((label) => ( +
+ {label} +
+ ))} +
+
+ ), + size + ); +} \ No newline at end of file diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index 66dfa39..3243ec6 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -1,32 +1,41 @@ import type { Metadata } from 'next'; import { HomeClient } from './client'; +import { HomepageTagline } from '@/features/marketing/components/homepage-tagline'; export const metadata: Metadata = { - title: 'Typing Test, WPM Tracker & Multiplayer Races - Typecade', - description: 'Free multiplayer typing test with real-time races, leaderboards, and detailed WPM stats. Challenge friends or race strangers.', + title: 'Typecade — typing speed test', + description: 'Test your typing speed. Race others. Climb the board.', keywords: ['typing test', 'wpm test', 'typing speed test', 'typecade', 'multiplayer typing game', 'touch typing'], + alternates: { + canonical: '/', + languages: { + en: 'https://typecade.com/', + 'x-default': 'https://typecade.com/', + id: 'https://typecade.com/id', + }, + }, openGraph: { - title: 'Typing Test, WPM Tracker & Multiplayer Races - Typecade', - description: 'Free multiplayer typing test with real-time races, leaderboards, and detailed WPM stats. Challenge friends or race strangers.', + title: 'Typecade — typing speed test', + description: 'Test your typing speed. Race others. Climb the board.', url: 'https://typecade.com', siteName: 'Typecade', locale: 'en_US', type: 'website', images: [ { - url: "/opengraph-image.png", + url: '/opengraph-image.png', width: 1200, height: 630, - alt: "Typecade | Free Typing Speed Test", - type: "image/png", + alt: 'Typecade — typing speed test', + type: 'image/png', }, ], }, twitter: { card: 'summary_large_image', - title: 'Typecade - Free Typing Speed Test', - description: 'Free multiplayer typing test with real-time races, leaderboards, and detailed WPM stats.', - images: ["/opengraph-image.png"], + title: 'Typecade — typing speed test', + description: 'Test your typing speed. Race others. Climb the board.', + images: ['/opengraph-image.png'], }, }; @@ -35,8 +44,12 @@ export default function Page() { '@context': 'https://schema.org', '@type': 'WebSite', name: 'Typecade', - url: 'https://typecade.com', // Placeholder URL - description: 'Free multiplayer typing test with real-time races, leaderboards, and detailed WPM stats.', + url: 'https://typecade.com', + description: 'Test your typing speed. Race others. Climb the board.', + potentialAction: { + '@type': 'SearchAction', + target: 'https://typecade.com/?q={search_term_string}', + }, }; return ( @@ -46,10 +59,14 @@ export default function Page() { dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> - {/* Search Engine Optimization */} -

Typecade: Free Typing Speed Test & Touch Typing Trainer

+ {/* SEO H1 — visible to crawlers only. The user-facing headline lives + in HomepageTagline, which is shorter and punchier. */} +

+ Typecade: Free Typing Speed Test, WPM Tracker & Multiplayer Races +

+ ); -} +} \ No newline at end of file diff --git a/app/api/overdrive/daily/route.ts b/app/api/overdrive/daily/route.ts new file mode 100644 index 0000000..3399856 --- /dev/null +++ b/app/api/overdrive/daily/route.ts @@ -0,0 +1,73 @@ +import { NextResponse } from "next/server" +import { + OVERDRIVE_RNG_VERSION, + OVERDRIVE_RULESET_VERSION, + OVERDRIVE_WORD_POOL_VERSION, +} from "@/lib/telemetry" +import { deriveDailySeed } from "@/lib/overdrive/server/daily-seed" +import { getSupabaseAdminClient } from "@/lib/supabase/admin" + +const competitiveEnabled = process.env.NEXT_PUBLIC_OVERDRIVE_COMPETITIVE === "true" + +function isLanguage(value: string | null): value is "EN" | "ID" { + return value === "EN" || value === "ID" +} + +export async function GET(request: Request) { + if (!competitiveEnabled) return new NextResponse(null, { status: 404 }) + + const language = new URL(request.url).searchParams.get("language") + if (!isLanguage(language)) { + return NextResponse.json({ error: "language must be EN or ID" }, { status: 400 }) + } + + const runDate = new Date().toISOString().slice(0, 10) + const secret = process.env.OVERDRIVE_DAILY_SECRET + if (!secret) { + return NextResponse.json({ error: "Daily seed is not configured" }, { status: 503 }) + } + + try { + const admin = getSupabaseAdminClient() + const existing = await admin + .from("daily_seeds") + .select("id, run_date, language, ruleset_version, rng_version, word_pool_version, seed") + .eq("run_date", runDate) + .eq("language", language) + .eq("ruleset_version", OVERDRIVE_RULESET_VERSION) + .maybeSingle() + + if (existing.error) throw existing.error + if (existing.data) return NextResponse.json(existing.data) + + const seed = await deriveDailySeed(secret, runDate, language, OVERDRIVE_RULESET_VERSION) + const inserted = await admin + .from("daily_seeds") + .insert({ + run_date: runDate, + language, + ruleset_version: OVERDRIVE_RULESET_VERSION, + rng_version: OVERDRIVE_RNG_VERSION, + word_pool_version: OVERDRIVE_WORD_POOL_VERSION, + seed, + }) + .select("id, run_date, language, ruleset_version, rng_version, word_pool_version, seed") + .single() + + if (inserted.error) { + const raced = await admin + .from("daily_seeds") + .select("id, run_date, language, ruleset_version, rng_version, word_pool_version, seed") + .eq("run_date", runDate) + .eq("language", language) + .eq("ruleset_version", OVERDRIVE_RULESET_VERSION) + .single() + if (raced.error || !raced.data) throw inserted.error + return NextResponse.json(raced.data) + } + + return NextResponse.json(inserted.data) + } catch { + return NextResponse.json({ error: "Daily seed unavailable" }, { status: 503 }) + } +} diff --git a/app/globals.css b/app/globals.css index fcfe956..4e5cef1 100644 --- a/app/globals.css +++ b/app/globals.css @@ -12,28 +12,31 @@ - All accent colors pass WCAG AA 3:1 on bg for UI elements ────────────────────────────────────── */ -/* Warm Precision - Default (Slightly warm off-white and cool dark bg) */ +/* Warm Precision - Default (Slightly warm off-white on cool dark bg). + Bg pushed darker (#08090d) for a more premium, focused feel. + Accent kept at #4952E3 — distinctive indigo, not generic SaaS blue. + Text-dim nudged lighter (#9aa0b4) for better contrast on the darker bg. */ :root { - --background: #0c0d14; - --background-rgb: 12, 13, 20; - --background-raised: #111219; - --foreground: #e6e4df; - --foreground-rgb: 230, 228, 223; + --background: #08090d; + --background-rgb: 8, 9, 13; + --background-raised: #0d0e14; + --foreground: #e8e6e0; + --foreground-rgb: 232, 230, 224; --accent: #4952E3; --accent-rgb: 73, 82, 227; --accent-secondary: #5eead4; --accent-secondary-rgb: 94, 234, 212; --error-bg: #3f1521; --error-text: #f0717b; - --text-dim: #858794; - --text-dim-rgb: 133, 135, 148; - --panel-bg: #111219; - --panel-bg-rgb: 17, 18, 25; - --panel-elevated: #1a1b26; - --panel-elevated-rgb: 26, 27, 38; - --border-dim: #222330; - --border-dim-rgb: 34, 35, 48; - --border-accent: rgba(99, 102, 241, 0.12); + --text-dim: #9aa0b4; + --text-dim-rgb: 154, 160, 180; + --panel-bg: #0d0e14; + --panel-bg-rgb: 13, 14, 20; + --panel-elevated: #14151c; + --panel-elevated-rgb: 20, 21, 28; + --border-dim: #1d1e27; + --border-dim-rgb: 29, 30, 39; + --border-accent: rgba(99, 102, 241, 0.14); --gold: #f5c542; --gold-rgb: 245, 197, 66; --silver: #94a3b8; @@ -183,9 +186,9 @@ --color-silver: var(--silver); --color-bronze: var(--bronze); - --font-sans: var(--font-inter); - --font-mono: var(--font-jetbrains-mono); - --font-display: var(--font-outfit); + --font-sans: var(--font-sans); + --font-mono: var(--font-mono); + --font-display: var(--font-display); --animate-caret-blink: caret-blink 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; --animate-aurora-1: aurora-drift-1 24s ease-in-out infinite; diff --git a/app/id/about/page.tsx b/app/id/about/page.tsx new file mode 100644 index 0000000..c94a815 --- /dev/null +++ b/app/id/about/page.tsx @@ -0,0 +1,155 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft, Trophy, Users, Zap, GraduationCap } from "@/components/icons"; + +export const metadata: Metadata = { + title: "Tentang Typecade - Platform Latihan Mengetik Profesional", + description: + "Pelajari metodologi perhitungan WPM, kurikulum 5 modul, dan bagaimana Typecade membantu Anda mencapai 100+ WPM melalui latihan presisi.", + keywords: [ + "tentang typecade", + "metodologi mengetik", + "standar wpm", + "platform mengetik profesional", + "latihan mengetik 10 jari", + ], + alternates: { + canonical: "/id/about", + languages: { + en: "https://typecade.com/about", + "x-default": "https://typecade.com/about", + id: "https://typecade.com/id/about", + }, + }, +}; + +export default function IndonesianAboutPage() { + return ( +
+ + + Kembali ke Beranda + + +
+
+ + Fokus Performa +
+

+ Latihan Mengetik +
+ + Yang Presisi. + +

+

+ Typecade adalah platform latihan mengetik minimalis dan berperforma tinggi, + dirancang untuk pengguna yang mengutamakan presisi, desain bersih, dan metrik + teknis yang mendalam. Kami membuang semua gangguan untuk membantu Anda fokus + pada yang penting: muscle memory mengetik Anda. +

+
+ +
+
+
+ +

+ Metodologi Standar +

+
+

+ Untuk memastikan hasil yang comparable secara global, Typecade mengikuti + aturan standar "5 karakter = 1 kata". Setiap 5 + tombol yang ditekan (termasuk spasi dan tanda baca) dihitung sebagai satu + kata. +

+

+ Pendekatan ini menghilangkan variabel antara teks sederhana dan kompleks, + memberikan metrik dasar yang konsisten di semua mode — dari sprint 15 detik + hingga latihan kutipan panjang. +

+
+
+ 5K + + Standar Kata + +
+
+ 98%⁺ + + Akurasi Target + +
+
+
+ +
+

+ Akurasi vs Kecepatan +

+

+ Engine kami mengutamakan akurasi. Dalam algoritma penilaian, kesalahan yang + tidak dikoreksi dikenai penalti. Kami merekomendasikan typist mencapai + akurasi mendekati sempurna sebelum mencoba melampaui 100 WPM. +

+
    +
  • ✓ Highlight kesalahan secara real-time
  • +
  • ✓ Pengaturan backspace korektif
  • +
  • ✓ Tracking akurasi per tombol
  • +
+
+
+ +
+

+ Dirancang untuk Mastery +

+

+ Semua yang Anda butuhkan untuk mencapai kecepatan mengetik elite, dibangun + dengan filosofi performa-pertama. +

+
+ +
+ {[ + { icon: GraduationCap, title: "5 Modul Terstruktur", body: "Kurikulum dari home row hingga integrasi penuh dengan simbol." }, + { icon: Users, title: "Arena Real-time", body: "Balapan latensi rendah dengan matchmaking global dan lobby privat." }, + { icon: Trophy, title: "Papan Performa", body: "Peringkat global. Leaderboard menangkap skor WPM terbaik di semua mode." }, + ].map((feature) => ( +
+ +

{feature.title}

+

{feature.body}

+
+ ))} +
+ +
+

+ Siap meningkatkan kecepatan Anda? +

+

+ Mulai latihan pertama Anda sekarang. Gratis, tanpa pendaftaran, tanpa batas. +

+
+ + +
+
+
+ ); +} \ No newline at end of file diff --git a/app/id/page.tsx b/app/id/page.tsx new file mode 100644 index 0000000..e124e87 --- /dev/null +++ b/app/id/page.tsx @@ -0,0 +1,147 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { HomeClient } from "@/app/(main)/client"; +import { Globe, ArrowRight } from "@/components/icons"; + +export const metadata: Metadata = { + title: "Tes Mengetik Online Gratis & Latihan Touch Typing - Typecade", + description: + "Tes kecepatan mengetik gratis dengan balapan multiplayer waktu nyata, papan peringkat global, dan modul latihan mengetik 5 tingkat. Tanpa pendaftaran.", + keywords: [ + "tes mengetik", + "tes kecepatan mengetik", + "latihan mengetik", + "typecade", + "mengetik 10 jari", + "touch typing indonesia", + "wpm test indonesia", + ], + alternates: { + canonical: "/id", + languages: { + en: "https://typecade.com/", + "x-default": "https://typecade.com/", + id: "https://typecade.com/id", + }, + }, + openGraph: { + title: "Tes Mengetik Online Gratis - Typecade", + description: + "Tes kecepatan mengetik gratis dengan balapan multiplayer dan papan peringkat global. Latihan mengetik 10 jari dengan kurikulum terstruktur.", + url: "https://typecade.com/id", + siteName: "Typecade", + locale: "id_ID", + type: "website", + }, + twitter: { + card: "summary_large_image", + title: "Tes Mengetik Online Gratis - Typecade", + description: + "Tes kecepatan mengetik gratis dengan balapan multiplayer dan papan peringkat global.", + }, +}; + +export default function IndonesianHomePage() { + const jsonLd = { + "@context": "https://schema.org", + "@type": "WebApplication", + name: "Typecade", + url: "https://typecade.com/id", + description: + "Tes mengetik online gratis dengan balapan multiplayer waktu nyata dan latihan touch typing.", + inLanguage: "id-ID", + applicationCategory: "EducationalApplication", + operatingSystem: "All", + }; + + return ( + <> +