├── .eslintrc.json
├── .vscode
└── settings.json
├── src
├── app
│ ├── [locale]
│ │ ├── loaders.ts
│ │ ├── signOut
│ │ │ └── action.ts
│ │ ├── admin
│ │ │ └── page.tsx
│ │ ├── signIn
│ │ │ ├── action.tsx
│ │ │ ├── page.tsx
│ │ │ └── form.tsx
│ │ ├── [id]
│ │ │ ├── edit
│ │ │ │ ├── action.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── form.tsx
│ │ │ ├── delete
│ │ │ │ ├── action.tsx
│ │ │ │ ├── page.tsx
│ │ │ │ └── form.tsx
│ │ │ └── page.tsx
│ │ ├── register
│ │ │ ├── action.tsx
│ │ │ ├── page.tsx
│ │ │ └── form.tsx
│ │ ├── page.tsx
│ │ ├── new
│ │ │ ├── action.tsx
│ │ │ ├── page.tsx
│ │ │ └── form.tsx
│ │ ├── layout.tsx
│ │ ├── pokemons.tsx
│ │ ├── pokemon-list-item.tsx
│ │ ├── globals.css
│ │ └── main-layout.tsx
│ ├── favicon.ico
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.ts
│ └── i18n.ts
├── db
│ ├── schema
│ │ ├── index.ts
│ │ ├── pokemons.ts
│ │ └── users.ts
│ ├── migrations
│ │ ├── meta
│ │ │ ├── _journal.json
│ │ │ └── 0000_snapshot.json
│ │ └── 0000_amused_stephen_strange.sql
│ ├── index.ts
│ └── seed.ts
├── i18nConfig.ts
├── lib
│ └── utils.ts
├── locales
│ ├── en-US
│ │ ├── pokemons.json
│ │ ├── delete.json
│ │ ├── main-layout.json
│ │ ├── signIn.json
│ │ ├── register.json
│ │ ├── edit.json
│ │ └── new.json
│ └── tr-TR
│ │ ├── pokemons.json
│ │ ├── delete.json
│ │ ├── main-layout.json
│ │ ├── register.json
│ │ ├── signIn.json
│ │ ├── edit.json
│ │ └── new.json
├── context
│ └── NextAuthProvider.tsx
├── theme
│ └── ThemeProvider.tsx
├── middleware.ts
├── components
│ ├── SignInMenuItem.tsx
│ ├── SignOutMenuItem.tsx
│ ├── TranslationsProvider.tsx
│ ├── ui
│ │ ├── label.tsx
│ │ ├── input.tsx
│ │ ├── checkbox.tsx
│ │ ├── button.tsx
│ │ ├── card.tsx
│ │ ├── sheet.tsx
│ │ ├── form.tsx
│ │ ├── select.tsx
│ │ └── dropdown-menu.tsx
│ ├── ThemeToggleButton.tsx
│ └── LanguageChanger.tsx
├── env.mjs
└── auth.ts
├── art
└── screens.png
├── next.config.mjs
├── postcss.config.mjs
├── .env.sample
├── drizzle.config.ts
├── components.json
├── .gitignore
├── public
├── vercel.svg
├── next.svg
└── placeholder.svg
├── db.json
├── tsconfig.json
├── README.md
├── package.json
└── tailwind.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n-ally.localesPaths": ["src/locales"]
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/[locale]/loaders.ts:
--------------------------------------------------------------------------------
1 | export const API_URL = "http://localhost:3001";
2 |
--------------------------------------------------------------------------------
/src/db/schema/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./pokemons";
2 | export * from "./users";
3 |
--------------------------------------------------------------------------------
/art/screens.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozcanzaferayan/next-workshop/HEAD/art/screens.png
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ozcanzaferayan/next-workshop/HEAD/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/i18nConfig.ts:
--------------------------------------------------------------------------------
1 | const i18nConfig = {
2 | locales: ["en-US", "tr-TR"],
3 | defaultLocale: "en-US",
4 | };
5 |
6 | export default i18nConfig;
7 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import "./src/env.mjs";
2 |
3 | /** @type {import('next').NextConfig} */
4 | const nextConfig = {};
5 |
6 | export default nextConfig;
7 |
--------------------------------------------------------------------------------
/src/app/[locale]/signOut/action.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { signOut } from "@/auth";
3 |
4 | export async function signOutAction() {
5 | await signOut();
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { handlers } from "@/auth"; // Referring to the auth.ts we just created
2 | export const { GET, POST } = handlers;
3 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | # Do not expose your Neon credentials to the browser
2 |
3 | PGHOST='yourhost.eu-central-1.aws.neon.tech'
4 | PGDATABASE='yourdb'
5 | PGUSER='yourdb_owner'
6 | PGPASSWORD='yourpassword'
7 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/locales/en-US/pokemons.json:
--------------------------------------------------------------------------------
1 | {
2 | "card": {
3 | "title": "Pokemons",
4 | "description": "You have {{count}} pokemons in Pokedex."
5 | },
6 | "button": {
7 | "add_new": "Add new"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/locales/tr-TR/pokemons.json:
--------------------------------------------------------------------------------
1 | {
2 | "card": {
3 | "title": "Pokemonlar",
4 | "description": "Pokedex'te {{count}} adet pokemonunuz var."
5 | },
6 | "button": {
7 | "add_new": "Yeni Ekle"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/app/[locale]/admin/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from "@/auth";
2 |
3 | const AdminPage = async () => {
4 | const session = await auth();
5 | if (!session) return
Not authenticated
;
6 |
7 | return Secret page
;
8 | };
9 |
10 | export default AdminPage;
11 |
--------------------------------------------------------------------------------
/src/db/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "5",
8 | "when": 1714925052625,
9 | "tag": "0000_amused_stephen_strange",
10 | "breakpoints": true
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "drizzle-kit";
2 | import { env } from "./src/env.mjs";
3 |
4 | export default {
5 | schema: "./src/db/schema",
6 | driver: "pg",
7 | out: "./src/db/migrations",
8 | dbCredentials: {
9 | connectionString: env.DATABASE_URL,
10 | },
11 | } satisfies Config;
12 |
--------------------------------------------------------------------------------
/src/db/schema/pokemons.ts:
--------------------------------------------------------------------------------
1 | import { pgTable, serial, text } from "drizzle-orm/pg-core";
2 |
3 | export const pokemons = pgTable("pokemons", {
4 | id: serial("id").primaryKey(),
5 | name: text("name").notNull(),
6 | type: text("type").notNull(),
7 | });
8 |
9 | export type Pokemon = typeof pokemons.$inferSelect;
10 |
--------------------------------------------------------------------------------
/src/db/schema/users.ts:
--------------------------------------------------------------------------------
1 | import { pgTable, serial, text, varchar } from "drizzle-orm/pg-core";
2 |
3 | export const users = pgTable("users", {
4 | id: serial("id").primaryKey(),
5 | username: text("username"),
6 | email: varchar("email", { length: 256 }),
7 | });
8 |
9 | export type User = typeof users.$inferSelect;
10 |
--------------------------------------------------------------------------------
/src/context/NextAuthProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { SessionProvider } from "next-auth/react";
4 | import { ReactNode } from "react";
5 |
6 | export default function NextAuthProvider({
7 | children,
8 | }: {
9 | children: ReactNode;
10 | }) {
11 | return {children};
12 | }
13 |
--------------------------------------------------------------------------------
/src/locales/en-US/delete.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Delete {{pokemonName}}?",
4 | "description": "Are you sure you want to delete {{pokemonName}}? This action cannot be undone.",
5 | "buttons": {
6 | "cancel": "Cancel",
7 | "delete": "Delete",
8 | "pending": "Pending"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/locales/tr-TR/delete.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "{{pokemonName}} silinsin mi?",
4 | "description": "{{pokemonName}} silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
5 | "buttons": {
6 | "cancel": "İptal",
7 | "delete": "Sil",
8 | "pending": "Bekleniyor"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/theme/ThemeProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes";
4 | import { type ThemeProviderProps } from "next-themes/dist/types";
5 |
6 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
7 | return {children};
8 | }
9 |
--------------------------------------------------------------------------------
/src/db/migrations/0000_amused_stephen_strange.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "pokemons" (
2 | "id" serial PRIMARY KEY NOT NULL,
3 | "name" text NOT NULL,
4 | "type" text NOT NULL
5 | );
6 | --> statement-breakpoint
7 | CREATE TABLE IF NOT EXISTS "users" (
8 | "id" serial PRIMARY KEY NOT NULL,
9 | "username" text,
10 | "email" varchar(256)
11 | );
12 |
--------------------------------------------------------------------------------
/src/app/[locale]/signIn/action.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { signIn } from "@/auth";
3 | import { redirect } from "next/navigation";
4 |
5 | export async function signInAction(
6 | prevState: any,
7 | user: {
8 | email: string;
9 | password: string;
10 | }
11 | ) {
12 | await signIn("credentials", user);
13 |
14 | redirect("/");
15 | }
16 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { i18nRouter } from "next-i18n-router";
2 | import { NextRequest } from "next/server";
3 | import i18nConfig from "./i18nConfig";
4 |
5 | export function middleware(request: NextRequest) {
6 | return i18nRouter(request, i18nConfig);
7 | }
8 |
9 | export const config = {
10 | matcher: "/((?!api|static|.*\\..*|_next).*)",
11 | };
12 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/src/locales/en-US/main-layout.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigation": {
3 | "toggle_menu": "Toggle navigation menu",
4 | "toggle_user_menu": "Toggle user menu"
5 | },
6 | "links": {
7 | "acme_inc": "Acme Inc",
8 | "pokemons": "Pokemons",
9 | "add": "Add"
10 | },
11 | "user_menu": {
12 | "my_account": "My Account",
13 | "settings": "Settings",
14 | "support": "Support",
15 | "signOut": "Logout",
16 | "signIn": "SignIn"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/components/SignInMenuItem.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
3 | import Link from "next/link";
4 | import { useTranslation } from "react-i18next";
5 |
6 | const SignInMenuItem = () => {
7 | const { t } = useTranslation();
8 | return (
9 |
10 | {t("user_menu.signIn")}
11 |
12 | );
13 | };
14 |
15 | export default SignInMenuItem;
16 |
--------------------------------------------------------------------------------
/src/locales/tr-TR/main-layout.json:
--------------------------------------------------------------------------------
1 | {
2 | "navigation": {
3 | "toggle_menu": "Navigasyon menüsünü aç/kapa",
4 | "toggle_user_menu": "Kullanıcı menüsünü aç/kapa"
5 | },
6 | "links": {
7 | "acme_inc": "Acme A.Ş.",
8 | "pokemons": "Pokemonlar",
9 | "add": "Ekle"
10 | },
11 | "user_menu": {
12 | "my_account": "Hesabım",
13 | "settings": "Ayarlar",
14 | "support": "Destek",
15 | "signOut": "Çıkış Yap",
16 | "signIn": "Giriş Yap"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/[locale]/[id]/edit/action.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { db } from "@/db";
3 | import { Pokemon, pokemons } from "@/db/schema/pokemons";
4 | import { eq } from "drizzle-orm";
5 | import { revalidatePath } from "next/cache";
6 | import { redirect } from "next/navigation";
7 |
8 | export async function editPokemon(prevState: any, pokemon: Pokemon) {
9 | await db.update(pokemons).set(pokemon).where(eq(pokemons.id, pokemon.id));
10 |
11 | revalidatePath("/", "layout");
12 | redirect("/");
13 | }
14 |
--------------------------------------------------------------------------------
/src/components/SignOutMenuItem.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { signOutAction } from "@/app/[locale]/signOut/action";
3 | import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
4 | import { useTranslation } from "react-i18next";
5 |
6 | const SignOutMenuItem = () => {
7 | const { t } = useTranslation();
8 | return (
9 | signOutAction()}>
10 | {t("user_menu.signOut")}
11 |
12 | );
13 | };
14 |
15 | export default SignOutMenuItem;
16 |
--------------------------------------------------------------------------------
/src/app/[locale]/register/action.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { API_URL } from "@/app/[locale]/loaders";
4 | import { signIn } from "@/auth";
5 | import { redirect } from "next/navigation";
6 |
7 | export async function registerAction(prevState: any, user: any) {
8 | await fetch(`${API_URL}/register`, {
9 | method: "POST",
10 | headers: {
11 | "Content-Type": "application/json",
12 | },
13 | body: JSON.stringify(user),
14 | });
15 |
16 | await signIn("credentials", user);
17 |
18 | redirect("/");
19 | }
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
--------------------------------------------------------------------------------
/src/app/[locale]/page.tsx:
--------------------------------------------------------------------------------
1 | import Pokemons from "@/app/[locale]/pokemons";
2 | import initTranslations from "@/app/i18n";
3 | import { db } from "@/db";
4 | import { pokemons } from "@/db/schema/pokemons";
5 |
6 | type Props = {
7 | params: {
8 | locale: string;
9 | };
10 | };
11 |
12 | const i18nNamespaces = ["pokemons"];
13 |
14 | const page = async ({ params: { locale } }: Props) => {
15 | const data = await db.select().from(pokemons);
16 | const { t } = await initTranslations(locale, i18nNamespaces);
17 | return ;
18 | };
19 |
20 | export default page;
21 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/TranslationsProvider.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import initTranslations from "@/app/i18n";
4 | import { createInstance } from "i18next";
5 | import { I18nextProvider } from "react-i18next";
6 |
7 | export default function TranslationsProvider({
8 | children,
9 | locale,
10 | namespaces,
11 | resources,
12 | }: {
13 | children: React.ReactNode;
14 | locale: string;
15 | namespaces: string[];
16 | resources: any;
17 | }) {
18 | const i18n = createInstance();
19 |
20 | initTranslations(locale, namespaces, i18n, resources);
21 |
22 | return {children};
23 | }
24 |
--------------------------------------------------------------------------------
/src/app/[locale]/new/action.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { db } from "@/db";
3 | import { Pokemon, pokemons } from "@/db/schema/pokemons";
4 | import { revalidatePath } from "next/cache";
5 | import { redirect } from "next/navigation";
6 |
7 | export async function newPokemon(prevState: any, pokemon: Pokemon) {
8 | // await fetch(`${API_URL}/pokemons`, {
9 | // method: "POST",
10 | // headers: {
11 | // "Content-Type": "application/json",
12 | // },
13 | // body: JSON.stringify(pokemon),
14 | // });
15 |
16 | await db.insert(pokemons).values(pokemon);
17 |
18 | revalidatePath("/", "layout");
19 | redirect("/");
20 | }
21 |
--------------------------------------------------------------------------------
/src/locales/tr-TR/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Kayıt Ol",
4 | "description": "Hesabınıza giriş yapmak için aşağıya e-postanızı girin.",
5 | "labels": {
6 | "email": "E-posta",
7 | "password": "Şifre"
8 | },
9 | "placeholders": {
10 | "email": "ornek@ornek.com",
11 | "password": "Şifre"
12 | },
13 | "buttons": {
14 | "signIn": "Kayıt Ol",
15 | "cancel": "İptal"
16 | },
17 | "errors": {
18 | "email": "Geçerli bir e-posta adresi girin",
19 | "password_length": "Şifre en az {{min}} karakter olmalıdır",
20 | "password": "Şifre alanı boş bırakılamaz"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/locales/tr-TR/signIn.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Giriş Yap",
4 | "description": "Hesabınıza giriş yapmak için aşağıya e-postanızı girin.",
5 | "labels": {
6 | "email": "E-posta",
7 | "password": "Şifre"
8 | },
9 | "placeholders": {
10 | "email": "ornek@ornek.com",
11 | "password": "Şifre"
12 | },
13 | "buttons": {
14 | "signIn": "Giriş Yap",
15 | "cancel": "İptal"
16 | },
17 | "errors": {
18 | "email": "Geçerli bir e-posta adresi girin",
19 | "password_length": "Şifre en az {{min}} karakter olmalıdır",
20 | "password": "Şifre alanı boş bırakılamaz"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/locales/en-US/signIn.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "SignIn",
4 | "description": "Enter your email below to signIn to your account.",
5 | "labels": {
6 | "email": "Email",
7 | "password": "Password"
8 | },
9 | "placeholders": {
10 | "email": "m@example.com",
11 | "password": "Password"
12 | },
13 | "buttons": {
14 | "signIn": "Sign in",
15 | "cancel": "Cancel"
16 | },
17 | "errors": {
18 | "email": "Please enter a valid email address",
19 | "password": "Please enter a password",
20 | "password_length": "Password must be at least {{min}} characters long"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/locales/en-US/register.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Register",
4 | "description": "Enter your email below to signIn to your account.",
5 | "labels": {
6 | "email": "Email",
7 | "password": "Password"
8 | },
9 | "placeholders": {
10 | "email": "m@example.com",
11 | "password": "Password"
12 | },
13 | "buttons": {
14 | "signIn": "Register",
15 | "cancel": "Cancel"
16 | },
17 | "errors": {
18 | "email": "Please enter a valid email address",
19 | "password": "Please enter a password",
20 | "password_length": "Password must be at least {{min}} characters long"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/db.json:
--------------------------------------------------------------------------------
1 | {
2 | "users": [
3 | {
4 | "email": "olivier@mail.com",
5 | "password": "$2a$10$2qbdwaO.bpOv90.lmElwGOydYuUbA55/Msue0BUHQ57oz5ccMEdmO",
6 | "id": 1
7 | },
8 | {
9 | "email": "zafer@gmail.com",
10 | "password": "$2a$10$LNxTMYdvr3iyf7.vclOHKOBsd1xQkVO7jnAUfuXYbvi0ljzlVxVr.",
11 | "id": 2
12 | }
13 | ],
14 | "pokemons": [
15 | {
16 | "id": "1",
17 | "name": "Bulbasaur",
18 | "type": "grass"
19 | },
20 | {
21 | "id": "4",
22 | "name": "Charmander",
23 | "type": "fire"
24 | },
25 | {
26 | "id": "7",
27 | "name": "Squirtle",
28 | "type": "water"
29 | }
30 | ]
31 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/src/db/index.ts:
--------------------------------------------------------------------------------
1 | let { PGHOST, PGDATABASE, PGUSER, PGPASSWORD } = process.env;
2 | import { env } from "@/env.mjs";
3 | import { neon } from "@neondatabase/serverless";
4 | import { type Logger } from "drizzle-orm/logger";
5 | import { drizzle } from "drizzle-orm/neon-http";
6 |
7 | class QueryLogger implements Logger {
8 | logQuery(query: string, params: unknown[]): void {
9 | console.debug("___QUERY___");
10 | console.debug(query);
11 | console.debug(params);
12 | console.debug("___END_QUERY___");
13 | }
14 | }
15 |
16 | const sql = neon(env.DATABASE_URL);
17 |
18 | const db = drizzle(sql, { logger: new QueryLogger() });
19 |
20 | export * from "./schema";
21 |
22 | export { db };
23 |
--------------------------------------------------------------------------------
/src/app/[locale]/new/page.tsx:
--------------------------------------------------------------------------------
1 | import NewPokemonForm from "@/app/[locale]/new/form";
2 | import initTranslations from "@/app/i18n";
3 | import TranslationsProvider from "@/components/TranslationsProvider";
4 |
5 | type Props = {
6 | params: {
7 | locale: string;
8 | };
9 | };
10 |
11 | const i18nNamespaces = ["new"];
12 |
13 | const page = async ({ params: { locale } }: Props) => {
14 | const { resources } = await initTranslations(locale, i18nNamespaces);
15 | return (
16 |
21 |
22 |
23 | );
24 | };
25 |
26 | export default page;
27 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/src/app/[locale]/[id]/delete/action.tsx:
--------------------------------------------------------------------------------
1 | "use server";
2 | import { db } from "@/db";
3 | import { pokemons } from "@/db/schema/pokemons";
4 | import { eq } from "drizzle-orm";
5 | import { revalidatePath } from "next/cache";
6 | import { redirect } from "next/navigation";
7 | import { z } from "zod";
8 |
9 | export async function deletePokemon(prevState: any, formData: FormData) {
10 | const schema = z.object({
11 | id: z.string({
12 | message: "ID is required",
13 | }),
14 | });
15 |
16 | const parse = schema.safeParse({
17 | id: formData.get("id"),
18 | });
19 |
20 | if (!parse.success) {
21 | return { errors: parse.error.errors };
22 | }
23 |
24 | const pokemon = parse.data;
25 |
26 | await db.delete(pokemons).where(eq(pokemons.id, parseInt(pokemon.id)));
27 |
28 | revalidatePath("/", "layout");
29 | redirect("/");
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/[locale]/signIn/page.tsx:
--------------------------------------------------------------------------------
1 | import SignInForm from "@/app/[locale]/signIn/form";
2 | import initTranslations from "@/app/i18n";
3 | import { auth } from "@/auth";
4 | import TranslationsProvider from "@/components/TranslationsProvider";
5 | import { redirect } from "next/navigation";
6 |
7 | type Props = {
8 | params: {
9 | locale: string;
10 | };
11 | };
12 |
13 | const i18nNamespaces = ["signIn"];
14 |
15 | const SignIn = async ({ params: { locale } }: Props) => {
16 | const { resources } = await initTranslations(locale, i18nNamespaces);
17 | const session = await auth();
18 | if (session) {
19 | redirect("/");
20 | }
21 | return (
22 |
27 |
28 |
29 | );
30 | };
31 | export default SignIn;
32 |
--------------------------------------------------------------------------------
/src/app/[locale]/register/page.tsx:
--------------------------------------------------------------------------------
1 | import RegisterForm from "@/app/[locale]/register/form";
2 | import initTranslations from "@/app/i18n";
3 | import { auth } from "@/auth";
4 | import TranslationsProvider from "@/components/TranslationsProvider";
5 | import { redirect } from "next/navigation";
6 |
7 | type Props = {
8 | params: {
9 | locale: string;
10 | };
11 | };
12 |
13 | const i18nNamespaces = ["register"];
14 |
15 | const SignIn = async ({ params: { locale } }: Props) => {
16 | const { resources } = await initTranslations(locale, i18nNamespaces);
17 | const session = await auth();
18 | if (session) {
19 | redirect("/");
20 | }
21 | return (
22 |
27 |
28 |
29 | );
30 | };
31 | export default SignIn;
32 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/env.mjs:
--------------------------------------------------------------------------------
1 | // @ts-check
2 | import { createEnv } from "@t3-oss/env-nextjs";
3 | import { z } from "zod";
4 |
5 | export const env = createEnv({
6 | /*
7 | * Serverside Environment variables, not available on the client.
8 | * Will throw if you access these variables on the client.
9 | */
10 | server: {
11 | DATABASE_URL: z.string().url(),
12 | AUTH_SECRET: z.string().min(1),
13 | },
14 | /*
15 | * Environment variables available on the client (and server).
16 | *
17 | * 💡 You'll get type errors if these are not prefixed with NEXT_PUBLIC_.
18 | */
19 | client: {},
20 | /*
21 | * Due to how Next.js bundles environment variables on Edge and Client,
22 | * we need to manually destructure them to make sure all are included in bundle.
23 | *
24 | * 💡 You'll get type errors if not all variables from `server` & `client` are included here.
25 | */
26 | runtimeEnv: {
27 | DATABASE_URL: process.env.DATABASE_URL,
28 | AUTH_SECRET: process.env.AUTH_SECRET,
29 | },
30 | });
31 |
--------------------------------------------------------------------------------
/src/app/[locale]/[id]/edit/page.tsx:
--------------------------------------------------------------------------------
1 | import EditForm from "@/app/[locale]/[id]/edit/form";
2 | import initTranslations from "@/app/i18n";
3 | import TranslationsProvider from "@/components/TranslationsProvider";
4 | import { db } from "@/db";
5 | import { pokemons } from "@/db/schema/pokemons";
6 | import { eq } from "drizzle-orm";
7 |
8 | type Props = {
9 | params: {
10 | id: string;
11 | locale: string;
12 | };
13 | };
14 |
15 | const i18nNamespaces = ["edit"];
16 |
17 | const Edit = async ({ params: { id, locale } }: Props) => {
18 | const pokemon = (
19 | await db
20 | .select()
21 | .from(pokemons)
22 | .where(eq(pokemons.id, parseInt(id)))
23 | )[0];
24 |
25 | const { resources } = await initTranslations(locale, i18nNamespaces);
26 |
27 | return (
28 |
33 |
34 |
35 | );
36 | };
37 |
38 | export default Edit;
39 |
--------------------------------------------------------------------------------
/src/app/[locale]/[id]/delete/page.tsx:
--------------------------------------------------------------------------------
1 | import DeleteForm from "@/app/[locale]/[id]/delete/form";
2 | import initTranslations from "@/app/i18n";
3 | import TranslationsProvider from "@/components/TranslationsProvider";
4 | import { db } from "@/db";
5 | import { pokemons } from "@/db/schema/pokemons";
6 | import { eq } from "drizzle-orm";
7 |
8 | export type Props = {
9 | params: {
10 | id: string;
11 | locale: string;
12 | };
13 | };
14 |
15 | const i18nNamespaces = ["delete"];
16 |
17 | const DeletePokemon = async ({ params: { id, locale } }: Props) => {
18 | const { resources } = await initTranslations(locale, i18nNamespaces);
19 | const pokemon = (
20 | await db
21 | .select()
22 | .from(pokemons)
23 | .where(eq(pokemons.id, parseInt(id)))
24 | )[0];
25 |
26 | return (
27 |
32 |
33 |
34 | );
35 | };
36 |
37 | export default DeletePokemon;
38 |
--------------------------------------------------------------------------------
/src/app/[locale]/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import PokemonListItem from "@/app/[locale]/pokemon-list-item";
2 | import {
3 | Card,
4 | CardContent,
5 | CardDescription,
6 | CardHeader,
7 | CardTitle,
8 | } from "@/components/ui/card";
9 | import { db } from "@/db";
10 | import { pokemons } from "@/db/schema/pokemons";
11 | import { eq } from "drizzle-orm";
12 |
13 | export type Props = {
14 | params: {
15 | id: string;
16 | };
17 | };
18 |
19 | const PokemonDetail = async ({ params: { id } }: Props) => {
20 | const pokemon = (
21 | await db
22 | .select()
23 | .from(pokemons)
24 | .where(eq(pokemons.id, parseInt(id)))
25 | )[0];
26 |
27 | return (
28 |
29 |
30 | Pokemon Detail
31 | This is the detail of the Pokemon.
32 |
33 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default PokemonDetail;
41 |
--------------------------------------------------------------------------------
/src/locales/en-US/edit.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Edit pokemon",
4 | "description": "Edit the details of the pokemon",
5 | "labels": {
6 | "pokemon_name": "Pokemon name",
7 | "type": "Type"
8 | },
9 | "placeholders": {
10 | "enter_pokemon": "Enter a new pokemon",
11 | "select_type": "Select"
12 | },
13 | "type_options": {
14 | "fire": "Fire",
15 | "water": "Water",
16 | "grass": "Grass",
17 | "electric": "Electric",
18 | "psychic": "Psychic",
19 | "ice": "Ice",
20 | "dragon": "Dragon",
21 | "dark": "Dark",
22 | "fairy": "Fairy",
23 | "fighting": "Fighting",
24 | "flying": "Flying",
25 | "poison": "Poison",
26 | "ground": "Ground",
27 | "rock": "Rock",
28 | "bug": "Bug",
29 | "ghost": "Ghost",
30 | "steel": "Steel",
31 | "normal": "Normal"
32 | },
33 | "buttons": {
34 | "cancel": "Cancel",
35 | "save": "Save",
36 | "pending": "Pending"
37 | },
38 | "errors": {
39 | "name_length": "Name must be at least 3 characters",
40 | "select_type": "Please select a type"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/locales/tr-TR/edit.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Pokemon Düzenle",
4 | "description": "Pokemon detaylarını düzenleyin",
5 | "labels": {
6 | "pokemon_name": "Pokemon Adı",
7 | "type": "Tür"
8 | },
9 | "placeholders": {
10 | "enter_pokemon": "Yeni bir pokemon girin",
11 | "select_type": "Seçiniz"
12 | },
13 | "type_options": {
14 | "fire": "Ateş",
15 | "water": "Su",
16 | "grass": "Çim",
17 | "electric": "Elektrik",
18 | "psychic": "Psişik",
19 | "ice": "Buz",
20 | "dragon": "Ejderha",
21 | "dark": "Karanlık",
22 | "fairy": "Peri",
23 | "fighting": "Dövüş",
24 | "flying": "Uçan",
25 | "poison": "Zehir",
26 | "ground": "Toprak",
27 | "rock": "Kaya",
28 | "bug": "Böcek",
29 | "ghost": "Hayalet",
30 | "steel": "Çelik",
31 | "normal": "Normal"
32 | },
33 | "buttons": {
34 | "cancel": "İptal",
35 | "save": "Kaydet",
36 | "pending": "Bekleniyor"
37 | },
38 | "errors": {
39 | "name_length": "Ad en az 3 karakter olmalıdır",
40 | "select_type": "Lütfen bir tür seçiniz"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/locales/en-US/new.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Create pokemon",
4 | "description": "Fill the form to create a new pokemon.",
5 | "labels": {
6 | "pokemon_name": "Pokemon name",
7 | "type": "Type"
8 | },
9 | "placeholders": {
10 | "enter_pokemon": "Enter a new pokemon",
11 | "select_type": "Select"
12 | },
13 | "type_options": {
14 | "fire": "Fire",
15 | "water": "Water",
16 | "grass": "Grass",
17 | "electric": "Electric",
18 | "psychic": "Psychic",
19 | "ice": "Ice",
20 | "dragon": "Dragon",
21 | "dark": "Dark",
22 | "fairy": "Fairy",
23 | "fighting": "Fighting",
24 | "flying": "Flying",
25 | "poison": "Poison",
26 | "ground": "Ground",
27 | "rock": "Rock",
28 | "bug": "Bug",
29 | "ghost": "Ghost",
30 | "steel": "Steel",
31 | "normal": "Normal"
32 | },
33 | "buttons": {
34 | "cancel": "Cancel",
35 | "save": "Save",
36 | "pending": "Pending"
37 | },
38 | "errors": {
39 | "name_length": "Name must be at least 3 characters",
40 | "select_type": "Please select a type"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/locales/tr-TR/new.json:
--------------------------------------------------------------------------------
1 | {
2 | "form": {
3 | "title": "Pokemon Oluştur",
4 | "description": "Yeni bir pokemon oluşturmak için formu doldurun.",
5 | "labels": {
6 | "pokemon_name": "Pokemon Adı",
7 | "type": "Tür"
8 | },
9 | "placeholders": {
10 | "enter_pokemon": "Yeni bir pokemon girin",
11 | "select_type": "Seçiniz"
12 | },
13 | "type_options": {
14 | "fire": "Ateş",
15 | "water": "Su",
16 | "grass": "Çim",
17 | "electric": "Elektrik",
18 | "psychic": "Psişik",
19 | "ice": "Buz",
20 | "dragon": "Ejderha",
21 | "dark": "Karanlık",
22 | "fairy": "Peri",
23 | "fighting": "Dövüş",
24 | "flying": "Uçan",
25 | "poison": "Zehir",
26 | "ground": "Toprak",
27 | "rock": "Kaya",
28 | "bug": "Böcek",
29 | "ghost": "Hayalet",
30 | "steel": "Çelik",
31 | "normal": "Normal"
32 | },
33 | "buttons": {
34 | "cancel": "İptal",
35 | "save": "Kaydet",
36 | "pending": "Bekleniyor"
37 | },
38 | "errors": {
39 | "name_length": "Ad en az 3 karakter olmalıdır",
40 | "select_type": "Lütfen bir tür seçiniz"
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/[locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import MainLayout from "@/app/[locale]/main-layout";
2 | import NextAuthProvider from "@/context/NextAuthProvider";
3 | import { ThemeProvider } from "@/theme/ThemeProvider";
4 | import type { Metadata } from "next";
5 | import { Inter } from "next/font/google";
6 | import "./globals.css";
7 |
8 | const inter = Inter({ subsets: ["latin"] });
9 |
10 | export const metadata: Metadata = {
11 | title: "Create Next App",
12 | description: "Generated by create next app",
13 | };
14 |
15 | export default function RootLayout({
16 | children,
17 | params: { locale },
18 | }: Readonly<{
19 | children: React.ReactNode;
20 | params: {
21 | locale: string;
22 | };
23 | }>) {
24 | return (
25 |
26 |
27 |
33 |
34 | {children}
35 |
36 |
37 |
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/src/db/seed.ts:
--------------------------------------------------------------------------------
1 | import { faker } from "@faker-js/faker";
2 | import { neon } from "@neondatabase/serverless";
3 | import * as dotenv from "dotenv";
4 | import { drizzle } from "drizzle-orm/neon-http";
5 | import { Pokemon, pokemons, users } from "./schema";
6 |
7 | dotenv.config({ path: "./.env" });
8 |
9 | const seed = async () => {
10 | const sql = neon(process.env.DATABASE_URL as string);
11 | const db = drizzle(sql);
12 |
13 | const usersData = Array.from({ length: 20 }).map(() => ({
14 | username: faker.internet.userName(),
15 | email: faker.internet.email(),
16 | }));
17 |
18 | const pokemonsData: Pokemon[] = [
19 | {
20 | id: 1,
21 | name: "Bulbasaur",
22 | type: "grass",
23 | },
24 | {
25 | id: 4,
26 | name: "Charmander",
27 | type: "fire",
28 | },
29 | {
30 | id: 7,
31 | name: "Squirtle",
32 | type: "water",
33 | },
34 | ];
35 |
36 | console.log("Seed start");
37 | let res = await db.insert(users).values(usersData);
38 | console.log(`Inserted ${res.rowCount} users`);
39 | res = await db.insert(pokemons).values(pokemonsData);
40 | console.log(`Inserted ${res.rowCount} pokemons`);
41 | console.log("Seed done");
42 | };
43 |
44 | seed();
45 |
--------------------------------------------------------------------------------
/src/app/i18n.ts:
--------------------------------------------------------------------------------
1 | import i18nConfig from "@/i18nConfig";
2 | import { createInstance, i18n } from "i18next";
3 | import resourcesToBackend from "i18next-resources-to-backend";
4 | import { initReactI18next } from "react-i18next/initReactI18next";
5 |
6 | export default async function initTranslations(
7 | locale: string,
8 | namespaces: string[],
9 | i18nInstance?: i18n,
10 | resources?: Record>
11 | ) {
12 | i18nInstance = i18nInstance || createInstance();
13 |
14 | i18nInstance.use(initReactI18next);
15 |
16 | if (!resources) {
17 | i18nInstance.use(
18 | resourcesToBackend(
19 | (language: string, namespace: string) =>
20 | import(`@/locales/${language}/${namespace}.json`)
21 | )
22 | );
23 | }
24 |
25 | await i18nInstance.init({
26 | lng: locale,
27 | resources,
28 | fallbackLng: i18nConfig.defaultLocale,
29 | supportedLngs: i18nConfig.locales,
30 | defaultNS: namespaces[0],
31 | fallbackNS: namespaces[0],
32 | ns: namespaces,
33 | preload: resources ? [] : i18nConfig.locales,
34 | });
35 |
36 | return {
37 | i18n: i18nInstance,
38 | resources: i18nInstance.services.resourceStore.data,
39 | t: i18nInstance.t,
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next Workshop
2 |
3 | 
4 |
5 | # Commands
6 |
7 | ```bash
8 | pnpm i
9 | pnpm dev
10 | ```
11 |
12 | # Features
13 |
14 | - [x] [App router navigation](https://nextjs.org)
15 | - [x] [Shadcn components](https://ui.shadcn.com/docs/installation/next)
16 | - [x] TailwindCSS
17 | - [x] [JSON Server](https://github.com/typicode/json-server)
18 | - [x] CRUD operations
19 | - [x] [Server Actions and Mutations](https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations)
20 | - [x] [Cache revalidation](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating)
21 | - [x] [Form validations](https://ui.shadcn.com/docs/components/form)
22 | - [x] [Next Theme](https://ui.shadcn.com/docs/dark-mode/next)
23 | - [x] [i18n](https://i18nexus.com/tutorials/nextjs/react-i18next)
24 | - [x] [Next Auth](https://authjs.dev/getting-started/installation?framework=next.js)
25 | - [ ] Drizzle
26 | - [ ] Corepack
27 | - [ ] VSCode Extensions (e.g. i18n-ally)
28 | - [ ] .env instead .env.sample
29 | - [ ] db.json to data/db.json
30 | - [ ] Create NEXT_DATA_SOURCE and use Strategy pattern to fetch data
31 | - [ ] Create connectionString (drizzle and db) into one file db/conns.ts
32 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ThemeToggleButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Moon, Sun } from "lucide-react";
4 | import { useTheme } from "next-themes";
5 |
6 | import { Button } from "@/components/ui/button";
7 | import {
8 | DropdownMenu,
9 | DropdownMenuContent,
10 | DropdownMenuItem,
11 | DropdownMenuTrigger,
12 | } from "@/components/ui/dropdown-menu";
13 |
14 | export function ThemeToggleButton() {
15 | const { setTheme } = useTheme();
16 |
17 | return (
18 |
19 |
20 |
25 |
26 |
27 | setTheme("light")}>
28 | Light
29 |
30 | setTheme("dark")}>
31 | Dark
32 |
33 | setTheme("system")}>
34 | System
35 |
36 |
37 |
38 | );
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/[locale]/pokemons.tsx:
--------------------------------------------------------------------------------
1 | import PokemonListItem from "@/app/[locale]/pokemon-list-item";
2 | import { Button } from "@/components/ui/button";
3 | import {
4 | Card,
5 | CardContent,
6 | CardDescription,
7 | CardFooter,
8 | CardHeader,
9 | CardTitle,
10 | } from "@/components/ui/card";
11 | import { Pokemon } from "@/db/schema/pokemons";
12 | import { cn } from "@/lib/utils";
13 | import { PlusIcon } from "lucide-react";
14 | import Link from "next/link";
15 |
16 | type Props = {
17 | t: (key: string, values?: Record) => string;
18 | pokemons: Pokemon[];
19 | };
20 |
21 | const Pokemons = ({ pokemons, t }: Props) => {
22 | return (
23 |
24 |
25 | {t("card.title")}
26 |
27 | {t("card.description", { count: pokemons.length })}
28 |
29 |
30 |
31 | {pokemons.map((pokemon) => (
32 |
33 | ))}
34 |
35 |
36 |
37 |
40 |
41 |
42 |
43 | );
44 | };
45 |
46 | export default Pokemons;
47 |
--------------------------------------------------------------------------------
/src/app/[locale]/pokemon-list-item.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button";
2 | import { Pokemon } from "@/db/schema/pokemons";
3 | import { EditIcon, TrashIcon } from "lucide-react";
4 | import Image from "next/image";
5 | import Link from "next/link";
6 |
7 | type Props = {
8 | pokemon: Pokemon;
9 | hasActions?: boolean;
10 | };
11 |
12 | const PokemonListItem = ({ pokemon, hasActions = true }: Props) => {
13 | return (
14 |
18 |
19 |
30 |
31 |
{pokemon.name}
32 |
33 | {pokemon.type} Pokemon
34 |
35 |
36 | {hasActions && (
37 | <>
38 |
39 |
42 |
43 |
44 |
47 |
48 | >
49 | )}
50 |
51 |
52 | );
53 | };
54 |
55 | export default PokemonListItem;
56 |
--------------------------------------------------------------------------------
/src/db/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "8c1a2b4e-bc3c-4eb2-84ad-bbf0f62208a1",
3 | "prevId": "00000000-0000-0000-0000-000000000000",
4 | "version": "5",
5 | "dialect": "pg",
6 | "tables": {
7 | "pokemons": {
8 | "name": "pokemons",
9 | "schema": "",
10 | "columns": {
11 | "id": {
12 | "name": "id",
13 | "type": "serial",
14 | "primaryKey": true,
15 | "notNull": true
16 | },
17 | "name": {
18 | "name": "name",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "type": {
24 | "name": "type",
25 | "type": "text",
26 | "primaryKey": false,
27 | "notNull": true
28 | }
29 | },
30 | "indexes": {},
31 | "foreignKeys": {},
32 | "compositePrimaryKeys": {},
33 | "uniqueConstraints": {}
34 | },
35 | "users": {
36 | "name": "users",
37 | "schema": "",
38 | "columns": {
39 | "id": {
40 | "name": "id",
41 | "type": "serial",
42 | "primaryKey": true,
43 | "notNull": true
44 | },
45 | "username": {
46 | "name": "username",
47 | "type": "text",
48 | "primaryKey": false,
49 | "notNull": false
50 | },
51 | "email": {
52 | "name": "email",
53 | "type": "varchar(256)",
54 | "primaryKey": false,
55 | "notNull": false
56 | }
57 | },
58 | "indexes": {},
59 | "foreignKeys": {},
60 | "compositePrimaryKeys": {},
61 | "uniqueConstraints": {}
62 | }
63 | },
64 | "enums": {},
65 | "schemas": {},
66 | "_meta": {
67 | "columns": {},
68 | "schemas": {},
69 | "tables": {}
70 | }
71 | }
--------------------------------------------------------------------------------
/src/app/[locale]/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 222.2 84% 4.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 222.2 84% 4.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 222.2 84% 4.9%;
15 |
16 | --primary: 222.2 47.4% 11.2%;
17 | --primary-foreground: 210 40% 98%;
18 |
19 | --secondary: 210 40% 96.1%;
20 | --secondary-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 | --accent: 210 40% 96.1%;
26 | --accent-foreground: 222.2 47.4% 11.2%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 210 40% 98%;
30 |
31 | --border: 214.3 31.8% 91.4%;
32 | --input: 214.3 31.8% 91.4%;
33 | --ring: 222.2 84% 4.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 222.2 84% 4.9%;
40 | --foreground: 210 40% 98%;
41 |
42 | --card: 222.2 84% 4.9%;
43 | --card-foreground: 210 40% 98%;
44 |
45 | --popover: 222.2 84% 4.9%;
46 | --popover-foreground: 210 40% 98%;
47 |
48 | --primary: 210 40% 98%;
49 | --primary-foreground: 222.2 47.4% 11.2%;
50 |
51 | --secondary: 217.2 32.6% 17.5%;
52 | --secondary-foreground: 210 40% 98%;
53 |
54 | --muted: 217.2 32.6% 17.5%;
55 | --muted-foreground: 215 20.2% 65.1%;
56 |
57 | --accent: 217.2 32.6% 17.5%;
58 | --accent-foreground: 210 40% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 210 40% 98%;
62 |
63 | --border: 217.2 32.6% 17.5%;
64 | --input: 217.2 32.6% 17.5%;
65 | --ring: 212.7 26.8% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
1 | import { API_URL } from "@/app/[locale]/loaders";
2 | import NextAuth, { DefaultSession } from "next-auth";
3 | import Credentials from "next-auth/providers/credentials";
4 | import { z } from "zod";
5 |
6 | declare module "next-auth" {
7 | interface Session {
8 | user: {
9 | email: string;
10 | } & DefaultSession["user"];
11 | }
12 | }
13 |
14 | export const signInSchema = z.object({
15 | email: z
16 | .string({ required_error: "Email is required" })
17 | .min(1, "Email is required")
18 | .email("Invalid email"),
19 | password: z
20 | .string({ required_error: "Password is required" })
21 | .min(1, "Password is required")
22 | .min(8, "Password must be more than 8 characters")
23 | .max(32, "Password must be less than 32 characters"),
24 | });
25 |
26 | export const { handlers, signIn, signOut, auth } = NextAuth({
27 | session: {
28 | strategy: "jwt",
29 | },
30 | pages: {
31 | signIn: "/signIn",
32 | },
33 | providers: [
34 | Credentials({
35 | credentials: {
36 | email: {},
37 | password: {},
38 | },
39 | authorize: async (credentials) => {
40 | let user = null;
41 |
42 | const { email, password } = await signInSchema.parseAsync(credentials);
43 |
44 | // logic to verify if user exists
45 | const res = await fetch(`${API_URL}/signIn`, {
46 | method: "POST",
47 | headers: {
48 | "Content-Type": "application/json",
49 | },
50 | body: JSON.stringify({
51 | email,
52 | password,
53 | }),
54 | });
55 |
56 | user = (await res.json()).user;
57 |
58 | if (!user) {
59 | // No user found, so this is their first attempt to signIn
60 | // meaning this is also the place you could do registration
61 | throw new Error("User not found.");
62 | }
63 |
64 | // return user object with the their profile data
65 | return user;
66 | },
67 | }),
68 | ],
69 | });
70 |
--------------------------------------------------------------------------------
/src/components/LanguageChanger.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { usePathname, useRouter } from "next/navigation";
4 |
5 | import { Button } from "@/components/ui/button";
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from "@/components/ui/dropdown-menu";
12 | import i18nConfig from "@/i18nConfig";
13 | import { useTranslation } from "react-i18next";
14 |
15 | export default function LanguageChanger() {
16 | const { i18n } = useTranslation();
17 | const currentLocale = i18n.language;
18 | const router = useRouter();
19 | const currentPathname = usePathname();
20 |
21 | const handleChange = (newLocale: string) => {
22 | const days = 30;
23 | const date = new Date();
24 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
25 | const expires = date.toUTCString();
26 | document.cookie = `NEXT_LOCALE=${newLocale};expires=${expires};path=/`;
27 | // redirect to the new locale path
28 | if (currentLocale === i18nConfig.defaultLocale) {
29 | router.push("/" + newLocale + currentPathname);
30 | } else {
31 | router.push(
32 | currentPathname.replace(`/${currentLocale}`, `/${newLocale}`)
33 | );
34 | }
35 | router.refresh();
36 | };
37 |
38 | return (
39 |
40 |
41 |
47 |
48 |
49 | handleChange("en-US")}>
50 | English
51 |
52 | handleChange("tr-TR")}>
53 | Türkçe
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/app/[locale]/[id]/delete/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import Link from "next/link";
3 | import { useFormState, useFormStatus } from "react-dom";
4 | import { useTranslation } from "react-i18next";
5 |
6 | import { deletePokemon } from "@/app/[locale]/[id]/delete/action";
7 | import PokemonListItem from "@/app/[locale]/pokemon-list-item";
8 | import { Button } from "@/components/ui/button";
9 | import {
10 | Card,
11 | CardContent,
12 | CardDescription,
13 | CardFooter,
14 | CardHeader,
15 | CardTitle,
16 | } from "@/components/ui/card";
17 | import { Pokemon } from "@/db/schema/pokemons";
18 | import { TrashIcon } from "lucide-react";
19 |
20 | type Props = {
21 | pokemon: Pokemon;
22 | };
23 |
24 | const DeleteButton = () => {
25 | const { t } = useTranslation();
26 | const { pending } = useFormStatus();
27 |
28 | return (
29 |
32 | );
33 | };
34 |
35 | const DeleteForm = ({ pokemon }: Props) => {
36 | const { t } = useTranslation();
37 | const [state, formAction] = useFormState(deletePokemon, null);
38 |
39 | return (
40 |
64 | );
65 | };
66 |
67 | export default DeleteForm;
68 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "test-next-auth-2",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "build": "next build",
7 | "start": "next start",
8 | "lint": "next lint",
9 | "db": "json-server-auth --watch db.json --port 3001",
10 | "server": "next dev",
11 | "dev2": "concurrently \"npm run server\" \"npm run db\"",
12 | "dev": "npm run server",
13 | "drizzle:studio": "drizzle-kit studio --verbose",
14 | "drizzle:drop": "drizzle-kit drop",
15 | "drizzle:generate": "drizzle-kit generate:pg",
16 | "drizzle:push": "drizzle-kit push:pg",
17 | "drizzle:seed": "node --loader esbuild-register/loader -r esbuild-register ./src/db/seed.ts"
18 | },
19 | "dependencies": {
20 | "@hookform/resolvers": "^3.3.4",
21 | "@neondatabase/serverless": "^0.9.1",
22 | "@radix-ui/react-checkbox": "^1.0.4",
23 | "@radix-ui/react-dialog": "^1.0.5",
24 | "@radix-ui/react-dropdown-menu": "^2.0.6",
25 | "@radix-ui/react-label": "^2.0.2",
26 | "@radix-ui/react-select": "^2.0.0",
27 | "@radix-ui/react-slot": "^1.0.2",
28 | "@t3-oss/env-nextjs": "^0.10.1",
29 | "class-variance-authority": "^0.7.0",
30 | "clsx": "^2.1.1",
31 | "drizzle-kit": "^0.20.17",
32 | "drizzle-orm": "^0.30.10",
33 | "i18next": "^23.11.3",
34 | "i18next-resources-to-backend": "^1.2.1",
35 | "lucide-react": "^0.376.0",
36 | "next": "14.2.3",
37 | "next-auth": "5.0.0-beta.17",
38 | "next-i18n-router": "^5.4.2",
39 | "next-themes": "^0.3.0",
40 | "react": "^18",
41 | "react-dom": "^18",
42 | "react-hook-form": "^7.51.3",
43 | "react-i18next": "^14.1.1",
44 | "tailwind-merge": "^2.3.0",
45 | "tailwindcss-animate": "^1.0.7",
46 | "zod": "^3.23.4"
47 | },
48 | "devDependencies": {
49 | "@faker-js/faker": "^8.4.1",
50 | "@types/node": "^20",
51 | "@types/react": "^18",
52 | "@types/react-dom": "^18",
53 | "concurrently": "^8.2.2",
54 | "dotenv": "^16.4.5",
55 | "esbuild-register": "^3.5.0",
56 | "eslint": "^8",
57 | "eslint-config-next": "14.2.3",
58 | "express": "^4.19.2",
59 | "json-server": "0.17.4",
60 | "json-server-auth": "^2.1.0",
61 | "pg": "^8.11.5",
62 | "postcss": "^8",
63 | "tailwindcss": "^3.4.1",
64 | "ts-node": "^10.9.2",
65 | "tsx": "^4.9.1",
66 | "typescript": "^5"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/public/placeholder.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/[locale]/signIn/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { signInAction } from "@/app/[locale]/signIn/action";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import {
13 | Form,
14 | FormControl,
15 | FormField,
16 | FormItem,
17 | FormLabel,
18 | FormMessage,
19 | } from "@/components/ui/form";
20 | import { Input } from "@/components/ui/input";
21 | import { zodResolver } from "@hookform/resolvers/zod";
22 | import Link from "next/link";
23 | import { useFormState, useFormStatus } from "react-dom";
24 | import { useForm } from "react-hook-form";
25 | import { useTranslation } from "react-i18next";
26 | import { z } from "zod";
27 |
28 | const SignInButton = () => {
29 | const { t } = useTranslation();
30 | const { pending } = useFormStatus();
31 |
32 | return (
33 |
36 | );
37 | };
38 |
39 | const SignInForm = () => {
40 | const { t } = useTranslation();
41 | const schema = z.object({
42 | email: z.string().email(t("form.errors.email")),
43 | password: z.string().min(8, t("form.errors.password_length", { min: 8 })),
44 | });
45 |
46 | const [state, formAction] = useFormState(signInAction, null);
47 | const form = useForm>({
48 | resolver: zodResolver(schema),
49 | defaultValues: {
50 | email: "olivier@mail.com",
51 | password: "bestPassw0rd",
52 | },
53 | });
54 | return (
55 |
106 |
107 | );
108 | };
109 |
110 | export default SignInForm;
111 |
--------------------------------------------------------------------------------
/src/app/[locale]/register/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { registerAction } from "@/app/[locale]/register/action";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import {
13 | Form,
14 | FormControl,
15 | FormField,
16 | FormItem,
17 | FormLabel,
18 | FormMessage,
19 | } from "@/components/ui/form";
20 | import { Input } from "@/components/ui/input";
21 | import { zodResolver } from "@hookform/resolvers/zod";
22 | import Link from "next/link";
23 | import { useFormState, useFormStatus } from "react-dom";
24 | import { useForm } from "react-hook-form";
25 | import { useTranslation } from "react-i18next";
26 | import { z } from "zod";
27 |
28 | const SignInButton = () => {
29 | const { t } = useTranslation();
30 | const { pending } = useFormStatus();
31 |
32 | return (
33 |
36 | );
37 | };
38 |
39 | const RegisterForm = () => {
40 | const { t } = useTranslation();
41 | const schema = z.object({
42 | email: z.string().email(t("form.errors.email")),
43 | password: z.string().min(8, t("form.errors.password_length", { min: 8 })),
44 | });
45 |
46 | const [state, formAction] = useFormState(registerAction, null);
47 | const form = useForm>({
48 | resolver: zodResolver(schema),
49 | defaultValues: {
50 | email: "zafer@gmail.com",
51 | password: "12345678",
52 | },
53 | });
54 | return (
55 |
106 |
107 | );
108 | };
109 |
110 | export default RegisterForm;
111 |
--------------------------------------------------------------------------------
/src/app/[locale]/new/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { newPokemon } from "@/app/[locale]/new/action";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import {
13 | Form,
14 | FormControl,
15 | FormField,
16 | FormItem,
17 | FormLabel,
18 | FormMessage,
19 | } from "@/components/ui/form";
20 | import { Input } from "@/components/ui/input";
21 | import {
22 | Select,
23 | SelectContent,
24 | SelectItem,
25 | SelectTrigger,
26 | SelectValue,
27 | } from "@/components/ui/select";
28 | import { zodResolver } from "@hookform/resolvers/zod";
29 | import Link from "next/link";
30 | import { useFormState, useFormStatus } from "react-dom";
31 | import { useForm } from "react-hook-form";
32 | import { useTranslation } from "react-i18next";
33 | import { z } from "zod";
34 |
35 | const CreateButton = () => {
36 | const { pending } = useFormStatus();
37 | return (
38 |
41 | );
42 | };
43 |
44 | const NewPokemonForm = () => {
45 | const { t } = useTranslation();
46 |
47 | const schema = z.object({
48 | name: z.string().min(3, t("form.errors.name_length", { min: 3 })),
49 | type: z.string().min(1, t("form.errors.select_type")),
50 | });
51 |
52 | const [state, formAction] = useFormState(newPokemon, null);
53 | const form = useForm>({
54 | resolver: zodResolver(schema),
55 | defaultValues: {
56 | name: "",
57 | type: "",
58 | },
59 | });
60 | return (
61 |
127 |
128 | );
129 | };
130 |
131 | export default NewPokemonForm;
132 |
--------------------------------------------------------------------------------
/src/app/[locale]/[id]/edit/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { editPokemon } from "@/app/[locale]/[id]/edit/action";
3 | import { Button } from "@/components/ui/button";
4 | import {
5 | Card,
6 | CardContent,
7 | CardDescription,
8 | CardFooter,
9 | CardHeader,
10 | CardTitle,
11 | } from "@/components/ui/card";
12 | import {
13 | Form,
14 | FormControl,
15 | FormField,
16 | FormItem,
17 | FormLabel,
18 | FormMessage,
19 | } from "@/components/ui/form";
20 | import { Input } from "@/components/ui/input";
21 | import {
22 | Select,
23 | SelectContent,
24 | SelectItem,
25 | SelectTrigger,
26 | SelectValue,
27 | } from "@/components/ui/select";
28 | import { Pokemon } from "@/db/schema/pokemons";
29 | import { zodResolver } from "@hookform/resolvers/zod";
30 | import Link from "next/link";
31 | import { useFormState, useFormStatus } from "react-dom";
32 | import { useForm } from "react-hook-form";
33 | import { useTranslation } from "react-i18next";
34 | import { z } from "zod";
35 |
36 | type Props = {
37 | pokemon: Pokemon;
38 | };
39 |
40 | const EditButton = () => {
41 | const { pending } = useFormStatus();
42 |
43 | return (
44 |
47 | );
48 | };
49 |
50 | const EditForm = ({ pokemon }: Props) => {
51 | const { t } = useTranslation();
52 | const schema = z.object({
53 | id: z.number(),
54 | name: z.string().min(3, t("form.errors.name_length", { min: 3 })),
55 | type: z.string().min(1, t("form.errors.select_type")),
56 | });
57 |
58 | const [state, formAction] = useFormState(editPokemon, null);
59 | const form = useForm>({
60 | resolver: zodResolver(schema),
61 | defaultValues: {
62 | id: pokemon.id,
63 | name: pokemon.name,
64 | type: pokemon.type,
65 | },
66 | });
67 | return (
68 |
134 |
135 | );
136 | };
137 |
138 | export default EditForm;
139 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | }
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef<
57 | React.ElementRef,
58 | SheetContentProps
59 | >(({ side = "right", className, children, ...props }, ref) => (
60 |
61 |
62 |
67 | {children}
68 |
69 |
70 | Close
71 |
72 |
73 |
74 | ))
75 | SheetContent.displayName = SheetPrimitive.Content.displayName
76 |
77 | const SheetHeader = ({
78 | className,
79 | ...props
80 | }: React.HTMLAttributes) => (
81 |
88 | )
89 | SheetHeader.displayName = "SheetHeader"
90 |
91 | const SheetFooter = ({
92 | className,
93 | ...props
94 | }: React.HTMLAttributes) => (
95 |
102 | )
103 | SheetFooter.displayName = "SheetFooter"
104 |
105 | const SheetTitle = React.forwardRef<
106 | React.ElementRef,
107 | React.ComponentPropsWithoutRef
108 | >(({ className, ...props }, ref) => (
109 |
114 | ))
115 | SheetTitle.displayName = SheetPrimitive.Title.displayName
116 |
117 | const SheetDescription = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, ...props }, ref) => (
121 |
126 | ))
127 | SheetDescription.displayName = SheetPrimitive.Description.displayName
128 |
129 | export {
130 | Sheet,
131 | SheetPortal,
132 | SheetOverlay,
133 | SheetTrigger,
134 | SheetClose,
135 | SheetContent,
136 | SheetHeader,
137 | SheetFooter,
138 | SheetTitle,
139 | SheetDescription,
140 | }
141 |
--------------------------------------------------------------------------------
/src/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { Slot } from "@radix-ui/react-slot"
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from "react-hook-form"
12 |
13 | import { cn } from "@/lib/utils"
14 | import { Label } from "@/components/ui/label"
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error("useFormField should be used within ")
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = "FormItem"
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/src/app/[locale]/main-layout.tsx:
--------------------------------------------------------------------------------
1 | import { CircleUser, Menu, Package2 } from "lucide-react";
2 | import Link from "next/link";
3 |
4 | import { Button } from "@/components/ui/button";
5 |
6 | import initTranslations from "@/app/i18n";
7 | import { auth } from "@/auth";
8 | import LanguageChanger from "@/components/LanguageChanger";
9 | import SignInMenuItem from "@/components/SignInMenuItem";
10 | import SignOutMenuItem from "@/components/SignOutMenuItem";
11 | import { ThemeToggleButton } from "@/components/ThemeToggleButton";
12 | import TranslationsProvider from "@/components/TranslationsProvider";
13 | import {
14 | DropdownMenu,
15 | DropdownMenuContent,
16 | DropdownMenuItem,
17 | DropdownMenuLabel,
18 | DropdownMenuSeparator,
19 | DropdownMenuTrigger,
20 | } from "@/components/ui/dropdown-menu";
21 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
22 |
23 | type Props = {
24 | children: React.ReactNode;
25 | locale: string;
26 | };
27 |
28 | const i18nNamespaces = ["main-layout"];
29 |
30 | export default async function MainLayout({ children, locale }: Props) {
31 | const { t, i18n, resources } = await initTranslations(locale, i18nNamespaces);
32 | const session = await auth();
33 | return (
34 |
39 |
40 |
41 |
62 |
63 |
64 |
72 |
73 |
74 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
116 |
117 |
118 |
119 | {t("user_menu.my_account")}
120 |
121 |
122 | {t("user_menu.settings")}
123 | {t("user_menu.support")}
124 |
125 | {session ? : }
126 |
127 |
128 |
129 |
130 |
131 | {children}
132 |
133 |
134 |
135 | );
136 | }
137 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------