├── .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 |
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 |
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 |
81 |
82 |
83 | {value > 0 ? (
84 |
88 | ) : null}
89 |
90 |
91 |
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 |
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 |
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 |
97 |
98 |
99 | );
100 | })
101 | ) : (
102 | -
103 |
104 |
No Pokémon
105 |
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 |
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 ;
10 | });
11 |
--------------------------------------------------------------------------------
/libs/feature/pokemon-team/src/server.ts:
--------------------------------------------------------------------------------
1 | // Use this file to export React server components
2 | export * from "./lib/pokemon-list/pokemon-list";
3 |
--------------------------------------------------------------------------------
/libs/feature/pokemon-team/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/feature/pokemon-team/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "files": [
8 | "../../../node_modules/@nx/react/typings/cssmodule.d.ts",
9 | "../../../node_modules/@nx/next/typings/image.d.ts"
10 | ],
11 | "exclude": [
12 | "jest.config.ts",
13 | "src/**/*.spec.ts",
14 | "src/**/*.test.ts",
15 | "src/**/*.spec.tsx",
16 | "src/**/*.test.tsx",
17 | "src/**/*.spec.js",
18 | "src/**/*.test.js",
19 | "src/**/*.spec.jsx",
20 | "src/**/*.test.jsx"
21 | ],
22 | "include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx", "../../../types/next-auth.d.ts"]
23 | }
24 |
--------------------------------------------------------------------------------
/libs/ui/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@nx/next/babel"],
3 | "plugins": []
4 | }
5 |
--------------------------------------------------------------------------------
/libs/ui/.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/ui/README.md:
--------------------------------------------------------------------------------
1 | # ui
2 |
3 | This library was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test ui` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/libs/ui/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ui",
3 | "$schema": "../../node_modules/nx/schemas/project-schema.json",
4 | "sourceRoot": "libs/ui/src",
5 | "projectType": "library",
6 | "tags": ["type:utility"],
7 | "targets": {
8 | "lint": {
9 | "executor": "@nx/linter:eslint",
10 | "outputs": ["{options.outputFile}"],
11 | "options": {
12 | "lintFilePatterns": ["libs/ui/**/*.{ts,tsx,js,jsx}"]
13 | }
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/libs/ui/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/button/button";
4 | export * from "./lib/dialog/dialog";
5 | export * from "./lib/github-button/github-button";
6 | export * from "./lib/input/input";
7 | export * from "./lib/intercept-dialog/intercept-dialog";
8 | export * from "./lib/pokemon-type/pokemon-type";
9 | export * from "./lib/pokemon-types-combo-box/pokemon-types-combo-box";
10 | export * from "./lib/search/search";
11 | export * from "./lib/theme-provider/theme-provider";
12 | export * from "./lib/theme-toggle/theme-toggle";
13 | export * from "./lib/tooltip/tooltip";
14 |
--------------------------------------------------------------------------------
/libs/ui/src/lib/avatar/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | // Adapted from https://github.com/shadcn/ui
3 | import { classnames } from "@pokemon/utility/shared";
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar";
5 | import * as React from "react";
6 |
7 | const Avatar = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
16 | ));
17 | Avatar.displayName = AvatarPrimitive.Root.displayName;
18 |
19 | const AvatarImage = React.forwardRef<
20 | React.ElementRef,
21 | React.ComponentPropsWithoutRef
22 | >(({ className, ...props }, ref) => (
23 |
24 | ));
25 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
26 |
27 | const AvatarFallback = React.forwardRef<
28 | React.ElementRef,
29 | React.ComponentPropsWithoutRef
30 | >(({ className, ...props }, ref) => (
31 |
36 | ));
37 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
38 |
39 | export { Avatar, AvatarImage, AvatarFallback };
40 |
--------------------------------------------------------------------------------
/libs/ui/src/lib/button/button.tsx:
--------------------------------------------------------------------------------
1 | // Adapted from https://github.com/shadcn/ui
2 | import { classnames } from "@pokemon/utility/shared";
3 | import { Slot } from "@radix-ui/react-slot";
4 | import { VariantProps, cva } from "class-variance-authority";
5 | import Link, { type LinkProps } from "next/link";
6 | import * as React from "react";
7 | import styles from "../loading-dots.module.css";
8 |
9 | const buttonVariants = cva(
10 | "rounded-md py-1 px-3 inline-flex gap-0.5 justify-center items-center overflow-hidden text-sm font-medium transition",
11 | {
12 | variants: {
13 | variant: {
14 | default:
15 | "bg-zinc-900 text-white hover:bg-zinc-700 dark:bg-gray-3 dark:text-gray-11 dark:ring-1 dark:ring-inset dark:ring-gray-6 dark:hover:bg-primary-400/10 dark:hover:text-gray-12 dark:hover:ring-gray-7",
16 | secondary:
17 | "bg-zinc-100 text-gray-12 hover:bg-zinc-200 dark:bg-zinc-800/40 dark:text-gray-400 dark:ring-1 dark:ring-inset dark:ring-zinc-800 dark:hover:bg-zinc-800 dark:hover:text-gray-300",
18 | filled:
19 | "bg-zinc-900 text-white hover:bg-zinc-700 dark:bg-primary-500 dark:text-white dark:hover:bg-primary-400",
20 | outline:
21 | "text-gray-11 border border-gray-6 hover:bg-gray-1 hover:text-gray-12 dark:hover:bg-gray-3 dark:hover:text-gray-12",
22 | text: "text-primary-500 hover:text-primary-600 dark:text-primary-400 dark:hover:text-primary-500",
23 | ghost:
24 | "bg-transparent text-slate-12 hover:bg-slate-1 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
25 | },
26 | size: {
27 | default: "h-7 py-1 px-3",
28 | sm: "h-5 py-0.5 px-2",
29 | lg: "h-9 py-2 px-5 text-lg",
30 | xl: "h-11 py-3 px-7 text-xl",
31 | },
32 | },
33 | defaultVariants: {
34 | variant: "default",
35 | size: "default",
36 | },
37 | },
38 | );
39 |
40 | export interface ButtonProps
41 | extends React.ButtonHTMLAttributes,
42 | VariantProps {
43 | asChild?: boolean;
44 | isLoading?: boolean;
45 | }
46 |
47 | const Button = React.forwardRef(
48 | ({ children, className, variant, size, asChild = false, isLoading, ...props }, ref) => {
49 | const Component = asChild ? Slot : "button";
50 | return (
51 |
52 | {isLoading ? (
53 |
54 |
55 |
56 |
57 |
58 | ) : (
59 | children
60 | )}
61 |
62 | );
63 | },
64 | );
65 |
66 | Button.displayName = "Button";
67 |
68 | type ButtonLinkProps = LinkProps & ButtonProps;
69 |
70 | const ButtonLink: React.FC = ({ children, href, isLoading, ...rest }) => {
71 | return (
72 |
73 |
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 |
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 |
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 |
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 |
40 |
41 |
42 | {value > 0 ? (
43 |
47 | ) : null}
48 |
49 |
50 |
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 |
86 |
{pokemon.weight} kg
87 |
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 |
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 |
--------------------------------------------------------------------------------