尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Enter your address and see the live drive time, with traffic, to 10 Colorado ski resorts. Resorts are sorted fastest first and shown on a map.
Stack: Next.js (React) · Google Maps Routes API · GitHub · Vercel
┌──────────────── YOUR BROWSER ────────────────┐
│ AddressInput.tsx (BROWSER key) ─────────────┼─────► Places API: suggestions as you type
│ Dashboard.tsx │
│ 1. Pick a suggestion (or click the button) │
│ 2. POST /api/commute { address } ─────────┼──┐
│ │ │
│ 6. Draw the cards and markers ◄───────────┼──┼── JSON: 10 resorts, sorted
│ ResortMap.tsx (uses the BROWSER key) ───────┼──┼──► Google Maps JavaScript API
└──────────────────────────────────────────────┘ │ (map tiles only)
▼
┌──────────── NEXT.JS SERVER (Vercel) ─────────────────┐
│ src/app/api/commute/route.ts (uses the SECRET key) │
│ 3. Geocoding API: address → lat/lng │
│ 4. Routes API computeRouteMatrix: │
│ 1 origin × 10 resorts, TRAFFIC_AWARE_OPTIMAL │
│ 5. Turn seconds into numbers, sort fastest first │
└──────────────────────────────────────────────────────┘
Why the server step? API keys are passwords. Anything that runs in the browser can be read by anyone who opens your site. The browser therefore never talks to the Routes API directly. It calls your own endpoint, /api/commute. That endpoint runs on Vercel's servers, where the secret key lives in an environment variable.
The map has to load in the browser, so it uses a second, public key. That key is locked to your website's URL and to the Maps JavaScript API only, so a copied key is useless anywhere else.
One request asks for travel times from a set of origins (here, just your home) to a set of destinations (here, the 10 resorts). Each origin→destination pair is one element, and Google bills per element. One refresh therefore costs 10 elements.
The request we send (see src/lib/google.ts):
POST https://routes.googleapis.com/distanceMatrix/v2:computeRouteMatrix
X-Goog-Api-Key: <secret key>
X-Goog-FieldMask: originIndex,destinationIndex,status,condition,distanceMeters,duration,staticDuration
{
"origins": [{ "waypoint": { "location": { "latLng": { "latitude": 39.75, "longitude": -105.00 }}}}],
"destinations": [ ...10 resorts, same shape... ],
"travelMode": "DRIVE",
"routingPreference": "TRAFFIC_AWARE_OPTIMAL"
}
What comes back is one entry per resort:
[
{ "destinationIndex": 3, "condition": "ROUTE_EXISTS", "distanceMeters": 127140,
"duration": "6060s", "staticDuration": "4980s" },
...
]
| Field | Meaning | Shown on the dashboard as |
|---|---|---|
duration | Drive time with current traffic | Big number on each card |
staticDuration | Drive time ignoring traffic | "Normal 1h 23m" |
duration − staticDuration | Extra time caused by traffic | "+18m Heavy" badge |
destinationIndex | Which resort the entry is for. Entries can arrive in any order. | n/a |
TRAFFIC_AWARE_OPTIMAL: Uses live and historical traffic, the same quality as the Google Maps app. With no departureTime, Google assumes "leaving now."Tap a resort and the map draws your actual route to it. Each stretch is colored by live traffic, like Google Maps: blue means moving normally, orange means slow, and red means jammed.
This uses a second Routes API method, computeRoutes, with extraComputations: ["TRAFFIC_ON_POLYLINE"]. Google returns the road shape as an encoded polyline plus a list of speedReadingIntervals, each saying "points 120–188 are SLOW". The server (src/lib/google.ts) decodes the line, cuts it into those colored pieces, and the map draws them.
Traffic on route lines is billed at Google's Enterprise level, which includes 1,000 free requests a month. That's why routes only load when you tap a resort.
Google's API doesn't include the accident and closure icons you see in the Google Maps app. Their effect still shows up here, as orange or red stretches.
localStorage. Reopening the page within 10 minutes shows them for free.src/
├─ app/
│ ├─ page.tsx the home page (just renders <Dashboard/>)
│ ├─ layout.tsx fonts, page title
│ ├─ globals.css all styling (dark theme + mobile rules at the bottom)
│ ├─ api/commute/route.ts ★ drive times to all 10 resorts (secret key used here)
│ └─ api/route/route.ts the road path to ONE resort, colored by traffic
├─ components/
│ ├─ Dashboard.tsx refresh, stats, resort cards (loads Google's script once)
│ ├─ AddressInput.tsx address box with suggestions as you type
│ └─ ResortMap.tsx the Google Map + markers
└─ lib/
├─ resorts.ts ★ the 10 resorts (edit this to change them)
├─ google.ts calls Geocoding + Route Matrix (server only)
├─ config.ts reads the public browser key
├─ demo.ts sample data used when no key is set
├─ format.ts "4512s" → "1h 15m", traffic levels
└─ types.ts shape of the API response
You need Node.js (already installed: node -v shows v24).
cd ~/Documents/Claude/ski-commute
npm install
npm run dev
Open http://localhost:3000. With no keys set, you'll see demo data (sample times from downtown Denver) and a placeholder where the map goes. That's expected, and it means the app works. Press Ctrl + C in the terminal to stop the server.
Allow about 15 minutes. You'll create two keys:
| Key | Env variable | Who can see it | Locked to |
|---|---|---|---|
| Server key | GOOGLE_MAPS_API_KEY | Only the server | Routes API + Geocoding API |
| Browser key | NEXT_PUBLIC_GOOGLE_MAPS_API_KEY | Anyone (it's in the page) | Maps JavaScript API + Places API (New) + your URLs |
In Next.js, anything starting with
NEXT_PUBLIC_is sent to the browser. Never put the server key in aNEXT_PUBLIC_variable.
ski-commute → Create. Make sure it's selected.Go to ☰ → APIs & Services → Library, then search for and Enable each of these:
ski-commute-serverski-commute-browserhttp://localhost:3000/*Anyone who finds your site could press Refresh a lot. These steps put a hard ceiling on what that could cost:
100.cp .env.example .env.local
Open .env.local and paste in both keys:
GOOGLE_MAPS_API_KEY=AIza...server...
NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=AIza...browser...
Restart npm run dev. Env files are read only at startup. The demo banner should disappear, and you'll get live times and a map.
.env.local is listed in .gitignore, so git will never upload it.
GitHub stores your code online, and Vercel deploys straight from it.
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
.pkg) from https://cli.github.com and run it, or use brew install gh if you have Homebrew. Then log in:gh auth login
Choose GitHub.com → HTTPS → Login with a web browser.
From the project folder:
git init
git add .
Check that .env.local is not in the list this shows. If it is, stop and ask for help.
git status
git commit -m "First version of ski commute dashboard"
gh repo create ski-commute --private --source=. --push
This creates a private repo at github.com/<you>/ski-commute and uploads ("pushes") your code.
git add .
git commit -m "Describe what you changed"
git push
Once Vercel is connected, every push redeploys your site automatically.
ski-commute → Import. You may need to click "Adjust GitHub App Permissions" to let Vercel see the repo.GOOGLE_MAPS_API_KEY = your server keyNEXT_PUBLIC_GOOGLE_MAPS_API_KEY = your browser keyhttps://ski-commute-abc123.vercel.app.https://ski-commute-abc123.vercel.app/* (use your real one)Changing a key later? Update it in Vercel → Project → Settings → Environment Variables, then go to Deployments → ⋯ → Redeploy.
NEXT_PUBLIC_values are baked in when the site is built, so a redeploy is required.
Google Maps Platform gives a free monthly allowance per API:
| What | Google's billing name | Free per month | This app uses |
|---|---|---|---|
| Drive times (traffic-aware) | Routes: Compute Route Matrix Pro | 5,000 elements | 10 per refresh → ~500 refreshes |
| Address lookup | Geocoding (Essentials) | 10,000 requests | 1 per refresh |
| Interactive map | Dynamic Maps (Essentials) | 10,000 loads | 1 per page open |
| Address suggestions | Autocomplete Session Usage + Place Details Essentials | Unlimited sessions + 10,000 details | 1 per address you pick |
| Traffic-colored route line | Routes: Compute Routes Enterprise | 1,000 requests | 1 per resort you tap (cached 5 min) |
Suggestions use a session token. All the keystrokes for one address count as a single session, which is free. Only the final pick counts toward the 10,000-a-month details allowance. The app also waits a quarter-second after you stop typing before asking Google.
Traffic-aware routing is billed as the Pro tier. Plain routing without traffic would be cheaper, but it can't show traffic delays, which is the point of this app. A few checks a day stays well within the free tier.
Vercel Hobby and GitHub are free for a project like this.
Change the resorts: Edit src/lib/resorts.ts. To get coordinates, right-click a spot in Google Maps and click the numbers to copy them. Keep the list at 25 or fewer. Every resort you add costs 1 more element per refresh.
Common problems:
| Symptom | Fix |
|---|---|
| Yellow "Demo data" banner | GOOGLE_MAPS_API_KEY isn't set. Check .env.local (local) or the Vercel env vars, then restart or redeploy. |
| "Geocoding failed (REQUEST_DENIED)" | Geocoding API isn't enabled, or the server key's API restrictions don't include it. |
| "...API has not been used in project..." / "PERMISSION_DENIED" | Routes API isn't enabled, or billing isn't linked. |
| No suggestions appear while typing | Places API (New) isn't enabled, or it isn't ticked on the browser key. The browser console shows "Address suggestions failed". |
| Map is grey with "This page can't load Google Maps correctly" | The browser key is missing Maps JavaScript API, or your URL isn't in its website list. Wait 5 minutes after editing. |
| Map works locally but not on Vercel | Add the https://<your-app>.vercel.app/* URL to the browser key's website list. |
| See server errors on Vercel | Vercel → Project → Logs |