├── src ├── components │ ├── ClientWrapper │ │ ├── RatingDisplay.tsx │ │ ├── AnimeQuestionClient.tsx │ │ ├── AnimeCardClient.tsx │ │ ├── OverviewClient.tsx │ │ ├── AddCommentClient.tsx │ │ ├── LikePostClient.tsx │ │ ├── PostClient.tsx │ │ ├── ReviewDropdownClient.tsx │ │ ├── PollClient.tsx │ │ ├── PostDropdownClient.tsx │ │ ├── CommentClient.tsx │ │ ├── AnimeRatingClient.tsx │ │ ├── ReviewClient.tsx │ │ ├── CommunityClient.tsx │ │ ├── AnimeClient.tsx │ │ ├── LeaderboardClient.tsx │ │ ├── CreatePostClient.tsx │ │ ├── BrowseClient.tsx │ │ ├── CreateCommunityClient.tsx │ │ ├── CreatePollClient.tsx │ │ ├── UpdateCommunityClient.tsx │ │ ├── CreateAnimeClient.tsx │ │ ├── DragContainerClient.tsx │ │ ├── UserClient.tsx │ │ └── UpdateAnimeClient.tsx │ ├── ui │ │ ├── AspectRatio.tsx │ │ ├── Skeleton.tsx │ │ ├── Label.tsx │ │ ├── Separator.tsx │ │ ├── Input.tsx │ │ ├── Toaster.tsx │ │ ├── Textarea.tsx │ │ ├── Tooltip.tsx │ │ ├── Popover.tsx │ │ ├── DatePicker.tsx │ │ ├── Avatar.tsx │ │ ├── ErrorCard.tsx │ │ ├── ScrollArea.tsx │ │ ├── Button.tsx │ │ ├── Accordion.tsx │ │ └── Card.tsx │ ├── ThemeProvider.tsx │ ├── TypingMessage.tsx │ ├── ServerComponents │ │ ├── AdminAnimes.tsx │ │ ├── TopRated.tsx │ │ ├── RecentlyAdded.tsx │ │ ├── Comments.tsx │ │ ├── TopTenAnimeCheck.tsx │ │ ├── Reviews.tsx │ │ ├── MoreLikeThis.tsx │ │ ├── AnimeStatus.tsx │ │ ├── UserDesigned.tsx │ │ └── Overview.tsx │ ├── Custom-UI │ │ ├── CustomToolTip.tsx │ │ ├── CustomAlertBox.tsx │ │ ├── CustomAdminSheet.tsx │ │ ├── CustomReviewSheet.tsx │ │ └── CustomSheet.tsx │ ├── SkeletonLoaders │ │ ├── HeaderSkeleton.tsx │ │ ├── ComPostSkeleton.tsx │ │ ├── ReviewSkeleton.tsx │ │ ├── CommentSkeleton.tsx │ │ ├── PollCardsSkeleton.tsx │ │ ├── TableSkeleton.tsx │ │ ├── AnimeCardSkeleton.tsx │ │ ├── AdminAnimeSkeleton.tsx │ │ └── OverviewSkeleton.tsx │ ├── User │ │ └── UserAvatar.tsx │ ├── Footer │ │ ├── CommunityCategories.tsx │ │ └── ThemeToggle.tsx │ ├── Providers.tsx │ ├── Shell.tsx │ ├── DragDrop │ │ ├── DragItem.tsx │ │ ├── DragDropProvider.tsx │ │ └── DragContainer.tsx │ ├── Cards │ │ ├── CommentCard.tsx │ │ ├── PostCard.tsx │ │ ├── CommunityCard.tsx │ │ └── AnimeCard.tsx │ ├── Navbar │ │ ├── PollSidebar.tsx │ │ ├── SidebarNav.tsx │ │ └── CommunitySidebar.tsx │ ├── Header.tsx │ ├── AnimeWatchers.tsx │ ├── OverviewDisplay.tsx │ └── Forms │ │ └── AuthForm.tsx ├── app │ ├── opengraph-image.png │ ├── (auth) │ │ └── sign-in │ │ │ └── layout.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ ├── uploadthing │ │ │ ├── route.ts │ │ │ └── core.ts │ │ ├── anime │ │ │ ├── search │ │ │ │ └── route.ts │ │ │ ├── review │ │ │ │ ├── delete │ │ │ │ │ └── route.ts │ │ │ │ ├── like │ │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ │ ├── delete │ │ │ │ └── route.ts │ │ │ ├── rate │ │ │ │ └── route.ts │ │ │ └── watchlist │ │ │ │ └── delete │ │ │ │ └── route.ts │ │ ├── leaderboard │ │ │ └── route.ts │ │ ├── poll │ │ │ ├── delete │ │ │ │ └── route.ts │ │ │ └── vote │ │ │ │ ├── undo │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ ├── community │ │ │ └── delete │ │ │ │ └── route.ts │ │ ├── post │ │ │ ├── delete │ │ │ │ └── route.ts │ │ │ ├── like │ │ │ │ └── route.ts │ │ │ └── comment │ │ │ │ └── route.ts │ │ └── user │ │ │ └── route.ts │ ├── robots.ts │ ├── (site) │ │ ├── anime │ │ │ ├── not-found.tsx │ │ │ └── loading.tsx │ │ ├── layout.tsx │ │ ├── admin │ │ │ ├── anime │ │ │ │ ├── [anime] │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── new │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── users │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── community │ │ │ ├── [category] │ │ │ │ ├── not-found.tsx │ │ │ │ ├── [communityId] │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── edit │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── post │ │ │ │ │ │ ├── [postId] │ │ │ │ │ │ └── loading.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── loading.tsx │ │ │ ├── page.tsx │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ ├── poll │ │ │ ├── loading.tsx │ │ │ ├── results │ │ │ │ └── page.tsx │ │ │ ├── create │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ ├── leaderboard │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── browse │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── watchlist │ │ │ └── loading.tsx │ ├── not-found.tsx │ ├── icon.tsx │ ├── sitemap.ts │ ├── styles │ │ └── globals.css │ └── layout.tsx ├── lib │ ├── validators │ │ ├── user.ts │ │ ├── like.ts │ │ ├── poll.ts │ │ └── community.ts │ ├── uploadthing.ts │ └── db.ts ├── hooks │ ├── use-debounce.ts │ ├── useAuthToast.tsx │ └── watchlist │ │ ├── useNotStartedModal.ts │ │ ├── useFinishedWatching.ts │ │ └── useCurrentlyWatching.ts ├── data │ ├── community.ts │ └── anime.ts ├── types │ ├── next-auth.d.ts │ ├── db.d.ts │ └── item-type.ts ├── middleware.ts ├── env.mjs └── config │ └── index.ts ├── public └── images │ ├── logo.png │ ├── anime-placeholder.png │ └── home-page-snapshot.png ├── postcss.config.js ├── .eslintrc.json ├── next.config.js ├── components.json ├── .gitignore ├── tsconfig.json ├── SECURITY.md ├── LICENSE ├── .env.example ├── README.md └── tailwind.config.js /src/components/ClientWrapper/RatingDisplay.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sukrittt/Otaku-Sphere/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /src/app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sukrittt/Otaku-Sphere/HEAD/src/app/opengraph-image.png -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/anime-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sukrittt/Otaku-Sphere/HEAD/public/images/anime-placeholder.png -------------------------------------------------------------------------------- /public/images/home-page-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Sukrittt/Otaku-Sphere/HEAD/public/images/home-page-snapshot.png -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "rules": { 4 | "no-console": ["warn"], 5 | "no-unused-vars": ["warn"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function RootLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return <>{children}; 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ["uploadthing.com"], 5 | }, 6 | }; 7 | 8 | module.exports = nextConfig; 9 | -------------------------------------------------------------------------------- /src/components/ui/AspectRatio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | 3 | import { authOptions } from "@/lib/auth"; 4 | 5 | const handler = NextAuth(authOptions); 6 | 7 | export { handler as GET, handler as POST }; 8 | -------------------------------------------------------------------------------- /src/lib/validators/user.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const AddAdminUserPayload = z.object({ 4 | email: z.string().email(), 5 | }); 6 | 7 | export type AddAdminUserPayloadType = z.infer; 8 | -------------------------------------------------------------------------------- /src/lib/uploadthing.ts: -------------------------------------------------------------------------------- 1 | import { generateReactHelpers } from "@uploadthing/react/hooks"; 2 | 3 | import type { OurFileRouter } from "@/app/api/uploadthing/core"; 4 | 5 | export const { uploadFiles } = generateReactHelpers(); 6 | -------------------------------------------------------------------------------- /src/app/api/uploadthing/route.ts: -------------------------------------------------------------------------------- 1 | import { createNextRouteHandler } from "uploadthing/next"; 2 | 3 | import { ourFileRouter } from "./core"; 4 | 5 | // Export routes for Next App Router 6 | export const { GET, POST } = createNextRouteHandler({ 7 | router: ourFileRouter, 8 | }); 9 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { type MetadataRoute } from "next"; 2 | 3 | import { siteConfig } from "@/config"; 4 | 5 | export default function robots(): MetadataRoute.Robots { 6 | return { 7 | rules: { 8 | userAgent: "*", 9 | allow: "/", 10 | }, 11 | sitemap: `${siteConfig.url}/sitemap.xml`, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ui/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ); 13 | } 14 | 15 | export { Skeleton }; 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tailwind": { 6 | "config": "tailwind.config.js", 7 | "css": "app/globals.css", 8 | "baseColor": "zinc", 9 | "cssVariables": true 10 | }, 11 | "aliases": { 12 | "components": "@/components", 13 | "utils": "@/lib/utils" 14 | } 15 | } -------------------------------------------------------------------------------- /src/lib/validators/like.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const LikeValidator = z.object({ 4 | postId: z.string(), 5 | }); 6 | 7 | export type LikeValidatorType = z.infer; 8 | 9 | export const ReviewLikeValidator = z.object({ 10 | reviewId: z.string(), 11 | }); 12 | 13 | export type ReviewLikeValidatorType = z.infer; 14 | -------------------------------------------------------------------------------- /src/components/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import type { ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/TypingMessage.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { FC } from "react"; 3 | import { TypeAnimation } from "react-type-animation"; 4 | 5 | interface TypingMessageProps { 6 | text: string; 7 | delay?: number; 8 | } 9 | 10 | const TypingMessage: FC = ({ text }) => { 11 | return ; 12 | }; 13 | 14 | export default TypingMessage; 15 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/AnimeQuestionClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | const AnimeStatusQuestion = dynamic( 5 | () => import("@/components/AnimeStatusQuestion"), 6 | { 7 | ssr: false, 8 | } 9 | ); 10 | 11 | const AnimeQuestionClient = ({ animeId }: { animeId: string }) => { 12 | return ; 13 | }; 14 | 15 | export default AnimeQuestionClient; 16 | -------------------------------------------------------------------------------- /src/hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = React.useState(value); 5 | 6 | React.useEffect(() => { 7 | const timer = setTimeout(() => setDebouncedValue(value), delay || 500); 8 | 9 | return () => { 10 | clearTimeout(timer); 11 | }; 12 | }, [value, delay]); 13 | 14 | return debouncedValue; 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(site)/anime/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from "@/ui/ErrorCard"; 2 | import { Shell } from "@/components/Shell"; 3 | 4 | export default function PageNotFound() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from "@/ui/ErrorCard"; 2 | import { Shell } from "@/components/Shell"; 3 | 4 | export default function PageNotFound() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(site)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/components/Footer/Footer"; 2 | import Navbar from "@/components/Navbar/Navbar"; 3 | 4 | interface SiteLayoutProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export default async function SiteLayout({ children }: SiteLayoutProps) { 9 | return ( 10 |
11 | 12 |
{children}
13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(site)/admin/anime/[anime]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from "@/ui/ErrorCard"; 2 | import { Shell } from "@/components/Shell"; 3 | 4 | export default function PageNotFound() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(site)/community/[category]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { ErrorCard } from "@/ui/ErrorCard"; 2 | import { Shell } from "@/components/Shell"; 3 | 4 | export default function PageNotFound() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/data/community.ts: -------------------------------------------------------------------------------- 1 | import { ComboBoxItemType } from "@/types/item-type"; 2 | 3 | export const categories: ComboBoxItemType[] = [ 4 | { 5 | value: "general", 6 | label: "General", 7 | }, 8 | { 9 | value: "anime", 10 | label: "Anime", 11 | }, 12 | { 13 | value: "manga", 14 | label: "Manga", 15 | }, 16 | { 17 | value: "question", 18 | label: "Question", 19 | }, 20 | { 21 | value: "feedback", 22 | label: "Feedback", 23 | }, 24 | ]; 25 | -------------------------------------------------------------------------------- /src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import "server-only"; 3 | 4 | declare global { 5 | // eslint-disable-next-line no-var, no-unused-vars 6 | var cachedPrisma: PrismaClient; 7 | } 8 | 9 | let prisma: PrismaClient; 10 | if (process.env.NODE_ENV === "production") { 11 | prisma = new PrismaClient(); 12 | } else { 13 | if (!global.cachedPrisma) { 14 | global.cachedPrisma = new PrismaClient(); 15 | } 16 | prisma = global.cachedPrisma; 17 | } 18 | 19 | export const db = prisma; 20 | -------------------------------------------------------------------------------- /src/components/ServerComponents/AdminAnimes.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { INFINITE_SCROLLING_PAGINATION_ANIME } from "@/config"; 3 | import AnimeClient from "@/components/ClientWrapper/AnimeClient"; 4 | 5 | const AdminAnimes = async () => { 6 | const allAnime = await db.anime.findMany({ 7 | orderBy: { 8 | createdAt: "desc", 9 | }, 10 | take: INFINITE_SCROLLING_PAGINATION_ANIME, 11 | }); 12 | 13 | return ; 14 | }; 15 | 16 | export default AdminAnimes; 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { Role } from "@prisma/client"; 2 | import type { Session, User } from "next-auth"; 3 | import type { JWT } from "next-auth/jwt"; 4 | 5 | type UserId = string; 6 | 7 | declare module "next-auth/jwt" { 8 | interface JWT { 9 | id: UserId; 10 | username?: string | null; 11 | role: Role; 12 | } 13 | } 14 | 15 | declare module "next-auth" { 16 | interface Session { 17 | user: User & { 18 | id: UserId; 19 | username?: string | null; 20 | role: Role; 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/AnimeCardClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | import { Anime } from "@prisma/client"; 4 | 5 | import { SingleAnimeCardSkeleton } from "@/components/SkeletonLoaders/AnimeCardSkeleton"; 6 | 7 | const AnimeCard = dynamic(() => import("@/components/Cards/AnimeCard"), { 8 | ssr: false, 9 | loading: () => , 10 | }); 11 | 12 | const AnimeCardClient = ({ anime }: { anime: Anime }) => { 13 | return ; 14 | }; 15 | 16 | export default AnimeCardClient; 17 | -------------------------------------------------------------------------------- /src/app/(site)/poll/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/Shell"; 2 | import HeaderSkeleton from "@/components/SkeletonLoaders/HeaderSkeleton"; 3 | import PollCardsSkeleton from "@/components/SkeletonLoaders/PollCardsSkeleton"; 4 | import { Skeleton } from "@/ui/Skeleton"; 5 | 6 | const PollLoading = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default PollLoading; 18 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/OverviewClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { OverviewType } from "@/types/item-type"; 5 | import OverviewSkeleton from "@/components/SkeletonLoaders/OverviewSkeleton"; 6 | 7 | const OverviewDisplay = dynamic(() => import("@/components/OverviewDisplay"), { 8 | ssr: false, 9 | loading: () => , 10 | }); 11 | 12 | const OverviewClient = ({ data }: { data: OverviewType[] }) => { 13 | return ; 14 | }; 15 | 16 | export default OverviewClient; 17 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/AddCommentClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | import { CommentBoxSkeleton } from "@/app/(site)/community/[category]/[communityId]/post/[postId]/loading"; 4 | 5 | const AddCommentForm = dynamic( 6 | () => import("@/components/Forms/AddCommentForm"), 7 | { 8 | ssr: false, 9 | loading: () => , 10 | } 11 | ); 12 | 13 | const AddCommentClient = ({ postId }: { postId: string }) => { 14 | return ; 15 | }; 16 | 17 | export default AddCommentClient; 18 | -------------------------------------------------------------------------------- /src/app/api/uploadthing/core.ts: -------------------------------------------------------------------------------- 1 | import { getToken } from "next-auth/jwt"; 2 | import { createUploadthing, type FileRouter } from "uploadthing/next"; 3 | 4 | const f = createUploadthing(); 5 | 6 | export const ourFileRouter = { 7 | imageUploader: f({ image: { maxFileSize: "4MB" } }) 8 | .middleware(async ({ req }) => { 9 | const user = await getToken({ req }); 10 | 11 | if (!user) throw new Error("Unauthorized"); 12 | 13 | return { userId: user.id }; 14 | }) 15 | .onUploadComplete(async ({ metadata, file }) => {}), 16 | } satisfies FileRouter; 17 | 18 | export type OurFileRouter = typeof ourFileRouter; 19 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/LikePostClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { Skeleton } from "@/ui/Skeleton"; 5 | 6 | const LikePost = dynamic(() => import("@/components/LikePost"), { 7 | ssr: false, 8 | loading: () => , 9 | }); 10 | 11 | interface LikePostProps { 12 | initialLike: boolean; 13 | likes: number; 14 | postId: string; 15 | } 16 | 17 | const LikePostClient = ({ initialLike, likes, postId }: LikePostProps) => { 18 | return ; 19 | }; 20 | 21 | export default LikePostClient; 22 | -------------------------------------------------------------------------------- /src/components/ServerComponents/TopRated.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import AnimeCardClient from "@/components/ClientWrapper/AnimeCardClient"; 3 | 4 | const TopRated = async () => { 5 | const animes = await db.anime.findMany({ 6 | take: 5, 7 | orderBy: { 8 | totalRatings: "desc", 9 | }, 10 | }); 11 | 12 | if (animes.length === 0) return; 13 | 14 | return ( 15 |
16 | {animes.map((anime) => { 17 | return ; 18 | })} 19 |
20 | ); 21 | }; 22 | 23 | export default TopRated; 24 | -------------------------------------------------------------------------------- /src/app/(site)/community/[category]/[communityId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/ui/Skeleton"; 2 | import { Shell } from "@/components/Shell"; 3 | import ComPostSkeleton from "@/components/SkeletonLoaders/ComPostSkeleton"; 4 | import HeaderSkeleton from "@/components/SkeletonLoaders/HeaderSkeleton"; 5 | 6 | const PostsLoading = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 |
16 | ); 17 | }; 18 | 19 | export default PostsLoading; 20 | -------------------------------------------------------------------------------- /src/components/ServerComponents/RecentlyAdded.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import AnimeCardClient from "@/components/ClientWrapper/AnimeCardClient"; 3 | 4 | const RecentlyAdded = async () => { 5 | const animes = await db.anime.findMany({ 6 | take: 5, 7 | orderBy: { 8 | createdAt: "desc", 9 | }, 10 | }); 11 | 12 | if (animes.length === 0) return; 13 | 14 | return ( 15 |
16 | {animes.map((anime) => { 17 | return ; 18 | })} 19 |
20 | ); 21 | }; 22 | 23 | export default RecentlyAdded; 24 | -------------------------------------------------------------------------------- /src/components/Custom-UI/CustomToolTip.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { 3 | Tooltip, 4 | TooltipContent, 5 | TooltipProvider, 6 | TooltipTrigger, 7 | } from "@/ui/Tooltip"; 8 | 9 | interface CustomToolTipProps { 10 | children: React.ReactNode; 11 | text: string; 12 | } 13 | 14 | const CustomToolTip: FC = ({ children, text }) => { 15 | return ( 16 | 17 | 18 | {children} 19 | {text} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default CustomToolTip; 26 | -------------------------------------------------------------------------------- /src/app/(site)/community/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/Shell"; 2 | import { Skeleton } from "@/components/ui/Skeleton"; 3 | import ComPostSkeleton from "@/components/SkeletonLoaders/ComPostSkeleton"; 4 | import HeaderSkeleton from "@/components/SkeletonLoaders/HeaderSkeleton"; 5 | 6 | const CommunitiesLoading = () => { 7 | return ( 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 |
19 | ); 20 | }; 21 | 22 | export default CommunitiesLoading; 23 | -------------------------------------------------------------------------------- /src/components/ServerComponents/Comments.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { INFINITE_SCROLLING_PAGINATION_RESULTS } from "@/config"; 3 | import CommentClient from "@/components/ClientWrapper/CommentClient"; 4 | 5 | interface CommentsProps { 6 | postId: string; 7 | } 8 | 9 | const Comments = async ({ postId }: CommentsProps) => { 10 | const comments = await db.comment.findMany({ 11 | where: { 12 | postId, 13 | }, 14 | include: { 15 | author: true, 16 | }, 17 | orderBy: { 18 | createdAt: "desc", 19 | }, 20 | take: INFINITE_SCROLLING_PAGINATION_RESULTS, 21 | }); 22 | 23 | return ; 24 | }; 25 | 26 | export default Comments; 27 | -------------------------------------------------------------------------------- /src/app/(site)/leaderboard/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/Shell"; 2 | import { ScrollArea } from "@/ui/ScrollArea"; 3 | import TableSkeleton from "@/components/SkeletonLoaders/TableSkeleton"; 4 | 5 | const TableColumns = ["Rank", "Anime", "Director", "Genre", "Rating", "Votes"]; 6 | 7 | const StatisticsLoading = () => { 8 | return ( 9 | 10 |

11 | Leaderboard 12 |

13 | 14 | 15 | 16 |
17 | ); 18 | }; 19 | 20 | export default StatisticsLoading; 21 | -------------------------------------------------------------------------------- /src/components/ServerComponents/TopTenAnimeCheck.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { db } from "@/lib/db"; 4 | 5 | const TopTenAnimeCheck = async ({ name }: { name: string }) => { 6 | const topTenAnimes = await db.anime.findMany({ 7 | orderBy: { 8 | totalRatings: "desc", 9 | }, 10 | take: 10, 11 | }); 12 | 13 | const inTopTen = topTenAnimes.findIndex((anime) => anime.name === name); 14 | 15 | if (inTopTen < 0) return null; 16 | 17 | return ( 18 | 22 | Top Rated 23 | 24 | ); 25 | }; 26 | 27 | export default TopTenAnimeCheck; 28 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/PostClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { ExtendedPost } from "@/types/db"; 5 | import ComPostSkeleton from "@/components/SkeletonLoaders/ComPostSkeleton"; 6 | 7 | const Posts = dynamic(() => import("@/components/InfiniteQuery/Posts"), { 8 | ssr: false, 9 | loading: () => ( 10 |
11 | 12 |
13 | ), 14 | }); 15 | 16 | interface PostsProps { 17 | initialPosts: ExtendedPost[]; 18 | communityId: string; 19 | } 20 | 21 | const PostClient = ({ communityId, initialPosts }: PostsProps) => { 22 | return ; 23 | }; 24 | 25 | export default PostClient; 26 | -------------------------------------------------------------------------------- /src/components/SkeletonLoaders/HeaderSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Skeleton } from "@/ui/Skeleton"; 3 | 4 | const HeaderSkeleton = ({ 5 | showBack, 6 | description, 7 | large, 8 | }: { 9 | showBack?: boolean; 10 | description?: boolean; 11 | large?: boolean; 12 | }) => { 13 | return ( 14 |
15 | {showBack && } 16 |
17 | 22 | {description && } 23 |
24 |
25 | ); 26 | }; 27 | 28 | export default HeaderSkeleton; 29 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/ReviewDropdownClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { Skeleton } from "@/ui/Skeleton"; 5 | 6 | const ReviewDropdown = dynamic( 7 | () => import("@/components/Dropdown/ReviewDropdown"), 8 | { 9 | ssr: false, 10 | loading: () => , 11 | } 12 | ); 13 | 14 | interface ReviewDropdownProps { 15 | children: React.ReactNode; 16 | reviewId: string; 17 | animeId: string; 18 | } 19 | 20 | const ReviewDropdownClient = ({ 21 | children, 22 | animeId, 23 | reviewId, 24 | }: ReviewDropdownProps) => { 25 | return ( 26 | 27 | {children} 28 | 29 | ); 30 | }; 31 | 32 | export default ReviewDropdownClient; 33 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/PollClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { ExtendedPoll } from "@/types/db"; 5 | import PollCardsSkeleton from "@/components/SkeletonLoaders/PollCardsSkeleton"; 6 | 7 | const Polls = dynamic(() => import("@/components/InfiniteQuery/Polls"), { 8 | ssr: false, 9 | loading: () => , 10 | }); 11 | 12 | interface PollsProps { 13 | initialPolls: ExtendedPoll[]; 14 | interaction?: boolean; 15 | sessionId: string; 16 | } 17 | 18 | const PollClient = ({ initialPolls, sessionId, interaction }: PollsProps) => { 19 | return ( 20 | 25 | ); 26 | }; 27 | 28 | export default PollClient; 29 | -------------------------------------------------------------------------------- /src/app/icon.tsx: -------------------------------------------------------------------------------- 1 | import { ImageResponse } from "next/server"; 2 | 3 | // Route segment config 4 | export const runtime = "edge"; 5 | 6 | // Image metadata 7 | export const size = { 8 | width: 32, 9 | height: 32, 10 | }; 11 | export const contentType = "image/png"; 12 | 13 | // Image generation 14 | export default function Icon() { 15 | return new ImageResponse( 16 | ( 17 | // ImageResponse JSX element 18 |
19 | O 20 |
21 | ), 22 | // ImageResponse options 23 | { 24 | // For convenience, we can re-use the exported icons size metadata 25 | // config to also set the ImageResponse's width and height. 26 | ...size, 27 | } 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/components/User/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { User } from "next-auth"; 3 | import { AvatarProps } from "@radix-ui/react-avatar"; 4 | 5 | import { Icons } from "@/components/Icons"; 6 | import { Avatar, AvatarFallback, AvatarImage } from "@/ui/Avatar"; 7 | 8 | interface UserAvatarProps extends AvatarProps { 9 | user: Pick; 10 | } 11 | 12 | const UserAvatar: FC = ({ user, ...props }) => { 13 | return ( 14 | 15 | 16 | 17 | {user?.name} 18 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default UserAvatar; 25 | -------------------------------------------------------------------------------- /src/app/(site)/browse/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/Shell"; 2 | import AnimeCardSkeleton from "@/components/SkeletonLoaders/AnimeCardSkeleton"; 3 | import HeaderSkeleton from "@/components/SkeletonLoaders/HeaderSkeleton"; 4 | import { Skeleton } from "@/ui/Skeleton"; 5 | 6 | const BrowseLoading = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 | 16 |
17 | 18 |
19 | ); 20 | }; 21 | 22 | export default BrowseLoading; 23 | -------------------------------------------------------------------------------- /src/components/Footer/CommunityCategories.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { buttonVariants } from "@/ui/Button"; 5 | import { categories } from "@/data/community"; 6 | 7 | const CommunityCategories = () => { 8 | return ( 9 |
10 | {categories.map((category) => { 11 | const href = `/community/${category.value}`; 12 | 13 | return ( 14 | {`#${category.value}`} 22 | ); 23 | })} 24 |
25 | ); 26 | }; 27 | 28 | export default CommunityCategories; 29 | -------------------------------------------------------------------------------- /src/components/ui/Label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ); 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )); 24 | Label.displayName = LabelPrimitive.Root.displayName; 25 | 26 | export { Label }; 27 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/PostDropdownClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { ExtendedPost } from "@/types/db"; 5 | import { Skeleton } from "@/ui/Skeleton"; 6 | 7 | const PostDropdown = dynamic( 8 | () => import("@/components/Dropdown/PostDropdown"), 9 | { 10 | ssr: false, 11 | loading: () => , 12 | } 13 | ); 14 | 15 | interface PostDropdownProps { 16 | children: React.ReactNode; 17 | post: Pick; 18 | sessionId: string; 19 | } 20 | 21 | const PostDropdownClient = ({ 22 | children, 23 | post, 24 | sessionId, 25 | }: PostDropdownProps) => { 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | export default PostDropdownClient; 34 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"], 24 | "@/styles/*": ["./src/app/styles/*"], 25 | "@/ui/*": ["./src/components/ui/*"], 26 | } 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Footer/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useTheme } from "next-themes"; 4 | 5 | import { Button } from "@/ui/Button"; 6 | import { Icons } from "@/components/Icons"; 7 | 8 | export function ThemeToggle() { 9 | const { setTheme, theme } = useTheme(); 10 | 11 | return ( 12 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; 3 | import { Adsense } from "@ctrl/react-adsense"; 4 | import { SessionProvider } from "next-auth/react"; 5 | 6 | import { ThemeProvider } from "@/components/ThemeProvider"; 7 | 8 | const Providers = ({ children }: { children: React.ReactNode }) => { 9 | const queryClient = new QueryClient(); 10 | 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | {children} 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default Providers; 26 | -------------------------------------------------------------------------------- /src/lib/validators/poll.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const CreatePollValidator = z.object({ 4 | question: z.string().min(3).max(150), 5 | }); 6 | 7 | export const CreatePollValidatorServer = CreatePollValidator.extend({ 8 | expiresAt: z.date().min(new Date()).optional(), 9 | options: z.array(z.string().min(1).max(80)), 10 | }); 11 | 12 | export type CreatePollValidatorType = z.infer; 13 | export type CreatePollValidatorServerType = z.infer< 14 | typeof CreatePollValidatorServer 15 | >; 16 | 17 | export const VotePollValidator = z.object({ 18 | optionId: z.string(), 19 | }); 20 | 21 | export type VotePollValidatorType = z.infer; 22 | 23 | export const DeletePollValidator = z.object({ 24 | pollId: z.string(), 25 | }); 26 | 27 | export type DeletePollValidatorType = z.infer; 28 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/CommentClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { ExtendedComment } from "@/types/db"; 5 | import CommentSkeleton from "@/components/SkeletonLoaders/CommentSkeleton"; 6 | import { CardFooter } from "@/ui/Card"; 7 | 8 | const InfiniteComments = dynamic( 9 | () => import("@/components/InfiniteQuery/Comments"), 10 | { 11 | ssr: false, 12 | loading: () => , 13 | } 14 | ); 15 | 16 | interface CommentsProps { 17 | initialComments: ExtendedComment[]; 18 | postId: string; 19 | } 20 | 21 | const CommentClient = ({ initialComments, postId }: CommentsProps) => { 22 | return ( 23 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default CommentClient; 30 | -------------------------------------------------------------------------------- /src/hooks/useAuthToast.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { toast } from "./use-toast"; 4 | import { buttonVariants } from "@/ui/Button"; 5 | 6 | export const useAuthToast = () => { 7 | const loginToast = () => { 8 | const { dismiss } = toast({ 9 | title: "Sign In required.", 10 | description: "You need to be signed in to do that.", 11 | variant: "destructive", 12 | action: ( 13 | dismiss()} 16 | className={buttonVariants()} 17 | > 18 | Sign In 19 | 20 | ), 21 | }); 22 | }; 23 | 24 | const endErrorToast = () => { 25 | toast({ 26 | title: "Error", 27 | description: "Something went wrong.", 28 | variant: "destructive", 29 | }); 30 | }; 31 | 32 | return { loginToast, endErrorToast }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/api/anime/search/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | 3 | export async function GET(req: Request) { 4 | const url = new URL(req.url); 5 | const q = url.searchParams.get("q"); 6 | 7 | if (!q || typeof q !== "string") { 8 | return new Response("Invalid Query", { status: 400 }); 9 | } 10 | 11 | try { 12 | const results = await db.anime.findMany({ 13 | where: { 14 | name: { 15 | contains: q, 16 | }, 17 | }, 18 | select: { 19 | id: true, 20 | name: true, 21 | }, 22 | orderBy: { 23 | totalRatings: "desc", 24 | }, 25 | take: 5, 26 | }); 27 | 28 | results.sort((a, b) => a.name.length - b.name.length); 29 | 30 | return new Response(JSON.stringify(results)); 31 | } catch (error) { 32 | return new Response("Something went wrong", { status: 500 }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/types/db.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Anime, 3 | Comment, 4 | Community, 5 | Like, 6 | Poll, 7 | PollOption, 8 | PollVote, 9 | Post, 10 | Rating, 11 | User, 12 | } from "@prisma/client"; 13 | 14 | export type ExtendedCommunity = Community & { 15 | creator: User; 16 | post: Post[]; 17 | }; 18 | 19 | export type ExtendedPost = Post & { 20 | creator: User; 21 | comment: Comment[]; 22 | like: Like[]; 23 | community: Community; 24 | }; 25 | 26 | export type ExtendedComment = Comment & { 27 | author: User; 28 | }; 29 | 30 | export type ExtendedAnime = Anime & { 31 | rating: Rating[]; 32 | }; 33 | 34 | export type ExtendedUser = User & { 35 | anime: Anime[]; 36 | pollVote: PollVote[]; 37 | post: Post[]; 38 | rating: Rating[]; 39 | }; 40 | 41 | export type ExtendedPoll = Poll & { 42 | creator: User; 43 | option: Array; 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/AnimeRatingClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Session } from "next-auth"; 3 | import dynamic from "next/dynamic"; 4 | 5 | import { Skeleton } from "@/ui/Skeleton"; 6 | 7 | const AnimeRating = dynamic(() => import("@/components/AnimeRating"), { 8 | ssr: false, 9 | loading: () => ( 10 |
11 | 12 | 13 |
14 | ), 15 | }); 16 | 17 | interface AnimeRatingProps { 18 | animeId: string; 19 | userRating: number | undefined; 20 | session: Session | null; 21 | } 22 | 23 | const AnimeRatingClient = ({ 24 | animeId, 25 | session, 26 | userRating, 27 | }: AnimeRatingProps) => { 28 | return ( 29 | 30 | ); 31 | }; 32 | 33 | export default AnimeRatingClient; 34 | -------------------------------------------------------------------------------- /src/components/ui/Separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/hooks/watchlist/useNotStartedModal.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | import { DragItemType } from "@/types/item-type"; 4 | 5 | interface NotStartedStore { 6 | board: DragItemType[]; 7 | addImageToBoard: (item: DragItemType) => void; 8 | removeItemFromBoard: (id: string) => void; 9 | setBoard: (board: DragItemType[]) => void; 10 | } 11 | 12 | const useNotStarted = create((set) => ({ 13 | board: [], 14 | addImageToBoard: (item) => { 15 | set((state) => { 16 | const updatedBoard = [item, ...state.board]; 17 | 18 | return { ...state, board: updatedBoard }; 19 | }); 20 | }, 21 | removeItemFromBoard: (id) => { 22 | set((state) => { 23 | const updatedBoard = state.board.filter((item) => item.id !== id); 24 | return { ...state, board: updatedBoard }; 25 | }); 26 | }, 27 | setBoard: (board) => set({ board }), 28 | })); 29 | 30 | export default useNotStarted; 31 | -------------------------------------------------------------------------------- /src/components/ui/Input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ); 21 | } 22 | ); 23 | Input.displayName = "Input"; 24 | 25 | export { Input }; 26 | -------------------------------------------------------------------------------- /src/components/Shell.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface ShellProps extends React.HTMLAttributes { 6 | as?: React.ElementType; 7 | children: React.ReactNode; 8 | layout?: "default" | "dashboard" | "auth" | "centered" | "markdown"; 9 | } 10 | 11 | export function Shell({ 12 | as: Comp = "section", 13 | children, 14 | layout = "default", 15 | className, 16 | ...props 17 | }: ShellProps) { 18 | return ( 19 | 30 | {children} 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ui/Toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | Toast, 5 | ToastClose, 6 | ToastDescription, 7 | ToastProvider, 8 | ToastTitle, 9 | ToastViewport, 10 | } from "@/components/ui/Toast"; 11 | import { useToast } from "@/hooks/use-toast"; 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast(); 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ); 31 | })} 32 | 33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/hooks/watchlist/useFinishedWatching.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | import { DragItemType } from "@/types/item-type"; 4 | 5 | interface FinishedWatchingStore { 6 | board: DragItemType[]; 7 | addImageToBoard: (item: DragItemType) => void; 8 | removeItemFromBoard: (id: string) => void; 9 | setBoard: (board: DragItemType[]) => void; 10 | } 11 | 12 | const useFinishedWatching = create((set) => ({ 13 | board: [], 14 | addImageToBoard: (item) => { 15 | set((state) => { 16 | const updatedBoard = [item, ...state.board]; 17 | 18 | return { ...state, board: updatedBoard }; 19 | }); 20 | }, 21 | removeItemFromBoard: (id) => { 22 | set((state) => { 23 | const updatedBoard = state.board.filter((item) => item.id !== id); 24 | return { ...state, board: updatedBoard }; 25 | }); 26 | }, 27 | setBoard: (board) => set({ board }), 28 | })); 29 | 30 | export default useFinishedWatching; 31 | -------------------------------------------------------------------------------- /src/components/SkeletonLoaders/ComPostSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardHeader, CardTitle } from "@/ui/Card"; 2 | import { Skeleton } from "@/ui/Skeleton"; 3 | 4 | const ComPostSkeleton = ({ length = 5 }: { length?: number }) => { 5 | return ( 6 | <> 7 | {Array.from({ length }).map((_, index) => ( 8 | 9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 |
18 |
19 |
20 |
21 | ))} 22 | 23 | ); 24 | }; 25 | 26 | export default ComPostSkeleton; 27 | -------------------------------------------------------------------------------- /src/hooks/watchlist/useCurrentlyWatching.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | import { DragItemType } from "@/types/item-type"; 4 | 5 | interface CurrentlyWatchingStore { 6 | board: DragItemType[]; 7 | addImageToBoard: (item: DragItemType) => void; 8 | removeItemFromBoard: (id: string) => void; 9 | setBoard: (board: DragItemType[]) => void; 10 | } 11 | 12 | const useCurrentlyWatching = create((set) => ({ 13 | board: [], 14 | addImageToBoard: (item) => { 15 | set((state) => { 16 | const updatedBoard = [item, ...state.board]; 17 | 18 | return { ...state, board: updatedBoard }; 19 | }); 20 | }, 21 | removeItemFromBoard: (id) => { 22 | set((state) => { 23 | const updatedBoard = state.board.filter((item) => item.id !== id); 24 | return { ...state, board: updatedBoard }; 25 | }); 26 | }, 27 | setBoard: (board) => set({ board }), 28 | })); 29 | 30 | export default useCurrentlyWatching; 31 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "@/config"; 2 | import { type MetadataRoute } from "next"; 3 | import { communitySidebarNavItems } from "@/data"; 4 | 5 | export default function sitemap(): MetadataRoute.Sitemap { 6 | const communities = communitySidebarNavItems.map((page) => ({ 7 | url: `${siteConfig.url}${page.href}`, 8 | lastModified: new Date().toISOString(), 9 | })); 10 | 11 | const routes = [ 12 | "", 13 | "/community", 14 | "/community/general", 15 | "/community/anime", 16 | "/community/manga", 17 | "/community/question", 18 | "/community/feedback", 19 | "/community/create", 20 | "/watchlist", 21 | "/leaderboard", 22 | "/browse", 23 | "/poll", 24 | "/poll/create", 25 | "/poll/results", 26 | "/anime", 27 | ].map((route) => ({ 28 | url: `${siteConfig.url}${route}`, 29 | lastModified: new Date().toISOString(), 30 | })); 31 | 32 | return [...routes, ...communities]; 33 | } 34 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/ReviewClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Session } from "next-auth"; 3 | import dynamic from "next/dynamic"; 4 | import { ReviewLike, Reviews, User } from "@prisma/client"; 5 | import ReviewSkeleton from "@/components/SkeletonLoaders/ReviewSkeleton"; 6 | 7 | const ReviewInfiniteFetching = dynamic(() => import("@/components/Reviews"), { 8 | ssr: false, 9 | loading: () => , 10 | }); 11 | 12 | type ExtendedReview = Reviews & { 13 | user: User; 14 | reviewLikes: ReviewLike[]; 15 | }; 16 | 17 | interface ReviewsProps { 18 | initialReviews: ExtendedReview[]; 19 | animeId: string; 20 | session: Session | null; 21 | } 22 | 23 | const ReviewClient = ({ animeId, initialReviews, session }: ReviewsProps) => { 24 | return ( 25 | 30 | ); 31 | }; 32 | 33 | export default ReviewClient; 34 | -------------------------------------------------------------------------------- /src/components/ServerComponents/Reviews.tsx: -------------------------------------------------------------------------------- 1 | import { db } from "@/lib/db"; 2 | import { getAuthSession } from "@/lib/auth"; 3 | import { INFINITE_SCROLLING_PAGINATION_ANIME } from "@/config"; 4 | import ReviewClient from "@/components/ClientWrapper/ReviewClient"; 5 | 6 | interface ReviewsProps { 7 | animeId: string; 8 | } 9 | 10 | const Reviews = async ({ animeId }: ReviewsProps) => { 11 | const session = await getAuthSession(); 12 | 13 | const reviews = await db.reviews.findMany({ 14 | where: { 15 | animeId, 16 | }, 17 | include: { 18 | user: true, 19 | reviewLikes: true, 20 | }, 21 | orderBy: { 22 | reviewLikes: { 23 | _count: "desc", 24 | }, 25 | }, 26 | take: INFINITE_SCROLLING_PAGINATION_ANIME, 27 | }); 28 | 29 | return ( 30 | 35 | ); 36 | }; 37 | 38 | export default Reviews; 39 | -------------------------------------------------------------------------------- /src/app/(site)/admin/users/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Shell } from "@/components/Shell"; 2 | import HeaderSkeleton from "@/components/SkeletonLoaders/HeaderSkeleton"; 3 | import TableSkeleton from "@/components/SkeletonLoaders/TableSkeleton"; 4 | import { ScrollArea } from "@/ui/ScrollArea"; 5 | import { Skeleton } from "@/ui/Skeleton"; 6 | 7 | const TableColumns = [ 8 | "Name", 9 | "Email", 10 | "Ratings", 11 | "Joined At", 12 | "Polls Voted", 13 | "Posts", 14 | ]; 15 | 16 | const AdminUserLoading = () => { 17 | return ( 18 | 19 | 20 |
21 | 22 | 23 |
24 | 25 | 26 | 27 |
28 | ); 29 | }; 30 | 31 | export default AdminUserLoading; 32 | -------------------------------------------------------------------------------- /src/components/ClientWrapper/CommunityClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import dynamic from "next/dynamic"; 3 | 4 | import { ExtendedCommunity } from "@/types/db"; 5 | import { Skeleton } from "@/ui/Skeleton"; 6 | import ComPostSkeleton from "@/components/SkeletonLoaders/ComPostSkeleton"; 7 | 8 | const Communities = dynamic( 9 | () => import("@/components/InfiniteQuery/Communities"), 10 | { 11 | ssr: false, 12 | loading: () => ( 13 |
14 | 15 | 16 |
17 | ), 18 | } 19 | ); 20 | 21 | interface CommunitiesProps { 22 | initialCommunities: ExtendedCommunity[]; 23 | category?: string; 24 | } 25 | 26 | const CommunityClient = ({ 27 | initialCommunities, 28 | category, 29 | }: CommunitiesProps) => { 30 | return ( 31 | 32 | ); 33 | }; 34 | 35 | export default CommunityClient; 36 | -------------------------------------------------------------------------------- /src/components/SkeletonLoaders/ReviewSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardFooter } from "@/ui/Card"; 2 | import { Skeleton } from "@/ui/Skeleton"; 3 | 4 | const ReviewSkeleton = ({ length = 3 }: { length?: number }) => { 5 | return ( 6 |
7 | {Array.from({ length }).map((_, index) => ( 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 | ))} 21 |
22 | ); 23 | }; 24 | 25 | export default ReviewSkeleton; 26 | -------------------------------------------------------------------------------- /src/components/ui/Textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes { 7 | large?: boolean; 8 | } 9 | 10 | const Textarea = React.forwardRef( 11 | ({ className, large, ...props }, ref) => { 12 | return ( 13 |