Skip to content

feat: implement onboarding flow & create league#198

Open
mbeacom wants to merge 2 commits intomainfrom
feat-onboarding-1
Open

feat: implement onboarding flow & create league#198
mbeacom wants to merge 2 commits intomainfrom
feat-onboarding-1

Conversation

@mbeacom
Copy link
Copy Markdown
Owner

@mbeacom mbeacom commented Mar 23, 2026

This pull request introduces a new onboarding flow for users without teams or leagues, streamlining the process of creating a team or league and improving the overall user experience. It also standardizes the way sports are selected across forms and includes several UI and validation enhancements.

Onboarding Experience Improvements:

  • Introduced a new OnboardingFlow component that guides users through creating either a team or a league, replacing the previous static welcome screen in the dashboard. This provides a more interactive and user-friendly onboarding process. [1] [2] [3]
  • Added a dedicated CreateLeagueOnboardingForm for league creation, with client-side validation and improved error handling.

Form and Validation Enhancements:

  • Standardized the sport selection field across CreateTeamForm, CreateLeagueTeamForm, and CreateLeagueOnboardingForm:
    • The sport dropdown now lists featured sports first, followed by all other sports, and uses consistent labels. The default selection is set to "HOCKEY". [1] [2] [3] [4] [5] [6]
  • Improved field-level validation and error handling in CreateTeamForm, including more precise error clearing and helper text updates. [1] [2] [3]

UI and Styling Updates:

  • Adjusted the color of the logo in the Logo component to use inherit instead of always primary.main, except in the footer.
  • Updated the dashboard shell card styling for better visual consistency.

Backend/Data Handling:

  • Refined the migrateTeamToLeague action to select only necessary fields when returning the new league admin, improving performance and data safety.

@mbeacom mbeacom self-assigned this Mar 23, 2026
Copilot AI review requested due to automatic review settings March 23, 2026 03:37
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openleague Error Error Mar 23, 2026 3:50am

Request Review

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the initial user experience by introducing a dynamic onboarding flow for new users, guiding them through the creation of their first team or league. It also brings consistency to sport selection across various forms and improves client-side validation, making the application more robust and user-friendly. Backend data handling for league migration and various data access patterns have been optimized for efficiency and data integrity.

Highlights

  • Enhanced Onboarding Experience: Implemented a new interactive onboarding flow for users without existing teams or leagues, replacing the previous static welcome screen with a dynamic OnboardingFlow component.
  • New League Creation Form: Introduced a dedicated CreateLeagueOnboardingForm component, complete with client-side validation and improved error handling, to streamline the league creation process.
  • Standardized Sport Selection: Standardized sport selection fields across CreateTeamForm, CreateLeagueTeamForm, and CreateLeagueOnboardingForm to display featured sports first, use consistent labels, and default to 'HOCKEY'.
  • Improved Form Validation: Enhanced field-level validation and error handling within CreateTeamForm for more precise feedback and clearer error clearing.
  • UI Consistency Updates: Adjusted the Logo component to use inherit for its color (except in the footer) for better theme integration and updated dashboard shell card styling.
  • Backend Data Optimization: Refined the migrateTeamToLeague action and several teamMember queries to select only necessary fields, improving data safety and performance.
  • Prisma Schema and Validation Update: Updated the Prisma schema to include a Sport enum and modified Team and League models to use this enum, along with updating various Zod validation schemas to leverage the new sportSchema.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a great step forward in improving the user onboarding experience and strengthening data integrity. The introduction of the OnboardingFlow component provides a much cleaner and more guided process for new users. The transition of the sport field to a database-level enum, along with the corresponding updates to validation schemas and forms, is a significant improvement for consistency and correctness. The performance optimizations in the backend actions using select instead of include are also excellent and show great attention to detail.

I've left a few comments with suggestions for improving code clarity, reusability, and accessibility. These are mostly minor points in what is overall a very solid set of changes.

Comment on lines +151 to +158
{orderedSports.map((sport, index) => [
index === FEATURED_SPORTS.length && (
<MenuItem key="divider" disabled divider sx={{ opacity: 0 }} />
),
<MenuItem key={sport} value={sport}>
{SPORT_LABELS[sport]}
</MenuItem>,
])}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for rendering the sports dropdown with a divider is a bit complex inside the JSX. Returning an array [element, element] from .map() can be hard to read. A more declarative approach would be to render the featured sports and other sports in separate blocks with a proper <Divider /> component in between. This is more readable and semantic than using a disabled MenuItem with opacity: 0 as a separator. You would need to import Divider from @mui/material.

Additionally, this dropdown logic is now duplicated in CreateTeamForm and CreateLeagueTeamForm. Consider extracting it into a reusable SportSelectOptions component to improve maintainability.

        {FEATURED_SPORTS.map((sport) => (
          <MenuItem key={sport} value={sport}>
            {SPORT_LABELS[sport]}
          </MenuItem>
        ))}
        <Divider />
        {SPORTS.filter((s) => !FEATURED_SPORTS.includes(s)).map((sport) => (
          <MenuItem key={sport} value={sport}>
            {SPORT_LABELS[sport]}
          </MenuItem>
        ))}

Comment on lines +196 to +201
disabled={
isSubmitting ||
!formData.name ||
!formData.sport ||
!formData.contactEmail
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The submit button's disabled logic doesn't account for existing validation errors. After a failed submission, fieldErrors will be populated, but the button could become enabled again if the user makes a trivial change to a field. To prevent submission of a form with known errors, you should also disable the button if fieldErrors contains any errors.

        disabled={
          isSubmitting ||
          !formData.name ||
          !formData.sport ||
          !formData.contactEmail ||
          Object.values(fieldErrors).some((error) => !!error)
        }

Comment on lines +187 to +205
<Typography
component="button"
onClick={onBack}
sx={{
background: "none",
border: "none",
cursor: "pointer",
color: "text.secondary",
fontSize: "0.875rem",
mb: 3,
display: "flex",
alignItems: "center",
gap: 0.5,
p: 0,
"&:hover": { color: "text.primary" },
}}
>
← Back
</Typography>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For better accessibility and semantic correctness, it's recommended to use the MUI <Button> component instead of <Typography component="button">. The <Button> component provides proper keyboard navigation, focus handling, and screen reader support out of the box. You can use variant="text" to achieve a similar visual appearance. You will need to import Button from @mui/material.

        <Button
          onClick={onBack}
          sx={{
            background: "none",
            border: "none",
            color: "text.secondary",
            fontSize: "0.875rem",
            mb: 3,
            display: "flex",
            alignItems: "center",
            gap: 0.5,
            p: 0,
            textTransform: "none",
            "&:hover": { color: "text.primary", backgroundColor: "transparent" },
          }}
        >
          ← Back
        </Button>


// Validate individual field on blur
if (name === 'name' || name === 'sport' || name === 'season') {
if (name === "name" || name === "sport" || name === "season") {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since the onBlur handler is no longer attached to the 'sport' field (which is now a select dropdown), the check for name === 'sport' inside handleBlur is redundant and can be removed for clarity.

Suggested change
if (name === "name" || name === "sport" || name === "season") {
if (name === "name" || name === "season") {

Comment on lines 109 to 111
name: sanitizedStringWithMin(1, 100).refine(val => val.length > 0, "Team name is required"),
sport: sanitizedStringWithMin(1, 50).refine(val => val.length > 0, "Sport is required"),
sport: sportSchema,
season: sanitizedStringWithMin(1, 50).refine(val => val.length > 0, "Season is required"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The .refine() calls here are redundant for validation because sanitizedStringWithMin(1, ...) already enforces a minimum length. To provide a custom error message for the minimum length check, it's more idiomatic to chain .min() with a message after getting the base sanitized string schema using the sanitizedString helper. This avoids the redundant check and makes the intent clearer.

Suggested change
name: sanitizedStringWithMin(1, 100).refine(val => val.length > 0, "Team name is required"),
sport: sanitizedStringWithMin(1, 50).refine(val => val.length > 0, "Sport is required"),
sport: sportSchema,
season: sanitizedStringWithMin(1, 50).refine(val => val.length > 0, "Season is required"),
name: sanitizedString(100).min(1, "Team name is required"),
sport: sportSchema,
season: sanitizedString(50).min(1, "Season is required"),

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an interactive onboarding experience for new users (no teams/leagues) and standardizes sport selection/validation across team & league creation flows, while tightening Prisma query selection in several actions for improved performance/safety.

Changes:

  • Replaced the dashboard’s empty-state welcome screen with a new OnboardingFlow that routes users to team or league creation.
  • Standardized sport input across forms via a shared Zod enum + label mapping and switched Prisma sport fields to a Sport enum with defaults.
  • Reduced over-fetching in multiple Prisma queries by switching include to targeted select.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
prisma/schema.prisma Introduces Sport enum; updates Team.sport and League.sport to use it with defaults.
lib/utils/validation.ts Adds SPORTS/SPORT_LABELS/FEATURED_SPORTS and sportSchema; applies to team/league schemas.
lib/utils/league-mode.ts Narrows membership checks to select: { id: true }.
lib/actions/venues.ts Narrows membership queries to select: { role: true }.
lib/actions/team-context.ts Replaces include with explicit select for team context queries.
lib/actions/league.ts Narrows migrateTeamToLeague/admin verification queries with explicit select.
components/ui/Logo.tsx Uses inherit color outside footer for better theming control.
components/features/team/CreateTeamForm.tsx Converts sport field to a standardized select with featured-first ordering and improved validation UX.
components/features/team/CreateLeagueTeamForm.tsx Aligns sport selection UX with shared sport constants/labels.
components/features/onboarding/OnboardingFlow.tsx New onboarding intent UI (team vs league) with back navigation wrapper.
components/features/onboarding/CreateLeagueOnboardingForm.tsx New league-creation form used during onboarding with client-side validation.
components/features/dashboard/DashboardShell.tsx Tweaks logo styling in desktop sidebar.
app/(dashboard)/dashboard/page.tsx Uses OnboardingFlow when the user has no teams/leagues.

Comment on lines +157 to +165
{orderedSports.map((sport, index) => [
// Divider after featured sports
index === FEATURED_SPORTS.length && (
<MenuItem key="divider" disabled divider sx={{ opacity: 0 }} />
),
<MenuItem key={sport} value={sport}>
{SPORT_LABELS[sport]}
</MenuItem>,
])}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “divider” menu item is styled with opacity: 0, which also hides the divider line and creates an invisible option in the dropdown. Prefer using Divider/ListSubheader (supported by MUI Select) or render a visible separator without inserting an invisible MenuItem.

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +158
{orderedSports.map((sport, index) => [
index === FEATURED_SPORTS.length && (
<MenuItem key="divider" disabled divider sx={{ opacity: 0 }} />
),
<MenuItem key={sport} value={sport}>
{SPORT_LABELS[sport]}
</MenuItem>,
])}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “divider” menu item is styled with opacity: 0, which hides the divider and inserts an invisible option into the select menu. Consider using Divider/ListSubheader instead (or a visible separator) to avoid confusing keyboard/screen-reader navigation.

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +195
{([...FEATURED_SPORTS, ...SPORTS.filter(s => !FEATURED_SPORTS.includes(s))] as SportValue[]).map((sport, index) => [
index === FEATURED_SPORTS.length && (
<MenuItem key="divider" disabled divider sx={{ opacity: 0 }} />
),
<MenuItem key={sport} value={sport}>
{SPORT_LABELS[sport]}
</MenuItem>,
])}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dropdown inserts an invisible MenuItem (opacity 0) as a divider, which hides the separator and can create a confusing blank option for assistive tech. Prefer Divider/ListSubheader (or another visible grouping approach) instead.

Copilot uses AI. Check for mistakes.
Comment on lines 78 to 83
// Team model
model Team {
id String @id @default(cuid())
name String
sport String
sport Sport @default(HOCKEY)
season String
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing Team.sport and League.sport from String to the new Sport enum will require a Prisma migration (including handling/mapping any existing rows whose sport values aren’t one of the enum variants). This PR updates the schema but doesn’t add a corresponding migration, which will break deployments that run prisma migrate deploy against an existing database.

Copilot uses AI. Check for mistakes.
VOLLEYBALL: "Volleyball",
OTHER: "Other",
};

Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that sports are stored as enum values (e.g., HOCKEY), rendering team.sport / league.sport directly will show uppercase values across the UI. Consider exporting a small formatter (e.g., based on SPORT_LABELS) and updating sport displays to use it so user-facing chips/labels remain readable.

Suggested change
/**
* Format a sport enum value into a user-friendly label.
*
* Use this for all user-facing sport chips/labels instead of rendering
* the raw enum (e.g. "HOCKEY") directly.
*/
export function formatSport(sport: SportValue | null | undefined): string {
if (!sport) {
return "Unknown";
}
return SPORT_LABELS[sport] ?? "Unknown";
}

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +131
boxShadow: "0 0 0 2px",
boxShadowColor: "primary.light",
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

boxShadowColor isn’t a valid CSS property, so this hover style won’t apply as intended. Set the shadow color by including it directly in the boxShadow value (or use a theme callback to build the shadow string).

Suggested change
boxShadow: "0 0 0 2px",
boxShadowColor: "primary.light",
boxShadow: (theme) => `0 0 0 2px ${theme.palette.primary.light}`,

Copilot uses AI. Check for mistakes.
Comment on lines 314 to 320
export const addTeamToLeagueSchema = z.object({
leagueId: z.string().cuid("Invalid league ID format"),
name: sanitizedStringWithMin(1, 100),
sport: sanitizedStringWithMin(1, 50),
sport: sportSchema,
season: sanitizedStringWithMin(1, 50),
divisionId: z.string().cuid("Invalid division ID format").optional(),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The getLeagueTeamsSchema uses z.string() for the sport field instead of the enum-based sportSchema, allowing invalid sport values to cause runtime errors in the database query.
Severity: MEDIUM

Suggested Fix

In lib/utils/validation.ts, update the getLeagueTeamsSchema to use the correct enum-based validation for the sport field. Change sport: z.string().optional() to sport: sportSchema.optional() to ensure type safety.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: lib/utils/validation.ts#L314-L320

Potential issue: The `getLeagueTeamsSchema` validation schema incorrectly defines the
`sport` field as `z.string().optional()` instead of the more specific
`sportSchema.optional()`, which is based on the `SPORTS` enum. This allows an API
request with an invalid sport string to pass validation. When this invalid value is used
in the `prisma.team.findMany` query within the `getLeagueTeamsPaginated` function, it
will cause Prisma or the database to throw a runtime error because the value does not
conform to the expected `Sport` enum type. This leads to an unexpected API failure
instead of a proper validation error.

Copy link
Copy Markdown
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional Suggestion:

TypeScript build error due to type incompatibility: sport field typed as string but Prisma expects Sport enum type

Fix on Vercel

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants