BikeControl (SwiftControl)
Use Zwift Click, Zwift Play, Shimano Di2, or any Bluetooth controller with MyWhoosh, Rouvy, Zwift, and more. Free to try on iOS, Android, Windows, and macOS.
翻訳待ち
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A multi-club lacrosse team management app: rosters, game-day availability, lineup builder, stats tracking, activity history, and data export - with real-time updates between coaches.
This is a full rewrite of an earlier Google Sheets/Apps Script version, ported onto:
npm install
cp .env.example .env.local # then fill in the values below
npm run db:push # creates the schema in your Neon database
npm run dev
Open http://localhost:3000. You'll be sent to sign in, then to pick or create a club (a Clerk Organization) before you see any team data.
Create a project at neon.tech, copy its connection string into
DATABASE_URL in .env.local. Then run npm run db:push to create all tables (or
npm run db:generate + npm run db:migrate if you'd rather manage versioned migration files -
one migration is already checked in at drizzle/0000_*.sql).
Create an application at clerk.com.
.env.local.org:admin and org:member roles are used
as-is - org:admin is a club admin (can create teams, invite/assign coaches, edit anything
in the club); org:member is a coach who only sees teams they're explicitly assigned to.UserButton's
sign-out redirect is configured there, not in code, in this version of Clerk).Create a free app at pusher.com, fill in the four PUSHER_* /
NEXT_PUBLIC_PUSHER_* values. If you skip this, the app still works - coaches just need to
refresh to see each other's changes instead of it happening live.
Push this repo to GitHub and import it in Vercel, or run vercel. Set the same environment
variables from .env.local in the Vercel project settings. The Neon serverless driver and
Pusher both work fine from Vercel's Node.js runtime.
If you're migrating an existing club off the Sheets/Apps Script version:
GOOGLE_SERVICE_ACCOUNT_JSON (path to the key file), SHEETS_HUB_SPREADSHEET_ID,
IMPORT_TARGET_ORG_ID (the Clerk organization/club the data should land in - create it in
the app first via "select a club"), and IMPORT_TARGET_ADMIN_USER_ID (your Clerk user id -
recorded as the "creator"/"actor" of everything imported).npm run import:sheetsIt imports teams, rosters, games, availability, lineups, stat categories/values, and activity
history. Coaches are auto-assigned to their teams only if they're already members of the
target club in Clerk and CLERK_SECRET_KEY is set (it looks them up by email) - otherwise,
assign them from each team's Settings page afterward. The script only ever creates new teams,
so it's safe to point at a fresh/empty club to try it out first.
Every team's Settings page has two exports: a quick roster CSV, and a full .xlsx workbook
(Roster / Games / Availability / Lineup / Stats as separate sheets) - useful as a backup or for
handing data to someone outside the app.
There's no local users table. Identity and club-level role (org:admin vs org:member)
live entirely in Clerk. The one thing Clerk doesn't know about - which teams, within a club, a
given coach can see - is the team_coaches table (src/lib/db/schema.ts). See
src/lib/auth.ts for the three checks everything else is built on: requireOrgSession,
requireOrgAdmin, requireTeamAccess.
A player's stat cell for a given game is blacked out (not editable) whenever their
availability for that game is "Not Available" - computed live from game_players.status every
time the stats sheet loads, in getStatSheet (src/actions/stats.ts). It's never stored as a
flag, and changing someone's availability never deletes a stat value already recorded for
them.
Every server action that changes a team's data calls broadcastTeamUpdate(teamId, scope)
(src/lib/pusher-server.ts) after committing. Any screen that renders that team includes a
<TeamRealtime teamId scopes={[...]} /> component (src/components/team-realtime.tsx), which
subscribes to that team's Pusher channel and calls router.refresh() on a matching event - no
client state to merge, no polling.
Each team's channel is private-team-<teamId> - the private- prefix means Pusher won't hand
out a subscription until /api/pusher/auth (src/app/api/pusher/auth/route.ts) approves it,
which it only does for someone who actually has access to that team (same check as every Server
Action). Nothing sent over the channel is ever roster/lineup content anyway, just a "something
changed" ping - the client re-fetches the real data through the normal authenticated page
render - but this closes off even that ping to anyone who isn't supposed to see the team.
This is the beginning of the README. Read the rest at the original. Read the full README ↗