尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证制作者的介绍仅有上面的一句话。 更多详情请阅读仓库 README。 仓库 README ↗
正在加载完整 README。
查看完整 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.
src/
app/ Routes (App Router)
sign-in/, sign-up/ Clerk auth pages
select-org/ Pick/create a club
(app)/ Authenticated shell (org switcher, admin link, sign-out)
admin/ Club admin: teams, invites, member list
teams/[teamId]/ Team-scoped: roster, games, stats, activity, settings
api/teams/[teamId]/export/ CSV/XLSX export route
actions/ Server actions (the mutation layer - one file per feature)
components/ Client components (forms, tables, realtime listener)
lib/
db/schema.ts Drizzle schema - the source of truth for the data model
auth.ts Authorization helpers
pusher-server.ts / pusher-client.ts / realtime.ts Real-time plumbing
csv.ts, export-data.ts CSV parsing/serialization and export data-gathering
scripts/import-from-sheets.ts One-time Google Sheets migration
npm test # vitest - pure-logic unit tests (CSV parsing, schema shape, utils)
npm run lint # eslint
npx tsc --noEmit # typecheck
npm run build # full production build
The unit tests deliberately cover logic that doesn't need a database (CSV parsing, the fixed
12-slot lineup layout, default stat categories). The server actions and routes are thin wrappers
around Drizzle queries and are best exercised against a real (or locally-run) Postgres/Neon
branch - there's no seed script yet, but npm run db:push against an empty Neon branch plus
manually clicking through "create a club → create a team → add players → schedule a game" is a
quick way to smoke-test end to end.
<select> dropdowns per slot rather than drag-and-drop.
@dnd-kit is already a dependency if you want to upgrade that later.