diff --git a/src/components/molecules/Header.tsx b/src/components/molecules/Header.tsx
index bb3924a..d8ff757 100644
--- a/src/components/molecules/Header.tsx
+++ b/src/components/molecules/Header.tsx
@@ -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';
@@ -12,15 +13,19 @@ const Header: FC = () => {
diff --git a/src/env.mjs b/src/env.mjs
index 70abbc2..312895b 100644
--- a/src/env.mjs
+++ b/src/env.mjs
@@ -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()
});
/**
@@ -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
diff --git a/src/pages/leaderboard.tsx b/src/pages/leaderboard.tsx
new file mode 100644
index 0000000..9a93c1c
--- /dev/null
+++ b/src/pages/leaderboard.tsx
@@ -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 (
+ <>
+
+
+ Leaderboard
+
+ Top 100 ideas generated by users on the website
+
+
+
+ {isLoading &&
Loading...
}
+ {!isLoading && (
+
+
+
+ Rank
+ Idea
+
+
+
+
+
+
+
+
+
+
+ {data &&
+ data.ideas.map((idea, index) => {
+ return (
+
+ {index + 1}
+ {idea.title}
+ {idea.rating ?? ''}
+ {idea.saveCount}
+
+
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+ >
+ );
+};
+
+export default Leaderboard;
diff --git a/src/server/api/routers/ideas.ts b/src/server/api/routers/ideas.ts
index 90e725f..bb92346 100644
--- a/src/server/api/routers/ideas.ts
+++ b/src/server/api/routers/ideas.ts
@@ -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(
diff --git a/src/server/auth.ts b/src/server/auth.ts
index f2f2377..95196de 100644
--- a/src/server/auth.ts
+++ b/src/server/auth.ts
@@ -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.
@@ -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
})
]
};
diff --git a/src/utils/ideas.ts b/src/utils/ideas.ts
index 89cc2a5..650796d 100644
--- a/src/utils/ideas.ts
+++ b/src/utils/ideas.ts
@@ -8,6 +8,7 @@ export const ideaToIdeaDto = (
component: Component;
})[];
ratings: Rating[];
+ savedBy?: User[];
},
saved?: boolean,
rated?: boolean
@@ -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
};
};
diff --git a/src/validation/generate.ts b/src/validation/generate.ts
index 6e53f2d..f97b125 100644
--- a/src/validation/generate.ts
+++ b/src/validation/generate.ts
@@ -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()
});