├── .editorconfig ├── .env.sample ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE.md ├── README.md ├── apps ├── .gitkeep └── web │ ├── .eslintrc.json │ ├── .vscode │ └── settings.json │ ├── app │ ├── (authentication) │ │ └── layout.tsx │ ├── @pokemonModal │ │ ├── (.)pokemon │ │ │ └── [id] │ │ │ │ └── page.tsx │ │ └── default.tsx │ ├── @signInModal │ │ ├── (.)sign-in │ │ │ └── page.tsx │ │ └── default.tsx │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.ts │ │ └── team │ │ │ └── [userId] │ │ │ └── route.ts │ ├── error.tsx │ ├── global-error.tsx │ ├── layout.tsx │ ├── opengraph-image.tsx │ ├── page.tsx │ ├── pokemon │ │ └── [id] │ │ │ ├── opengraph-image.tsx │ │ │ └── page.tsx │ ├── robots.ts │ ├── sign-in │ │ └── page.tsx │ ├── sitemap.ts │ └── styles.css │ ├── index.d.ts │ ├── jest.config.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── postcss.config.js │ ├── project.json │ ├── public │ ├── .gitkeep │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest │ ├── specs │ └── index.spec.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ └── tsconfig.spec.json ├── drizzle.config.json ├── jest.config.ts ├── jest.preset.js ├── libs ├── .gitkeep ├── configuration │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── project.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── auth.ts │ │ │ └── web.ts │ │ └── server.ts │ ├── tsconfig.json │ └── tsconfig.lib.json ├── db │ ├── .eslintrc.json │ ├── README.md │ ├── project.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── adapter.ts │ │ │ ├── db.ts │ │ │ ├── drizzle │ │ │ ├── 0000_dizzy_red_ghost.sql │ │ │ ├── 0001_light_night_nurse.sql │ │ │ ├── 0002_simple_nightcrawler.sql │ │ │ ├── 0003_silly_diamondback.sql │ │ │ └── meta │ │ │ │ ├── 0000_snapshot.json │ │ │ │ ├── 0001_snapshot.json │ │ │ │ ├── 0002_snapshot.json │ │ │ │ ├── 0003_snapshot.json │ │ │ │ └── _journal.json │ │ │ ├── schema.ts │ │ │ └── seed.ts │ ├── tsconfig.json │ └── tsconfig.lib.json ├── feature │ └── pokemon-team │ │ ├── .babelrc │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── project.json │ │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── add-pokemon │ │ │ │ ├── action.ts │ │ │ │ └── add-pokemon.tsx │ │ │ ├── poke-ball │ │ │ │ ├── poke-ball.module.css │ │ │ │ └── poke-ball.tsx │ │ │ ├── poke-balls │ │ │ │ └── poke-balls.tsx │ │ │ ├── pokemon-list │ │ │ │ └── pokemon-list.tsx │ │ │ ├── remove-pokemon │ │ │ │ ├── action.ts │ │ │ │ └── remove-pokemon.tsx │ │ │ └── submit │ │ │ │ └── submit.tsx │ │ └── server.ts │ │ ├── tsconfig.json │ │ └── tsconfig.lib.json ├── ui │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── project.json │ ├── src │ │ ├── index.ts │ │ ├── lib │ │ │ ├── avatar │ │ │ │ └── avatar.tsx │ │ │ ├── button │ │ │ │ └── button.tsx │ │ │ ├── command │ │ │ │ └── command.tsx │ │ │ ├── dialog │ │ │ │ └── dialog.tsx │ │ │ ├── dropdown-menu │ │ │ │ └── dropdown-menu.tsx │ │ │ ├── github-button │ │ │ │ └── github-button.tsx │ │ │ ├── header │ │ │ │ └── header.tsx │ │ │ ├── input │ │ │ │ └── input.tsx │ │ │ ├── intercept-dialog │ │ │ │ └── intercept-dialog.tsx │ │ │ ├── loading-dots.module.css │ │ │ ├── pokemon-type │ │ │ │ └── pokemon-type.tsx │ │ │ ├── pokemon-types-combo-box │ │ │ │ └── pokemon-types-combo-box.tsx │ │ │ ├── pokemon │ │ │ │ └── pokemon.tsx │ │ │ ├── popover │ │ │ │ └── popover.tsx │ │ │ ├── search │ │ │ │ └── search.tsx │ │ │ ├── theme-provider │ │ │ │ └── theme-provider.tsx │ │ │ ├── theme-toggle │ │ │ │ └── theme-toggle.tsx │ │ │ ├── tooltip │ │ │ │ └── tooltip.tsx │ │ │ └── user-button │ │ │ │ └── user-button.tsx │ │ └── server.ts │ ├── tsconfig.json │ └── tsconfig.lib.json └── utility │ └── shared │ ├── .eslintrc.json │ ├── README.md │ ├── project.json │ ├── src │ ├── index.ts │ └── lib │ │ └── classnames │ │ └── classnames.ts │ ├── tsconfig.json │ └── tsconfig.lib.json ├── nx.json ├── package-lock.json ├── package.json ├── tools └── tsconfig.tools.json ├── tsconfig.base.json └── types └── next-auth.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Database 2 | DATABASE_URL="libsql://[your-database]-[your-github].turso.io" 3 | DATABASE_AUTH_TOKEN="" 4 | 5 | # GitHub 6 | GITHUB_ID="" 7 | GITHUB_SECRET="" 8 | 9 | # NextAuth 10 | NEXTAUTH_URL="http://localhost:4200" 11 | NEXTAUTH_SECRET="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "type:application", 17 | "onlyDependOnLibsWithTags": ["type:feature", "type:utility"] 18 | }, 19 | { 20 | "sourceTag": "type:feature", 21 | "onlyDependOnLibsWithTags": ["type:feature", "type:utility"] 22 | }, 23 | { 24 | "sourceTag": "type:utility", 25 | "onlyDependOnLibsWithTags": ["type:utility"] 26 | } 27 | ] 28 | } 29 | ] 30 | } 31 | }, 32 | { 33 | "files": ["*.ts", "*.tsx"], 34 | "extends": ["plugin:@nx/typescript"], 35 | "rules": {} 36 | }, 37 | { 38 | "files": ["*.js", "*.jsx"], 39 | "extends": ["plugin:@nx/javascript"], 40 | "rules": {} 41 | } 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | 41 | # Next.js 42 | .next 43 | 44 | # Env 45 | .env 46 | 47 | # Database 48 | sqlite.db -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | /dist 3 | /coverage -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "firsttris.vscode-jest-runner", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Amos Bastian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pokémon 2 | 3 | A Next.js 13 application built using the new router, server components and server actions, as well as [NextAuth.js](https://github.com/nextauthjs/next-auth), [Drizzle ORM](https://github.com/drizzle-team/drizzle-orm) and [Turso](https://turso.tech/) in an [Nx monorepo](https://github.com/nrwl/nx) 4 | 5 | > **Warning** 6 | > This app is more than likely not using best practices. Server actions are still in alpha and some things currently don't work in an Nx monorepo. 7 | 8 | ## About this project 9 | 10 | I built this project to play around with server actions as well as server components, Drizzle ORM and the new intercepting / parallel routes. Take everything you see in here with a grain of salt. 11 | 12 | ## Note on performance / thoughts 13 | 14 | > **Warning** 15 | > This app is using server actions, which are in alpha, and do not (yet) have the best UX or DX. 16 | > **Expect to see some jank when playing around with the app**. 17 | > As for a personal recommendation, I'd stick to something like tRPC for now and wait until they improve things in the future. 18 | 19 | ## Some highlights 20 | 21 | - Using [the new Next.js app directory](https://nextjs.org/docs) and nearly everything that comes with it (dynamic open graph images, metadata etc.) 22 | - [Intercepting](https://nextjs.org/docs/app/building-your-application/routing/intercepting-routes) / [parallel routes](https://nextjs.org/docs/app/building-your-application/routing/parallel-routes) 23 | - Authentication using **[NextAuth.js](https://github.com/nextauthjs/next-auth)** 24 | - **[Drizzle ORM](https://github.com/drizzle-team/drizzle-orm)** 25 | - Using **[Turso](https://turso.tech/)** for the database 26 | - **TypeScript** 27 | 28 | ## Known issues 29 | 30 | A list of things not working right now: 31 | 32 | 1. Constant warnings in the terminal, probably caused by Nx 33 | 2. Loading state when adding / removing Pokémon is janky because of server actions 34 | 35 | ## Running locally 36 | 37 | 1. Install dependencies using npm: 38 | 2. Copy `.env.example` to `.env` and update the variables (you will need to [set up GitHub OAuth](https://github.com/settings/developers)) 39 | 40 | ```sh 41 | cp .env.example .env.local 42 | ``` 43 | 44 | 3. Run the migrations & seed the database 45 | 46 | ```sh 47 | npx drizzle-kit generate:sqlite 48 | npx drizzle-kit up:sqlite 49 | npm run seed 50 | ``` 51 | 52 | 4. Start the development server: 53 | 54 | ```sh 55 | npx nx serve web 56 | ``` 57 | 58 | ## Acknowledgements 59 | 60 | Just a few things that helped to build this app 61 | 62 | - Adapted a lot of components from @shadcn's [UI library](https://github.com/shadcn/ui) 63 | - Used @anthonyshrew's [Drizzle adapter for NextAuth](https://github.com/nextauthjs/next-auth/pull/7165) as a base 64 | - @samselikoff's [Build UI](https://buildui.com/) for inspiration for the Pokémon ball dock 65 | - [Ryan Toronto's tweet](https://twitter.com/ryantotweets/status/1622632894278533130) about implementing search with search params 66 | 67 | ## License 68 | 69 | Licensed under the [MIT license](https://github.com/amosbastian/pokemon/blob/main/LICENSE.md). 70 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/.gitkeep -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@nx/react-typescript", 4 | "next", 5 | "next/core-web-vitals", 6 | "../../.eslintrc.json" 7 | ], 8 | "ignorePatterns": ["!**/*", ".next/**/*"], 9 | "overrides": [ 10 | { 11 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 12 | "rules": { 13 | "@next/next/no-html-link-for-pages": ["error", "apps/web/pages"] 14 | } 15 | }, 16 | { 17 | "files": ["*.ts", "*.tsx"], 18 | "rules": {} 19 | }, 20 | { 21 | "files": ["*.js", "*.jsx"], 22 | "rules": {} 23 | } 24 | ], 25 | "rules": { 26 | "@next/next/no-html-link-for-pages": "off" 27 | }, 28 | "env": { 29 | "jest": true 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "../../node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } -------------------------------------------------------------------------------- /apps/web/app/(authentication)/layout.tsx: -------------------------------------------------------------------------------- 1 | interface AuthLayoutProps { 2 | children: React.ReactNode; 3 | } 4 | 5 | export default function AuthLayout({ children }: AuthLayoutProps) { 6 | return
{children}
; 7 | } 8 | -------------------------------------------------------------------------------- /apps/web/app/@pokemonModal/(.)pokemon/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { InterceptDialog } from "@pokemon/ui"; 2 | import { Pokemon } from "@pokemon/ui/server"; 3 | 4 | export default async function Page({ params }: { params: { id: number } }) { 5 | return ( 6 | 7 | {/* @ts-expect-error Server component */} 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/app/@pokemonModal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/app/@signInModal/(.)sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { GithubButton, InterceptDialog } from "@pokemon/ui"; 2 | 3 | export default async function Page() { 4 | return ( 5 | 6 |

Sign in with

7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /apps/web/app/@signInModal/default.tsx: -------------------------------------------------------------------------------- 1 | export default function Default() { 2 | return null; 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { nextAuthOptions } from "@pokemon/configuration/server"; 2 | import NextAuth from "next-auth"; 3 | 4 | const handler = NextAuth(nextAuthOptions); 5 | 6 | export { handler as GET, handler as POST }; 7 | -------------------------------------------------------------------------------- /apps/web/app/api/team/[userId]/route.ts: -------------------------------------------------------------------------------- 1 | import { getUserTeam } from "@pokemon/db"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET( 5 | request: Request, 6 | { 7 | params, 8 | }: { 9 | params: { userId: string }; 10 | }, 11 | ) { 12 | const userId = params.userId; 13 | const team = await getUserTeam(userId); 14 | 15 | return NextResponse.json({ team }); 16 | } 17 | -------------------------------------------------------------------------------- /apps/web/app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error components must be Client components 2 | 3 | import * as React from "react"; 4 | 5 | export default function Error({ error, reset }: { error: Error; reset: () => void }) { 6 | React.useEffect(() => { 7 | // Log the error to an error reporting service 8 | console.error(error); 9 | }, [error]); 10 | 11 | return ( 12 |
13 |

Something went wrong!

14 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/global-error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // Error components must be Client components 2 | 3 | import * as React from "react"; 4 | 5 | export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) { 6 | React.useEffect(() => { 7 | // Log the error to an error reporting service 8 | console.error(error); 9 | }, [error]); 10 | 11 | return ( 12 | 13 | 14 |

Something went wrong!

15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BASE_URL, BRAND_DESCRIPTION, BRAND_NAME } from "@pokemon/configuration"; 2 | import { ThemeProvider } from "@pokemon/ui"; 3 | import { Header } from "@pokemon/ui/server"; 4 | import type { Metadata } from "next"; 5 | import { Inter } from "next/font/google"; 6 | import "./styles.css"; 7 | 8 | // If loading a variable font, you don't need to specify the font weight 9 | const inter = Inter({ 10 | subsets: ["latin"], 11 | display: "swap", 12 | }); 13 | 14 | export default function RootLayout({ 15 | children, 16 | signInModal, 17 | pokemonModal, 18 | }: { 19 | children: React.ReactNode; 20 | signInModal: React.ReactNode; 21 | pokemonModal: React.ReactNode; 22 | }) { 23 | return ( 24 | // Note! If you do not add suppressHydrationWarning to your you will get warnings 25 | // because next-themes updates that element. This property only applies one level deep, 26 | // so it won't block hydration warnings on other elements. 27 | 32 | 33 | 34 |
35 | {/* @ts-expect-error Server component */} 36 |
37 |
38 | {pokemonModal} 39 | {children} 40 | {signInModal} 41 |
42 |
43 |
44 | 45 | 46 | ); 47 | } 48 | 49 | export const metadata: Metadata = { 50 | title: { 51 | default: BRAND_NAME, 52 | template: `%s | ${BRAND_NAME}`, 53 | }, 54 | description: BRAND_DESCRIPTION, 55 | metadataBase: new URL(BASE_URL), 56 | authors: [ 57 | { 58 | name: "Amos Bastian", 59 | url: "https://amosbastian.com", 60 | }, 61 | ], 62 | keywords: [ 63 | "Next.js", 64 | "React", 65 | "Tailwind CSS", 66 | "Server Components", 67 | "Radix UI", 68 | "Drizzle ORM", 69 | "Server Actions", 70 | "Parallel Routes", 71 | "shadcn UI", 72 | ], 73 | themeColor: [ 74 | { media: "(prefers-color-scheme: light)", color: "white" }, 75 | { media: "(prefers-color-scheme: dark)", color: "black" }, 76 | ], 77 | openGraph: { 78 | title: BRAND_NAME, 79 | description: BRAND_DESCRIPTION, 80 | url: BASE_URL, 81 | siteName: BRAND_NAME, 82 | images: [ 83 | { 84 | url: `${BASE_URL}/api/og?heading=Pokémon RSC`, 85 | width: 1200, 86 | height: 630, 87 | }, 88 | ], 89 | locale: "en-US", 90 | type: "website", 91 | }, 92 | robots: { 93 | index: true, 94 | follow: true, 95 | googleBot: { 96 | index: true, 97 | follow: true, 98 | "max-video-preview": -1, 99 | "max-image-preview": "large", 100 | "max-snippet": -1, 101 | }, 102 | }, 103 | twitter: { 104 | title: BRAND_NAME, 105 | description: BRAND_DESCRIPTION, 106 | card: "summary_large_image", 107 | creator: "@amosbastian", 108 | }, 109 | icons: { 110 | icon: "/favicon.ico", 111 | shortcut: "/favicon-32x32.png", 112 | apple: "/apple-touch-icon.png", 113 | }, 114 | manifest: `${BASE_URL}/site.webmanifest`, 115 | }; 116 | -------------------------------------------------------------------------------- /apps/web/app/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/shadcn/taxonomy 2 | import { ImageResponse } from "next/server"; 3 | 4 | async function handler() { 5 | try { 6 | const fontSize = "100px"; 7 | 8 | return new ImageResponse( 9 | ( 10 |
11 | 18 |
19 |
28 | Pokémon RSC 29 |
30 |
31 |
32 |
33 | pokemon-rsc.vercel.app 34 |
35 |
36 | 37 | 44 | 51 | 52 |
github.com/amosbastian/pokemon
53 |
54 |
55 |
56 | ), 57 | { 58 | width: 1200, 59 | height: 630, 60 | }, 61 | ); 62 | } catch (error) { 63 | return new Response(`Failed to generate image`, { 64 | status: 500, 65 | }); 66 | } 67 | } 68 | 69 | export const runtime = "edge"; 70 | 71 | export default handler; 72 | -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { db, typesTable } from "@pokemon/db"; 2 | import { PokemonList } from "@pokemon/feature/pokemon-team/server"; 3 | import { PokemonTypesComboBox, Search } from "@pokemon/ui"; 4 | 5 | export default async function Page({ searchParams }: { searchParams: { search?: string; type?: string } }) { 6 | const search = searchParams.search; 7 | const type = searchParams.type; 8 | const types = await db.select().from(typesTable).all(); 9 | 10 | return ( 11 | <> 12 |
13 | 14 | 15 |
16 | {/* @ts-expect-error Server component */} 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /apps/web/app/pokemon/[id]/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import { getSinglePokemon } from "@pokemon/db"; 2 | import { ImageResponse } from "next/server"; 3 | 4 | const WIDTH = 800; 5 | const HEIGHT = 400; 6 | 7 | const ColorMap: Record = { 8 | normal: "bg-neutral-500 dark:bg-neutral-400", 9 | fighting: "bg-orange-500 dark:bg-orange-400", 10 | flying: "bg-sky-500 dark:bg-sky-400", 11 | poison: "bg-fuchsia-500 dark:bg-fuchsia-400", 12 | ground: "bg-amber-500 dark:bg-amber-400", 13 | rock: "bg-orange-500 dark:bg-orange-400", 14 | bug: "bg-lime-500 dark:bg-lime-400", 15 | ghost: "bg-violet-500 dark:bg-violet-400", 16 | steel: "bg-zinc-500 dark:bg-zinc-400", 17 | fire: "bg-red-500 dark:bg-red-400", 18 | water: "bg-blue-500 dark:bg-blue-400", 19 | grass: "bg-green-500 dark:bg-green-400", 20 | electric: "bg-yellow-500 dark:bg-yellow-400", 21 | psychic: "bg-pink-500 dark:bg-pink-400", 22 | ice: "bg-cyan-500 dark:bg-cyan-400", 23 | dragon: "bg-purple-500 dark:bg-purple-400", 24 | dark: "bg-gray-500 dark:bg-gray-400", 25 | fairy: "bg-pink-500 dark:bg-pink-400", 26 | }; 27 | 28 | type Type = { 29 | id: number; 30 | name: string; 31 | }; 32 | 33 | const PokemonType = ({ type }: { type: Type }) => { 34 | return ( 35 | 39 |
40 | {type.name} 41 | 42 | ); 43 | }; 44 | 45 | type Statistic = "hp" | "attack" | "defense" | "specialAttack" | "specialDefense" | "speed"; 46 | 47 | const StatisticMap: Record = { 48 | hp: { 49 | label: "HP", 50 | className: "bg-red-500 border-red-600", 51 | }, 52 | attack: { 53 | label: "Attack", 54 | className: "bg-orange-500 border-orange-600", 55 | }, 56 | defense: { 57 | label: "Defense", 58 | className: "bg-yellow-500 border-yellow-600", 59 | }, 60 | specialAttack: { 61 | label: "Special attack", 62 | className: "bg-blue-500 border-blue-600", 63 | }, 64 | specialDefense: { 65 | label: "Special defense", 66 | className: "bg-green-500 border-green-600", 67 | }, 68 | speed: { 69 | label: "Speed", 70 | className: "bg-pink-500 border-pink-600", 71 | }, 72 | }; 73 | 74 | const StatisticBar = ({ statistic, value }: { statistic: Statistic; value: number }) => { 75 | const width = (value / MAX_VALUE) * WIDTH; 76 | return ( 77 |
78 |
79 |

{StatisticMap[statistic].label}

80 |
92 |
{value}
93 |
94 | ); 95 | }; 96 | 97 | const MAX_VALUE = 255; 98 | 99 | async function handler({ params }: { params: { id: string } }) { 100 | try { 101 | if (params.id) { 102 | const { pokemon, types } = (await getSinglePokemon(Number.parseInt(params.id, 10))) ?? {}; 103 | 104 | if (!pokemon || !types) { 105 | return null; 106 | } 107 | 108 | return new ImageResponse( 109 | ( 110 |
111 |
112 | 119 |
120 |
121 |

{pokemon.name}

122 |
123 |

{pokemon.weight} kg

124 |
125 |

{pokemon.height * 10} cm

126 |
127 |
128 | {types.map((type) => { 129 | return ; 130 | })} 131 |
132 |
133 |
134 |
135 | 136 | 137 | 138 | 139 | 140 | 141 |
142 |
143 | ), 144 | { 145 | width: WIDTH, 146 | height: HEIGHT, 147 | }, 148 | ); 149 | } 150 | } catch (error) { 151 | return new Response(`Failed to generate image`, { 152 | status: 500, 153 | }); 154 | } 155 | } 156 | 157 | export default handler; 158 | -------------------------------------------------------------------------------- /apps/web/app/pokemon/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "@pokemon/configuration"; 2 | import { getAllPokemon, getSinglePokemon } from "@pokemon/db"; 3 | import { Pokemon } from "@pokemon/ui/server"; 4 | import { Metadata } from "next"; 5 | 6 | type PageProps = { 7 | params: { 8 | id: string; 9 | }; 10 | }; 11 | 12 | export default async function Page({ params }: PageProps) { 13 | return ( 14 |
15 | {/* @ts-expect-error Server component */} 16 | 17 |
18 | ); 19 | } 20 | 21 | export async function generateMetadata({ params }: PageProps): Promise { 22 | const { pokemon, types } = (await getSinglePokemon(Number.parseInt(params.id, 10))) ?? {}; 23 | 24 | if (!pokemon || !types) { 25 | return {}; 26 | } 27 | 28 | const title = pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1); 29 | const description = `HP: ${pokemon.hp}. Attack: ${pokemon.attack}. Defense: ${pokemon.defense}. Special attack: ${pokemon.specialAttack}. Special defense: ${pokemon.specialDefense}. Speed: ${pokemon.speed}.`; 30 | 31 | return { 32 | title, 33 | description, 34 | openGraph: { 35 | title, 36 | description, 37 | url: `${BASE_URL}/pokemon/${params.id}`, 38 | tags: types.map((type) => type.name), 39 | }, 40 | twitter: { 41 | card: "summary_large_image", 42 | title, 43 | description, 44 | }, 45 | }; 46 | } 47 | 48 | export async function generateStaticParams(): Promise { 49 | const pokemon = await getAllPokemon(); 50 | 51 | return pokemon.map(({ pokemon: { id } }) => ({ 52 | id: `${id}`, 53 | })); 54 | } 55 | -------------------------------------------------------------------------------- /apps/web/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "@pokemon/configuration"; 2 | import { MetadataRoute } from "next"; 3 | 4 | export default function robots(): MetadataRoute.Robots { 5 | return { 6 | rules: { 7 | userAgent: "*", 8 | allow: "/", 9 | }, 10 | sitemap: `${BASE_URL}/sitemap.xml`, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/app/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { BRAND_NAME } from "@pokemon/configuration"; 2 | import { GithubButton } from "@pokemon/ui"; 3 | import { Metadata } from "next"; 4 | 5 | export default async function Page() { 6 | return ( 7 |
8 |
9 |

10 | Sign in to your account 11 |

12 |
13 | 14 |
15 |
16 |
17 |
18 | 19 |
20 |
21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export const metadata: Metadata = { 28 | title: "Sign in", 29 | description: `Sign in to ${BRAND_NAME} and assemble your own Pokémon team`, 30 | }; 31 | -------------------------------------------------------------------------------- /apps/web/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "@pokemon/configuration"; 2 | import { getAllPokemon } from "@pokemon/db"; 3 | import { MetadataRoute } from "next"; 4 | 5 | export default async function sitemap(): Promise { 6 | const allPokemon = await getAllPokemon(); 7 | 8 | return [ 9 | { 10 | url: `${BASE_URL}/sign-in`, 11 | lastModified: new Date(), 12 | }, 13 | { 14 | url: `${BASE_URL}/`, 15 | lastModified: new Date(), 16 | }, 17 | ...allPokemon.map(({ pokemon }) => { 18 | return { 19 | url: `${BASE_URL}/pokemon/${pokemon.id}`, 20 | lastModified: new Date(), 21 | }; 22 | }), 23 | ]; 24 | } 25 | -------------------------------------------------------------------------------- /apps/web/app/styles.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | } 9 | 10 | /* Prevent scrolling on body when modal is open */ 11 | body[style*="pointer-events: none"] .overflow-auto, 12 | body[style*="pointer-events: none"] .overflow-y-auto, 13 | body[style*="pointer-events: none"] .overflow-x-auto { 14 | overflow: hidden !important; 15 | } 16 | 17 | @layer base { 18 | :root { 19 | --background: 0 0% 100%; 20 | --foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --popover: 0 0% 100%; 26 | --popover-foreground: 222.2 47.4% 11.2%; 27 | 28 | --card: 0 0% 100%; 29 | --card-foreground: 222.2 47.4% 11.2%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | 34 | --primary: 222.2 47.4% 11.2%; 35 | --primary-foreground: 210 40% 98%; 36 | 37 | --secondary: 210 40% 96.1%; 38 | --secondary-foreground: 222.2 47.4% 11.2%; 39 | 40 | --accent: 210 40% 96.1%; 41 | --accent-foreground: 222.2 47.4% 11.2%; 42 | 43 | --destructive: 0 100% 50%; 44 | --destructive-foreground: 210 40% 98%; 45 | 46 | --ring: 215 20.2% 65.1%; 47 | 48 | --radius: 0.5rem; 49 | } 50 | 51 | .dark { 52 | --background: 224 71% 4%; 53 | --foreground: 213 31% 91%; 54 | 55 | --muted: 223 47% 11%; 56 | --muted-foreground: 215.4 16.3% 56.9%; 57 | 58 | --popover: 224 71% 4%; 59 | --popover-foreground: 215 20.2% 65.1%; 60 | 61 | --card: 224 71% 4%; 62 | --card-foreground: 213 31% 91%; 63 | 64 | --border: 216 34% 17%; 65 | --input: 216 34% 17%; 66 | 67 | --primary: 210 40% 98%; 68 | --primary-foreground: 222.2 47.4% 1.2%; 69 | 70 | --secondary: 222.2 47.4% 11.2%; 71 | --secondary-foreground: 210 40% 98%; 72 | 73 | --accent: 216 34% 17%; 74 | --accent-foreground: 210 40% 98%; 75 | 76 | --destructive: 0 63% 31%; 77 | --destructive-foreground: 210 40% 98%; 78 | 79 | --ring: 216 34% 17%; 80 | 81 | --radius: 0.5rem; 82 | } 83 | } 84 | 85 | @layer base { 86 | * { 87 | @apply border-border; 88 | } 89 | body { 90 | @apply bg-background text-foreground; 91 | font-feature-settings: "rlig" 1, "calt" 1; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /apps/web/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | -------------------------------------------------------------------------------- /apps/web/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'web', 4 | preset: '../../jest.preset.js', 5 | transform: { 6 | '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nx/react/plugins/jest', 7 | '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nx/next/babel'] }], 8 | }, 9 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 10 | coverageDirectory: '../../coverage/apps/web', 11 | }; 12 | -------------------------------------------------------------------------------- /apps/web/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 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { composePlugins, withNx } = require("@nx/next"); 5 | 6 | /** 7 | * @type {import('@nx/next/plugins/with-nx').WithNxOptions} 8 | **/ 9 | const nextConfig = { 10 | nx: { 11 | // Set this to true if you would like to use SVGR 12 | // See: https://github.com/gregberge/svgr 13 | svgr: false, 14 | }, 15 | reactStrictMode: true, 16 | experimental: { 17 | appDir: true, 18 | serverActions: true, 19 | }, 20 | images: { 21 | remotePatterns: [ 22 | { 23 | protocol: "https", 24 | hostname: "raw.githubusercontent.com", 25 | port: "", 26 | pathname: "/PokeAPI/sprites/master/sprites/pokemon/**", 27 | }, 28 | ], 29 | }, 30 | }; 31 | 32 | const plugins = [ 33 | // Add more Next.js plugins to this list if needed. 34 | withNx, 35 | ]; 36 | 37 | module.exports = composePlugins(...plugins)(nextConfig); 38 | -------------------------------------------------------------------------------- /apps/web/postcss.config.js: -------------------------------------------------------------------------------- 1 | const { join } = require("path"); 2 | 3 | // Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build 4 | // option from your application's configuration (i.e. project.json). 5 | // 6 | // See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries 7 | 8 | module.exports = { 9 | plugins: { 10 | tailwindcss: { 11 | config: join(__dirname, "tailwind.config.js"), 12 | }, 13 | autoprefixer: {}, 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /apps/web/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "apps/web", 5 | "projectType": "application", 6 | "targets": { 7 | "build": { 8 | "executor": "@nx/next:build", 9 | "outputs": ["{options.outputPath}"], 10 | "defaultConfiguration": "production", 11 | "options": { 12 | "root": "apps/web", 13 | "outputPath": "dist/apps/web" 14 | }, 15 | "configurations": { 16 | "development": { 17 | "outputPath": "apps/web" 18 | }, 19 | "production": {} 20 | } 21 | }, 22 | "serve": { 23 | "executor": "@nx/next:server", 24 | "defaultConfiguration": "development", 25 | "options": { 26 | "buildTarget": "web:build", 27 | "dev": true 28 | }, 29 | "configurations": { 30 | "development": { 31 | "buildTarget": "web:build:development", 32 | "dev": true 33 | }, 34 | "production": { 35 | "buildTarget": "web:build:production", 36 | "dev": false 37 | } 38 | } 39 | }, 40 | "export": { 41 | "executor": "@nx/next:export", 42 | "options": { 43 | "buildTarget": "web:build:production" 44 | } 45 | }, 46 | "test": { 47 | "executor": "@nx/jest:jest", 48 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 49 | "options": { 50 | "jestConfig": "apps/web/jest.config.ts", 51 | "passWithNoTests": true 52 | }, 53 | "configurations": { 54 | "ci": { 55 | "ci": true, 56 | "codeCoverage": true 57 | } 58 | } 59 | }, 60 | "lint": { 61 | "executor": "@nx/linter:eslint", 62 | "outputs": ["{options.outputFile}"], 63 | "options": { 64 | "lintFilePatterns": ["apps/web/**/*.{ts,tsx,js,jsx}"] 65 | } 66 | } 67 | }, 68 | "tags": ["type:feature"] 69 | } 70 | -------------------------------------------------------------------------------- /apps/web/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/web/public/.gitkeep -------------------------------------------------------------------------------- /apps/web/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/web/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /apps/web/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/web/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /apps/web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /apps/web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /apps/web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Pokémon app with Next.js 13 and Drizzle", 3 | "short_name": "Pokémon", 4 | "icons": [ 5 | { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, 6 | { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } 7 | ], 8 | "theme_color": "#ffffff", 9 | "background_color": "#ffffff", 10 | "display": "standalone" 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/specs/index.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | 4 | import Index from '../pages/index'; 5 | 6 | describe('Index', () => { 7 | it('should render successfully', () => { 8 | const { baseElement } = render(); 9 | expect(baseElement).toBeTruthy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require("tailwindcss/defaultTheme"); 2 | const { createGlobPatternsForDependencies } = require("@nrwl/react/tailwind"); 3 | const { join } = require("path"); 4 | 5 | /** @type {import('tailwindcss').Config} */ 6 | module.exports = { 7 | content: [ 8 | join(__dirname, "{src,app,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}"), 9 | ...createGlobPatternsForDependencies(__dirname), 10 | ], 11 | darkMode: ["class"], 12 | theme: { 13 | extend: { 14 | colors: { 15 | border: "hsl(var(--border))", 16 | input: "hsl(var(--input))", 17 | ring: "hsl(var(--ring))", 18 | background: "hsl(var(--background))", 19 | foreground: "hsl(var(--foreground))", 20 | primary: { 21 | DEFAULT: "hsl(var(--primary))", 22 | foreground: "hsl(var(--primary-foreground))", 23 | }, 24 | secondary: { 25 | DEFAULT: "hsl(var(--secondary))", 26 | foreground: "hsl(var(--secondary-foreground))", 27 | }, 28 | destructive: { 29 | DEFAULT: "hsl(var(--destructive))", 30 | foreground: "hsl(var(--destructive-foreground))", 31 | }, 32 | muted: { 33 | DEFAULT: "hsl(var(--muted))", 34 | foreground: "hsl(var(--muted-foreground))", 35 | }, 36 | accent: { 37 | DEFAULT: "hsl(var(--accent))", 38 | foreground: "hsl(var(--accent-foreground))", 39 | }, 40 | popover: { 41 | DEFAULT: "hsl(var(--popover))", 42 | foreground: "hsl(var(--popover-foreground))", 43 | }, 44 | card: { 45 | DEFAULT: "hsl(var(--card))", 46 | foreground: "hsl(var(--card-foreground))", 47 | }, 48 | }, 49 | borderRadius: { 50 | lg: `var(--radius)`, 51 | md: `calc(var(--radius) - 2px)`, 52 | sm: "calc(var(--radius) - 4px)", 53 | }, 54 | fontFamily: { 55 | sans: ["var(--font-inter)", ...fontFamily.sans], 56 | }, 57 | keyframes: { 58 | "accordion-down": { 59 | from: { height: 0 }, 60 | to: { height: "var(--radix-accordion-content-height)" }, 61 | }, 62 | "accordion-up": { 63 | from: { height: "var(--radix-accordion-content-height)" }, 64 | to: { height: 0 }, 65 | }, 66 | }, 67 | animation: { 68 | "accordion-down": "accordion-down 0.2s ease-out", 69 | "accordion-up": "accordion-up 0.2s ease-out", 70 | }, 71 | }, 72 | }, 73 | plugins: [ 74 | require("@tailwindcss/forms"), 75 | require("windy-radix-palette"), 76 | require("@tailwindcss/typography"), 77 | require("windy-radix-typography"), 78 | require("tailwindcss-animate"), 79 | ], 80 | }; 81 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true, 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "incremental": true, 14 | "plugins": [ 15 | { 16 | "name": "next" 17 | } 18 | ], 19 | "types": ["jest", "node"] 20 | }, 21 | "include": [ 22 | "**/*.ts", 23 | "**/*.tsx", 24 | "**/*.js", 25 | "**/*.jsx", 26 | "next-env.d.ts", 27 | "../../apps/web/.next/types/**/*.ts", 28 | "../../dist/apps/web/.next/types/**/*.ts", 29 | "../../types/next-auth.d.ts" 30 | ], 31 | "exclude": ["node_modules", "jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"], 7 | "jsx": "react" 8 | }, 9 | "include": [ 10 | "jest.config.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.ts", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.tsx", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.js", 17 | "src/**/*.test.jsx", 18 | "src/**/*.spec.jsx", 19 | "src/**/*.d.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /drizzle.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema": "./libs/db/src/lib/schema.ts", 3 | "out": "./libs/db/src/lib/drizzle", 4 | "breakpoints": true 5 | } 6 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import { getJestProjects } from '@nx/jest'; 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { ...nxPreset }; 4 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amosbastian/pokemon/10d3d2bc467b4a76b70a71d7366e37595cd1c0da/libs/.gitkeep -------------------------------------------------------------------------------- /libs/configuration/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@nx/next/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /libs/configuration/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/configuration/README.md: -------------------------------------------------------------------------------- 1 | # configuration 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | -------------------------------------------------------------------------------- /libs/configuration/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "configuration", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/configuration/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nx/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/configuration/**/*.{ts,tsx,js,jsx}"] 12 | } 13 | } 14 | }, 15 | "tags": ["type:utility"] 16 | } 17 | -------------------------------------------------------------------------------- /libs/configuration/src/index.ts: -------------------------------------------------------------------------------- 1 | // Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities 2 | 3 | export * from "./lib/web"; 4 | -------------------------------------------------------------------------------- /libs/configuration/src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { DrizzleAdapter, db, teamsTable } from "@pokemon/db"; 2 | import type { NextAuthOptions } from "next-auth"; 3 | import GitHubProvider from "next-auth/providers/github"; 4 | 5 | export const nextAuthOptions: NextAuthOptions = { 6 | adapter: DrizzleAdapter(db), 7 | session: { 8 | strategy: "jwt", 9 | }, 10 | callbacks: { 11 | session({ session, token }) { 12 | if (session.user && token.sub) { 13 | session.user.id = token.sub; 14 | } 15 | 16 | return session; 17 | }, 18 | }, 19 | providers: [ 20 | GitHubProvider({ 21 | clientId: process.env.GITHUB_ID as string, 22 | clientSecret: process.env.GITHUB_SECRET as string, 23 | }), 24 | ], 25 | pages: { 26 | signIn: "/sign-in", 27 | newUser: "/", 28 | }, 29 | secret: process.env.NEXTAUTH_SECRET, 30 | events: { 31 | async createUser({ user }) { 32 | const team = await db.insert(teamsTable).values({ userId: user.id }).returning().get(); 33 | 34 | console.log(`Created team: ${team.id}`); 35 | }, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /libs/configuration/src/lib/web.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = 2 | process.env.NODE_ENV === "production" ? "https://pokemon-rsc.vercel.app" : "http://localhost:4200"; 3 | export const BRAND_NAME = "Pokémon RSC"; 4 | export const BRAND_DESCRIPTION = 5 | "A Next.js 13 application built using the new router, server components and server actions, as well as NextAuth.js, Drizzle ORM and Turso in an Nx monorepo"; 6 | -------------------------------------------------------------------------------- /libs/configuration/src/server.ts: -------------------------------------------------------------------------------- 1 | // Use this file to export React server components 2 | export * from "./lib/auth"; 3 | -------------------------------------------------------------------------------- /libs/configuration/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "allowJs": false, 6 | "esModuleInterop": false, 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "files": [], 11 | "include": [], 12 | "references": [ 13 | { 14 | "path": "./tsconfig.lib.json" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /libs/configuration/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": ["../../node_modules/@nx/react/typings/cssmodule.d.ts", "../../node_modules/@nx/next/typings/image.d.ts"], 8 | "exclude": [ 9 | "jest.config.ts", 10 | "src/**/*.spec.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.tsx", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.js", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.jsx", 17 | "src/**/*.test.jsx" 18 | ], 19 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", "../../types/next-auth.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /libs/db/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/db/README.md: -------------------------------------------------------------------------------- 1 | # db 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | -------------------------------------------------------------------------------- /libs/db/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "db", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/db/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nx/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/db/**/*.ts"] 12 | } 13 | }, 14 | "seed": { 15 | "executor": "nx:run-commands", 16 | "configurations": { 17 | "seed": { 18 | "commands": ["ts-node libs/db/src/lib/seed.ts"] 19 | } 20 | } 21 | } 22 | }, 23 | "tags": ["type:utility"] 24 | } 25 | -------------------------------------------------------------------------------- /libs/db/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/adapter"; 2 | export * from "./lib/db"; 3 | export * from "./lib/schema"; 4 | -------------------------------------------------------------------------------- /libs/db/src/lib/adapter.ts: -------------------------------------------------------------------------------- 1 | // Most of the code is taken from: https://github.com/nextauthjs/next-auth/pull/7165 2 | import { and, eq } from "drizzle-orm"; 3 | import type { Adapter, VerificationToken } from "next-auth/adapters"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | import { db } from "./db"; 6 | import { accountsTable, sessionsTable, usersTable, verificationTokensTable } from "./schema"; 7 | 8 | export function DrizzleAdapter(database: typeof db): Adapter { 9 | return { 10 | createUser: (data) => { 11 | return database 12 | .insert(usersTable) 13 | .values({ ...data, id: uuidv4() }) 14 | .returning() 15 | .get(); 16 | }, 17 | getUser: (id) => { 18 | return database.select().from(usersTable).where(eq(usersTable.id, id)).get() ?? null; 19 | }, 20 | getUserByEmail: (email) => { 21 | return database.select().from(usersTable).where(eq(usersTable.email, email)).get() ?? null; 22 | }, 23 | createSession: (session) => { 24 | return database.insert(sessionsTable).values(session).returning().get(); 25 | }, 26 | getSessionAndUser: (sessionToken) => { 27 | return ( 28 | database 29 | .select({ 30 | session: sessionsTable, 31 | user: usersTable, 32 | }) 33 | .from(sessionsTable) 34 | .where(eq(sessionsTable.sessionToken, sessionToken)) 35 | .innerJoin(usersTable, eq(usersTable.id, sessionsTable.userId)) 36 | .get() ?? null 37 | ); 38 | }, 39 | updateUser: (user) => { 40 | return database.update(usersTable).set(user).where(eq(usersTable.id, user.id)).returning().get(); 41 | }, 42 | updateSession: (session) => { 43 | return database 44 | .update(sessionsTable) 45 | .set(session) 46 | .where(eq(sessionsTable.sessionToken, session.sessionToken)) 47 | .returning() 48 | .get(); 49 | }, 50 | linkAccount: async (rawAccount) => { 51 | const updatedAccount = await database.insert(accountsTable).values(rawAccount).returning().get(); 52 | 53 | const account: ReturnType = { 54 | ...updatedAccount, 55 | access_token: updatedAccount.access_token ?? undefined, 56 | token_type: updatedAccount.token_type ?? undefined, 57 | id_token: updatedAccount.id_token ?? undefined, 58 | refresh_token: updatedAccount.refresh_token ?? undefined, 59 | scope: updatedAccount.scope ?? undefined, 60 | expires_at: updatedAccount.expires_at ?? undefined, 61 | session_state: updatedAccount.session_state ?? undefined, 62 | }; 63 | 64 | return account; 65 | }, 66 | getUserByAccount: (account) => { 67 | return ( 68 | database 69 | .select({ 70 | id: usersTable.id, 71 | email: usersTable.email, 72 | emailVerified: usersTable.emailVerified, 73 | image: usersTable.image, 74 | name: usersTable.name, 75 | }) 76 | .from(usersTable) 77 | .innerJoin( 78 | accountsTable, 79 | and( 80 | eq(accountsTable.providerAccountId, account.providerAccountId), 81 | eq(accountsTable.provider, account.provider), 82 | ), 83 | ) 84 | .get() ?? null 85 | ); 86 | }, 87 | deleteSession: (sessionToken) => { 88 | return ( 89 | database.delete(sessionsTable).where(eq(sessionsTable.sessionToken, sessionToken)).returning().get() ?? null 90 | ); 91 | }, 92 | createVerificationToken: (verificationToken) => { 93 | return database.insert(verificationTokensTable).values(verificationToken).returning().get(); 94 | }, 95 | useVerificationToken: async (verificationToken) => { 96 | try { 97 | return (database 98 | .delete(verificationTokensTable) 99 | .where( 100 | and( 101 | eq(verificationTokensTable.identifier, verificationToken.identifier), 102 | eq(verificationTokensTable.token, verificationToken.token), 103 | ), 104 | ) 105 | .returning() 106 | .get() ?? null) as Promise; 107 | } catch { 108 | throw new Error("No verification token found."); 109 | } 110 | }, 111 | deleteUser: (id) => { 112 | return database.delete(usersTable).where(eq(usersTable.id, id)).returning().get(); 113 | }, 114 | unlinkAccount: (account) => { 115 | database 116 | .delete(accountsTable) 117 | .where( 118 | and( 119 | eq(accountsTable.providerAccountId, account.providerAccountId), 120 | eq(accountsTable.provider, account.provider), 121 | ), 122 | ) 123 | .run(); 124 | 125 | return undefined; 126 | }, 127 | }; 128 | } 129 | -------------------------------------------------------------------------------- /libs/db/src/lib/db.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | 3 | import { createClient } from "@libsql/client"; 4 | import { InferModel, and, eq, placeholder, sql } from "drizzle-orm"; 5 | import { drizzle } from "drizzle-orm/libsql"; 6 | import { migrate } from "drizzle-orm/libsql/migrator"; 7 | import { pokemonTable, pokemonTeamsTable, pokemonTypesTable, teamsTable, typesTable } from "./schema"; 8 | 9 | type Pokemon = InferModel; 10 | type Type = InferModel; 11 | 12 | const client = createClient({ 13 | url: process.env["DATABASE_URL"] as string, 14 | authToken: process.env["DATABASE_AUTH_TOKEN"] as string, 15 | }); 16 | 17 | export const db = drizzle(client); 18 | 19 | if (process.env["NODE_ENV"] !== "production") { 20 | migrate(db, { migrationsFolder: "./libs/db/src/lib/drizzle" }); 21 | } 22 | 23 | export const getSinglePokemon = async (id: number) => { 24 | const rows = await db 25 | .select({ 26 | pokemon: pokemonTable, 27 | type: typesTable, 28 | }) 29 | .from(pokemonTable) 30 | .leftJoin(pokemonTypesTable, eq(pokemonTypesTable.pokemonId, pokemonTable.id)) 31 | .leftJoin(typesTable, eq(typesTable.id, pokemonTypesTable.typeId)) 32 | .where(eq(pokemonTable.id, id)) 33 | .all(); 34 | 35 | const result = rows.reduce>((accumulator, row) => { 36 | const pokemon = row.pokemon; 37 | const type = row.type; 38 | 39 | if (!accumulator[pokemon.id]) { 40 | accumulator[pokemon.id] = { pokemon, types: [] }; 41 | } 42 | 43 | if (type) { 44 | accumulator[pokemon.id].types.push(type); 45 | } 46 | 47 | return accumulator; 48 | }, {}); 49 | 50 | return result[id] ? result[id] : null; 51 | }; 52 | 53 | interface GetAllPokemonParams { 54 | search?: string; 55 | type?: string; 56 | } 57 | 58 | export const getAllPokemon = async (params?: GetAllPokemonParams) => { 59 | const { search, type } = params ?? {}; 60 | 61 | let pokemonList: { pokemon: Pokemon; type: Type | null }[]; 62 | 63 | let query = db 64 | .select({ 65 | pokemon: pokemonTable, 66 | type: typesTable, 67 | }) 68 | .from(pokemonTable) 69 | .leftJoin(pokemonTypesTable, eq(pokemonTypesTable.pokemonId, pokemonTable.id)) 70 | .leftJoin(typesTable, eq(pokemonTypesTable.typeId, typesTable.id)); 71 | 72 | if (type) { 73 | query = query.where(eq(typesTable.name, type)); 74 | } 75 | 76 | // TODO: figure out better where to do conditional .where() 77 | if (search) { 78 | if (type) { 79 | query = query.where(and(sql`lower(${pokemonTable.name}) like ${placeholder("name")}`, eq(typesTable.name, type))); 80 | } else { 81 | query = query.where(sql`lower(${pokemonTable.name}) like ${placeholder("name")}`); 82 | } 83 | pokemonList = await query.prepare().all({ name: `%${search}%` }); 84 | } else { 85 | pokemonList = await query.all(); 86 | } 87 | 88 | const reducedPokemonList = pokemonList.reduce>( 89 | (accumulator, row) => { 90 | const pokemon = row.pokemon; 91 | const type = row.type; 92 | 93 | if (!accumulator[pokemon.id]) { 94 | accumulator[pokemon.id] = { pokemon, types: [] }; 95 | } 96 | 97 | if (type) { 98 | accumulator[pokemon.id].types.push(type); 99 | } 100 | 101 | return accumulator; 102 | }, 103 | {}, 104 | ); 105 | 106 | return Object.values(reducedPokemonList); 107 | }; 108 | 109 | export const getUserTeam = async (userId: string) => { 110 | const rows = await db 111 | .select({ 112 | team: teamsTable, 113 | pokemon: pokemonTable, 114 | position: pokemonTeamsTable.position, 115 | }) 116 | .from(teamsTable) 117 | .where(eq(teamsTable.userId, userId)) 118 | .leftJoin(pokemonTeamsTable, eq(pokemonTeamsTable.teamId, teamsTable.id)) 119 | .leftJoin(pokemonTable, eq(pokemonTable.id, pokemonTeamsTable.pokemonId)) 120 | .all(); 121 | 122 | const pokemon = rows.reduce<(Pokemon & { position: number })[]>((accumulator, row) => { 123 | const pokemon = row.pokemon; 124 | const position = row.position; 125 | 126 | if (pokemon && position) { 127 | accumulator.push({ ...pokemon, position }); 128 | } 129 | 130 | return accumulator; 131 | }, []); 132 | 133 | return { 134 | ...rows[0].team, 135 | pokemon, 136 | }; 137 | }; 138 | -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/0000_dizzy_red_ghost.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `pokemon` ( 2 | `id` integer PRIMARY KEY NOT NULL, 3 | `name` text NOT NULL, 4 | `weight` integer NOT NULL, 5 | `height` integer NOT NULL, 6 | `hp` integer NOT NULL, 7 | `attack` integer NOT NULL, 8 | `defense` integer NOT NULL, 9 | `special_attack` integer NOT NULL, 10 | `special_defense` integer NOT NULL, 11 | `speed` integer NOT NULL, 12 | `sprite` text NOT NULL 13 | ); 14 | --> statement-breakpoint 15 | CREATE TABLE `pokemon_types` ( 16 | `pokemon_id` integer NOT NULL, 17 | `type_id` integer NOT NULL, 18 | FOREIGN KEY (`pokemon_id`) REFERENCES `pokemon`(`id`), 19 | FOREIGN KEY (`type_id`) REFERENCES `types`(`id`) 20 | ); 21 | --> statement-breakpoint 22 | CREATE TABLE `types` ( 23 | `id` integer PRIMARY KEY NOT NULL, 24 | `name` text NOT NULL 25 | ); 26 | -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/0001_light_night_nurse.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `accounts` ( 2 | `userId` text NOT NULL, 3 | `type` text NOT NULL, 4 | `provider` text NOT NULL, 5 | `providerAccountId` text NOT NULL, 6 | `refresh_token` text, 7 | `access_token` text, 8 | `expires_at` integer, 9 | `token_type` text, 10 | `scope` text, 11 | `id_token` text, 12 | `session_state` text, 13 | PRIMARY KEY(`provider`, `providerAccountId`), 14 | FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE cascade 15 | ); 16 | --> statement-breakpoint 17 | CREATE TABLE `sessions` ( 18 | `sessionToken` text PRIMARY KEY NOT NULL, 19 | `userId` text NOT NULL, 20 | `expires` integer NOT NULL, 21 | FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE cascade 22 | ); 23 | --> statement-breakpoint 24 | CREATE TABLE `users` ( 25 | `id` text PRIMARY KEY NOT NULL, 26 | `name` text, 27 | `email` text NOT NULL, 28 | `emailVerified` integer, 29 | `image` text 30 | ); 31 | --> statement-breakpoint 32 | CREATE TABLE `verificationToken` ( 33 | `identifier` text NOT NULL, 34 | `token` text NOT NULL, 35 | `expires` integer NOT NULL, 36 | PRIMARY KEY(`identifier`, `token`) 37 | ); 38 | -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/0002_simple_nightcrawler.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `pokemon_teams` ( 2 | `team_id` integer NOT NULL, 3 | `pokemon_id` integer NOT NULL, 4 | FOREIGN KEY (`team_id`) REFERENCES `teams`(`id`), 5 | FOREIGN KEY (`pokemon_id`) REFERENCES `pokemon`(`id`) 6 | ); 7 | --> statement-breakpoint 8 | CREATE TABLE `teams` ( 9 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 10 | `user_id` text NOT NULL, 11 | FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) 12 | ); 13 | -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/0003_silly_diamondback.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE pokemon_teams ADD `position` integer NOT NULL; -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "sqlite", 4 | "id": "974d91fc-d3b5-4a6a-a02d-2ff49b98db5a", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "pokemon": { 8 | "name": "pokemon", 9 | "columns": { 10 | "id": { 11 | "name": "id", 12 | "type": "integer", 13 | "primaryKey": true, 14 | "notNull": true, 15 | "autoincrement": false 16 | }, 17 | "name": { 18 | "name": "name", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "weight": { 24 | "name": "weight", 25 | "type": "integer", 26 | "primaryKey": false, 27 | "notNull": true, 28 | "autoincrement": false 29 | }, 30 | "height": { 31 | "name": "height", 32 | "type": "integer", 33 | "primaryKey": false, 34 | "notNull": true, 35 | "autoincrement": false 36 | }, 37 | "hp": { 38 | "name": "hp", 39 | "type": "integer", 40 | "primaryKey": false, 41 | "notNull": true, 42 | "autoincrement": false 43 | }, 44 | "attack": { 45 | "name": "attack", 46 | "type": "integer", 47 | "primaryKey": false, 48 | "notNull": true, 49 | "autoincrement": false 50 | }, 51 | "defense": { 52 | "name": "defense", 53 | "type": "integer", 54 | "primaryKey": false, 55 | "notNull": true, 56 | "autoincrement": false 57 | }, 58 | "special_attack": { 59 | "name": "special_attack", 60 | "type": "integer", 61 | "primaryKey": false, 62 | "notNull": true, 63 | "autoincrement": false 64 | }, 65 | "special_defense": { 66 | "name": "special_defense", 67 | "type": "integer", 68 | "primaryKey": false, 69 | "notNull": true, 70 | "autoincrement": false 71 | }, 72 | "speed": { 73 | "name": "speed", 74 | "type": "integer", 75 | "primaryKey": false, 76 | "notNull": true, 77 | "autoincrement": false 78 | }, 79 | "sprite": { 80 | "name": "sprite", 81 | "type": "text", 82 | "primaryKey": false, 83 | "notNull": true 84 | } 85 | }, 86 | "indexes": {}, 87 | "foreignKeys": {}, 88 | "compositePrimaryKeys": {} 89 | }, 90 | "pokemon_types": { 91 | "name": "pokemon_types", 92 | "columns": { 93 | "pokemon_id": { 94 | "name": "pokemon_id", 95 | "type": "integer", 96 | "primaryKey": false, 97 | "notNull": true, 98 | "autoincrement": false 99 | }, 100 | "type_id": { 101 | "name": "type_id", 102 | "type": "integer", 103 | "primaryKey": false, 104 | "notNull": true, 105 | "autoincrement": false 106 | } 107 | }, 108 | "indexes": {}, 109 | "foreignKeys": { 110 | "pokemon_types_pokemon_id_pokemon_id_fk": { 111 | "name": "pokemon_types_pokemon_id_pokemon_id_fk", 112 | "tableFrom": "pokemon_types", 113 | "tableTo": "pokemon", 114 | "columnsFrom": [ 115 | "pokemon_id" 116 | ], 117 | "columnsTo": [ 118 | "id" 119 | ] 120 | }, 121 | "pokemon_types_type_id_types_id_fk": { 122 | "name": "pokemon_types_type_id_types_id_fk", 123 | "tableFrom": "pokemon_types", 124 | "tableTo": "types", 125 | "columnsFrom": [ 126 | "type_id" 127 | ], 128 | "columnsTo": [ 129 | "id" 130 | ] 131 | } 132 | }, 133 | "compositePrimaryKeys": {} 134 | }, 135 | "types": { 136 | "name": "types", 137 | "columns": { 138 | "id": { 139 | "name": "id", 140 | "type": "integer", 141 | "primaryKey": true, 142 | "notNull": true, 143 | "autoincrement": false 144 | }, 145 | "name": { 146 | "name": "name", 147 | "type": "text", 148 | "primaryKey": false, 149 | "notNull": true 150 | } 151 | }, 152 | "indexes": {}, 153 | "foreignKeys": {}, 154 | "compositePrimaryKeys": {} 155 | } 156 | }, 157 | "enums": {}, 158 | "_meta": { 159 | "schemas": {}, 160 | "tables": {}, 161 | "columns": {} 162 | } 163 | } -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "sqlite", 4 | "id": "3ce01027-a2f1-4c87-8611-2be9d5699bb2", 5 | "prevId": "974d91fc-d3b5-4a6a-a02d-2ff49b98db5a", 6 | "tables": { 7 | "accounts": { 8 | "name": "accounts", 9 | "columns": { 10 | "userId": { 11 | "name": "userId", 12 | "type": "text", 13 | "primaryKey": false, 14 | "notNull": true 15 | }, 16 | "type": { 17 | "name": "type", 18 | "type": "text", 19 | "primaryKey": false, 20 | "notNull": true 21 | }, 22 | "provider": { 23 | "name": "provider", 24 | "type": "text", 25 | "primaryKey": false, 26 | "notNull": true 27 | }, 28 | "providerAccountId": { 29 | "name": "providerAccountId", 30 | "type": "text", 31 | "primaryKey": false, 32 | "notNull": true 33 | }, 34 | "refresh_token": { 35 | "name": "refresh_token", 36 | "type": "text", 37 | "primaryKey": false, 38 | "notNull": false 39 | }, 40 | "access_token": { 41 | "name": "access_token", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": false 45 | }, 46 | "expires_at": { 47 | "name": "expires_at", 48 | "type": "integer", 49 | "primaryKey": false, 50 | "notNull": false, 51 | "autoincrement": false 52 | }, 53 | "token_type": { 54 | "name": "token_type", 55 | "type": "text", 56 | "primaryKey": false, 57 | "notNull": false 58 | }, 59 | "scope": { 60 | "name": "scope", 61 | "type": "text", 62 | "primaryKey": false, 63 | "notNull": false 64 | }, 65 | "id_token": { 66 | "name": "id_token", 67 | "type": "text", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "session_state": { 72 | "name": "session_state", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": false 76 | } 77 | }, 78 | "indexes": {}, 79 | "foreignKeys": { 80 | "accounts_userId_users_id_fk": { 81 | "name": "accounts_userId_users_id_fk", 82 | "tableFrom": "accounts", 83 | "tableTo": "users", 84 | "columnsFrom": [ 85 | "userId" 86 | ], 87 | "columnsTo": [ 88 | "id" 89 | ], 90 | "onDelete": "cascade" 91 | } 92 | }, 93 | "compositePrimaryKeys": { 94 | "accounts_provider_providerAccountId_pk": { 95 | "columns": [ 96 | "provider", 97 | "providerAccountId" 98 | ] 99 | } 100 | } 101 | }, 102 | "pokemon": { 103 | "name": "pokemon", 104 | "columns": { 105 | "id": { 106 | "name": "id", 107 | "type": "integer", 108 | "primaryKey": true, 109 | "notNull": true, 110 | "autoincrement": false 111 | }, 112 | "name": { 113 | "name": "name", 114 | "type": "text", 115 | "primaryKey": false, 116 | "notNull": true 117 | }, 118 | "weight": { 119 | "name": "weight", 120 | "type": "integer", 121 | "primaryKey": false, 122 | "notNull": true, 123 | "autoincrement": false 124 | }, 125 | "height": { 126 | "name": "height", 127 | "type": "integer", 128 | "primaryKey": false, 129 | "notNull": true, 130 | "autoincrement": false 131 | }, 132 | "hp": { 133 | "name": "hp", 134 | "type": "integer", 135 | "primaryKey": false, 136 | "notNull": true, 137 | "autoincrement": false 138 | }, 139 | "attack": { 140 | "name": "attack", 141 | "type": "integer", 142 | "primaryKey": false, 143 | "notNull": true, 144 | "autoincrement": false 145 | }, 146 | "defense": { 147 | "name": "defense", 148 | "type": "integer", 149 | "primaryKey": false, 150 | "notNull": true, 151 | "autoincrement": false 152 | }, 153 | "special_attack": { 154 | "name": "special_attack", 155 | "type": "integer", 156 | "primaryKey": false, 157 | "notNull": true, 158 | "autoincrement": false 159 | }, 160 | "special_defense": { 161 | "name": "special_defense", 162 | "type": "integer", 163 | "primaryKey": false, 164 | "notNull": true, 165 | "autoincrement": false 166 | }, 167 | "speed": { 168 | "name": "speed", 169 | "type": "integer", 170 | "primaryKey": false, 171 | "notNull": true, 172 | "autoincrement": false 173 | }, 174 | "sprite": { 175 | "name": "sprite", 176 | "type": "text", 177 | "primaryKey": false, 178 | "notNull": true 179 | } 180 | }, 181 | "indexes": {}, 182 | "foreignKeys": {}, 183 | "compositePrimaryKeys": {} 184 | }, 185 | "pokemon_types": { 186 | "name": "pokemon_types", 187 | "columns": { 188 | "pokemon_id": { 189 | "name": "pokemon_id", 190 | "type": "integer", 191 | "primaryKey": false, 192 | "notNull": true, 193 | "autoincrement": false 194 | }, 195 | "type_id": { 196 | "name": "type_id", 197 | "type": "integer", 198 | "primaryKey": false, 199 | "notNull": true, 200 | "autoincrement": false 201 | } 202 | }, 203 | "indexes": {}, 204 | "foreignKeys": { 205 | "pokemon_types_pokemon_id_pokemon_id_fk": { 206 | "name": "pokemon_types_pokemon_id_pokemon_id_fk", 207 | "tableFrom": "pokemon_types", 208 | "tableTo": "pokemon", 209 | "columnsFrom": [ 210 | "pokemon_id" 211 | ], 212 | "columnsTo": [ 213 | "id" 214 | ] 215 | }, 216 | "pokemon_types_type_id_types_id_fk": { 217 | "name": "pokemon_types_type_id_types_id_fk", 218 | "tableFrom": "pokemon_types", 219 | "tableTo": "types", 220 | "columnsFrom": [ 221 | "type_id" 222 | ], 223 | "columnsTo": [ 224 | "id" 225 | ] 226 | } 227 | }, 228 | "compositePrimaryKeys": {} 229 | }, 230 | "sessions": { 231 | "name": "sessions", 232 | "columns": { 233 | "sessionToken": { 234 | "name": "sessionToken", 235 | "type": "text", 236 | "primaryKey": true, 237 | "notNull": true 238 | }, 239 | "userId": { 240 | "name": "userId", 241 | "type": "text", 242 | "primaryKey": false, 243 | "notNull": true 244 | }, 245 | "expires": { 246 | "name": "expires", 247 | "type": "integer", 248 | "primaryKey": false, 249 | "notNull": true, 250 | "autoincrement": false 251 | } 252 | }, 253 | "indexes": {}, 254 | "foreignKeys": { 255 | "sessions_userId_users_id_fk": { 256 | "name": "sessions_userId_users_id_fk", 257 | "tableFrom": "sessions", 258 | "tableTo": "users", 259 | "columnsFrom": [ 260 | "userId" 261 | ], 262 | "columnsTo": [ 263 | "id" 264 | ], 265 | "onDelete": "cascade" 266 | } 267 | }, 268 | "compositePrimaryKeys": {} 269 | }, 270 | "types": { 271 | "name": "types", 272 | "columns": { 273 | "id": { 274 | "name": "id", 275 | "type": "integer", 276 | "primaryKey": true, 277 | "notNull": true, 278 | "autoincrement": false 279 | }, 280 | "name": { 281 | "name": "name", 282 | "type": "text", 283 | "primaryKey": false, 284 | "notNull": true 285 | } 286 | }, 287 | "indexes": {}, 288 | "foreignKeys": {}, 289 | "compositePrimaryKeys": {} 290 | }, 291 | "users": { 292 | "name": "users", 293 | "columns": { 294 | "id": { 295 | "name": "id", 296 | "type": "text", 297 | "primaryKey": true, 298 | "notNull": true 299 | }, 300 | "name": { 301 | "name": "name", 302 | "type": "text", 303 | "primaryKey": false, 304 | "notNull": false 305 | }, 306 | "email": { 307 | "name": "email", 308 | "type": "text", 309 | "primaryKey": false, 310 | "notNull": true 311 | }, 312 | "emailVerified": { 313 | "name": "emailVerified", 314 | "type": "integer", 315 | "primaryKey": false, 316 | "notNull": false, 317 | "autoincrement": false 318 | }, 319 | "image": { 320 | "name": "image", 321 | "type": "text", 322 | "primaryKey": false, 323 | "notNull": false 324 | } 325 | }, 326 | "indexes": {}, 327 | "foreignKeys": {}, 328 | "compositePrimaryKeys": {} 329 | }, 330 | "verificationToken": { 331 | "name": "verificationToken", 332 | "columns": { 333 | "identifier": { 334 | "name": "identifier", 335 | "type": "text", 336 | "primaryKey": false, 337 | "notNull": true 338 | }, 339 | "token": { 340 | "name": "token", 341 | "type": "text", 342 | "primaryKey": false, 343 | "notNull": true 344 | }, 345 | "expires": { 346 | "name": "expires", 347 | "type": "integer", 348 | "primaryKey": false, 349 | "notNull": true, 350 | "autoincrement": false 351 | } 352 | }, 353 | "indexes": {}, 354 | "foreignKeys": {}, 355 | "compositePrimaryKeys": { 356 | "verificationToken_identifier_token_pk": { 357 | "columns": [ 358 | "identifier", 359 | "token" 360 | ] 361 | } 362 | } 363 | } 364 | }, 365 | "enums": {}, 366 | "_meta": { 367 | "schemas": {}, 368 | "tables": {}, 369 | "columns": {} 370 | } 371 | } -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/meta/0002_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "sqlite", 4 | "id": "4717c693-8cf1-4d6e-b268-e7cb93a4f746", 5 | "prevId": "3ce01027-a2f1-4c87-8611-2be9d5699bb2", 6 | "tables": { 7 | "accounts": { 8 | "name": "accounts", 9 | "columns": { 10 | "userId": { 11 | "name": "userId", 12 | "type": "text", 13 | "primaryKey": false, 14 | "notNull": true 15 | }, 16 | "type": { 17 | "name": "type", 18 | "type": "text", 19 | "primaryKey": false, 20 | "notNull": true 21 | }, 22 | "provider": { 23 | "name": "provider", 24 | "type": "text", 25 | "primaryKey": false, 26 | "notNull": true 27 | }, 28 | "providerAccountId": { 29 | "name": "providerAccountId", 30 | "type": "text", 31 | "primaryKey": false, 32 | "notNull": true 33 | }, 34 | "refresh_token": { 35 | "name": "refresh_token", 36 | "type": "text", 37 | "primaryKey": false, 38 | "notNull": false 39 | }, 40 | "access_token": { 41 | "name": "access_token", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": false 45 | }, 46 | "expires_at": { 47 | "name": "expires_at", 48 | "type": "integer", 49 | "primaryKey": false, 50 | "notNull": false, 51 | "autoincrement": false 52 | }, 53 | "token_type": { 54 | "name": "token_type", 55 | "type": "text", 56 | "primaryKey": false, 57 | "notNull": false 58 | }, 59 | "scope": { 60 | "name": "scope", 61 | "type": "text", 62 | "primaryKey": false, 63 | "notNull": false 64 | }, 65 | "id_token": { 66 | "name": "id_token", 67 | "type": "text", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "session_state": { 72 | "name": "session_state", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": false 76 | } 77 | }, 78 | "indexes": {}, 79 | "foreignKeys": { 80 | "accounts_userId_users_id_fk": { 81 | "name": "accounts_userId_users_id_fk", 82 | "tableFrom": "accounts", 83 | "tableTo": "users", 84 | "columnsFrom": [ 85 | "userId" 86 | ], 87 | "columnsTo": [ 88 | "id" 89 | ], 90 | "onDelete": "cascade" 91 | } 92 | }, 93 | "compositePrimaryKeys": { 94 | "accounts_provider_providerAccountId_pk": { 95 | "columns": [ 96 | "provider", 97 | "providerAccountId" 98 | ] 99 | } 100 | } 101 | }, 102 | "pokemon": { 103 | "name": "pokemon", 104 | "columns": { 105 | "id": { 106 | "name": "id", 107 | "type": "integer", 108 | "primaryKey": true, 109 | "notNull": true, 110 | "autoincrement": false 111 | }, 112 | "name": { 113 | "name": "name", 114 | "type": "text", 115 | "primaryKey": false, 116 | "notNull": true 117 | }, 118 | "weight": { 119 | "name": "weight", 120 | "type": "integer", 121 | "primaryKey": false, 122 | "notNull": true, 123 | "autoincrement": false 124 | }, 125 | "height": { 126 | "name": "height", 127 | "type": "integer", 128 | "primaryKey": false, 129 | "notNull": true, 130 | "autoincrement": false 131 | }, 132 | "hp": { 133 | "name": "hp", 134 | "type": "integer", 135 | "primaryKey": false, 136 | "notNull": true, 137 | "autoincrement": false 138 | }, 139 | "attack": { 140 | "name": "attack", 141 | "type": "integer", 142 | "primaryKey": false, 143 | "notNull": true, 144 | "autoincrement": false 145 | }, 146 | "defense": { 147 | "name": "defense", 148 | "type": "integer", 149 | "primaryKey": false, 150 | "notNull": true, 151 | "autoincrement": false 152 | }, 153 | "special_attack": { 154 | "name": "special_attack", 155 | "type": "integer", 156 | "primaryKey": false, 157 | "notNull": true, 158 | "autoincrement": false 159 | }, 160 | "special_defense": { 161 | "name": "special_defense", 162 | "type": "integer", 163 | "primaryKey": false, 164 | "notNull": true, 165 | "autoincrement": false 166 | }, 167 | "speed": { 168 | "name": "speed", 169 | "type": "integer", 170 | "primaryKey": false, 171 | "notNull": true, 172 | "autoincrement": false 173 | }, 174 | "sprite": { 175 | "name": "sprite", 176 | "type": "text", 177 | "primaryKey": false, 178 | "notNull": true 179 | } 180 | }, 181 | "indexes": {}, 182 | "foreignKeys": {}, 183 | "compositePrimaryKeys": {} 184 | }, 185 | "pokemon_teams": { 186 | "name": "pokemon_teams", 187 | "columns": { 188 | "team_id": { 189 | "name": "team_id", 190 | "type": "integer", 191 | "primaryKey": false, 192 | "notNull": true, 193 | "autoincrement": false 194 | }, 195 | "pokemon_id": { 196 | "name": "pokemon_id", 197 | "type": "integer", 198 | "primaryKey": false, 199 | "notNull": true, 200 | "autoincrement": false 201 | } 202 | }, 203 | "indexes": {}, 204 | "foreignKeys": { 205 | "pokemon_teams_team_id_teams_id_fk": { 206 | "name": "pokemon_teams_team_id_teams_id_fk", 207 | "tableFrom": "pokemon_teams", 208 | "tableTo": "teams", 209 | "columnsFrom": [ 210 | "team_id" 211 | ], 212 | "columnsTo": [ 213 | "id" 214 | ] 215 | }, 216 | "pokemon_teams_pokemon_id_pokemon_id_fk": { 217 | "name": "pokemon_teams_pokemon_id_pokemon_id_fk", 218 | "tableFrom": "pokemon_teams", 219 | "tableTo": "pokemon", 220 | "columnsFrom": [ 221 | "pokemon_id" 222 | ], 223 | "columnsTo": [ 224 | "id" 225 | ] 226 | } 227 | }, 228 | "compositePrimaryKeys": {} 229 | }, 230 | "pokemon_types": { 231 | "name": "pokemon_types", 232 | "columns": { 233 | "pokemon_id": { 234 | "name": "pokemon_id", 235 | "type": "integer", 236 | "primaryKey": false, 237 | "notNull": true, 238 | "autoincrement": false 239 | }, 240 | "type_id": { 241 | "name": "type_id", 242 | "type": "integer", 243 | "primaryKey": false, 244 | "notNull": true, 245 | "autoincrement": false 246 | } 247 | }, 248 | "indexes": {}, 249 | "foreignKeys": { 250 | "pokemon_types_pokemon_id_pokemon_id_fk": { 251 | "name": "pokemon_types_pokemon_id_pokemon_id_fk", 252 | "tableFrom": "pokemon_types", 253 | "tableTo": "pokemon", 254 | "columnsFrom": [ 255 | "pokemon_id" 256 | ], 257 | "columnsTo": [ 258 | "id" 259 | ] 260 | }, 261 | "pokemon_types_type_id_types_id_fk": { 262 | "name": "pokemon_types_type_id_types_id_fk", 263 | "tableFrom": "pokemon_types", 264 | "tableTo": "types", 265 | "columnsFrom": [ 266 | "type_id" 267 | ], 268 | "columnsTo": [ 269 | "id" 270 | ] 271 | } 272 | }, 273 | "compositePrimaryKeys": {} 274 | }, 275 | "sessions": { 276 | "name": "sessions", 277 | "columns": { 278 | "sessionToken": { 279 | "name": "sessionToken", 280 | "type": "text", 281 | "primaryKey": true, 282 | "notNull": true 283 | }, 284 | "userId": { 285 | "name": "userId", 286 | "type": "text", 287 | "primaryKey": false, 288 | "notNull": true 289 | }, 290 | "expires": { 291 | "name": "expires", 292 | "type": "integer", 293 | "primaryKey": false, 294 | "notNull": true, 295 | "autoincrement": false 296 | } 297 | }, 298 | "indexes": {}, 299 | "foreignKeys": { 300 | "sessions_userId_users_id_fk": { 301 | "name": "sessions_userId_users_id_fk", 302 | "tableFrom": "sessions", 303 | "tableTo": "users", 304 | "columnsFrom": [ 305 | "userId" 306 | ], 307 | "columnsTo": [ 308 | "id" 309 | ], 310 | "onDelete": "cascade" 311 | } 312 | }, 313 | "compositePrimaryKeys": {} 314 | }, 315 | "teams": { 316 | "name": "teams", 317 | "columns": { 318 | "id": { 319 | "name": "id", 320 | "type": "integer", 321 | "primaryKey": true, 322 | "notNull": true, 323 | "autoincrement": true 324 | }, 325 | "user_id": { 326 | "name": "user_id", 327 | "type": "text", 328 | "primaryKey": false, 329 | "notNull": true 330 | } 331 | }, 332 | "indexes": {}, 333 | "foreignKeys": { 334 | "teams_user_id_users_id_fk": { 335 | "name": "teams_user_id_users_id_fk", 336 | "tableFrom": "teams", 337 | "tableTo": "users", 338 | "columnsFrom": [ 339 | "user_id" 340 | ], 341 | "columnsTo": [ 342 | "id" 343 | ] 344 | } 345 | }, 346 | "compositePrimaryKeys": {} 347 | }, 348 | "types": { 349 | "name": "types", 350 | "columns": { 351 | "id": { 352 | "name": "id", 353 | "type": "integer", 354 | "primaryKey": true, 355 | "notNull": true, 356 | "autoincrement": false 357 | }, 358 | "name": { 359 | "name": "name", 360 | "type": "text", 361 | "primaryKey": false, 362 | "notNull": true 363 | } 364 | }, 365 | "indexes": {}, 366 | "foreignKeys": {}, 367 | "compositePrimaryKeys": {} 368 | }, 369 | "users": { 370 | "name": "users", 371 | "columns": { 372 | "id": { 373 | "name": "id", 374 | "type": "text", 375 | "primaryKey": true, 376 | "notNull": true 377 | }, 378 | "name": { 379 | "name": "name", 380 | "type": "text", 381 | "primaryKey": false, 382 | "notNull": false 383 | }, 384 | "email": { 385 | "name": "email", 386 | "type": "text", 387 | "primaryKey": false, 388 | "notNull": true 389 | }, 390 | "emailVerified": { 391 | "name": "emailVerified", 392 | "type": "integer", 393 | "primaryKey": false, 394 | "notNull": false, 395 | "autoincrement": false 396 | }, 397 | "image": { 398 | "name": "image", 399 | "type": "text", 400 | "primaryKey": false, 401 | "notNull": false 402 | } 403 | }, 404 | "indexes": {}, 405 | "foreignKeys": {}, 406 | "compositePrimaryKeys": {} 407 | }, 408 | "verificationToken": { 409 | "name": "verificationToken", 410 | "columns": { 411 | "identifier": { 412 | "name": "identifier", 413 | "type": "text", 414 | "primaryKey": false, 415 | "notNull": true 416 | }, 417 | "token": { 418 | "name": "token", 419 | "type": "text", 420 | "primaryKey": false, 421 | "notNull": true 422 | }, 423 | "expires": { 424 | "name": "expires", 425 | "type": "integer", 426 | "primaryKey": false, 427 | "notNull": true, 428 | "autoincrement": false 429 | } 430 | }, 431 | "indexes": {}, 432 | "foreignKeys": {}, 433 | "compositePrimaryKeys": { 434 | "verificationToken_identifier_token_pk": { 435 | "columns": [ 436 | "identifier", 437 | "token" 438 | ] 439 | } 440 | } 441 | } 442 | }, 443 | "enums": {}, 444 | "_meta": { 445 | "schemas": {}, 446 | "tables": {}, 447 | "columns": {} 448 | } 449 | } -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/meta/0003_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "sqlite", 4 | "id": "08d60c55-dc11-4785-8238-cae463a9839c", 5 | "prevId": "4717c693-8cf1-4d6e-b268-e7cb93a4f746", 6 | "tables": { 7 | "accounts": { 8 | "name": "accounts", 9 | "columns": { 10 | "userId": { 11 | "name": "userId", 12 | "type": "text", 13 | "primaryKey": false, 14 | "notNull": true 15 | }, 16 | "type": { 17 | "name": "type", 18 | "type": "text", 19 | "primaryKey": false, 20 | "notNull": true 21 | }, 22 | "provider": { 23 | "name": "provider", 24 | "type": "text", 25 | "primaryKey": false, 26 | "notNull": true 27 | }, 28 | "providerAccountId": { 29 | "name": "providerAccountId", 30 | "type": "text", 31 | "primaryKey": false, 32 | "notNull": true 33 | }, 34 | "refresh_token": { 35 | "name": "refresh_token", 36 | "type": "text", 37 | "primaryKey": false, 38 | "notNull": false 39 | }, 40 | "access_token": { 41 | "name": "access_token", 42 | "type": "text", 43 | "primaryKey": false, 44 | "notNull": false 45 | }, 46 | "expires_at": { 47 | "name": "expires_at", 48 | "type": "integer", 49 | "primaryKey": false, 50 | "notNull": false, 51 | "autoincrement": false 52 | }, 53 | "token_type": { 54 | "name": "token_type", 55 | "type": "text", 56 | "primaryKey": false, 57 | "notNull": false 58 | }, 59 | "scope": { 60 | "name": "scope", 61 | "type": "text", 62 | "primaryKey": false, 63 | "notNull": false 64 | }, 65 | "id_token": { 66 | "name": "id_token", 67 | "type": "text", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "session_state": { 72 | "name": "session_state", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": false 76 | } 77 | }, 78 | "indexes": {}, 79 | "foreignKeys": { 80 | "accounts_userId_users_id_fk": { 81 | "name": "accounts_userId_users_id_fk", 82 | "tableFrom": "accounts", 83 | "tableTo": "users", 84 | "columnsFrom": [ 85 | "userId" 86 | ], 87 | "columnsTo": [ 88 | "id" 89 | ], 90 | "onDelete": "cascade" 91 | } 92 | }, 93 | "compositePrimaryKeys": { 94 | "accounts_provider_providerAccountId_pk": { 95 | "columns": [ 96 | "provider", 97 | "providerAccountId" 98 | ] 99 | } 100 | } 101 | }, 102 | "pokemon": { 103 | "name": "pokemon", 104 | "columns": { 105 | "id": { 106 | "name": "id", 107 | "type": "integer", 108 | "primaryKey": true, 109 | "notNull": true, 110 | "autoincrement": false 111 | }, 112 | "name": { 113 | "name": "name", 114 | "type": "text", 115 | "primaryKey": false, 116 | "notNull": true 117 | }, 118 | "weight": { 119 | "name": "weight", 120 | "type": "integer", 121 | "primaryKey": false, 122 | "notNull": true, 123 | "autoincrement": false 124 | }, 125 | "height": { 126 | "name": "height", 127 | "type": "integer", 128 | "primaryKey": false, 129 | "notNull": true, 130 | "autoincrement": false 131 | }, 132 | "hp": { 133 | "name": "hp", 134 | "type": "integer", 135 | "primaryKey": false, 136 | "notNull": true, 137 | "autoincrement": false 138 | }, 139 | "attack": { 140 | "name": "attack", 141 | "type": "integer", 142 | "primaryKey": false, 143 | "notNull": true, 144 | "autoincrement": false 145 | }, 146 | "defense": { 147 | "name": "defense", 148 | "type": "integer", 149 | "primaryKey": false, 150 | "notNull": true, 151 | "autoincrement": false 152 | }, 153 | "special_attack": { 154 | "name": "special_attack", 155 | "type": "integer", 156 | "primaryKey": false, 157 | "notNull": true, 158 | "autoincrement": false 159 | }, 160 | "special_defense": { 161 | "name": "special_defense", 162 | "type": "integer", 163 | "primaryKey": false, 164 | "notNull": true, 165 | "autoincrement": false 166 | }, 167 | "speed": { 168 | "name": "speed", 169 | "type": "integer", 170 | "primaryKey": false, 171 | "notNull": true, 172 | "autoincrement": false 173 | }, 174 | "sprite": { 175 | "name": "sprite", 176 | "type": "text", 177 | "primaryKey": false, 178 | "notNull": true 179 | } 180 | }, 181 | "indexes": {}, 182 | "foreignKeys": {}, 183 | "compositePrimaryKeys": {} 184 | }, 185 | "pokemon_teams": { 186 | "name": "pokemon_teams", 187 | "columns": { 188 | "team_id": { 189 | "name": "team_id", 190 | "type": "integer", 191 | "primaryKey": false, 192 | "notNull": true, 193 | "autoincrement": false 194 | }, 195 | "pokemon_id": { 196 | "name": "pokemon_id", 197 | "type": "integer", 198 | "primaryKey": false, 199 | "notNull": true, 200 | "autoincrement": false 201 | }, 202 | "position": { 203 | "name": "position", 204 | "type": "integer", 205 | "primaryKey": false, 206 | "notNull": true, 207 | "autoincrement": false 208 | } 209 | }, 210 | "indexes": {}, 211 | "foreignKeys": { 212 | "pokemon_teams_team_id_teams_id_fk": { 213 | "name": "pokemon_teams_team_id_teams_id_fk", 214 | "tableFrom": "pokemon_teams", 215 | "tableTo": "teams", 216 | "columnsFrom": [ 217 | "team_id" 218 | ], 219 | "columnsTo": [ 220 | "id" 221 | ] 222 | }, 223 | "pokemon_teams_pokemon_id_pokemon_id_fk": { 224 | "name": "pokemon_teams_pokemon_id_pokemon_id_fk", 225 | "tableFrom": "pokemon_teams", 226 | "tableTo": "pokemon", 227 | "columnsFrom": [ 228 | "pokemon_id" 229 | ], 230 | "columnsTo": [ 231 | "id" 232 | ] 233 | } 234 | }, 235 | "compositePrimaryKeys": {} 236 | }, 237 | "pokemon_types": { 238 | "name": "pokemon_types", 239 | "columns": { 240 | "pokemon_id": { 241 | "name": "pokemon_id", 242 | "type": "integer", 243 | "primaryKey": false, 244 | "notNull": true, 245 | "autoincrement": false 246 | }, 247 | "type_id": { 248 | "name": "type_id", 249 | "type": "integer", 250 | "primaryKey": false, 251 | "notNull": true, 252 | "autoincrement": false 253 | } 254 | }, 255 | "indexes": {}, 256 | "foreignKeys": { 257 | "pokemon_types_pokemon_id_pokemon_id_fk": { 258 | "name": "pokemon_types_pokemon_id_pokemon_id_fk", 259 | "tableFrom": "pokemon_types", 260 | "tableTo": "pokemon", 261 | "columnsFrom": [ 262 | "pokemon_id" 263 | ], 264 | "columnsTo": [ 265 | "id" 266 | ] 267 | }, 268 | "pokemon_types_type_id_types_id_fk": { 269 | "name": "pokemon_types_type_id_types_id_fk", 270 | "tableFrom": "pokemon_types", 271 | "tableTo": "types", 272 | "columnsFrom": [ 273 | "type_id" 274 | ], 275 | "columnsTo": [ 276 | "id" 277 | ] 278 | } 279 | }, 280 | "compositePrimaryKeys": {} 281 | }, 282 | "sessions": { 283 | "name": "sessions", 284 | "columns": { 285 | "sessionToken": { 286 | "name": "sessionToken", 287 | "type": "text", 288 | "primaryKey": true, 289 | "notNull": true 290 | }, 291 | "userId": { 292 | "name": "userId", 293 | "type": "text", 294 | "primaryKey": false, 295 | "notNull": true 296 | }, 297 | "expires": { 298 | "name": "expires", 299 | "type": "integer", 300 | "primaryKey": false, 301 | "notNull": true, 302 | "autoincrement": false 303 | } 304 | }, 305 | "indexes": {}, 306 | "foreignKeys": { 307 | "sessions_userId_users_id_fk": { 308 | "name": "sessions_userId_users_id_fk", 309 | "tableFrom": "sessions", 310 | "tableTo": "users", 311 | "columnsFrom": [ 312 | "userId" 313 | ], 314 | "columnsTo": [ 315 | "id" 316 | ], 317 | "onDelete": "cascade" 318 | } 319 | }, 320 | "compositePrimaryKeys": {} 321 | }, 322 | "teams": { 323 | "name": "teams", 324 | "columns": { 325 | "id": { 326 | "name": "id", 327 | "type": "integer", 328 | "primaryKey": true, 329 | "notNull": true, 330 | "autoincrement": true 331 | }, 332 | "user_id": { 333 | "name": "user_id", 334 | "type": "text", 335 | "primaryKey": false, 336 | "notNull": true 337 | } 338 | }, 339 | "indexes": {}, 340 | "foreignKeys": { 341 | "teams_user_id_users_id_fk": { 342 | "name": "teams_user_id_users_id_fk", 343 | "tableFrom": "teams", 344 | "tableTo": "users", 345 | "columnsFrom": [ 346 | "user_id" 347 | ], 348 | "columnsTo": [ 349 | "id" 350 | ] 351 | } 352 | }, 353 | "compositePrimaryKeys": {} 354 | }, 355 | "types": { 356 | "name": "types", 357 | "columns": { 358 | "id": { 359 | "name": "id", 360 | "type": "integer", 361 | "primaryKey": true, 362 | "notNull": true, 363 | "autoincrement": false 364 | }, 365 | "name": { 366 | "name": "name", 367 | "type": "text", 368 | "primaryKey": false, 369 | "notNull": true 370 | } 371 | }, 372 | "indexes": {}, 373 | "foreignKeys": {}, 374 | "compositePrimaryKeys": {} 375 | }, 376 | "users": { 377 | "name": "users", 378 | "columns": { 379 | "id": { 380 | "name": "id", 381 | "type": "text", 382 | "primaryKey": true, 383 | "notNull": true 384 | }, 385 | "name": { 386 | "name": "name", 387 | "type": "text", 388 | "primaryKey": false, 389 | "notNull": false 390 | }, 391 | "email": { 392 | "name": "email", 393 | "type": "text", 394 | "primaryKey": false, 395 | "notNull": true 396 | }, 397 | "emailVerified": { 398 | "name": "emailVerified", 399 | "type": "integer", 400 | "primaryKey": false, 401 | "notNull": false, 402 | "autoincrement": false 403 | }, 404 | "image": { 405 | "name": "image", 406 | "type": "text", 407 | "primaryKey": false, 408 | "notNull": false 409 | } 410 | }, 411 | "indexes": {}, 412 | "foreignKeys": {}, 413 | "compositePrimaryKeys": {} 414 | }, 415 | "verificationToken": { 416 | "name": "verificationToken", 417 | "columns": { 418 | "identifier": { 419 | "name": "identifier", 420 | "type": "text", 421 | "primaryKey": false, 422 | "notNull": true 423 | }, 424 | "token": { 425 | "name": "token", 426 | "type": "text", 427 | "primaryKey": false, 428 | "notNull": true 429 | }, 430 | "expires": { 431 | "name": "expires", 432 | "type": "integer", 433 | "primaryKey": false, 434 | "notNull": true, 435 | "autoincrement": false 436 | } 437 | }, 438 | "indexes": {}, 439 | "foreignKeys": {}, 440 | "compositePrimaryKeys": { 441 | "verificationToken_identifier_token_pk": { 442 | "columns": [ 443 | "identifier", 444 | "token" 445 | ] 446 | } 447 | } 448 | } 449 | }, 450 | "enums": {}, 451 | "_meta": { 452 | "schemas": {}, 453 | "tables": {}, 454 | "columns": {} 455 | } 456 | } -------------------------------------------------------------------------------- /libs/db/src/lib/drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1683546949447, 9 | "tag": "0000_dizzy_red_ghost", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "5", 15 | "when": 1683653617899, 16 | "tag": "0001_light_night_nurse", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "5", 22 | "when": 1683747794213, 23 | "tag": "0002_simple_nightcrawler", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "5", 29 | "when": 1683840361066, 30 | "tag": "0003_silly_diamondback", 31 | "breakpoints": true 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /libs/db/src/lib/schema.ts: -------------------------------------------------------------------------------- 1 | import { integer, primaryKey, sqliteTable, text } from "drizzle-orm/sqlite-core"; 2 | import { ProviderType } from "next-auth/providers"; 3 | 4 | export const pokemonTable = sqliteTable("pokemon", { 5 | id: integer("id").primaryKey(), 6 | name: text("name").notNull(), 7 | weight: integer("weight").notNull(), 8 | height: integer("height").notNull(), 9 | hp: integer("hp").notNull(), 10 | attack: integer("attack").notNull(), 11 | defense: integer("defense").notNull(), 12 | specialAttack: integer("special_attack").notNull(), 13 | specialDefense: integer("special_defense").notNull(), 14 | speed: integer("speed").notNull(), 15 | sprite: text("sprite").notNull(), 16 | }); 17 | 18 | export const pokemonTypesTable = sqliteTable("pokemon_types", { 19 | pokemonId: integer("pokemon_id") 20 | .notNull() 21 | .references(() => pokemonTable.id), 22 | typeId: integer("type_id") 23 | .notNull() 24 | .references(() => typesTable.id), 25 | }); 26 | 27 | export const typesTable = sqliteTable("types", { 28 | id: integer("id").primaryKey(), 29 | name: text("name").notNull(), 30 | }); 31 | 32 | export const teamsTable = sqliteTable("teams", { 33 | id: integer("id").primaryKey({ autoIncrement: true }), 34 | userId: text("user_id") 35 | .notNull() 36 | .references(() => usersTable.id), 37 | }); 38 | 39 | export const pokemonTeamsTable = sqliteTable("pokemon_teams", { 40 | teamId: integer("team_id") 41 | .notNull() 42 | .references(() => teamsTable.id), 43 | pokemonId: integer("pokemon_id") 44 | .notNull() 45 | .references(() => pokemonTable.id), 46 | position: integer("position").notNull(), 47 | }); 48 | 49 | // NextAuth 50 | export const usersTable = sqliteTable("users", { 51 | id: text("id").notNull().primaryKey(), 52 | name: text("name"), 53 | email: text("email").notNull(), 54 | emailVerified: integer("emailVerified", { mode: "timestamp_ms" }), 55 | image: text("image"), 56 | }); 57 | 58 | export const accountsTable = sqliteTable( 59 | "accounts", 60 | { 61 | userId: text("userId") 62 | .notNull() 63 | .references(() => usersTable.id, { onDelete: "cascade" }), 64 | type: text("type").$type().notNull(), 65 | provider: text("provider").notNull(), 66 | providerAccountId: text("providerAccountId").notNull(), 67 | refresh_token: text("refresh_token"), 68 | access_token: text("access_token"), 69 | expires_at: integer("expires_at"), 70 | token_type: text("token_type"), 71 | scope: text("scope"), 72 | id_token: text("id_token"), 73 | session_state: text("session_state"), 74 | }, 75 | (account) => ({ 76 | compositePk: primaryKey(account.provider, account.providerAccountId), 77 | }), 78 | ); 79 | 80 | export const sessionsTable = sqliteTable("sessions", { 81 | sessionToken: text("sessionToken").notNull().primaryKey(), 82 | userId: text("userId") 83 | .notNull() 84 | .references(() => usersTable.id, { onDelete: "cascade" }), 85 | expires: integer("expires", { mode: "timestamp_ms" }).notNull(), 86 | }); 87 | 88 | export const verificationTokensTable = sqliteTable( 89 | "verificationToken", 90 | { 91 | identifier: text("identifier").notNull(), 92 | token: text("token").notNull(), 93 | expires: integer("expires", { mode: "timestamp_ms" }).notNull(), 94 | }, 95 | (vt) => ({ 96 | compositePk: primaryKey(vt.identifier, vt.token), 97 | }), 98 | ); 99 | -------------------------------------------------------------------------------- /libs/db/src/lib/seed.ts: -------------------------------------------------------------------------------- 1 | import { db } from "./db"; 2 | import { pokemonTable, pokemonTypesTable, typesTable } from "./schema"; 3 | 4 | export interface AllPokemonResponse { 5 | count: number; 6 | next: string; 7 | previous: any; 8 | results: Result[]; 9 | } 10 | 11 | export interface Result { 12 | name: string; 13 | url: string; 14 | } 15 | 16 | export interface PokemonResponse { 17 | height: number; 18 | id: number; 19 | name: string; 20 | sprites: Sprites; 21 | stats: Stat[]; 22 | types: Type[]; 23 | weight: number; 24 | } 25 | 26 | export interface Sprites { 27 | front_default: string; 28 | } 29 | 30 | export interface Stat { 31 | base_stat: number; 32 | effort: number; 33 | stat: StatDetail; 34 | } 35 | 36 | export interface StatDetail { 37 | name: "attack" | "hp" | "defense" | "special-attack" | "special-defense" | "speed"; 38 | url: string; 39 | } 40 | 41 | export interface Type { 42 | slot: number; 43 | type: TypeDetail; 44 | } 45 | 46 | export interface TypeDetail { 47 | name: string; 48 | url: string; 49 | } 50 | 51 | export default async function seed() { 52 | const allPokemonResult = await fetch("https://pokeapi.co/api/v2/pokemon/?offset=0&limit=2000", { method: "GET" }); 53 | const allPokemonResponse: AllPokemonResponse = await allPokemonResult.json(); 54 | 55 | for (let i = 0; i < allPokemonResponse.results.length; i++) { 56 | const pokemonResult = await fetch(allPokemonResponse.results[i].url, { method: "GET" }); 57 | const pokemonResponse: PokemonResponse = await pokemonResult.json(); 58 | console.log(`Inserting ${pokemonResponse.name}...`); 59 | 60 | const hpStat = pokemonResponse.stats.find((stat) => stat.stat.name === "hp"); 61 | const attackStat = pokemonResponse.stats.find((stat) => stat.stat.name === "attack"); 62 | const defenseStat = pokemonResponse.stats.find((stat) => stat.stat.name === "defense"); 63 | const specialAttackStat = pokemonResponse.stats.find((stat) => stat.stat.name === "special-attack"); 64 | const specialDefenseStat = pokemonResponse.stats.find((stat) => stat.stat.name === "special-defense"); 65 | const speedStat = pokemonResponse.stats.find((stat) => stat.stat.name === "speed"); 66 | 67 | if (!pokemonResponse.sprites.front_default) { 68 | continue; 69 | } 70 | 71 | const values = { 72 | attack: attackStat!.base_stat, 73 | defense: defenseStat!.base_stat, 74 | height: pokemonResponse.height, 75 | hp: hpStat!.base_stat, 76 | name: pokemonResponse.name, 77 | specialAttack: specialAttackStat!.base_stat, 78 | specialDefense: specialDefenseStat!.base_stat, 79 | speed: speedStat!.base_stat, 80 | sprite: pokemonResponse.sprites.front_default, 81 | weight: pokemonResponse.weight, 82 | id: pokemonResponse.id, 83 | }; 84 | 85 | db.insert(pokemonTable).values(values).onConflictDoUpdate({ target: pokemonTable.id, set: values }).run(); 86 | 87 | for (let j = 0; j < pokemonResponse.types.length; j++) { 88 | const { type } = pokemonResponse.types[j]; 89 | const pattern = /(\d+)\/$/; 90 | const match = type.url.match(pattern); 91 | const typeId = match ? Number.parseInt(match[1], 10) : null; 92 | 93 | if (!typeId) { 94 | continue; 95 | } 96 | 97 | await db 98 | .insert(typesTable) 99 | .values({ id: typeId, name: type.name }) 100 | .onConflictDoUpdate({ target: typesTable.id, set: { id: typeId, name: type.name } }) 101 | .run(); 102 | 103 | await db 104 | .insert(pokemonTypesTable) 105 | .values({ pokemonId: pokemonResponse.id, typeId }) 106 | // .onConflictDoUpdate({ 107 | // target: [pokemonTypes.pokemonId, pokemonTypes.typeId], 108 | // set: { pokemonId: pokemonResponse.id, typeId }, 109 | // }) 110 | .run(); 111 | } 112 | } 113 | } 114 | 115 | seed(); 116 | -------------------------------------------------------------------------------- /libs/db/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "allowSyntheticDefaultImports": true 12 | }, 13 | "files": [], 14 | "include": [], 15 | "references": [ 16 | { 17 | "path": "./tsconfig.lib.json" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /libs/db/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@nx/next/babel"], 3 | "plugins": [] 4 | } 5 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:@nx/react", "../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/README.md: -------------------------------------------------------------------------------- 1 | # feature-pokemon-team 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test feature-pokemon-team` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feature-pokemon-team", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/feature/pokemon-team/src", 5 | "projectType": "library", 6 | "tags": ["type:feature"], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/linter:eslint", 10 | "outputs": ["{options.outputFile}"], 11 | "options": { 12 | "lintFilePatterns": ["libs/feature/pokemon-team/**/*.{ts,tsx,js,jsx}"] 13 | } 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/index.ts: -------------------------------------------------------------------------------- 1 | // Use this file to export React client components (e.g. those with 'use client' directive) or other non-server utilities 2 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/add-pokemon/action.ts: -------------------------------------------------------------------------------- 1 | import { db, getSinglePokemon, getUserTeam, pokemonTeamsTable } from "@pokemon/db"; 2 | import { revalidateTag } from "next/cache"; 3 | import { z } from "zod"; 4 | import { zfd } from "zod-form-data"; 5 | 6 | const MAX_POKEMON = 6; 7 | 8 | const addPokemonSchema = zfd.formData({ 9 | userId: zfd.text(z.string().min(1)), 10 | pokemonId: zfd.numeric(z.number().min(1)), 11 | position: zfd.numeric(z.number().nullable()), 12 | }); 13 | 14 | export async function addPokemon(formData: FormData) { 15 | "use server"; 16 | const input = addPokemonSchema.parse(formData); 17 | const team = await getUserTeam(input.userId); 18 | 19 | if (!team) { 20 | throw new Error(`Could not find user ${input.userId}'s team`); 21 | } 22 | 23 | if (team.pokemon.length >= 6) { 24 | throw new Error(`You can have a maximum of ${MAX_POKEMON} Pokémon in your team!`); 25 | } 26 | 27 | const inTeam = team.pokemon.find((pokemon) => pokemon.id === input.pokemonId); 28 | 29 | if (inTeam) { 30 | throw new Error(`${inTeam.name.charAt(0).toUpperCase() + inTeam.name.slice(1)} is already in your team`); 31 | } 32 | 33 | const { pokemon } = (await getSinglePokemon(input.pokemonId)) ?? {}; 34 | 35 | if (!pokemon) { 36 | throw new Error(`Pokémon with ID ${input.pokemonId} does not exist`); 37 | } 38 | 39 | if (!input.position) { 40 | throw new Error("Something went wrong!"); 41 | } 42 | 43 | db.insert(pokemonTeamsTable).values({ pokemonId: input.pokemonId, teamId: team.id, position: input.position }).run(); 44 | 45 | revalidateTag("user-team"); 46 | } 47 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/add-pokemon/add-pokemon.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@pokemon/ui"; 2 | import { PlusIcon } from "lucide-react"; 3 | import { addPokemon } from "../add-pokemon/action"; 4 | import { Submit } from "../submit/submit"; 5 | 6 | interface AddPokemonProps { 7 | pokemonId: number; 8 | userId: string; 9 | position?: number | null; 10 | } 11 | 12 | export function AddPokemon({ pokemonId, userId, position }: AddPokemonProps) { 13 | // I am doing it like this because I'm getting an error when trying with `zact` or 14 | // using `startTransition`: https://github.com/nrwl/nx/issues/16956 15 | return ( 16 |
17 | 18 | 19 | {position ? : null} 20 | 21 | 22 | 23 | 28 | 29 | 30 | 31 | 32 |

Add Pokémon to your team

33 |
34 |
35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/poke-ball/poke-ball.module.css: -------------------------------------------------------------------------------- 1 | .pokeball { 2 | background: linear-gradient(180deg, #ba0c2f 0%, #ba0c2f 48%, #262122 48%, #262122 52%, #fff 52%, #fff 100%); 3 | } 4 | 5 | .pokeball:before, 6 | .pokeball:after { 7 | content: " "; 8 | border-radius: 100%; 9 | display: block; 10 | position: absolute; 11 | } 12 | 13 | .pokeball:before { 14 | background: #262122; 15 | width: 25%; 16 | height: 25%; 17 | margin-top: 37.5%; 18 | margin-left: 37.5%; 19 | } 20 | 21 | .pokeball:after { 22 | background: #fff; 23 | width: 12.5%; 24 | height: 12.5%; 25 | margin-top: 43.75%; 26 | margin-left: 43.75%; 27 | border: 1px #262122; 28 | } 29 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/poke-ball/poke-ball.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { getUserTeam } from "@pokemon/db"; 4 | import { MotionValue, motion, useSpring, useTransform } from "framer-motion"; 5 | import * as React from "react"; 6 | import styles from "./poke-ball.module.css"; 7 | 8 | interface PokeBallProps { 9 | mouseX: MotionValue; 10 | pokemon?: Awaited>["pokemon"][number]; 11 | } 12 | 13 | export function PokeBall({ mouseX, pokemon }: PokeBallProps) { 14 | const pokemonSprite = pokemon?.sprite; 15 | const ref = React.useRef(null); 16 | 17 | const distance = useTransform(mouseX, (val) => { 18 | const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 }; 19 | 20 | return val - bounds.x - bounds.width / 2; 21 | }); 22 | 23 | const pokeballWidthSync = useTransform(distance, [-150, 0, 150], [40, 100, 40]); 24 | const pokeBallWidth = useSpring(pokeballWidthSync, { mass: 0.1, stiffness: 150, damping: 12 }); 25 | 26 | return ( 27 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/poke-balls/poke-balls.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // Check https://buildui.com/recipes/magnified-dock for an explanation 3 | import { getUserTeam } from "@pokemon/db"; 4 | import { AnimatePresence, motion, useMotionValue } from "framer-motion"; 5 | import { PokeBall } from "../poke-ball/poke-ball"; 6 | 7 | interface PokeBallsProps { 8 | pokemon?: Awaited>["pokemon"]; 9 | } 10 | 11 | export function PokeBalls({ pokemon = [] }: PokeBallsProps) { 12 | const mouseX = useMotionValue(Infinity); 13 | 14 | return ( 15 | mouseX.set(e.pageX)} 17 | onMouseLeave={() => mouseX.set(Infinity)} 18 | className="bg-gray-1 dark:bg-gray-2 ring-gray-3 mx-auto flex h-16 items-end justify-center gap-4 rounded-2xl px-4 pb-3 shadow-sm ring-1" 19 | > 20 | 21 | {[...Array(6).keys()].map((i) => { 22 | const pokemonInPosition = pokemon.find((p) => p.position === i + 1); 23 | return ; 24 | })} 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/pokemon-list/pokemon-list.tsx: -------------------------------------------------------------------------------- 1 | import { BASE_URL } from "@pokemon/configuration"; 2 | import { nextAuthOptions } from "@pokemon/configuration/server"; 3 | import { getAllPokemon, getUserTeam } from "@pokemon/db"; 4 | import { ButtonLink, PokemonType } from "@pokemon/ui"; 5 | import { ChevronRightIcon, PlusIcon, SearchIcon } from "lucide-react"; 6 | import { getServerSession } from "next-auth"; 7 | import Image from "next/image"; 8 | import Link from "next/link"; 9 | import { AddPokemon } from "../add-pokemon/add-pokemon"; 10 | import { PokeBalls } from "../poke-balls/poke-balls"; 11 | import { RemovePokemon } from "../remove-pokemon/remove-pokemon"; 12 | 13 | function getFirstEmptyPosition(positions: number[]): number | undefined { 14 | const sequence = [1, 2, 3, 4, 5, 6]; 15 | for (let i = 0; i < sequence.length; i++) { 16 | if (!positions.includes(sequence[i])) { 17 | return sequence[i]; 18 | } 19 | } 20 | } 21 | 22 | const fetchTeam = async (userId: string): Promise> => { 23 | const response = await fetch(`${BASE_URL}/api/team/${userId}`, { next: { tags: ["user-team"] } }); 24 | const json = await response.json(); 25 | 26 | return json.team; 27 | }; 28 | 29 | interface PokemonListProps { 30 | search?: string; 31 | type?: string; 32 | } 33 | 34 | // React server components are async so you make database or API calls. 35 | export async function PokemonList({ search, type }: PokemonListProps) { 36 | const session = await getServerSession(nextAuthOptions); 37 | 38 | const pokemon = await getAllPokemon({ search, type }); 39 | const user = session && session.user; 40 | const team = user ? await fetchTeam(user.id) : null; 41 | const pokemonTeam = team ? team.pokemon ?? [] : null; 42 | 43 | const position = pokemonTeam ? getFirstEmptyPosition(pokemonTeam.map((pokemon) => pokemon.position)) : null; 44 | 45 | return ( 46 | <> 47 |
    48 | {pokemon.length > 0 ? ( 49 | pokemon.map(({ pokemon, types }) => { 50 | const inTeam = pokemonTeam ? pokemonTeam.find((teamPokemon) => teamPokemon.id === pokemon.id) : false; 51 | const Action = inTeam ? RemovePokemon : AddPokemon; 52 | 53 | return ( 54 |
  • 58 |
    59 | 69 |
    70 |

    71 | 72 | 73 | {pokemon.name} 74 | 75 |

    76 |
    77 | {types.map((type) => { 78 | return ; 79 | })} 80 |
    81 |
    82 |
    83 |
    84 | {user ? ( 85 | 86 | ) : ( 87 | 93 | 94 | 95 | )} 96 |
    98 |
  • 99 | ); 100 | }) 101 | ) : ( 102 |
  • 103 |
  • 106 | )} 107 |
108 | 109 | 110 | ); 111 | } 112 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/remove-pokemon/action.ts: -------------------------------------------------------------------------------- 1 | import { db, getSinglePokemon, getUserTeam, pokemonTeamsTable } from "@pokemon/db"; 2 | import { and, eq } from "drizzle-orm"; 3 | import { revalidateTag } from "next/cache"; 4 | import { z } from "zod"; 5 | import { zfd } from "zod-form-data"; 6 | 7 | const MIN_POKEMON = 0; 8 | 9 | const removePokemonSchema = zfd.formData({ 10 | userId: zfd.text(z.string().min(1)), 11 | pokemonId: zfd.numeric(z.number().min(1)), 12 | }); 13 | 14 | export async function removePokemon(formData: FormData) { 15 | "use server"; 16 | const input = removePokemonSchema.parse(formData); 17 | const team = await getUserTeam(input.userId); 18 | 19 | if (!team) { 20 | throw new Error(`Could not find user ${input.userId}'s team`); 21 | } 22 | 23 | if (team.pokemon.length === MIN_POKEMON) { 24 | throw new Error(`You don't have any Pokémon in your team!`); 25 | } 26 | 27 | const inTeam = team.pokemon.find((pokemon) => pokemon.id === input.pokemonId); 28 | 29 | if (!inTeam) { 30 | throw new Error(`This Pokémon is not in your team`); 31 | } 32 | 33 | const { pokemon } = (await getSinglePokemon(input.pokemonId)) ?? {}; 34 | 35 | if (!pokemon) { 36 | throw new Error(`Pokémon with ID ${input.pokemonId} does not exist`); 37 | } 38 | 39 | db.delete(pokemonTeamsTable) 40 | .where(and(eq(pokemonTeamsTable.pokemonId, input.pokemonId), eq(pokemonTeamsTable.teamId, team.id))) 41 | .run(); 42 | 43 | revalidateTag("user-team"); 44 | } 45 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/remove-pokemon/remove-pokemon.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@pokemon/ui"; 2 | import { TrashIcon } from "lucide-react"; 3 | import { removePokemon } from "../remove-pokemon/action"; 4 | import { Submit } from "../submit/submit"; 5 | 6 | interface RemovePokemonProps { 7 | pokemonId: number; 8 | userId: string; 9 | } 10 | 11 | export function RemovePokemon({ pokemonId, userId }: RemovePokemonProps) { 12 | // I am doing it like this because I'm getting an error when trying with `zact` or 13 | // using `startTransition`: https://github.com/nrwl/nx/issues/16956 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | 21 | 26 | 27 | 28 | 29 | 30 |

Remove Pokémon from your team

31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /libs/feature/pokemon-team/src/lib/submit/submit.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button, ButtonProps } from "@pokemon/ui"; 4 | import * as React from "react"; 5 | import { experimental_useFormStatus as useFormStatus } from "react-dom"; 6 | 7 | export const Submit = React.forwardRef((props, ref) => { 8 | const { pending } = useFormStatus(); 9 | return 84 | 85 | ); 86 | }; 87 | 88 | export { Button, ButtonLink, buttonVariants }; 89 | -------------------------------------------------------------------------------- /libs/ui/src/lib/command/command.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // Adapted from https://github.com/shadcn/ui 3 | import { classnames } from "@pokemon/utility/shared"; 4 | import { DialogProps } from "@radix-ui/react-dialog"; 5 | import { Command as CommandPrimitive } from "cmdk"; 6 | import { Search } from "lucide-react"; 7 | import * as React from "react"; 8 | import { Dialog, DialogContent } from "../dialog/dialog"; 9 | 10 | const Command = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, ...props }, ref) => ( 14 | 22 | )); 23 | Command.displayName = CommandPrimitive.displayName; 24 | 25 | type CommandDialogProps = DialogProps; 26 | 27 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 28 | return ( 29 | 30 | 31 | 32 | {children} 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | const CommandInput = React.forwardRef< 40 | React.ElementRef, 41 | React.ComponentPropsWithoutRef 42 | >(({ className, ...props }, ref) => ( 43 |
44 | 45 | 53 |
54 | )); 55 | 56 | CommandInput.displayName = CommandPrimitive.Input.displayName; 57 | 58 | const CommandList = React.forwardRef< 59 | React.ElementRef, 60 | React.ComponentPropsWithoutRef 61 | >(({ className, ...props }, ref) => ( 62 | 67 | )); 68 | 69 | CommandList.displayName = CommandPrimitive.List.displayName; 70 | 71 | const CommandEmpty = React.forwardRef< 72 | React.ElementRef, 73 | React.ComponentPropsWithoutRef 74 | >((props, ref) => ); 75 | 76 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName; 77 | 78 | const CommandGroup = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef 81 | >(({ className, ...props }, ref) => ( 82 | 90 | )); 91 | 92 | CommandGroup.displayName = CommandPrimitive.Group.displayName; 93 | 94 | const CommandSeparator = React.forwardRef< 95 | React.ElementRef, 96 | React.ComponentPropsWithoutRef 97 | >(({ className, ...props }, ref) => ( 98 | 103 | )); 104 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName; 105 | 106 | const CommandItem = React.forwardRef< 107 | React.ElementRef, 108 | React.ComponentPropsWithoutRef 109 | >(({ className, ...props }, ref) => ( 110 | 118 | )); 119 | 120 | CommandItem.displayName = CommandPrimitive.Item.displayName; 121 | 122 | const CommandShortcut = ({ className, ...props }: React.HTMLAttributes) => { 123 | return ; 124 | }; 125 | CommandShortcut.displayName = "CommandShortcut"; 126 | 127 | export { 128 | Command, 129 | CommandDialog, 130 | CommandInput, 131 | CommandList, 132 | CommandEmpty, 133 | CommandGroup, 134 | CommandItem, 135 | CommandShortcut, 136 | CommandSeparator, 137 | }; 138 | -------------------------------------------------------------------------------- /libs/ui/src/lib/dialog/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // Adapted from https://github.com/shadcn/ui 3 | import { classnames } from "@pokemon/utility/shared"; 4 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 5 | import { X } from "lucide-react"; 6 | import * as React from "react"; 7 | 8 | const Dialog = DialogPrimitive.Root; 9 | 10 | const DialogTrigger = DialogPrimitive.Trigger; 11 | 12 | const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => ( 13 | 14 |
{children}
15 |
16 | ); 17 | DialogPortal.displayName = DialogPrimitive.Portal.displayName; 18 | 19 | const DialogOverlay = React.forwardRef< 20 | React.ElementRef, 21 | React.ComponentPropsWithoutRef 22 | >(({ className, ...props }, ref) => ( 23 | 31 | )); 32 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 33 | 34 | const DialogContent = React.forwardRef< 35 | React.ElementRef, 36 | React.ComponentPropsWithoutRef 37 | >(({ className, children, ...props }, ref) => ( 38 | 39 | 40 | 48 | {children} 49 | 50 | 51 | Close 52 | 53 | 54 | 55 | )); 56 | DialogContent.displayName = DialogPrimitive.Content.displayName; 57 | 58 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( 59 |
60 | ); 61 | DialogHeader.displayName = "DialogHeader"; 62 | 63 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( 64 |
65 | ); 66 | DialogFooter.displayName = "DialogFooter"; 67 | 68 | const DialogTitle = React.forwardRef< 69 | React.ElementRef, 70 | React.ComponentPropsWithoutRef 71 | >(({ className, ...props }, ref) => ( 72 | 77 | )); 78 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 79 | 80 | const DialogDescription = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 85 | )); 86 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 87 | 88 | export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }; 89 | -------------------------------------------------------------------------------- /libs/ui/src/lib/dropdown-menu/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // Adapted from https://github.com/shadcn/ui 3 | import { classnames } from "@pokemon/utility/shared"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | import * as React from "react"; 7 | 8 | const DropdownMenu = DropdownMenuPrimitive.Root; 9 | 10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 11 | 12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 13 | 14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 15 | 16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 17 | 18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 19 | 20 | const DropdownMenuSubTrigger = React.forwardRef< 21 | React.ElementRef, 22 | React.ComponentPropsWithoutRef & { 23 | inset?: boolean; 24 | } 25 | >(({ className, inset, children, ...props }, ref) => ( 26 | 35 | {children} 36 | 37 | 38 | )); 39 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName; 40 | 41 | const DropdownMenuSubContent = React.forwardRef< 42 | React.ElementRef, 43 | React.ComponentPropsWithoutRef 44 | >(({ className, ...props }, ref) => ( 45 | 53 | )); 54 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName; 55 | 56 | const DropdownMenuContent = React.forwardRef< 57 | React.ElementRef, 58 | React.ComponentPropsWithoutRef 59 | >(({ className, sideOffset = 4, ...props }, ref) => ( 60 | 61 | 70 | 71 | )); 72 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 73 | 74 | const DropdownMenuItem = React.forwardRef< 75 | React.ElementRef, 76 | React.ComponentPropsWithoutRef & { 77 | inset?: boolean; 78 | } 79 | >(({ className, inset, ...props }, ref) => ( 80 | 89 | )); 90 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 91 | 92 | const DropdownMenuCheckboxItem = React.forwardRef< 93 | React.ElementRef, 94 | React.ComponentPropsWithoutRef 95 | >(({ className, children, checked, ...props }, ref) => ( 96 | 105 | 106 | 107 | 108 | 109 | 110 | {children} 111 | 112 | )); 113 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName; 114 | 115 | const DropdownMenuRadioItem = React.forwardRef< 116 | React.ElementRef, 117 | React.ComponentPropsWithoutRef 118 | >(({ className, children, ...props }, ref) => ( 119 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )); 135 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 136 | 137 | const DropdownMenuLabel = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef & { 140 | inset?: boolean; 141 | } 142 | >(({ className, inset, ...props }, ref) => ( 143 | 148 | )); 149 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 150 | 151 | const DropdownMenuSeparator = React.forwardRef< 152 | React.ElementRef, 153 | React.ComponentPropsWithoutRef 154 | >(({ className, ...props }, ref) => ( 155 | 156 | )); 157 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 158 | 159 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { 160 | return ; 161 | }; 162 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 163 | 164 | export { 165 | DropdownMenu, 166 | DropdownMenuTrigger, 167 | DropdownMenuContent, 168 | DropdownMenuItem, 169 | DropdownMenuCheckboxItem, 170 | DropdownMenuRadioItem, 171 | DropdownMenuLabel, 172 | DropdownMenuSeparator, 173 | DropdownMenuShortcut, 174 | DropdownMenuGroup, 175 | DropdownMenuPortal, 176 | DropdownMenuSub, 177 | DropdownMenuSubContent, 178 | DropdownMenuSubTrigger, 179 | DropdownMenuRadioGroup, 180 | }; 181 | -------------------------------------------------------------------------------- /libs/ui/src/lib/github-button/github-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { signIn } from "next-auth/react"; 4 | import * as React from "react"; 5 | import { Button } from "../button/button"; 6 | 7 | export const GithubButton = () => { 8 | const [isGitHubLoading, setIsGitHubLoading] = React.useState(false); 9 | 10 | return ( 11 | 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /libs/ui/src/lib/header/header.tsx: -------------------------------------------------------------------------------- 1 | import { nextAuthOptions } from "@pokemon/configuration/server"; 2 | import { HomeIcon } from "lucide-react"; 3 | import { getServerSession } from "next-auth"; 4 | import { ButtonLink } from "../button/button"; 5 | import { ThemeToggle } from "../theme-toggle/theme-toggle"; 6 | import { UserButton } from "../user-button/user-button"; 7 | 8 | export async function Header() { 9 | const session = await getServerSession(nextAuthOptions); 10 | 11 | return ( 12 |
13 | 34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /libs/ui/src/lib/input/input.tsx: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/shadcn/ui 2 | import * as React from "react"; 3 | import { classnames } from "@pokemon/utility/shared"; 4 | import styles from "../loading-dots.module.css"; 5 | 6 | export type InputProps = React.InputHTMLAttributes & { 7 | isLoading?: boolean; 8 | }; 9 | 10 | const Input = React.forwardRef(({ className, type, isLoading, ...props }, ref) => { 11 | return ( 12 |
13 | 22 | {isLoading ? ( 23 | 24 | 25 | 26 | 27 | 28 | ) : ( 29 | props.children 30 | )} 31 |
32 | ); 33 | }); 34 | 35 | Input.displayName = "Input"; 36 | 37 | export { Input }; 38 | -------------------------------------------------------------------------------- /libs/ui/src/lib/intercept-dialog/intercept-dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { Dialog, DialogContent } from "../dialog/dialog"; 5 | 6 | interface InterceptDialogProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export function InterceptDialog({ children }: InterceptDialogProps) { 11 | const router = useRouter(); 12 | 13 | return ( 14 | { 17 | if (!open) { 18 | router.back(); 19 | } 20 | }} 21 | > 22 | 23 |
{children}
24 |
25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /libs/ui/src/lib/loading-dots.module.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | display: inline-flex; 3 | align-items: center; 4 | } 5 | 6 | .loading .spacer { 7 | margin-right: 2px; 8 | } 9 | 10 | .loading span { 11 | animation-name: blink; 12 | animation-duration: 1.4s; 13 | animation-iteration-count: infinite; 14 | animation-fill-mode: both; 15 | width: 5px; 16 | height: 5px; 17 | border-radius: 50%; 18 | display: inline-block; 19 | margin: 0 1px; 20 | } 21 | 22 | .loading span:nth-of-type(2) { 23 | animation-delay: 0.2s; 24 | } 25 | 26 | .loading span:nth-of-type(3) { 27 | animation-delay: 0.4s; 28 | } 29 | 30 | @keyframes blink { 31 | 0% { 32 | opacity: 0.2; 33 | } 34 | 20% { 35 | opacity: 1; 36 | } 37 | 100% { 38 | opacity: 0.2; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /libs/ui/src/lib/pokemon-type/pokemon-type.tsx: -------------------------------------------------------------------------------- 1 | export const ColorMap: Record = { 2 | normal: "fill-neutral-500 dark:fill-neutral-400", 3 | fighting: "fill-orange-500 dark:fill-orange-400", 4 | flying: "fill-sky-500 dark:fill-sky-400", 5 | poison: "fill-fuchsia-500 dark:fill-fuchsia-400", 6 | ground: "fill-amber-500 dark:fill-amber-400", 7 | rock: "fill-orange-500 dark:fill-orange-400", 8 | bug: "fill-lime-500 dark:fill-lime-400", 9 | ghost: "fill-violet-500 dark:fill-violet-400", 10 | steel: "fill-zinc-500 dark:fill-zinc-400", 11 | fire: "fill-red-500 dark:fill-red-400", 12 | water: "fill-blue-500 dark:fill-blue-400", 13 | grass: "fill-green-500 dark:fill-green-400", 14 | electric: "fill-yellow-500 dark:fill-yellow-400", 15 | psychic: "fill-pink-500 dark:fill-pink-400", 16 | ice: "fill-cyan-500 dark:fill-cyan-400", 17 | dragon: "fill-purple-500 dark:fill-purple-400", 18 | dark: "fill-gray-500 dark:fill-gray-400", 19 | fairy: "fill-pink-500 dark:fill-pink-400", 20 | }; 21 | 22 | type Type = { 23 | id: number; 24 | name: string; 25 | }; 26 | 27 | export const PokemonType = ({ type }: { type: Type }) => { 28 | return ( 29 | 33 | 36 | {type.name} 37 | 38 | ); 39 | }; 40 | -------------------------------------------------------------------------------- /libs/ui/src/lib/pokemon-types-combo-box/pokemon-types-combo-box.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { classnames } from "@pokemon/utility/shared"; 4 | import { Check, ChevronsUpDown } from "lucide-react"; 5 | import { usePathname, useRouter } from "next/navigation"; 6 | import * as React from "react"; 7 | import { Button } from "../button/button"; 8 | import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "../command/command"; 9 | import styles from "../loading-dots.module.css"; 10 | import { ColorMap } from "../pokemon-type/pokemon-type"; 11 | import { Popover, PopoverContent, PopoverTrigger } from "../popover/popover"; 12 | 13 | interface PokemonTypesComboBoxProps { 14 | defaultValue?: string; 15 | types: { id: number; name: string }[]; 16 | } 17 | 18 | export function PokemonTypesComboBox({ defaultValue, types }: PokemonTypesComboBoxProps) { 19 | const [open, setOpen] = React.useState(false); 20 | const [value, setValue] = React.useState(defaultValue); 21 | const { replace } = useRouter(); 22 | const pathname = usePathname(); 23 | const [isPending, startTransition] = React.useTransition(); 24 | 25 | const onSelect = (newValue: string) => { 26 | const searchParams = new URLSearchParams(window.location.search); 27 | if (newValue === value) { 28 | searchParams.delete("type"); 29 | setValue(undefined); 30 | } else { 31 | searchParams.set("type", newValue); 32 | setValue(newValue); 33 | } 34 | setOpen(false); 35 | 36 | startTransition(() => { 37 | replace(`${pathname}?${searchParams.toString()}`); 38 | }); 39 | }; 40 | 41 | return ( 42 | 43 | 44 | 61 | 62 | 63 | 64 | 65 | No type found. 66 | 67 | {types.map((type) => ( 68 | 74 | 75 | 78 | {type.name} 79 | 80 | ))} 81 | 82 | 83 | 84 | 85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /libs/ui/src/lib/pokemon/pokemon.tsx: -------------------------------------------------------------------------------- 1 | import { getSinglePokemon } from "@pokemon/db"; 2 | import Image from "next/image"; 3 | import { PokemonType } from "../pokemon-type/pokemon-type"; 4 | 5 | type Statistic = "hp" | "attack" | "defense" | "specialAttack" | "specialDefense" | "speed"; 6 | 7 | const StatisticMap: Record = { 8 | hp: { 9 | label: "HP", 10 | className: "bg-red-6 border-red-6", 11 | }, 12 | attack: { 13 | label: "Attack", 14 | className: "bg-orange-6 border-orange-6", 15 | }, 16 | defense: { 17 | label: "Defense", 18 | className: "bg-yellow-6 border-yellow-6", 19 | }, 20 | specialAttack: { 21 | label: "Special attack", 22 | className: "bg-blue-6 border-blue-6", 23 | }, 24 | specialDefense: { 25 | label: "Special defense", 26 | className: "bg-green-6 border-green-6", 27 | }, 28 | speed: { 29 | label: "Speed", 30 | className: "bg-pink-6 border-pink-6", 31 | }, 32 | }; 33 | 34 | const StatisticBar = ({ statistic, value }: { statistic: Statistic; value: number }) => { 35 | return ( 36 |
37 |
38 |

{StatisticMap[statistic].label}

39 |
51 |
{value}
52 |
53 | ); 54 | }; 55 | 56 | const MAX_VALUE = 255; 57 | 58 | interface PokemonProps { 59 | id: number; 60 | } 61 | 62 | export async function Pokemon({ id }: PokemonProps) { 63 | const { pokemon, types } = (await getSinglePokemon(id)) ?? {}; 64 | 65 | if (!pokemon || !types) { 66 | return null; 67 | } 68 | 69 | return ( 70 |
71 |
72 | 80 |
81 |
82 |

{pokemon.name}

83 | 84 | 85 | 86 |

{pokemon.weight} kg

87 | 88 | 89 | 90 |

{pokemon.height * 10} cm

91 |
92 |
93 | {types.map((type) => { 94 | return ; 95 | })} 96 |
97 |
98 |
99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /libs/ui/src/lib/popover/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // Adapted from https://github.com/shadcn/ui 3 | import * as React from "react"; 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"; 5 | import { classnames } from "@pokemon/utility/shared"; 6 | 7 | const Popover = PopoverPrimitive.Root; 8 | 9 | const PopoverTrigger = PopoverPrimitive.Trigger; 10 | 11 | const PopoverContent = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 15 | 16 | 26 | 27 | )); 28 | PopoverContent.displayName = PopoverPrimitive.Content.displayName; 29 | 30 | export { Popover, PopoverTrigger, PopoverContent }; 31 | -------------------------------------------------------------------------------- /libs/ui/src/lib/search/search.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Input } from "../input/input"; 5 | import { useRouter, usePathname } from "next/navigation"; 6 | 7 | export type SearchProps = React.InputHTMLAttributes; 8 | 9 | export const Search = React.forwardRef(({ className, type, ...props }, ref) => { 10 | const { replace } = useRouter(); 11 | const pathname = usePathname(); 12 | const [isPending, startTransition] = React.useTransition(); 13 | 14 | const onChange = (event: React.ChangeEvent) => { 15 | const searchParams = new URLSearchParams(window.location.search); 16 | 17 | if (event.target.value) { 18 | searchParams.set("search", event.target.value); 19 | } else { 20 | searchParams.delete("search"); 21 | } 22 | 23 | startTransition(() => { 24 | replace(`${pathname}?${searchParams.toString()}`); 25 | }); 26 | }; 27 | 28 | return ( 29 | 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /libs/ui/src/lib/theme-provider/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import { ThemeProviderProps } from "next-themes/dist/types"; 5 | 6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 7 | return {children}; 8 | } 9 | -------------------------------------------------------------------------------- /libs/ui/src/lib/theme-toggle/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Moon, SunMedium } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | import { Button } from "../button/button"; 6 | 7 | export const ThemeToggle = () => { 8 | const { theme, setTheme } = useTheme(); 9 | 10 | return ( 11 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /libs/ui/src/lib/tooltip/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | // Adapted from https://github.com/shadcn/ui 3 | import { classnames } from "@pokemon/utility/shared"; 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"; 5 | import * as React from "react"; 6 | 7 | const TooltipProvider = TooltipPrimitive.Provider; 8 | 9 | const Tooltip = TooltipPrimitive.Root; 10 | 11 | const TooltipTrigger = TooltipPrimitive.Trigger; 12 | 13 | const TooltipContent = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef 16 | >(({ className, sideOffset = 4, ...props }, ref) => ( 17 | 26 | )); 27 | TooltipContent.displayName = TooltipPrimitive.Content.displayName; 28 | 29 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; 30 | -------------------------------------------------------------------------------- /libs/ui/src/lib/user-button/user-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AvatarProps } from "@radix-ui/react-avatar"; 4 | import { UserIcon } from "lucide-react"; 5 | import { Session } from "next-auth"; 6 | import { signOut } from "next-auth/react"; 7 | import { Avatar, AvatarFallback, AvatarImage } from "../avatar/avatar"; 8 | import { 9 | DropdownMenu, 10 | DropdownMenuContent, 11 | DropdownMenuItem, 12 | DropdownMenuSeparator, 13 | DropdownMenuTrigger, 14 | } from "../dropdown-menu/dropdown-menu"; 15 | 16 | type User = NonNullable; 17 | 18 | interface UserButtonProps extends AvatarProps { 19 | user: User; 20 | } 21 | 22 | export function UserButton({ user, ...props }: UserButtonProps) { 23 | return ( 24 | 25 | 26 | 27 | {user.image ? ( 28 |
29 | 30 |
31 | ) : ( 32 | 33 | {user.name} 34 | 35 | 36 | )} 37 |
38 |
39 | 40 |
41 |
42 | {user.name &&

{user.name}

} 43 | {user.email &&

{user.email}

} 44 |
45 |
46 | 47 | { 50 | event.preventDefault(); 51 | signOut({ 52 | callbackUrl: "/", 53 | }); 54 | }} 55 | > 56 | Sign out 57 | 58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /libs/ui/src/server.ts: -------------------------------------------------------------------------------- 1 | // Use this file to export React server components 2 | export * from "./lib/header/header"; 3 | export * from "./lib/pokemon/pokemon"; 4 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "allowJs": false, 5 | "esModuleInterop": false, 6 | "allowSyntheticDefaultImports": true, 7 | "strict": true 8 | }, 9 | "files": [], 10 | "include": [], 11 | "references": [ 12 | { 13 | "path": "./tsconfig.lib.json" 14 | } 15 | ], 16 | "extends": "../../tsconfig.base.json" 17 | } 18 | -------------------------------------------------------------------------------- /libs/ui/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["node"] 6 | }, 7 | "files": ["../../node_modules/@nx/react/typings/cssmodule.d.ts", "../../node_modules/@nx/next/typings/image.d.ts"], 8 | "exclude": [ 9 | "jest.config.ts", 10 | "src/**/*.spec.ts", 11 | "src/**/*.test.ts", 12 | "src/**/*.spec.tsx", 13 | "src/**/*.test.tsx", 14 | "src/**/*.spec.js", 15 | "src/**/*.test.js", 16 | "src/**/*.spec.jsx", 17 | "src/**/*.test.jsx" 18 | ], 19 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /libs/utility/shared/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/utility/shared/README.md: -------------------------------------------------------------------------------- 1 | # utility-shared 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | -------------------------------------------------------------------------------- /libs/utility/shared/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "utility-shared", 3 | "$schema": "../../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/utility/shared/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nx/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/utility/shared/**/*.ts"] 12 | } 13 | } 14 | }, 15 | "tags": ["type:utility"] 16 | } 17 | -------------------------------------------------------------------------------- /libs/utility/shared/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./lib/classnames/classnames"; 2 | -------------------------------------------------------------------------------- /libs/utility/shared/src/lib/classnames/classnames.ts: -------------------------------------------------------------------------------- 1 | import { ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function classnames(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /libs/utility/shared/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "forceConsistentCasingInFileNames": true, 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true 11 | }, 12 | "files": [], 13 | "include": [], 14 | "references": [ 15 | { 16 | "path": "./tsconfig.lib.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /libs/utility/shared/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], 10 | "include": ["src/**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 3 | "npmScope": "pokemon", 4 | "tasksRunnerOptions": { 5 | "default": { 6 | "runner": "nx/tasks-runners/default", 7 | "options": { 8 | "cacheableOperations": ["build", "lint", "test", "e2e"] 9 | } 10 | } 11 | }, 12 | "targetDefaults": { 13 | "build": { 14 | "dependsOn": ["^build"], 15 | "inputs": ["production", "^production"] 16 | }, 17 | "test": { 18 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"] 19 | }, 20 | "e2e": { 21 | "inputs": ["default", "^production"] 22 | }, 23 | "lint": { 24 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintignore"] 25 | } 26 | }, 27 | "namedInputs": { 28 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 29 | "production": [ 30 | "default", 31 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 32 | "!{projectRoot}/tsconfig.spec.json", 33 | "!{projectRoot}/jest.config.[jt]s", 34 | "!{projectRoot}/.eslintrc.json" 35 | ], 36 | "sharedGlobals": [] 37 | }, 38 | "generators": { 39 | "@nx/react": { 40 | "application": { 41 | "babel": true 42 | }, 43 | "library": { 44 | "unitTestRunner": "none" 45 | } 46 | }, 47 | "@nx/next": { 48 | "application": { 49 | "style": "css", 50 | "linter": "eslint" 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokemon", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "seed": "npx nx run db:seed:seed" 7 | }, 8 | "private": true, 9 | "dependencies": { 10 | "@libsql/client": "^0.1.6", 11 | "@nx/next": "16.1.4", 12 | "@radix-ui/react-avatar": "^1.0.2", 13 | "@radix-ui/react-dialog": "^1.0.3", 14 | "@radix-ui/react-dropdown-menu": "^2.0.4", 15 | "@radix-ui/react-popover": "^1.0.5", 16 | "@radix-ui/react-tooltip": "^1.0.5", 17 | "@swc/helpers": "~0.5.1", 18 | "@tw-classed/react": "^1.4.4", 19 | "class-variance-authority": "^0.6.0", 20 | "clsx": "^1.2.1", 21 | "cmdk": "^0.2.0", 22 | "dotenv": "^16.0.3", 23 | "drizzle-orm": "^0.25.4", 24 | "framer-motion": "^10.12.10", 25 | "lucide-react": "^0.216.0", 26 | "next": "13.4.2", 27 | "next-auth": "^4.22.1", 28 | "next-themes": "^0.2.1", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "tailwind-merge": "^1.12.0", 32 | "tslib": "^2.5.0", 33 | "uuid": "^9.0.0", 34 | "zod": "^3.21.4", 35 | "zod-form-data": "^2.0.1" 36 | }, 37 | "devDependencies": { 38 | "@nrwl/node": "16.1.4", 39 | "@nx/cypress": "16.1.4", 40 | "@nx/eslint-plugin": "16.1.4", 41 | "@nx/jest": "16.1.4", 42 | "@nx/js": "16.1.4", 43 | "@nx/linter": "16.1.4", 44 | "@nx/node": "16.1.4", 45 | "@nx/react": "16.1.4", 46 | "@nx/workspace": "16.1.4", 47 | "@radix-ui/colors": "^0.1.8", 48 | "@swc/cli": "~0.1.62", 49 | "@swc/core": "~1.3.57", 50 | "@tailwindcss/forms": "^0.5.3", 51 | "@tailwindcss/typography": "^0.5.9", 52 | "@testing-library/react": "14.0.0", 53 | "@types/jest": "^29.5.1", 54 | "@types/node": "20.1.3", 55 | "@types/react": "18.2.6", 56 | "@types/react-dom": "18.2.4", 57 | "@types/uuid": "^9.0.1", 58 | "@typescript-eslint/eslint-plugin": "^5.59.5", 59 | "@typescript-eslint/parser": "^5.59.5", 60 | "autoprefixer": "10.4.14", 61 | "babel-jest": "^29.5.0", 62 | "cypress": "^12.12.0", 63 | "drizzle-kit": "^0.17.6", 64 | "eslint": "~8.40.0", 65 | "eslint-config-next": "13.4.2", 66 | "eslint-config-prettier": "8.8.0", 67 | "eslint-plugin-cypress": "^2.13.3", 68 | "eslint-plugin-import": "2.27.5", 69 | "eslint-plugin-jsx-a11y": "6.7.1", 70 | "eslint-plugin-react": "7.32.2", 71 | "eslint-plugin-react-hooks": "4.6.0", 72 | "jest": "^29.5.0", 73 | "jest-environment-jsdom": "^29.5.0", 74 | "nx": "16.1.4", 75 | "postcss": "8.4.23", 76 | "prettier": "^2.8.8", 77 | "prettier-plugin-tailwindcss": "^0.2.8", 78 | "react-test-renderer": "18.2.0", 79 | "tailwindcss": "3.3.2", 80 | "tailwindcss-animate": "^1.0.5", 81 | "ts-jest": "^29.1.0", 82 | "ts-node": "10.9.1", 83 | "typescript": "~5.0.4", 84 | "windy-radix-palette": "^0.6.1", 85 | "windy-radix-typography": "^0.2.1" 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "lib": ["es2020", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "paths": { 18 | "@pokemon/configuration": ["libs/configuration/src/index.ts"], 19 | "@pokemon/configuration/server": ["libs/configuration/src/server.ts"], 20 | "@pokemon/db": ["libs/db/src/index.ts"], 21 | "@pokemon/feature/pokemon-team": ["libs/feature/pokemon-team/src/index.ts"], 22 | "@pokemon/feature/pokemon-team/server": ["libs/feature/pokemon-team/src/server.ts"], 23 | "@pokemon/ui": ["libs/ui/src/index.ts"], 24 | "@pokemon/ui/server": ["libs/ui/src/server.ts"], 25 | "@pokemon/utility/shared": ["libs/utility/shared/src/index.ts"] 26 | } 27 | }, 28 | "exclude": ["node_modules", "tmp"] 29 | } 30 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { type DefaultSession } from "next-auth"; 2 | 3 | declare module "next-auth" { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context 6 | */ 7 | interface Session { 8 | user?: { 9 | id: string; 10 | } & DefaultSession["user"]; 11 | } 12 | } 13 | --------------------------------------------------------------------------------