├── .env-example ├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── components.json ├── next-env.d.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma ├── migrations │ ├── 20220718122256_init │ │ └── migration.sql │ ├── 20220719031012_pokemon │ │ └── migration.sql │ ├── 20220719051129_pokemon_rate │ │ └── migration.sql │ └── migration_lock.toml ├── schema.prisma └── seed.ts ├── public └── favicon.ico ├── src ├── components │ ├── Container.tsx │ ├── Nav.tsx │ ├── NavItem.tsx │ ├── NavItemMobile.tsx │ ├── PokeCard.tsx │ ├── PokeCardEmpty.tsx │ ├── PokeRate.tsx │ ├── ThemeSwitcher.tsx │ └── ui │ │ ├── input.tsx │ │ └── select.tsx ├── env │ ├── client.mjs │ ├── schema.mjs │ └── server.mjs ├── hooks │ └── useDebounce.ts ├── lib │ └── utils.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth].ts │ │ ├── restricted.ts │ │ └── trpc │ │ │ └── [trpc].ts │ ├── index.tsx │ └── rate.tsx ├── server │ ├── api │ │ ├── root.ts │ │ ├── routers │ │ │ ├── example.ts │ │ │ └── pokemon.ts │ │ └── trpc.ts │ ├── auth.ts │ └── db.ts ├── styles │ └── globals.css ├── types │ └── next-auth.d.ts └── utils │ └── api.ts ├── tailwind.config.js └── tsconfig.json /.env-example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENVIRONMENT= 2 | NEXT_PUBLIC_GA_TRACKING_ID= 3 | 4 | DATABASE_URL=postgresql://postgres:@localhost:5832/db 5 | 6 | NEXTAUTH_SECRET= 7 | NEXTAUTH_URL=http://localhost:3000 8 | 9 | GITHUB_CLIENT_ID= 10 | GITHUB_CLIENT_SECRET= 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /.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 | # database 12 | /prisma/db.sqlite 13 | 14 | # next.js 15 | /.next/ 16 | /out/ 17 | 18 | # production 19 | /build 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env 33 | .env*.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "printWidth": 80, 7 | "endOfLine": "lf", 8 | "plugins": ["prettier-plugin-tailwindcss"], 9 | "tailwindFunctions": ["cva"] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true, 3 | "peacock.color": "#b3dfff", 4 | "editor.tabSize": 2, 5 | "editor.insertSpaces": true, 6 | "editor.detectIndentation": false, 7 | "editor.inlineSuggest.enabled": true, 8 | "explorer.autoReveal": false, 9 | "editor.formatOnSave": true, 10 | "[json]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[jsonc]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "[javascript]": { 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[typescript]": { 20 | "editor.defaultFormatter": "esbenp.prettier-vscode" 21 | }, 22 | "[html]": { 23 | "editor.defaultFormatter": "esbenp.prettier-vscode" 24 | }, 25 | "[css]": { 26 | "editor.defaultFormatter": "stylelint.vscode-stylelint" 27 | }, 28 | "[scss]": { 29 | "editor.defaultFormatter": "stylelint.vscode-stylelint" 30 | }, 31 | "eslint.options": { 32 | "extensions": [".ts", ".html"] 33 | }, 34 | "eslint.validate": [ 35 | "javascript", 36 | "javascriptreact", 37 | "typescript", 38 | "typescriptreact", 39 | "html" 40 | ], 41 | "html.format.wrapAttributes": "force-aligned", 42 | "html.format.wrapLineLength": 110, 43 | "editor.rulers": [ 44 | { 45 | "column": 80, 46 | "color": "#1C4532" 47 | }, 48 | { 49 | "column": 110, 50 | "color": "#63171B" 51 | } 52 | ], 53 | "css.validate": false, 54 | "less.validate": false, 55 | "scss.validate": false, 56 | "stylelint.validate": ["css", "scss"] 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PokeRate 2 | 3 | ## Todo 4 | 5 | - [x] Search for a pokemon 6 | - [x] Sort by name & rate 7 | - [ ] Dashboard page 8 | - [ ] Seed more pokemon 9 | 10 | ## 🚀 11 | 12 | - [create-t3-app](https://github.com/t3-oss/create-t3-app) 13 | - [Next.js](https://nextjs.org/) 14 | - [tRPC](https://trpc.io/) 15 | - [Prisma](https://www.prisma.io/) 16 | - [TailwindCSS](https://tailwindcss.com/) 17 | - [Railway](https://railway.app/) 18 | - [PokeAPI](https://pokeapi.co) 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 4 | * This is especially useful for Docker builds. 5 | */ 6 | !process.env.SKIP_ENV_VALIDATION && (await import('./src/env/server.mjs')); 7 | 8 | /** @type {import('next').NextConfig} */ 9 | const config = { 10 | reactStrictMode: true, 11 | swcMinify: true, 12 | images: { 13 | domains: ['raw.githubusercontent.com'], 14 | }, 15 | experimental: { 16 | nextScriptWorkers: true, 17 | }, 18 | }; 19 | 20 | export default config; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-trpc", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "postinstall": "prisma generate", 11 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts", 12 | "partytown": "partytown copylib public/~partytown", 13 | "vercel-build": "prisma generate && prisma migrate deploy && npm run partytown && next build" 14 | }, 15 | "dependencies": { 16 | "@builder.io/partytown": "^0.7.4", 17 | "@next-auth/prisma-adapter": "^1.0.5", 18 | "@prisma/client": "^4.8.1", 19 | "@radix-ui/react-collapsible": "^1.0.1", 20 | "@radix-ui/react-select": "^1.2.2", 21 | "@tanstack/react-query": "^4.22.0", 22 | "@trpc/client": "^10.8.1", 23 | "@trpc/next": "^10.8.1", 24 | "@trpc/react-query": "^10.8.1", 25 | "@trpc/server": "^10.8.1", 26 | "class-variance-authority": "^0.7.0", 27 | "classnames": "^2.3.1", 28 | "clsx": "^2.0.0", 29 | "lucide-react": "^0.263.1", 30 | "next": "^13.1.1", 31 | "next-auth": "^4.22.3", 32 | "next-seo": "^5.4.0", 33 | "next-themes": "^0.2.0", 34 | "pokenode-ts": "^1.16.0", 35 | "rc-rate": "^2.9.2", 36 | "react": "^18.2.0", 37 | "react-dom": "^18.2.0", 38 | "react-query": "^3.39.2", 39 | "superjson": "^1.9.1", 40 | "tailwind-merge": "^1.14.0", 41 | "tailwindcss-animate": "^1.0.6", 42 | "valibot": "^0.2.1", 43 | "zod": "^3.20.2" 44 | }, 45 | "devDependencies": { 46 | "@types/node": "18.0.0", 47 | "@types/react": "18.0.14", 48 | "@types/react-dom": "18.0.5", 49 | "autoprefixer": "^10.4.7", 50 | "eslint": "8.18.0", 51 | "eslint-config-next": "^13.1.1", 52 | "postcss": "^8.4.14", 53 | "prettier": "^2.8.8", 54 | "prettier-plugin-tailwindcss": "^0.4.1", 55 | "prisma": "^4.0.0", 56 | "tailwindcss": "^3.1.6", 57 | "ts-node": "^10.9.1", 58 | "typescript": "4.7.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/migrations/20220718122256_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Example" ( 3 | "id" TEXT NOT NULL, 4 | 5 | CONSTRAINT "Example_pkey" PRIMARY KEY ("id") 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "Account" ( 10 | "id" TEXT NOT NULL, 11 | "userId" TEXT NOT NULL, 12 | "type" TEXT NOT NULL, 13 | "provider" TEXT NOT NULL, 14 | "providerAccountId" TEXT NOT NULL, 15 | "refresh_token" TEXT, 16 | "access_token" TEXT, 17 | "expires_at" INTEGER, 18 | "token_type" TEXT, 19 | "scope" TEXT, 20 | "id_token" TEXT, 21 | "session_state" TEXT, 22 | 23 | CONSTRAINT "Account_pkey" PRIMARY KEY ("id") 24 | ); 25 | 26 | -- CreateTable 27 | CREATE TABLE "Session" ( 28 | "id" TEXT NOT NULL, 29 | "sessionToken" TEXT NOT NULL, 30 | "userId" TEXT NOT NULL, 31 | "expires" TIMESTAMP(3) NOT NULL, 32 | 33 | CONSTRAINT "Session_pkey" PRIMARY KEY ("id") 34 | ); 35 | 36 | -- CreateTable 37 | CREATE TABLE "User" ( 38 | "id" TEXT NOT NULL, 39 | "name" TEXT, 40 | "email" TEXT, 41 | "emailVerified" TIMESTAMP(3), 42 | "image" TEXT, 43 | 44 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 45 | ); 46 | 47 | -- CreateTable 48 | CREATE TABLE "VerificationToken" ( 49 | "identifier" TEXT NOT NULL, 50 | "token" TEXT NOT NULL, 51 | "expires" TIMESTAMP(3) NOT NULL 52 | ); 53 | 54 | -- CreateIndex 55 | CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); 56 | 57 | -- CreateIndex 58 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 59 | 60 | -- CreateIndex 61 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 62 | 63 | -- CreateIndex 64 | CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); 65 | 66 | -- CreateIndex 67 | CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); 68 | 69 | -- AddForeignKey 70 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 71 | 72 | -- AddForeignKey 73 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 74 | -------------------------------------------------------------------------------- /prisma/migrations/20220719031012_pokemon/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Pokemon" ( 3 | "id" INTEGER NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "image" TEXT NOT NULL, 6 | 7 | CONSTRAINT "Pokemon_pkey" PRIMARY KEY ("id") 8 | ); 9 | -------------------------------------------------------------------------------- /prisma/migrations/20220719051129_pokemon_rate/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "PokemonRate" ( 3 | "id" TEXT NOT NULL, 4 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 5 | "updatedAt" TIMESTAMP(3) NOT NULL, 6 | "userId" TEXT NOT NULL, 7 | "pokemonId" INTEGER NOT NULL, 8 | "rate" INTEGER NOT NULL, 9 | 10 | CONSTRAINT "PokemonRate_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "PokemonRate" ADD CONSTRAINT "PokemonRate_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "PokemonRate" ADD CONSTRAINT "PokemonRate_pokemonId_fkey" FOREIGN KEY ("pokemonId") REFERENCES "Pokemon"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Example { 14 | id String @id @default(cuid()) 15 | } 16 | 17 | // Necessary for Next auth 18 | model Account { 19 | id String @id @default(cuid()) 20 | userId String 21 | type String 22 | provider String 23 | providerAccountId String 24 | refresh_token String? 25 | access_token String? 26 | expires_at Int? 27 | token_type String? 28 | scope String? 29 | id_token String? 30 | session_state String? 31 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 32 | 33 | @@unique([provider, providerAccountId]) 34 | } 35 | 36 | model Session { 37 | id String @id @default(cuid()) 38 | sessionToken String @unique 39 | userId String 40 | expires DateTime 41 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 42 | } 43 | 44 | model User { 45 | id String @id @default(cuid()) 46 | name String? 47 | email String? @unique 48 | emailVerified DateTime? 49 | image String? 50 | accounts Account[] 51 | sessions Session[] 52 | PokemonRate PokemonRate[] 53 | } 54 | 55 | model VerificationToken { 56 | identifier String 57 | token String @unique 58 | expires DateTime 59 | 60 | @@unique([identifier, token]) 61 | } 62 | 63 | model Pokemon { 64 | id Int @id 65 | name String 66 | image String 67 | PokemonRate PokemonRate[] 68 | } 69 | 70 | model PokemonRate { 71 | id String @id @default(cuid()) 72 | createdAt DateTime @default(now()) 73 | updatedAt DateTime @updatedAt 74 | userId String 75 | pokemonId Int 76 | rate Int 77 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 78 | pokemon Pokemon @relation(fields: [pokemonId], references: [id], onDelete: Cascade) 79 | } 80 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | const prisma = new PrismaClient(); 3 | 4 | import { MainClient } from 'pokenode-ts'; 5 | 6 | async function main() { 7 | const api = new MainClient(); 8 | 9 | for (let index = 1; index <= 20; index++) { 10 | await api.pokemon 11 | .getPokemonById(index) 12 | .then(async (data) => { 13 | let image = data.sprites.other['dream_world']?.front_default; 14 | 15 | if (!image) { 16 | image = data.sprites.other['official-artwork']?.front_default; 17 | } 18 | 19 | await prisma.pokemon.upsert({ 20 | where: { id: Number(data.id) }, 21 | update: {}, 22 | create: { 23 | id: Number(data.id), 24 | name: data.name, 25 | image: image || '', 26 | }, 27 | }); 28 | }) 29 | .catch((error) => console.error(error)); 30 | } 31 | } 32 | 33 | main() 34 | .catch((e) => { 35 | console.error(e); 36 | process.exit(1); 37 | }) 38 | .finally(async () => { 39 | await prisma.$disconnect(); 40 | }); 41 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rendiriz/next-trpc/7423005fbf17e7f3bade33b8112cbcbdd3339236/public/favicon.ico -------------------------------------------------------------------------------- /src/components/Container.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import NextLink from 'next/link'; 3 | import { NextSeo } from 'next-seo'; 4 | import Nav from '@/components/Nav'; 5 | 6 | type ContainerProps = { 7 | children: React.ReactNode; 8 | title?: string; 9 | description?: string; 10 | }; 11 | 12 | export default function Container(props: ContainerProps) { 13 | const { children, ...customMeta } = props; 14 | const router = useRouter(); 15 | const meta = { 16 | title: 'PokeRate', 17 | description: 'A simple rating system for Pokemon.', 18 | type: 'website', 19 | ...customMeta, 20 | }; 21 | 22 | return ( 23 | <> 24 | 36 |
37 |
51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Nav.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import NextLink from 'next/link'; 3 | import { signIn, signOut, useSession } from 'next-auth/react'; 4 | import NavItem from '@/components/NavItem'; 5 | import NavItemMobile from '@/components/NavItemMobile'; 6 | import ThemeSwitcher from '@/components/ThemeSwitcher'; 7 | import * as Collapsible from '@radix-ui/react-collapsible'; 8 | import cn from 'classnames'; 9 | 10 | const Nav = () => { 11 | const { data: session } = useSession(); 12 | const [open, setOpen] = useState(false); 13 | 14 | return ( 15 | 20 | 108 | 109 | 110 |
118 |
119 |
120 | 121 | {session && } 122 |
123 |
124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | export default Nav; 131 | -------------------------------------------------------------------------------- /src/components/NavItem.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import Link from 'next/link'; 3 | import cn from 'classnames'; 4 | 5 | interface INavItem { 6 | icon?: JSX.Element; 7 | href: string; 8 | text: string; 9 | className?: string; 10 | } 11 | 12 | const NavItem = (props: INavItem) => { 13 | const router = useRouter(); 14 | const isActive = router.asPath === props.href; 15 | 16 | return ( 17 | 28 | {props.icon} 29 | {props.text} 30 | 31 | ); 32 | }; 33 | 34 | export default NavItem; 35 | -------------------------------------------------------------------------------- /src/components/NavItemMobile.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import Link from 'next/link'; 3 | import cn from 'classnames'; 4 | 5 | interface INavItemMobile { 6 | icon?: JSX.Element; 7 | href: string; 8 | text: string; 9 | className?: string; 10 | } 11 | 12 | const NavItemMobile = (props: INavItemMobile) => { 13 | const router = useRouter(); 14 | const isActive = router.asPath === props.href; 15 | 16 | return ( 17 | 28 | {props.icon} 29 | {props.text} 30 | 31 | ); 32 | }; 33 | 34 | export default NavItemMobile; 35 | -------------------------------------------------------------------------------- /src/components/PokeCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/legacy/image'; 2 | import { useRouter } from 'next/router'; 3 | import { useSession } from 'next-auth/react'; 4 | import Rate from 'rc-rate'; 5 | import 'rc-rate/assets/index.css'; 6 | import cn from 'classnames'; 7 | 8 | type PokeCardProps = { 9 | id: number; 10 | image: string; 11 | name: string; 12 | rate?: number; 13 | vote?: number; 14 | }; 15 | 16 | export default function PokeCard({ 17 | id, 18 | image, 19 | name, 20 | rate, 21 | vote, 22 | }: PokeCardProps) { 23 | const { data: session } = useSession(); 24 | const router = useRouter(); 25 | 26 | return ( 27 |
28 |
34 |
35 | {name} 36 | {(!session || router.pathname === '/') && ( 37 |
38 | 39 |
40 | Score: {Number(rate?.toFixed(1))}/5 · {Number(vote)} vote 41 |
42 |
43 | )} 44 |
45 |
46 |
{name}
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/PokeCardEmpty.tsx: -------------------------------------------------------------------------------- 1 | export default function PokeCardEmpty() { 2 | return ( 3 |
4 |
5 |
6 |
7 | ); 8 | } 9 | -------------------------------------------------------------------------------- /src/components/PokeRate.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useSession } from 'next-auth/react'; 3 | import PokeCard from '@/components/PokeCard'; 4 | import { api } from '@/utils/api'; 5 | import Rate from 'rc-rate'; 6 | import 'rc-rate/assets/index.css'; 7 | 8 | export default function PokeRate(pokemon: any) { 9 | const { data: session } = useSession(); 10 | const [rating, setRating] = useState(pokemon.rate); 11 | const mutation = api.pokemon.upsert.useMutation(); 12 | 13 | const leaveRate = async (id: number, rate: number) => { 14 | setRating(rate); 15 | mutation.mutate({ 16 | pokemonId: id, 17 | rate, 18 | }); 19 | }; 20 | 21 | return ( 22 | <> 23 | 24 | {session && ( 25 |
26 | leaveRate(pokemon.id, rate)} 31 | /> 32 |
33 | )} 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useTheme } from 'next-themes'; 3 | import cn from 'classnames'; 4 | import { Moon, Sun } from 'lucide-react'; 5 | 6 | const ThemeSwitcher = () => { 7 | const [isMounted, setIsMounted] = useState(false); 8 | const { theme, setTheme } = useTheme(); 9 | 10 | const handleClick = () => { 11 | setTheme(theme === 'light' ? 'dark' : 'light'); 12 | }; 13 | 14 | useEffect(() => { 15 | setIsMounted(true); 16 | }, []); 17 | 18 | return ( 19 | 30 | ); 31 | }; 32 | 33 | export default ThemeSwitcher; 34 | -------------------------------------------------------------------------------- /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/ui/select.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SelectPrimitive from "@radix-ui/react-select" 3 | import { Check, ChevronDown } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Select = SelectPrimitive.Root 8 | 9 | const SelectGroup = SelectPrimitive.Group 10 | 11 | const SelectValue = SelectPrimitive.Value 12 | 13 | const SelectTrigger = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, children, ...props }, ref) => ( 17 | 25 | {children} 26 | 27 | 28 | 29 | 30 | )) 31 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 32 | 33 | const SelectContent = React.forwardRef< 34 | React.ElementRef, 35 | React.ComponentPropsWithoutRef 36 | >(({ className, children, position = "popper", ...props }, ref) => ( 37 | 38 | 49 | 56 | {children} 57 | 58 | 59 | 60 | )) 61 | SelectContent.displayName = SelectPrimitive.Content.displayName 62 | 63 | const SelectLabel = React.forwardRef< 64 | React.ElementRef, 65 | React.ComponentPropsWithoutRef 66 | >(({ className, ...props }, ref) => ( 67 | 72 | )) 73 | SelectLabel.displayName = SelectPrimitive.Label.displayName 74 | 75 | const SelectItem = React.forwardRef< 76 | React.ElementRef, 77 | React.ComponentPropsWithoutRef 78 | >(({ className, children, ...props }, ref) => ( 79 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | {children} 94 | 95 | )) 96 | SelectItem.displayName = SelectPrimitive.Item.displayName 97 | 98 | const SelectSeparator = React.forwardRef< 99 | React.ElementRef, 100 | React.ComponentPropsWithoutRef 101 | >(({ className, ...props }, ref) => ( 102 | 107 | )) 108 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 109 | 110 | export { 111 | Select, 112 | SelectGroup, 113 | SelectValue, 114 | SelectTrigger, 115 | SelectContent, 116 | SelectLabel, 117 | SelectItem, 118 | SelectSeparator, 119 | } 120 | -------------------------------------------------------------------------------- /src/env/client.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { clientEnv, clientSchema } from './schema.mjs'; 3 | 4 | const _clientEnv = clientSchema.safeParse(clientEnv); 5 | 6 | export const formatErrors = ( 7 | /** @type {import('zod').ZodFormattedError,string>} */ 8 | errors, 9 | ) => 10 | Object.entries(errors) 11 | .map(([name, value]) => { 12 | if (value && '_errors' in value) 13 | return `${name}: ${value._errors.join(', ')}\n`; 14 | }) 15 | .filter(Boolean); 16 | 17 | if (!_clientEnv.success) { 18 | console.error( 19 | '❌ Invalid environment variables:\n', 20 | ...formatErrors(_clientEnv.error.format()), 21 | ); 22 | throw new Error('Invalid environment variables'); 23 | } 24 | 25 | for (let key of Object.keys(_clientEnv.data)) { 26 | if (!key.startsWith('NEXT_PUBLIC_')) { 27 | console.warn( 28 | `❌ Invalid public environment variable name: ${key}. It must begin with 'NEXT_PUBLIC_'`, 29 | ); 30 | 31 | throw new Error('Invalid public environment variable name'); 32 | } 33 | } 34 | 35 | export const env = _clientEnv.data; 36 | -------------------------------------------------------------------------------- /src/env/schema.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { z } from 'zod'; 3 | 4 | /** 5 | * Specify your server-side environment variables schema here. 6 | * This way you can ensure the app isn't built with invalid env vars. 7 | */ 8 | export const serverSchema = z.object({ 9 | DATABASE_URL: z.string().url(), 10 | NODE_ENV: z.enum(['development', 'test', 'production']), 11 | NEXTAUTH_SECRET: 12 | process.env.NODE_ENV === 'production' 13 | ? z.string().min(1) 14 | : z.string().min(1).optional(), 15 | NEXTAUTH_URL: z.preprocess( 16 | // This makes Vercel deployments not fail if you don't set NEXTAUTH_URL 17 | // Since NextAuth.js automatically uses the VERCEL_URL if present. 18 | (str) => process.env.VERCEL_URL ?? str, 19 | // VERCEL_URL doesn't include `https` so it cant be validated as a URL 20 | process.env.VERCEL ? z.string() : z.string().url(), 21 | ), 22 | GITHUB_CLIENT_ID: z.string(), 23 | GITHUB_CLIENT_SECRET: z.string(), 24 | }); 25 | 26 | /** 27 | * Specify your client-side environment variables schema here. 28 | * This way you can ensure the app isn't built with invalid env vars. 29 | * To expose them to the client, prefix them with `NEXT_PUBLIC_`. 30 | */ 31 | export const clientSchema = z.object({ 32 | NEXT_PUBLIC_ENVIRONMENT: z.string(), 33 | NEXT_PUBLIC_GA_TRACKING_ID: z.string(), 34 | }); 35 | 36 | /** 37 | * You can't destruct `process.env` as a regular object, so you have to do 38 | * it manually here. This is because Next.js evaluates this at build time, 39 | * and only used environment variables are included in the build. 40 | * @type {{ [k in keyof z.infer]: z.infer[k] | undefined }} 41 | */ 42 | export const clientEnv = { 43 | NEXT_PUBLIC_ENVIRONMENT: process.env.NEXT_PUBLIC_ENVIRONMENT, 44 | NEXT_PUBLIC_GA_TRACKING_ID: process.env.NEXT_PUBLIC_GA_TRACKING_ID, 45 | }; 46 | -------------------------------------------------------------------------------- /src/env/server.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /** 3 | * This file is included in `/next.config.mjs` which ensures the app isn't built with invalid env vars. 4 | * It has to be a `.mjs`-file to be imported there. 5 | */ 6 | import { serverSchema } from './schema.mjs'; 7 | import { env as clientEnv, formatErrors } from './client.mjs'; 8 | 9 | const _serverEnv = serverSchema.safeParse(process.env); 10 | 11 | if (!_serverEnv.success) { 12 | console.error( 13 | '❌ Invalid environment variables:\n', 14 | ...formatErrors(_serverEnv.error.format()), 15 | ); 16 | throw new Error('Invalid environment variables'); 17 | } 18 | 19 | for (let key of Object.keys(_serverEnv.data)) { 20 | if (key.startsWith('NEXT_PUBLIC_')) { 21 | console.warn('❌ You are exposing a server-side env-variable:', key); 22 | 23 | throw new Error('You are exposing a server-side env-variable'); 24 | } 25 | } 26 | 27 | export const env = { ..._serverEnv.data, ...clientEnv }; 28 | -------------------------------------------------------------------------------- /src/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export function useDebounce(value: T, delay?: number): T { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | 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/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Script from 'next/script'; 2 | import type { AppType } from 'next/dist/shared/lib/utils'; 3 | import { SessionProvider } from 'next-auth/react'; 4 | import { ThemeProvider } from 'next-themes'; 5 | import '../styles/globals.css'; 6 | 7 | import { api } from '../utils/api'; 8 | 9 | const isProduction = process.env.NEXT_PUBLIC_ENVIRONMENT === 'production'; 10 | 11 | const MyApp: AppType<{ session: any }> = ({ 12 | Component, 13 | pageProps: { session, ...pageProps }, 14 | }) => { 15 | return ( 16 | <> 17 | 18 | 19 | 20 | 21 | 22 | {isProduction && ( 23 |