Rate My Clip

Rocket League clip posting, rating, and Discord integration.

A website where Rocket League players upload clips from YouTube, tag them with categories and a rank, and let other players rate them out of 5 stars. There is a Discord bot that posts every new clip into a Discord server and replies under that post whenever someone rates the clip.

Rate My Clip preview

Built With

Next.jsReactPrismaPostgresAuth.jsDiscord APIYouTube Data APITailwind

Vibes usage

Vibes usage0%

Zero AI usage across this entire website.

Goals

The core objectives driving this projects development and design

1

Allow users to upload clips by pasting a YouTube URL. The site fetches the thumbnail from the YouTube Data API.

2

Allow users to rate clips out of 5 stars and leave a comment.

3

Tie every clip and profile to a Rocket League rank, from Bronze I to Supersonic Legend, with the option for an admin to override a clip's rank.

4

Hook the site up to Discord so a server can follow new uploads and ratings without anyone having to visit the website.

How a clip moves through the site

From signing in to getting rated, here is what happens when someone uploads a clip.

  1. 🔐
    01

    Sign in with Discord

    Auth.js v5 handles the OAuth flow. If it is a new user, the signIn callback creates the user and profile in Postgres.

  2. 📋
    02

    Paste a YouTube URL

    The form validates and cleans the URL, then fetches the thumbnail from the YouTube Data API.

  3. 🏷️
    03

    Pick categories, rank, and teammates

    The user selects one or more categories, optionally overrides the rank, and credits up to two featured teammates.

  4. 🤖
    04

    Bot posts to Discord

    After the clip is saved, the bot posts it to a Discord server and stores the message ID on the clip.

  5. 05

    Other users rate the clip

    A server action writes a 1 to 5 star rating with an optional comment.

  6. 🧮
    06

    Rating recalculates and bot replies

    The aggregate rating is recalculated, and the bot threads a reply under the original Discord post.

Key Features

🎬

Upload clips from YouTube with a title and tags

Rate clips out of 5 stars and leave a comment

💬

Discord login through OAuth

🤖

Discord bot that posts clips and replies with ratings

🏆

22 Rocket League rank badges

🏷️

Categories like Flip Reset, Air Dribble, Backboard, Redirect

🤝

Credit up to two featured teammates on a clip

📊

Per-user stats: total clips, average rating, top categories

⚙️

Admin panel for moderation and edits

Categories

Every clip can have one or more categories. They are a many-to-many in Postgres, so a clip can be both a flip reset and a backboard read.

#FlipReset#AirDribble#Ceiling#Backboard#Redirect#Pass#FreePlay#Ranked#Compilation#Luck#Weird

Ranks

Rocket League has 22 ranks, from Bronze I to Supersonic Legend. Every profile and clip is tied to one. An admin can override a clip's rank if needed.

Bronze I badge
01Bronze I
Bronze II badge
02Bronze II
Bronze III badge
03Bronze III
Silver I badge
04Silver I
Silver II badge
05Silver II
Silver III badge
06Silver III
Gold I badge
07Gold I
Gold II badge
08Gold II
Gold III badge
09Gold III
Platinum I badge
10Platinum I
Platinum II badge
11Platinum II
Platinum III badge
12Platinum III
Diamond I badge
13Diamond I
Diamond II badge
14Diamond II
Diamond III badge
15Diamond III
Champion I badge
16Champion I
Champion II badge
17Champion II
Champion III badge
18Champion III
Grand Champion I badge
19Grand Champion I
Grand Champion II badge
20Grand Champion II
Grand Champion III badge
21Grand Champion III
Supersonic Legend badge
22Supersonic Legend

Challenges & Solutions

Challenge: Relational Database

For the website to work, I needed a relational data structure. Users had clips, clips had comments and ratings, clips had categories, clips had a rank, and clips could credit teammates.

Solution

I used Prisma ORM with code-first migrations to define the relationships. Categories and featured users are many-to-many. Ratings are one-to-many under both users and clips. The rank is a foreign key on both clips and profiles.

Challenge: Authentication

I wanted users to sign in with Discord so the bot integration would feel natural and so the site would have access to a Discord account from the start.

Solution

I used Auth.js v5 (still in beta at the time) with the Discord provider. The signIn callback checks the database for an existing user with the same email before letting Auth.js create a new row, so signing in and out does not create duplicate users.

Challenge: Discord Integration

I wanted Discord to be more than just a login button. When someone uploads a clip, the bot should post it to a Discord server. When someone rates the clip, the bot should reply under that post.

Solution

I wrote a small Discord bot client as a singleton living next to the Prisma client. It exposes PostClipToDiscord, which runs after a clip is created and stores the resulting message ID on the clip, and ReplyRatingToClipMessage, which runs after a rating is submitted and uses that message ID to thread the reply.

Code Highlights

Discord OAuth

When a user signs in with Discord, this callback checks the database for an existing user with that email. If there is not one, it creates a new user and profile in one transaction.

1
async signIn({ user }) {
2
const existingUser = await getUserByEmailWithProfile(user.email ?? '')
3
const email = user.email as string
4
if (!existingUser) {
5
try {
6
await prisma.user.create({
7
data: {
8
email: email,
9
isExternal: true,
10
profile: {
11
create: {
12
name: user?.name as string,
13
photoUrl: user.image ?? '',
14
},
15
},
16
},
17
})
18
} catch (e) {
19
if (e instanceof Prisma.PrismaClientKnownRequestError) {
20
console.error(e.message)
21
return false
22
}
23
throw e
24
}
25
}
26
return true
27
},

Knowledge acquired

Key insights and knowledge gained throughout this project

💡

How to use OAuth 2.0 with a third-party provider (Discord) and Auth.js v5.

💡

How to use server components and server actions in Next.js.

💡

How to use Prisma ORM and code-first migrations.

💡

How to integrate with the YouTube Data API.

💡

How to write a small Discord bot that posts messages and replies to threads.

💡

The difference between Prisma's connect and set when updating relations. Connect adds a relation, set replaces all of them. I learned this after accidentally wiping a clip's tags.

Request a Demo

Send me a message if you want a walkthrough of Rate My Clip.

Want to see more?