Giter Club home page Giter Club logo

kyoso's Introduction

Kyoso

Repository for the Kyoso website. This README is WIP. If you want to contribute read CONTRIBUTING.md.

kyoso's People

Contributors

l-mario564 avatar tttaevas avatar artemosuskyi avatar entropy-10 avatar dependabot[bot] avatar vadymsushkovdev avatar

Stargazers

Bruno Gomes avatar

Watchers

Mune avatar

Forkers

kyosotm

kyoso's Issues

Design manage form component

Depends on #58.

Provide a UI where the user can manage any kind of form.

  • Any conditions mentioned in #58 must bet met here and communicated to the user.
  • If the user desires to delete the form, prompt them for confirmation before deleting.
  • The user should have the option to either set a date for closing the form or immediately open/close it.
  • The description, thank you message, and closed message can be written in markdown, so the UI must provide markdown shortcuts.
  • The UI for managing fields should be relatively similar to Google Forms (again, following any requirements outlined in #58).
  • The toggle for anonymousResponses should be unavailable (hidden) if it's a tournament form for staff registration.
  • The select for target should be unavailable (hidden) if it's a tournament form for staff registration.

Create API route that deletes sessions older than 7 days

Create an API endpoint at api/crons/delete_old_sessions and via a DELETE request, the sessions that were last active more than 7 days ago will be deleted from the database. The only condition is that the Authorization header must equal to Bearer ${env.CRON_SECRET} (CRON_SECRET needs to be added to .env.example right below JWT_SECRET).

This route will run via a cron job I've yet to implement/host (this last bit is something I'll do).

Design user profile page

Why?

Users must be able to see the info about other users.

How?

The page will be located at /user/[user_id] where user_id is the ID of the user. Since it's common that some users may know someone via their osu! ID, we can also add a users/osu/[osu_user_id] route that simply redirects to the former.

If the user is currently banned, the page must return a 404 response.

The page must include the following data:

  • The user's ID, date registered, are they admin, are they an approved host, are they the owner of Kyoso.
  • Their osu! and Discord ID and username (links to the respective profiles can be used instead of boringly displaying long user IDs).
  • Their global osu! standard rank, are they restricted and the country listed on their osu! profile.
  • Tournament badges they've won (if any) displayed as images (like in their osu! profile).
  • Previous bans on Kyoso (if any) with each ban detailing when it was issued at, when it was lifted (if it applies), when it was revoked (if it applies), the ban reason, the revoke reason (if it applies).

If you want a reference as to how all this can be displayed, you can check the manage users admin page and look up a user to see how the user's data is displayed there.

To get all this data, we need to get it from the database during SSR and then have it be displayed to the user.

Display notifications

Why?

The user will receive notifications upon certain actions, such as being accepted/rejected into a tournament's staff team, having their join team request be reject/accepted, that a mappool, schedule or stats for a round's tournament has released, etc.

How?

There's already a table that stores notifications and another table that serves the purpose of linking a notification with a user. I previously created a getNotifications function get these notifications during server-side rendering. The function hasn't been tested though.

Assuming the function works as intended, then the only thing left is basically displaying these notifications to the user by having a bell icon next to their profile pic, upon clicking it, they'll be shown a menu with the 30 last notifications. The user must be able to mark these as read.

If they wish to see all notifications, a button can be displayed in the same menu, said button can either open large modal (or maybe we can implement Skeleton's drawer component) that displays all notifications being paginated, showing 30 results per page, sorted from newest to oldest. In this component, the user also must be able to filter by unread notifications and have a button that marks all unread as read.

Design view form responses component

Provide a UI where the user see a form's responses.

The user should be able to toggle between table view and card view.

In the table view, the responses should be displayed in a table, with each column having a maximum width, with some way to be able to view the full cell's input (maybe by clicking the cell, or a small button at the right of the cell). If no value was provided, display something that lets the user know there was no response.

In the card view, the responses should be displayed in a column direction flexbox and each response should be in a card where the all questions are present with the response below, and if no response was given, communicate this to the user.

Regardless of view, the responses should be paginated if there's more than 50 responses. The user should also be able to toggle the fields they want/don't want to see and save these options to local storage.

Change the data type of some `varchar` and `text` columns to be `citext`

Why?

citext is a Postgres extension that stores strings in a similar fashion to text but when comparing values, it's case insensitive. This could be useful on some fields for either searching capabilities or to avoid strings that only have one letter changed.

For searching capabilities, this would make it so for example, searching for mario564 will make it so the user Mario564 pops up, instead of having to search for Mario564. This is already possible using Postgres' ilike operator but I think it would simplify things a little bit more.

The more important case is the second one. Putting another example, right now, there's a unique constraint on the name field on the tournament table, which means there can't be two tournaments named osu! World Cup 2023, but if someone were to create one named osu! world cup 2023 then it would be allowed since the casing for W and C are different, but using the citext data type makes it so the latter string violates the unique constraint as citext checks a string's uniqueness case insensitively.

How?

We'd need to create a custom migration to add the extension to the database and then create a custom data type with Drizzle to maintain typesafety.

The affected tables and columns would be:

  • OsuUser: username.
  • DiscordUser: username.
  • Tournament: name.
  • Round: name.
  • StaffRole: name.

Citext Postgres docs: https://www.postgresql.org/docs/current/citext.html
Drizzle custom types docs: https://orm.drizzle.team/docs/custom-types

Make staff role tRPC procedures

Why?

The tournament host (and anyone with the necessary permissions) must be able to assign and revoke specific permissions from staff team members via roles.

How?

The following procedures must be created:

Create staff role
Insert a record into the StaffRole table, the input of the procedure containing the values for name and tournamentId. The value for order must be one greater than the amount of staff roles in that tournament (can be achieved using an insert statement with a with clause). The rest are defaults.

The unique constraint uni_staff_role_name_tournament_id violation must be caught for the frontend to display as an error.

Update staff role
Update the StaffRole record by StaffRole.id, the input of the procedure containing the values for name, color and permissions.

The unique constraint uni_staff_role_name_tournament_id violation must be caught for the frontend to display as an error.

Swap staff role order
The order field dictates the order in which the staff roles are displayed within the tournament, being set by the host or anyone else with the permissions to manage staff roles (for example: 1- host -> 2- admin -> 3- referees -> n- ...).

The procedure must take as input the StaffRole.id of the two roles to swap the orders of and execute an update statement that does updates the order field values accordingly, using only one database query. Example:

1- Referee
2- Mappooler
3- Admin

Swap referee with admin (since admins are greater in importance, usually):
1- Admin
2- Mappooler
3- Referee

Delete staff role
Delete the StaffRole record by StaffRole.id.

You must also update the order value for whatever role had its order value greater than the role that will be deleted. Example:

1- Host
2- Co-host
3- Admin
4- Referee

If I want to delete the Co-host role, then the order for Admin and Referee must be updated too:
1- Host
2- Admin
3- Referee

Otherwise you end up with this:
1- Host
3- Admin
4- Referee

Create form procedures

Why?

Admins must be able to create forms for public forms and tournament staff must be able to create forms for any tournament use they may see fit.

How?

For each procedure:

  • Apply rate limit middleware.
  • Check the db/forms.ts file. You can find more details about input validation there.
  • Check lib/schemas.ts for plenty of already written schemas for some of these procedures' inputs.

Create form

Insert a record into the Form table.

Input: anonymousResponses, title.

Conditions

  • The user must be an admin.

Create tournament form

Insert a record into the TournamentForm table.

Input: anonymousResponses, title, type, target, tournamentId.

Conditions

  • The staff member should have the permissions: 'host', 'debug', 'manage_tournament'.
  • The tournament hasn't concluded nor has it been deleted.
  • If TournamentForm.type is staff_registration, the tournament's staff regs. close date must be null or in the future.
  • If TournamentForm.type is staff_registration, set target to public (do not throw an error).
  • If TournamentForm.type is staff_registration, set anonymousResponses to false (do not throw an error).
  • The tournament can only have up to 5 forms of type general and 5 forms of type staff_registration (forms are soft deleted, so make sure soft deleted forms aren't included in the count).

Update form

Update the Form and/or TournamentForm record by Form.id.

Input: public, anonymousResponses, closeAt, title, description, thanksMessage, closedMessage, fields, target, tournamentId. (The last one must be defined if a tournament form is being updated).

Conditions

  • If the form is for a tournament, the staff member should have the permissions: 'host', 'debug', 'manage_tournament'.
  • If the form is for a tournament, the tournament must not have concluded nor has it been deleted.
  • If the form is for a tournament and TournamentForm.type is staff_registration, TournamentDates.staffRegsCloseAt must be null or in the future.
  • If the form is for a tournament and TournamentForm.type is staff_registration, target can't be set (instead of throwing an error, just set target to public).
  • If the form is public, public can't be set to false.
  • If the form is public, anonymousResponses can't be changed.
  • Use userFormFieldsChecks function to check if fields is valid.
  • If comparing the inputted fields and the already existing fields results in the discovery that a field has been deleted and if that field's ID is present in fieldsWithResponses, then soft delete by marking field's delete as true.

Delete form

Set the date of Form.deletedAt.

Input: formId, deletedAt, tournamentId. (The last one must be defined if a tournament form is being updated).

Conditions

  • The validation for deletedAt must be v.date([v.minValue(oldestDatePossible), v.maxValue(maxPossibleDate)]).
  • If the form is for a tournament, the tournament must have not concluded nor has it been deleted.
  • If the form is for a tournament and TournamentForm.type is staff_registration, TournamentDates.staffRegsCloseAt must be null or in the future.

Submit form response

Insert a record into the FormResponse table.

Input: fieldResponses, formId.

Conditions

  • A user can only submit one response to a form. (Create unique index).
  • Each key in fieldResponses must correspond to a field ID.
  • The value for submittedByUserId must be from session.userId.
  • Form.closeAt must be null or in the future.
  • If the form is for a tournament and TournamentForm.target is staff, the user must be a staff member (regardless of roles or permissions).
  • If the form is for a tournament and TournamentForm.target is players, the user must be a player in the tournament (regardless of tournament type).
  • If the form is for a tournament and TournamentForm.target is team_captains, the user must be a a team captain (applies to team and draft tournaments; in solo, there's no need to check for this).
  • If the form is for a tournament, the tournament must have not concluded nor has it been deleted.
  • If the form is for a tournament and TournamentForm.type is staff_registration, TournamentDates.staffRegsCloseAt must be null or in the future.
  • Update Form.fieldsWithResponses by putting all the field IDs in fieldResponses into an array and adding any new values to fieldsWithResponses (in other words, no duplicate IDs).

Replace `swapStaffRoleOrder` with `swapStaffRoleOrders`

Why?

If multiple swaps are requested in the frontend, it would be slow to call swapStaffRoleOrder over and over again for each swap, so instead, we'll rename this procedure swapStaffRoleOrders and accept multiple swaps in one single procedure call.

How?

As mentioned before, swapStaffRoleOrder will be renamed swapStaffRoleOrders. As for the implementation for this procedure, instead of the input being:

v.object({
  tournamentId: positiveIntSchema,
  source: positiveIntSchema,
  target: positiveIntSchema
})

Make it:

v.object({
  tournamentId: positiveIntSchema,
  swaps: v.array(
    v.object({
      source: positiveIntSchema,
      target: positiveIntSchema
    }),
    [v.maxLength(25)]
  )
})

Since we're dealing with an array now, we need to filter out any redundant swaps (Example: Swap staff role 1 and 2, and then 2 and 1, that's redundant).

All checks already implemented in swapStaffRoleOrder must remain (error if tournament deleted or concluded, don't allow swapping defaults, etc.)

Make tournament ruleset configurable

Database

  • Create a Postgres enum that has the following values: standard, taiko, catch, mania.
  • Add a ruleset field on the Tournament table that is not null and type of the previously created enum. In the Typescript schema, the field doesn't have a default value, but for the migration to execute without errors we'll make ruleset nullable, then update Tournament.ruleset and set it to standard and then make the column non-nullable.
  • Add a reulsetConfig field on the Tournament table that is not null and type of jsonb. The default will be an empty object, and (at least for now) the type should include keys when rulset is mania and that can be a number between 4 and 7.

tRPC

  • Update createTournament and updateTournament to include the ruleset field and its config (add it to mutationSchemas).
  • For updateTournament, the ruleset and its config can only be changed by the host and if the tournament isn't public yet.

Tournament settings page

  • Pass the ruleset field to the frontend.
  • Add a <Select /> field so the user can change the ruleset.
  • The field must be under the general settings.
  • Just like other fields in that same category, warn the user that they can't change the value once it's public and disable and display to non-hosts that they can't change that option.
  • If the user selects mania as a ruleset, a button should be enabled that will show a form with the configuration options (the keys).

Make tRPC procedure for user and tournament searching

Issue #23 and #37 has to be addressed before doing this issue.

Why?

Any user should be able to search for other users and any published tournaments.

How?

The input would be a string, and the procedure would return an array of users and an array of tournaments.

Searching for users

Each user must return: User.id, User.osuUserId, OsuUser.username. The user must be able to search users using the following fields:

Only if exact match ({field} = {search_str}):

  • User.id
  • User.osuUserId
  • User.discordUserId

Relative search ({field} ilike '%{search_str}%'):

  • OsuUser.username

Conditions:

  • Users must be ordered alphabetically by osu! username, ascending.
  • Banned users should not show up in the results.
  • 10 results limit.

Searching for tournaments

Each tournament must return: Tournament.url_slug, Tournament.name, Tournament.acronym, Tournament.logoMetadata, Tournament.bannerMetadata. The user must be able to search tournaments using the following fields:

Only if exact match ({field} = {search_str}):

  • Tournament.id

Relative search ({field} ilike '%{search_str}%'):

  • Tournament.name
  • Tournament.acronym
  • Tournament.url_slug

Conditions:

  • Tournaments must be ordered alphabetically by name, ascending.
  • Tournaments marked as deleted should not show up.
  • Tournaments that aren't public yet should not show up.
  • 10 results limit.

Other considerations

Apply database indexes if needed to improve query performance.

Create invite tRPC procedures

For each procedure:

  • Apply rate limit middleware.

All inputs listed are required/must be defined unless stated with ? appended at the end of the variable name.

Cancel invite

Update Invite.status to cancelled.

Input: inviteId.

Conditions

  • The invite's byUserId must be of the session user.
  • Invite status should be of status pending.

Reject invite

Update Invite.status to rejected.

Input: inviteId.

Conditions

  • The invite's toUserId must be of the session user.
  • Invite status should be of status pending.

Design reusable component for uploading and previewing files

Why?

A single component that handles any file upload would be very convenient to have instead of re-implementing a similar UI with small differences for handling different assets.

How?

When the user desires to upload a file, a modal should appear and must have an upload button. Once the file is uploaded (locally), or if a file is already in place: if it's an image or GIF, preview the image/GIF; if it's any other file, show a file icon that just lets the user know that a file has been uploaded; in both cases, display the original file name below.

If the user has uploaded a file, they must be able to upload that file to the Bunny storage zone and update the metadata in the DB making a request to the respective API route for that asset.

Project restructure

I don't think there's anything that's outright "wrong" about the current structure of the project, but have been thinking about making the following changes:

Lib

The lib folder keeps growing more and more, with files and many subfolders. Been thinking about moving the folders within it, out of it, so they'd live under src instead of lib. This change would make it so you don't have to open lib before opening something like db or trpc, which each contain many other files and subfolders.

How would this look like?

Current

.
└── src/
    ├── components
    └── lib/
        ├── classes
        ├── db
        ├── env
        ├── stores
        ├── trpc
        ├── constants.ts
        ├── jwt.ts
        ├── modal-registry.ts
        ├── paypal.ts
        ├── schemas.ts
        ├── server-utils.ts
        ├── types.ts
        └── utils.ts

Proposed

.
└── src/
    ├── components
    ├── classes
    ├── db
    ├── env
    ├── stores
    ├── trpc
    └── lib/
        ├── constants.ts
        ├── jwt.ts
        ├── modal-registry.ts
        ├── paypal.ts
        ├── schemas.ts
        ├── server-utils.ts
        ├── types.ts
        └── utils.ts

Tournament routes

As they are right now, the URL to certain pages related to tournaments can be very long, like for example, to go to the tournament's general settings page, you have to go to /tournament/[tournamentId]/manage/settings/general. An idea proposed by @Entropy-10 was to have a tournament have a unique slug and I think that's very much possible, and have also thought about other ways to shorten the URLs a bit.

A few examples:

Tournament landing page: /tournament/[tournamentId] -> /[tournamentSlug]
Tournament manage general settings: /tournament/[tournamentId]/manage/settings/general -> /[tournamentSlug]/general-settings

To keep things organized, we can also make use of SvelteKit's (group) routes which don't affect the URL.

How would this look like?

Note: For the sake of an example, I'll be using the "hypothetical" final structure, as in, the rough structure the project would follow if it were finished.

Current

routes/
└── tournament/
    └── [tournamentId]/
        ├── mappools
        ├── bracket
        ├── stats
        ├── rules
        ├── teams
        ├── free-players
        ├── players
        └── manage/
            ├── referee-settings/
            │   ├── general
            │   ├── mod-multipliers
            │   └── rules
            ├── settings/
            │   ├── dates
            │   ├── general
            │   ├── graphics
            │   ├── links
            │   ├── prizes
            │   └── stages
            ├── staff/
            │   ├── roles
            │   ├── team
            │   └── apps
            ├── pooling/
            │   ├── suggestions
            │   ├── mappools
            │   └── playtesting
            ├── referee/
            │   └── matches
            └── stats/
                ├── calculate
                └── leaderboards

Proposed

routes/
└── [tournamentSlug]/
    ├── (public)/
    │   ├── mappools
    │   ├── bracket
    │   ├── stats
    │   ├── rules
    │   ├── teams
    │   ├── free-players
    │   └── players
    ├── (manage-settings)/
    │   ├── dates
    │   ├── settings
    │   ├── graphics
    │   ├── links
    │   ├── prizes
    │   ├── stages
    │   ├── referee-settings
    │   ├── mod-multipliers
    │   └── rules
    ├── (manage-staff)/
    │   ├── staff-roles
    │   ├── staff-team
    │   └── staff-apps
    ├── (manage-pooling)/
    │   ├── pool-suggestions
    │   ├── mappools
    │   └── playtesting
    ├── (manage-referee)/
    │   └── manage-matches
    └── (manage-stats)/
        ├── calculate-stats
        └── leaderboards

Remember that routes within parenthesis don't affect the URL structure, so if we want to navigate to the matches management page in the browser, we don't have to navigate to /[tournamentSlug]/(manage-referee)/manage-matches but rather just to /[tournamentSlug]/manage-matches.

Add other gamemode/ruleset global ranks

Database

Add globalTaikoRank, globalCtbRank and globalManiaRank to OsuUser table (same type as globalStdRank).

Auth

During the osu! OAuth callback (api/auth/callback/osu), the previously mentioned values should be fetched and set accordingly.

Create testers procedures

Currently, we're using an environment variable that is an array of osu! user IDs to verify who's allowed to test. At first, it was a fairly very easy and straight forward thing to manage, but as the array grows with more testers, it's hard to keep track who's ID is who (in case they need to be removed) and adding someone means re-deploying the test build just for that.

Instead of using an environment variable, we'll store the testers in the Redis DB, not in the Postgres DB because this is only relevant in test builds and not used in prod or dev (although dev is sort of relevant so we can test this).

For each procedure:

  • Apply rate limit middleware.
  • Create a middleware that throws an error if the environment isn't dev or testing (using env.ENV) and apply it.
  • Anything set in Redis in these procedures must persist (can't expire).

Add tester

Using Redis, if the testers key isn't define, create it as an empty array. Append the following object:

{
  osuUserId: /* From input */,
  username: undefined
}

Input: osuUserId.

Conditions

  • The user must be an admin.

Remove tester

Remove the array element that contains the user with the inputted osu! user ID.

Input: osuUserId.

Conditions

  • The user must be an admin.

Misc. tasks

  • During the osu! OAuth callback (/api/auth/callback/osu), if the user already exists, after creating authSession object and if the environment is testing or dev: Update the array element that contains the respective osu! user ID and update it so it the username is defined. If the user doesn't exist, then do the update after authSession is created during the Discord OAuth callback (/api/auth/callback/discord). This is so we can identify who the osu! user ID belongs to.

  • Create interface Tester in lib/types.ts:

export interface Tester {
  osuUserId: number;
  username?: string;
}

Design tournament settings page

Why?

The tournament host (and anyone with the necessary permissions) must be able to update the tournament's settings.

How?

Using the tournament related procedures (lib/server/procedures.ts) the frontend has provide a UI to update different fields within the Tournament and TournamentDates tables.

Only users with host, debug or manage_tournament permissions are able to view this page.

The tournament related tRPC procedures must be carefully checked that all of the below conditions for each item is met and add any missing functionality.

Tournament

Only the host change the following:

  • name and acronym are fields that are able to be updated at any time without any major restrictions, just ask the user if they're sure about updating these settings once the tournament is public, notifying them that changing this will impact the tournament's discoverability.
  • urlSlug can be updated at any time, but the frontend must ask the user if they're sure if they want to update it, warning them about the fact that this will heavily affect how the tournament's URLs and URLs with the old slug won't work.

Only the host change the following + Can only be updated while the tournament isn't public, after it's public, it shouldn't be possible to update these value. The UI must communicate this to the user:

  • type.
  • teamSettings.
  • rankRange must provide a switch to toggle between open rank and rank restricted. If rank restricted, provide inputs for the user to type in the desired rank range, if open rank, disable these inputs.
  • bwsValues must provide a switch to toggle between using BWS or not. If BWS is used, provide inputs for the x, y and z values, if no BWS will be used, disable these inputs.

Users with host, debug or manage_tournament permissions can update the following:

  • rules will be updated in a separate page, but there must be a button somewhere linking the user to said page. The page will be at m/[tournament_slug]/rules.
  • links can be updated via a UI where users are able to create, modify and delete links.
  • refereeSettings can be updated via a UI where users are able to update this JSON fields' properties.

TournamentDates

The following conditions applies for every column in this table (except for other).

  • Only the host can update.
  • The user must be warned and asked if they're sure if they want to update one of these fields, but only ask if the date they inputted a date that is equal or less than 24 hours into the future.
  • The user is not able to update these fields if they input a date that is equal or less than 1 hour into the future.
  • The user can't update the respective column once the column's date is in the present or in the past.

The other column, similarly to the link column in Tournament can be updated at any date or time, as they're just for the sake of display, not being tied to important functionality. Can be updated via UI that allows the user to create, modify and delete dates. Users with host, debug or manage_tournament permissions can update this column.

"Danger Zone"

At the bottom of the page, there should be a "Danger Zone" section that only the host is able to see, in which the following functionality is present:

  • A button for deleting the tournament, asking the user if they're sure about the deletion, then, asking one last time while also asking the user to input the tournament's URL slug to confirm the delete on, after which, they'll be redirected to the dashboard after the tournament's deleted.
  • A button to grant another user the host role and remove it from themselves. There should be a UI where all the users with the manage_tournament permission are displayed, from which the user can select who to delegate host to, after which, asking for confirmation that they want to delegate host to that user and making them type the tournament's URL slug.

Absolutely none of these settings can be changed once the tournament is concluded, which includes the inability to delete the tournament and grant host to another staff member.

Design UI to manage tournament description and rules

In the tournament settings page, the user must be able to write the tournament's description and rules, both supporting markdown syntax.

A toolbar should be provided for standard markdown shortcuts (headings, bold, italic, quote, inline code, block of code, link) and also be able to provide the ability to set the color of the text to primary-500 or any 500 shade of the Tailwind colors, which could be done by wrapping the selected text like so: <span class="{color}">{text}</span> (and limit the classes that can be passed to the class attribute to avoid possibly issues with other classes).

The user must be able to toggle between the markdown being written and the markdown once it's parsed (a preview of what's being written).

Revise privacy policy

Why?

I've written a privacy policy for Kyoso in the past, but needs updating now that authentication is handled differently compared to before.

How?

Update the current privacy policy's page content, as well as verify that the styling makes it easy to read.

Make uploaded assets display consistently

Why?

The display of the uploaded tournament related assets should be consistent across the entire website. If the image doesn't meet the recommendations, it should zoom at the center of the image without stretching it.

How?

Review all current places where the tournament assets are used (dashboard page, manage assets page and upload image component) and make it consistent everywhere, the image displaying as specified above, which can be achieved by using a div and displaying the image as the div's background.

Add button to add respondent to staff team

Depends on issues #74 and #80.

While the user is viewing the form responses, and the form is for a tournament and of type staff_registration then there must be a column of buttons so the tournament host and admins can add the respondent to the staff team. Disable if they're already in the staff team and communicate to the user this fact.

Improve creation and handling of forms for better DX

Proposition to improve the DX of creating and handling forms.

Goals

  • Reduce verbosity.
  • Make forms more dynamic.
  • Still handle everything from type-safety, validation and submission.
  • Improve readability.

Proposed solution

We'll take the tournament/[tournamentId]/manage/staff/roles page as an example. Currently, the form to create a staff role works as follows:

// src/routes/tournament/[tournamentId]/manage/staff/roles/+page.svelte
<script lang="ts">
  import ... from ...;

  async function onCreateRole(defaultValue?: { name: string }) {
    form.create<{
      name: string;
    }>({
      defaultValue,
      title: 'Create Staff Role',
      fields: ({ field }) => [
        field('Role name', 'name', 'string', {
          validation: (z) => z.max(45)
        })
      ],
      onSubmit: ...
    })
  }
</script>

A lot of stuff is handled by client-side Typescript, which leads to a ton of abstraction and bloats up the bundle size as many of these things can be handled by using more native Svelte and HTML functionality. This also leads to a difficulty in implementing anything that may be a "one off" like putting a disclaimer or warning, as that would require fiddling further with Typescript and updating the Form component that works under the hood.

This approach is quite dated, with me creating this when starting the project, but I want to rework this into something a lot more simpler. My proposition is as follows, always taking the same page as an example:

// src/routes/tournament/[tournamentId]/manage/staff/roles/+page.svelte
<script lang="ts">
  import ... from ...;
  import CreateStaffRole from '$forms/CreateStaffRole'; // "$forms" points to `src/forms`
  
  async function onCreateRole(defaultValue?: { name: string }) {
    form.create(CreateStaffRole, {
      defaultValue,
      onSubmit: ...
    });
  }
</script>

// src/forms/CreateStaffRole
<script lang="ts">
  import { z } from 'zod';
  import { Form, Text } from '$components/form';
  import type { FormValue } from '$types';

  const schemas = {
    name: z.string().max(45)
  };

  let value: FormValue<typeof schemas> = {};
</script>

<Form {value}>
  <svelte:fragment slot="header">
    <h2>Create Staff Role</h2>
  </svelte:fragment>
  <Text label="Role name" name="name" schema={schemas.name} bind:value={value.name} />
</Form>

This implementation looks more verbose at first glance, but unlike the current implementation, there's not much to hide here, with the form's elements being a lot more explicit, making use of Svelte components instead of purely Typescript functions and objects. This also means that, for the sake of an example, if we want to add a description for the form, we could just add a p tag below h2 instead of having to first add the description property to the current form's store and then have it displayed in the global Form component while trying not to break the type system or anything else related to the form.

Create round tRPC procedures

Why?

The tournament host (and anyone with the necessary permissions) must be able to have a flexible system to cover different tournament formats.

How?

For each procedure:

  • Take the tournament's ID as input.
  • Apply rate limit middleware.
  • Error if the tournament has concluded or is marked as deleted.

If the create and update procedures end up being too complex to implement as a single procedure, each operation can be divided into three procedures, for each type of round (so instead of (createRound, you'd have createStandardRound, createQualifiersRound, createBattleRoyaleRound).

Create round

Insert a record into the Round table.

Input: name, type, targetStarRating, tournamentId and config.

Conditions

  • The value for order must be one greater than the amount of staff roles in that tournament.
  • The value for config field depends on the value of type. If type is groups, swiss, single_elim or double_elim then config must be of type StandardRoundConfig; if type is qualifiers then QualifierRoundConfig and if battle_royale then BattleRoyaleRoundConfig.
  • The unique constraint uni_round_name_tournament_id violation must be caught for the frontend to display as an error.
  • Error if there's already 12 rounds in the tournament.
  • Error if the player regs. are closed (after they've been open).

Update round

Update the Round record by Round.id.

Input: name, targetStarRating and playtestingPool, publishPool, publishSchedules, publishStats and config.

Conditions

  • The unique constraint uni_round_name_tournament_id violation must be caught for the frontend to display as an error.
  • As stated previously, config can be updated in this procedure, but the config must match the corresponding type (as defined in the "Create round" section above this one). This means that you can update the config of the round, but without changing it's type, so, a round that was created with a StandardRoundConfig can be overwritten by a config with the same type but it can't be updated to QualifierRoundConfig or BattleRoyaleRoundConfig.
  • config can't be updated once publishSchedules is equal to true.

Swap round order

The order field dictates the order in which the rounds are displayed within the tournament. The procedure must take as input the Round.id of the two roles to swap the orders of and execute a transaction that updates the order field values accordingly.

Conditions

  • Error if the player regs. are closed (after they've been open).

Delete round

Delete the Round record by Round.id. The value for order must also be updated for whatever round had its order value greater than the round that will be deleted.

Conditions

  • Error if the player regs. are closed (after they've been open).

(Dev environment) Add the ability for the dev to impersonate a user

Why?

Impersonating a user is helpful to test stuff in a local environment, where the only real user is you. This will be especially helpful to test permissions, team invitation system, and other stuff that would require one or multiple users to perform an action. This would of course only be for development, not something that should be possible in testing or prod (for obvious reasons).

How?

My suggestion would be a simple menu that can be triggered in development through some key combination, where the dev then inputs the user ID of the user they want to impersonate. In the backend, an API route (that should error when not in development) can be used to re-sign the current session cookie with the data of the user to impersonate.

Pseudo-code:

> Dev has an ID of 100, wants to impersonate user with ID 20.
> Dev pulls up menu with key combination and inputs "20" somewhere and submits.
> The backend re-signs the current session cookie so the cookie has data of the user of ID 20, which means they can do anything as if they were user of ID 20.
> Reloads the current page.

Theme customization

Why?

The respective staff members should be able to customize the theme of public-facing tournament pages (management pages still under consideration).

How?

Database

Create a nullable theme field on the Tournament table of type jsonb. The jsonb's type will be an object containing:

  • A key with a boolean value to refer to whether or not the theme is being used in public-facing pages or not (useTheme: boolean maybe).
  • Keys containing the colors for the primary and surface colors, both from 50 to 900 shades.
  • A key that stores the base font family and another one for the headings.
  • A key that stores the base font color and another one for the headings.

tRPC

Update the updateTournament procedure to include the theme field mentioned above with the following as input (theme is an object): The base primary color (primary-500), the base surface color (surface-500), base font family, base font color, headings font family, headings font color.

To avoid the possibility of unmatching colors being passed as values, the 500 shade of primary and surface are passed and from there, 50, 100, 200, 300, 400, 600, 700, 800 and 900 are generated. For a possible reference on how to do this see generatePalette function in Skeleton's repo: https://github.com/skeletonlabs/skeleton/blob/dev/sites/skeleton.dev/src/lib/layouts/DocsThemer/colors.ts.

The font families (for both base and headings) can simply be a union of string literals containing the name of the font families.

The font colors for base must be a number between 1 and 3 (1 = white, 2 = surface-100, 3 = primary-100) and the font colors for headings must be a number between 1 and 6 (1 = white, 2 = primary-100, 3 = primary-200, 4 = primary-300, 5 = primary-400, 6 = primary-500).

UI

The theme customization UI will be in the Assets page, page which will be renamed to Design (/design). The design would be roughly like the image below.

image
Mock-up of the design, not the final one

Provide some way of previewing the theme.

Make tournament management sidebar and header responsive

Why?

Currently, the header and sidebar components in the tournament management pages look good on desktop but needs some changes to make it work and look good on mobile devices (and smaller screens in general).

How?

Apply the necessary classes and create the necessary components to make the layout for the tournament managements pages (any route under /m/[tournament_id]) responsive.

(UI) Search users and tournaments

Depends on: #26.

Why?

Any user should be able to search for other users and any published tournaments.

How?

Have a search icon next to the nav bar links, once clicked, a modal will appear where users can input anything. That input must be passed to the tRPC procedure defined to search for users and tournaments (#26) every time the user presses the enter key when focusing on the input or when they hit the search button. The results must be displayed, the user having an anchor that links to their Kyoso profile (/user/[user_id]) and the tournament having one that links to its landing page (/t/[tournament_slug]).

Add Bunny's CDN service to the stack

To improve the serving of files from Bunny's storage, we'll make use of the CDN from the same provider.

Tasks:

  • Add BUNNY_CACHE_URL as an environment variable.
  • Update database schema. Tournament table should have logoMetadata and bannerMetadata removed since those will likely no longer be necessary, and instead replace them with hasLogo and hasBanner, both booleans, not null with default set to false.
  • Make a server helper function that purges the cache of a specific URL. (See: https://docs.bunny.net/reference/purgepublic_indexpost).
  • For the API routes handling the uploads for tournament logos and banners, PUT and DELETE should purge the cache of the requested image after the (after upload on PUT and after delete on DELETE).
  • Rewrite the GET functions for the previously mentioned API routes to make use of the CDN instead of the storage directly. As of writing, I'm not sure how we could block access when needed (like when a tournament isn't public yet), but this could point us in some direction: https://support.bunny.net/hc/en-us/articles/360016055099-How-to-sign-URLs-for-BunnyCDN-Token-Authentication#h_01EG8VVJ1NNBFR85YG6PN8QEK3
  • Update the asset management page to match how everything works with the new CDN functionality.
  • Document CDN setup in CONTRIBUTING.md.

Design landing page

This won't be the "true final" design for the landing page, this page will be designed with the fact that Kyoso is a WIP project in mind rather than something that's already in production.

The hero section should contain the text "osu! tournament management beyond spreadsheets" with a call to action join our Discord server. It should also include a brief paragraph describing what Kyoso is.

The section below that should explain the (future) benefits of Kyoso, primarily focused on admin sheet related things like support for solo, teams and draft tournaments, all gamemodes/rulesets, customizability (custom themes, various options, etc.) and security (briefly explain staff roles and members system).

The next section should include a table comparing Kyoso to a suite of spreadsheets (not any specific one, just tournament spreadsheet suites as a whole) while also explaining that you can expand upon Kyoso's functionality with it's API that can be used for custom websites and spreadsheets.

Mention that the project is WIP and that we're looking for more contributors, having a call to action to join as a tester and another one to join as a developer.

The final section should credit any current and past contributor.

Disable all input and button elements in management pages when tournament has concluded

Why?

When a tournament has concluded, nobody should be able to create, update or delete anything related to it, as there is no reason to come back and modify elements after the tournament is done. We're already handling this in the backend, but the UI doesn't reflect this aspect.

How?

I'm personally thinking of having a div element overlay the contents of the main tag and have a fixed message at the top of the page letting users know that they can't modify anything related to the tournament because it has concluded.

Write terms of use

Why?

Kyoso must make clear obvious boundaries when using its services.

How?

Would be preferred to have it written down in a Google Docs or Word document first and then create a page for it. Just like the privacy policy, we must verify that it is easy to read with its styling.

Design manage tournament assets page

Why?

Users must be able to manage (upload and delete) the assets (logo and banner) for the tournament they're staffing for.

How?

The page will be located at /m/[tournament_slug]/assets. The user must have one of the following permissions if they want to access this page, otherwise it will 401: host, debug, manage_tournament, manage_assets.

The page must display the currently uploaded graphics, if they're not set, show some sort of placeholder and let the user know that an image hasn't been set for that asset. If the asset is present, the user must be able to delete via button (must be prompted for confirmation before deleting) and also must show a button that shows some component they can use to upload an image (see #36).

Create staff member tRPC procedures

For each procedure:

  • Apply rate limit middleware.

(All inputs listed are required/must be defined unless stated with ? appended at the end of the variable name).

Search users

Get a list of users via an inputted string.

Input: query, tournamentId, admin?.

Conditions

  • Session user must be a staff member with the permissions host, manage_tournament.
  • Tournament is not deleted.
  • Tournament hasn't concluded.
  • No banned users.
  • The query string must be either equal to a user's ID, osu! user ID, Discord user ID or is like the osu! username (use pg_trgm for that last one).
  • If admin is true then only return admin users.
  • Limit of 20.
  • Order ascendingly by osu! username.

Return

  • User.id
  • User.osuUserId (or OsuUser.osuUserId)
  • OsuUser.username
  • Whether or not (boolean) the user is already a staff member in that tournament.

Send join staff invite

Insert a record into the Invite table and insert records into InviteWithRole table accordingly.

Input: toUserId, tournamentId, staffRoleIds.
Notes

  • reason = join_staff.
  • byUserId = ID of the session user.

Conditions

  • Session user must be a staff member with the permissions host, manage_tournament.
  • Tournament is not deleted.
  • Tournament hasn't concluded.
  • The invited user isn't banned.
  • staffRoleIds must have at least one element.
  • Each staff role in staffRoleIds must be a role within that tournament.
  • Host and debugger roles can't be assigned via this procedure.
  • Only the host can assign a role with manage_tournament permissions.

Send join staff as debugger invite

Insert a record into the Invite table and insert records into InviteWithRole table so the user can be assigned the debugger role.

Input: toUserId, tournamentId.
Notes

  • reason = join_staff.
  • byUserId = ID of the session user.

Conditions

  • Session user must be a staff member with the permissions host.
  • Tournament is not deleted.
  • Tournament hasn't concluded.
  • The invited user isn't banned.
  • The invited user is an admin.

Accept join staff invite

Insert a record into the StaffMember and StaffMemberRole tables and set Invite.status to accepted.

Input: inviteIds.
Notes

  • Get the tournament via Invite.tournamentId.
  • Get the staff role IDs via InviteWithRole.staffRoleId.
  • StaffMember.userId should be the same as Invite.toUserId.

Conditions

  • Invite.status should be pending.
  • Invite.toUserId should be the same as the session user's ID.
  • If a StaffMember record with the same user ID and tournament ID already exists and it's soft deleted (deletedAt is defined and past the current date) then instead of inserting, update that record by setting deletedAt to null and joinedStaffAt to now().

Accept staff registration response

Insert a record into the StaffMember and StaffMemberRole.

Input: formResponseId, staffRoleIds.
Notes

  • StaffMember.userId = FormResponse.submittedByUserId

Conditions

  • Session user must be a staff member with the permissions host, manage_tournament.
  • Tournament is not deleted.
  • Tournament hasn't concluded.
  • The user who submitted the response isn't banned.
  • staffRoleIds must have at least one element.
  • Each staff role in staffRoleIds must be a role within that tournament.
  • Host and debugger roles can't be assigned via this procedure.
  • Only the host can assign a role with manage_tournament permissions.
  • If a StaffMember record with the same user ID and tournament ID already exists and it's soft deleted (deletedAt is defined and past the current date) then instead of inserting, update that record by setting deletedAt to null and joinedStaffAt to now().

Update staff member roles

Insert and delete records of the StaffMemberRole tables accordingly.

Input: staffMemberId, staffRoleIds.

Conditions

  • Session user must be a staff member with the permissions host, manage_tournament.
  • Session user can't assign themselves any roles.
  • Staff members with manage_tournament permission can't remove another staff member's roles that has that permission.
  • Roles with manage_tournament permission can only be assigned and removed by the host.
  • Tournament is not deleted.
  • Tournament hasn't concluded.
  • Staff member is not deleted.
  • staffRoleIds must have at least one element.
  • Each staff role in staffRoleIds must be a role within that tournament.
  • Host and debugger roles can't be assigned or removed via this procedure.
  • Any staff members with the debugger role can't be assigned any other role.

Pseudo-code example

current roles: [6, 7, 8]
input:         [7, 8, 9, 10]
=
remove:        [6]
add:           [9, 10]

Delete staff member

Set StaffMember.deletedAt to now() and unlink any roles assigned to this staff member.

Input: staffMemberId.

Conditions

  • Session user must be a staff member with the permissions host, manage_tournament.
  • Staff members with roles with manage_tournament permission can't delete other staff members with that same permission.
  • Tournament is not deleted.
  • Tournament hasn't concluded.
  • The host can't be deleted.
  • Session user can't delete themselves.

Leave staff team

Set StaffMember.deletedAt to now() and unlink any roles assigned to this staff member, said staff member being the current session user.

Conditions

  • The host can't leave.
  • Tournament is not deleted.
  • Tournament hasn't concluded.

Make API route to upload a tournament's logo

Why?

Some tournaments have logos and corresponding staff members should have the ability to do set or remove it. There is already a field for this in the database schema.

How?

Would be almost the same implementation for uploading tournament banners, just changing some values that are specific to the tournament's logo instead of banner. To see the implementation of getting, uploading and deleting a tournament's banner, check src/routes/api/assets/tournament_banner/+server.ts.

Change how tournament dates are stored

Why?

Moving the dates found in the dates JSON field into their own columns will make it so we can index by those dates and also make querying a lot easier.

How?

Move the JSON properties into their own timestamp column:

  • publish -> publishedAt (published_at)
  • concludes -> concludesAt (concludes_at)
  • playerRegs.open -> playerRegsOpenAt (player_regs_open_at)
  • playerRegs.close -> playerRegsCloseAt (player_regs_close_at)
  • staffRegs.open -> staffRegsOpenAt (staff_regs_open_at)
  • staffRegs.close -> staffRegsCloseAt (staff_regs_close_at)

All fields are nullable, with no default value.

As for the other property, we can map it as such:

otherDates: jsonb('other_dates').notNull().$type</* This type can be defined and exported in types.ts */ {
  label: string;
  fromDate: number; // Timestamp in milliseconds
  toDate?: number; // Timestamp in milliseconds
}[]>().default([])

The current dates column would be removed.

Create the appropriate indexes, for this, keep in mind we might provide future functionality for the user to filter by published tournaments, concluded tournaments, tournaments that have its player regs. open and tournaments that have its staff regs. open. If the indexes are to heavy for the tournament table, we might consider moving these dates into some sort of tournament_dates table with a 1-to-1 relationship with tournament.

Design testers admin page

Depends on #67.

In /admin page, add the link to "Manage testers" (/admin/testers), and show only if the environment is testing or development.

In the manage testers page itself, provide a UI where the user can easily view all testers, with the option to delete them (using the respective tRPC procedure to perform the deletion). The user should also be able to add an osu! user ID (with the respective tRPC procedure).

Create footer component

Add the following optional (set a default link, can be anything) and public environment variables:
PUBLIC_BLOG_URL
PUBLIC_DOCS_URL
PUBLIC_SYSTEM_STATUS_URL
PUBLIC_DISCORD_SERVER_URL
PUBLIC_GITHUB_TEAM_URL
PUBLIC_X_URL

The footer component must contain:

  • The Kyosso logo (without the text) that links to the landing page.
  • Link to the system status page.
  • Links to sister sites (blog and docs).
  • Links to privacy policy and terms of service.

The footer must be visible in all pages.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.