├── .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 |
38 |
39 | {children}
40 |
41 |
50 |
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 |
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 |
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