Skip to content

Commit

Permalink
feat: learderboard page
Browse files Browse the repository at this point in the history
  • Loading branch information
Stormix committed Apr 11, 2023
1 parent c71610e commit 3fb39f7
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 11 deletions.
17 changes: 11 additions & 6 deletions src/components/molecules/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Link from 'next/link';
import type { FC } from 'react';
import toast from 'react-hot-toast';
import DarkModeSwitch from '../atoms/DarkModeSwitch';
import Logo from '../atoms/Logo';
import UserDropdown from '../atoms/UserDropdown';
Expand All @@ -12,15 +13,19 @@ const Header: FC = () => {
<Logo />
</div>
<div className="flex flex-grow items-center justify-center gap-8">
<Link className="text-sm font-semibold leading-6 text-gray-900" href={'archive'}>
Ideas Archive
</Link>
<Link className="text-sm font-semibold leading-6 text-gray-900" href={'/'}>
<Link className="text-sm font-semibold leading-6 text-gray-900" href={'/leaderboard'}>
Leaderboard
</Link>
<Link className="text-sm font-semibold leading-6 text-gray-900" href={'/'}>
Contact
<Link className="text-sm font-semibold leading-6 text-gray-900" href={'/archive'}>
Ideas Archive
</Link>
<a
className="text-sm font-semibold leading-6 text-gray-900"
href={'/'}
onClick={() => toast.success('TODO - Coming soon!')}
>
Contact
</a>
</div>
<div className=" flex items-center justify-end gap-4">
<UserDropdown />
Expand Down
12 changes: 10 additions & 2 deletions src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ const server = z.object({
OPENAI_KEY: z.string(),
SENTRY_DSN: z.string().optional(),
RECAPTCHA_SECRET_KEY: z.string(),
ENABLE_OPENAI: z.string().optional()
ENABLE_OPENAI: z.string().optional(),
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
TWITTER_ID: z.string(),
TWITTER_SECRET: z.string()
});

/**
Expand Down Expand Up @@ -55,7 +59,11 @@ const processEnv = {
NEXT_PUBLIC_RECAPTCHA_SITE_KEY: process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY,
RECAPTCHA_SECRET_KEY: process.env.RECAPTCHA_SECRET_KEY,
NEXT_PUBLIC_SITE_URL: process.env.NEXT_PUBLIC_SITE_URL,
ENABLE_OPENAI: process.env.ENABLE_OPENAI
ENABLE_OPENAI: process.env.ENABLE_OPENAI,
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
TWITTER_ID: process.env.TWITTER_ID,
TWITTER_SECRET: process.env.TWITTER_SECRET
};

// Don't touch the part below
Expand Down
66 changes: 66 additions & 0 deletions src/pages/leaderboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import Hero from '@/components/molecules/Hero';
import { APP_NAME } from '@/config/app';
import { api } from '@/utils/api';
import { ArrowTopRightOnSquareIcon, BookmarkIcon, StarIcon } from '@heroicons/react/24/outline';
import { type NextPage } from 'next';
import { NextSeo } from 'next-seo';
import Link from 'next/link';
const Leaderboard: NextPage = () => {
const { isLoading, data } = api.ideas.leaderboard.useQuery();

return (
<>
<NextSeo
title={`Ideas leaderboard - Generate Random Development Project Ideas}`}
description={`${APP_NAME} is a free tool that generates random development project ideas based on your preferences. Use it to kickstart your next hackathon project or find inspiration for your next side project.`}
/>
<Hero>
<h1 className="max-w-2xl text-4xl font-bold tracking-tight text-gray-900">Leaderboard</h1>
<p className="mb-8 mt-6 max-w-2xl text-lg leading-8 text-gray-600">
Top 100 ideas generated by users on the website
</p>
</Hero>
<div className="container mx-auto mb-12 grid grid-cols-1 gap-8">
{isLoading && <p>Loading...</p>}
{!isLoading && (
<table className="z-50 table-auto border border-transparent">
<thead>
<tr className="border-b">
<th className="px-4 py-2 text-start">Rank</th>
<th className="px-4 py-2 text-start">Idea</th>
<th className="px-4 py-2 text-start">
<StarIcon className="h-4 w-4 text-gray-500" />
</th>
<th className="px-4 py-2 text-start">
<BookmarkIcon className="h-4 w-4 text-gray-500" />
</th>
<th></th>
</tr>
</thead>
<tbody>
{data &&
data.ideas.map((idea, index) => {
return (
<tr key={idea.id}>
<td className="px-4 py-2">{index + 1}</td>
<td className="px-4 py-2">{idea.title}</td>
<td className="px-4 py-2">{idea.rating ?? ''}</td>
<td className="px-4 py-2">{idea.saveCount}</td>

<td className="px-4 py-2">
<Link href={`/idea/${idea.id}`} target="_blank">
<ArrowTopRightOnSquareIcon className="h-4 w-4 text-gray-500" />
</Link>
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
</>
);
};

export default Leaderboard;
46 changes: 46 additions & 0 deletions src/server/api/routers/ideas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,52 @@ export const ideasRouter = createTRPCRouter({
};
}),

leaderboard: publicProcedure.query(async ({ ctx }) => {
const ideas = await ctx.prisma.idea.findMany({
include: {
author: true,
components: {
include: {
component: true
}
},
ratings: true,
savedBy: true
}
});

// Sort by average rating and then by number of saves

ideas.sort((a, b) => {
const aRating = (a.ratings?.reduce((acc, rating) => acc + rating.rating, 0) ?? 0) / (a.ratings.length || 1);
const bRating = (b.ratings?.reduce((acc, rating) => acc + rating.rating, 0) ?? 0) / (b.ratings.length || 1);
const aSaves = a.savedBy.length;
const bSaves = b.savedBy.length;

if (aRating > bRating) {
return -1;
}

if (aRating < bRating) {
return 1;
}

if (aSaves > bSaves) {
return -1;
}

if (aSaves < bSaves) {
return 1;
}

return 0;
});

return {
ideas: ideas.slice(0, 100).map((idea) => ideaToIdeaDto(idea, false, false))
};
}),

getOne: publicProcedure

.input(
Expand Down
11 changes: 10 additions & 1 deletion src/server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { type GetServerSidePropsContext } from 'next';
import { getServerSession, type DefaultSession, type NextAuthOptions } from 'next-auth';
import DiscordProvider from 'next-auth/providers/discord';
import GitHubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
import TwitterProvider from 'next-auth/providers/twitter';
import CreditsService from './services/credits';

/**
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
* object and keep type safety.
Expand Down Expand Up @@ -122,6 +123,14 @@ export const authOptions: NextAuthOptions = {
DiscordProvider({
clientId: env.DISCORD_CLIENT_ID,
clientSecret: env.DISCORD_CLIENT_SECRET
}),
GoogleProvider({
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET
}),
TwitterProvider({
clientId: env.TWITTER_ID,
clientSecret: env.TWITTER_SECRET
})
]
};
Expand Down
4 changes: 3 additions & 1 deletion src/utils/ideas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const ideaToIdeaDto = (
component: Component;
})[];
ratings: Rating[];
savedBy?: User[];
},
saved?: boolean,
rated?: boolean
Expand All @@ -33,7 +34,8 @@ export const ideaToIdeaDto = (
},
rating: averageRating,
ratedByThisUser: rated ?? false,
ratingsCount: idea.ratings?.length ?? 0
ratingsCount: idea.ratings?.length ?? 0,
saveCount: idea.savedBy?.length ?? 0
};
};

Expand Down
3 changes: 2 additions & 1 deletion src/validation/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export const generateOutputSchema = z.object({
saved: z.boolean(),
rating: z.number().nullish(),
ratingsCount: z.number(),
ratedByThisUser: z.boolean()
ratedByThisUser: z.boolean(),
saveCount: z.number()
});

1 comment on commit 3fb39f7

@vercel
Copy link

@vercel vercel bot commented on 3fb39f7 Apr 11, 2023

Choose a reason for hiding this comment

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

Please sign in to comment.