├── .env.example ├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── .prettierrc.json ├── README.md ├── bun.lockb ├── components.json ├── compose.yaml ├── cypress.config.ts ├── cypress ├── e2e │ ├── google-auth.cy.ts │ ├── login.cy.ts │ ├── register.cy.ts │ └── spec.cy.ts ├── fixtures │ └── example.json └── support │ ├── commands.ts │ └── e2e.ts ├── drizzle.config.ts ├── drizzle └── 0000_shocking_killer_shrike.sql ├── jest.config.ts ├── jest.setup.ts ├── messages ├── en.json └── es.json ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public └── images │ ├── logo.svg │ ├── next-js.svg │ └── placeholder.svg ├── src ├── app │ ├── [locale] │ │ ├── (auth) │ │ │ ├── 2fa │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ ├── login │ │ │ │ └── page.tsx │ │ │ └── register │ │ │ │ └── page.tsx │ │ ├── (dashboard) │ │ │ ├── layout.tsx │ │ │ ├── profile │ │ │ │ ├── page.tsx │ │ │ │ └── settings │ │ │ │ │ └── page.tsx │ │ │ └── subscription │ │ │ │ └── page.tsx │ │ ├── (error) │ │ │ └── not-found.tsx │ │ ├── (public) │ │ │ ├── about │ │ │ │ └── page.tsx │ │ │ ├── layout.tsx │ │ │ └── pricing │ │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ └── [...all] │ │ │ │ └── route.ts │ │ └── trpc │ │ │ └── [trpc] │ │ │ └── route.ts │ ├── globals.css │ ├── layout.tsx │ ├── page.tsx │ ├── robots.ts │ └── sitemap.ts ├── components │ ├── lang-switcher.tsx │ ├── providers │ │ └── theme-provider.tsx │ ├── responsive-modal.tsx │ ├── seo │ │ └── structured-data.tsx │ ├── shared │ │ └── icons.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── footer.tsx │ │ ├── form.tsx │ │ ├── input-otp.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── navigation-menu.tsx │ │ ├── radio-group.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── sidebar.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── theme-toggle.tsx │ │ └── tooltip.tsx ├── configs │ └── site.config.ts ├── db │ ├── index.ts │ └── schema.ts ├── emails │ ├── 2fa-verification-email.tsx │ ├── account-verification-email.tsx │ ├── admin-notification-email.tsx │ ├── contact-us-email.tsx │ ├── password-reset-email.tsx │ ├── static │ │ └── logo.svg │ └── welcome-email.tsx ├── hooks │ ├── use-confirm.tsx │ ├── use-mobile.ts │ └── use-mobile.tsx ├── i18n │ ├── navigation.ts │ ├── request.ts │ └── routing.ts ├── lib │ ├── auth-client.ts │ ├── auth.ts │ ├── fonts.ts │ ├── process-event.ts │ ├── ratelimit.ts │ ├── redis.ts │ ├── resend.ts │ ├── session.ts │ ├── stripe.ts │ ├── try-catch.ts │ └── utils.ts ├── middleware.ts ├── modules │ ├── auth │ │ ├── schema.ts │ │ └── ui │ │ │ ├── components │ │ │ ├── password-input.tsx │ │ │ ├── two-factor-form.tsx │ │ │ ├── two-factor-toggle.tsx │ │ │ ├── update-password-form.tsx │ │ │ ├── user-auth-form.tsx │ │ │ └── user-button.tsx │ │ │ └── layouts │ │ │ └── auth-layout.tsx │ ├── home │ │ └── ui │ │ │ ├── components │ │ │ ├── footer.tsx │ │ │ ├── header │ │ │ │ ├── index.tsx │ │ │ │ └── nav-link.tsx │ │ │ └── name-input.tsx │ │ │ └── layouts │ │ │ └── home-layout.tsx │ ├── pricing │ │ ├── constants.ts │ │ └── ui │ │ │ └── components │ │ │ ├── plan-card.tsx │ │ │ └── pricing-plans.tsx │ ├── subscription │ │ ├── constants.ts │ │ ├── schemas.ts │ │ └── ui │ │ │ ├── components │ │ │ ├── billing-history.tsx │ │ │ ├── change-plan.tsx │ │ │ ├── payment-methods-card.tsx │ │ │ └── user-current-plan.tsx │ │ │ ├── sections │ │ │ └── subscription-section.tsx │ │ │ └── views │ │ │ └── subscription-view.tsx │ └── users │ │ ├── schemas.ts │ │ ├── server │ │ └── procedures.ts │ │ ├── ui │ │ ├── components │ │ │ ├── danger-zone-card.tsx │ │ │ ├── security-settings.tsx │ │ │ └── user-profile-settings.tsx │ │ ├── layouts │ │ │ └── user-layout.tsx │ │ ├── sections │ │ │ ├── update-personal-information-section.tsx │ │ │ ├── user-profile-section.tsx │ │ │ └── user-profile-settings-section.tsx │ │ └── views │ │ │ ├── user-profile-settings-view.tsx │ │ │ └── user-profile-view.tsx │ │ └── utils │ │ └── get-user.ts └── trpc │ ├── client.tsx │ ├── init.ts │ ├── query-client.ts │ ├── routers │ └── _app.ts │ └── server.tsx └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Environment variables declared in this file are automatically made available to Prisma. 2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema 3 | 4 | # Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB. 5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings 6 | 7 | DATABASE_URL= -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | # Secret key for authentication 2 | AUTH_SECRET= 3 | 4 | # Google OAuth client ID 5 | AUTH_GOOGLE_ID= 6 | # Google OAuth client secret 7 | AUTH_GOOGLE_SECRET= 8 | 9 | # Public URL of the Next.js application 10 | NEXT_PUBLIC_URL= 11 | 12 | # Secret key for better authentication 13 | BETTER_AUTH_SECRET= 14 | # URL for better authentication service 15 | BETTER_AUTH_URL= 16 | 17 | # URL for Upstash Redis REST API 18 | UPSTASH_REDIS_REST_URL= 19 | # Token for Upstash Redis REST API 20 | UPSTASH_REDIS_REST_TOKEN= 21 | 22 | # Google API client ID 23 | GOOGLE_CLIENT_ID= 24 | # Google API client secret 25 | GOOGLE_CLIENT_SECRET= 26 | 27 | # Public key for Stripe 28 | NEXT_PUBLIC_STRIPE_PUBLIC_KEY= 29 | # Secret key for Stripe 30 | STRIPE_SECRET_KEY= 31 | # Monthly price ID for Stripe 32 | NEXT_PUBLIC_STRIPE_MONTHLY_PRICE_ID= 33 | # Customer portal URL for Stripe 34 | NEXT_PUBLIC_STRIPE_CUSTOMER_PORTAL_URL= 35 | # Webhook secret for Stripe 36 | STRIPE_WEBHOOK_SECRET= 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | # drizzle 3 | /drizzle 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | .yarn/install-state.gz 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env*.local 31 | .env 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nextjs-starter-template 2 | 3 | A robust and flexible Next.js starter template with TypeScript, tRPC, Jest, Cypress, Better-auth.js, Drizzle ORM, and more. This template provides everything you need to kickstart your modern web development projects with best practices and industry-standard tools. 4 | 5 | ## Features 6 | 7 | This template is built using the following technologies: 8 | 9 | - **Core Technologies:** 10 | 11 | - **Next.js**: Powerful React framework with server-side rendering and more. 12 | - **TypeScript**: Strongly-typed language for enhanced code quality and maintainability. 13 | 14 | - **UI & Styling:** 15 | 16 | - **Tailwind CSS**: Utility-first CSS framework for rapid UI development. 17 | - **Shadcn UI**: Beautifully designed and accessible UI components. 18 | 19 | - **Backend & Data:** 20 | 21 | - **tRPC**: End-to-end typesafe APIs made easy with React and TypeScript. 22 | - **Drizzle**: TypeScript ORM that's lightweight, performant, and type-safe. 23 | - **Stripe**: Powerful and flexible tools for internet commerce and online payments. 24 | 25 | - **Authentication & Security:** 26 | 27 | - **Better-auth**: Modern authentication solution for Next.js applications with enhanced security. 28 | 29 | - **Testing:** 30 | 31 | - **Jest**: Comprehensive unit and integration testing setup. 32 | - **Cypress**: End-to-end testing for simulating real user interactions. 33 | 34 | - **Other Features:** 35 | - **ESLint and Prettier**: Ensuring code quality and consistency. 36 | - **Husky and Lint-Staged**: Pre-commit hooks to automate tests and linting. 37 | - **dotenv**: Easy management of environment variables. 38 | - **next-seo** and **next-sitemap**: SEO and sitemap management for better search engine visibility. 39 | 40 | ## Getting Started 41 | 42 | ### Obtaining API Keys 43 | 44 | - **Auth Google keys**: [Generate your Google auth API key here](console.cloud.google.com). 45 | - **Auth Facebook keys**: [Get your Facebook auth API key here](developers.facebook.com). 46 | - **Stripe**: Follow [this guide](https://stripe.com/docs/keys) to obtain your Stripe API keys and configure your Stripe account. 47 | - **Better-auth**: Follow [this guide](https://better-auth.com/docs/get-started) to set up Better-auth and obtain your necessary credentials. 48 | - **Resend key**: [Get your Stripe Resend key here](resend.com). 49 | - **Upstash Redis (for rate limiting)**: Follow [this guide](https://upstash.com/docs/getting-started) to create an Upstash Redis database and obtain your connection details. 50 | 51 | Clone the repository and start building your application with confidence! 52 | 53 | git clone https://github.com/yaredow/next-starter.git 54 | cd next-starter 55 | npm install 56 | 57 | ``` 58 | 59 | ``` 60 | 61 | ## Usage 62 | 63 | 1. Clone the repository: 64 | 65 | ``` 66 | git clone https://github.com/your-username/your-repo-name.git 67 | cd your-repo-name 68 | ``` 69 | 70 | 2. Install dependencies: 71 | 72 | ``` 73 | npm install 74 | ``` 75 | 76 | 3. Create a `.env` file by copying the `.env.example` file: 77 | 78 | ``` 79 | cp .env.example .env 80 | ``` 81 | 82 | 4. Create a `.env.local` file by copying the `.env.local.example` file: 83 | 84 | ``` 85 | cp .env.local.example .env.local 86 | ``` 87 | 88 | 5. Open the `.env` and `.env.local` files and replace the placeholder values with your actual environment variable values. 89 | 90 | 6. Run the development server: 91 | 92 | ``` 93 | npm run dev 94 | ``` 95 | 96 | 7. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 97 | 98 | ## Running Tests 99 | 100 | - **End-to-End Tests (Cypress)**: 101 | 102 | To run Cypress end-to-end tests, use the following command: 103 | 104 | ``` 105 | npm run cypress 106 | ``` 107 | 108 | - **Unit Tests (Jest)**: 109 | 110 | To run Jest unit tests, use the following command: 111 | 112 | ``` 113 | npm run test 114 | ``` 115 | 116 | ## Running React Email in Development 117 | 118 | To start the React Email development server, use the following command: 119 | 120 | ``` 121 | npm run email:dev 122 | ``` 123 | 124 | ## Pre-commit Hook 125 | 126 | This project uses [Husky](https://github.com/typicode/husky) to manage Git hooks. When you commit a change, Husky runs the following commands automatically: 127 | 128 | ``` 129 | npm run format:fix && npm run lint && npm test 130 | ``` 131 | 132 | - `npm run format:fix`: This command formats your code using a formatter (e.g., Prettier). 133 | - `npm run lint`: This command runs the linter to check for code quality issues. 134 | - `npm test`: This command runs the test suite to ensure that your changes do not break existing functionality. 135 | 136 | These commands help maintain code quality and consistency across the project. 137 | 138 | ## License 139 | 140 | This project is licensed under the MIT License. 141 | 142 | ## Contributing 143 | 144 | Contributions are welcome! Please open an issue or submit a pull request. 145 | 146 | ``` 147 | 148 | ``` 149 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaredow/next-starter/06c3f9408c86324d1546417f5aa2355e421e8de8/bun.lockb -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | postgres: 3 | image: postgres:latest 4 | container_name: postgres_next-start 5 | environment: 6 | POSTGRES_USER: myuser 7 | POSTGRES_PASSWORD: mypassword 8 | POSTGRES_DB: mydatabase 9 | ports: 10 | - "5432:5432" 11 | volumes: 12 | - postgres_data:/var/lib/postgresql/data 13 | volumes: 14 | postgres_data: 15 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/e2e/google-auth.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Google Auth Flow", () => { 4 | it("should successfully log in with Google (mocked)", () => { 5 | cy.intercept("GET", "/api/auth/callback/google*", (req) => { 6 | // Adjust the URL to match your callback 7 | req.reply((res) => { 8 | // Simulate a successful Google login 9 | res.send(302, { Location: "/profile" }); // Redirect to your logged-in page 10 | }); 11 | }).as("googleCallback"); 12 | 13 | cy.visit("/login"); 14 | cy.contains("Google").click(); 15 | 16 | cy.wait("@googleCallback"); 17 | 18 | cy.url().should("include", "/"); 19 | cy.contains("Next Starter Template").should("be.visible"); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/e2e/login.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Authentication flow", () => { 4 | it("Should successfully log in an existing user", () => { 5 | cy.visit("http://localhost:3000/login"); 6 | 7 | // fill out the login form 8 | cy.get('input[name="email"]').type("yaredyilma11@gmail.com"); 9 | cy.get('input[name="password"]').type("Test1234@"); 10 | 11 | cy.get('button[type="submit"]').click(); 12 | cy.url().should("include", "/"); 13 | 14 | cy.contains("Next Starter Template").should("be.visible"); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /cypress/e2e/register.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("Authentication flow", () => { 4 | it("should sign up a new user", () => { 5 | cy.visit("http://localhost:3000/register"); 6 | 7 | // fill out the signup form 8 | cy.get('input[name="name"]').type("Test User"); 9 | cy.get('input[name="email"]').type(`testuser_${Date.now()}@example.com`); 10 | cy.get('input[name="password"]').type("TestPassword123"); 11 | 12 | //submit the form 13 | cy.get('button[type="submit"]').click(); 14 | 15 | // assert that signup was successful 16 | cy.url().should("include", "/"); 17 | cy.contains("Next Starter Template").should("be.visible"); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | describe("template spec", () => { 4 | it("does not do much", () => { 5 | expect(true).to.equal(true); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import { defineConfig } from "drizzle-kit"; 3 | 4 | export default defineConfig({ 5 | out: "./drizzle", 6 | schema: "./src/db/schema.ts", 7 | dialect: "postgresql", 8 | dbCredentials: { 9 | url: process.env.DATABASE_URL!, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /drizzle/0000_shocking_killer_shrike.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "account" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "account_id" text NOT NULL, 4 | "provider_id" text NOT NULL, 5 | "user_id" text NOT NULL, 6 | "access_token" text, 7 | "refresh_token" text, 8 | "id_token" text, 9 | "access_token_expires_at" timestamp, 10 | "refresh_token_expires_at" timestamp, 11 | "scope" text, 12 | "password" text, 13 | "created_at" timestamp NOT NULL, 14 | "updated_at" timestamp NOT NULL 15 | ); 16 | --> statement-breakpoint 17 | CREATE TABLE "session" ( 18 | "id" text PRIMARY KEY NOT NULL, 19 | "expires_at" timestamp NOT NULL, 20 | "token" text NOT NULL, 21 | "created_at" timestamp NOT NULL, 22 | "updated_at" timestamp NOT NULL, 23 | "ip_address" text, 24 | "user_agent" text, 25 | "user_id" text NOT NULL, 26 | CONSTRAINT "session_token_unique" UNIQUE("token") 27 | ); 28 | --> statement-breakpoint 29 | CREATE TABLE "two_factor" ( 30 | "id" text PRIMARY KEY NOT NULL, 31 | "secret" text NOT NULL, 32 | "backup_codes" text NOT NULL, 33 | "userId" text NOT NULL 34 | ); 35 | --> statement-breakpoint 36 | CREATE TABLE "user" ( 37 | "id" text PRIMARY KEY NOT NULL, 38 | "name" text NOT NULL, 39 | "email" text NOT NULL, 40 | "email_verified" boolean NOT NULL, 41 | "image" text, 42 | "two_factor_enabled" boolean, 43 | "created_at" timestamp NOT NULL, 44 | "updated_at" timestamp NOT NULL, 45 | "stripe_customer_id" text, 46 | "stripe_subscription_id" text, 47 | "stripe_price_id" text, 48 | "stripe_current_period_end" timestamp, 49 | "stripe_subscription_status" varchar(255), 50 | "payment_method_brand" varchar(255), 51 | "payment_method_last4" varchar(4), 52 | "cancel_at_period_end" boolean, 53 | CONSTRAINT "user_email_unique" UNIQUE("email") 54 | ); 55 | --> statement-breakpoint 56 | CREATE TABLE "verification" ( 57 | "id" text PRIMARY KEY NOT NULL, 58 | "identifier" text NOT NULL, 59 | "value" text NOT NULL, 60 | "expires_at" timestamp NOT NULL, 61 | "created_at" timestamp, 62 | "updated_at" timestamp 63 | ); 64 | --> statement-breakpoint 65 | ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 66 | ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint 67 | ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import nextJest from "next/jest"; 2 | 3 | const createJestConfig = nextJest({ 4 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 5 | dir: "./", 6 | }); 7 | 8 | // Add any custom config to be passed to Jest 9 | const customJestConfig = { 10 | setupFilesAfterEnv: ["/jest.setup.ts"], 11 | testEnvironment: "jsdom", 12 | }; 13 | 14 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 15 | export default createJestConfig(customJestConfig); 16 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom"; 2 | -------------------------------------------------------------------------------- /messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "HomePage": { 3 | "title": "Hello world!", 4 | "about": "Go to the about page", 5 | "mainHeading": "Next.js Starter Template", 6 | "subHeading": "A minimal template to kickstart your Next.js projects.", 7 | "getStartedButton": "Get Started", 8 | "githubButton": "Github" 9 | }, 10 | "label": "Select Language", 11 | "en": "English", 12 | "de": "German" 13 | } 14 | -------------------------------------------------------------------------------- /messages/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "HomePage": { 3 | "title": "¡Hola Mundo!", 4 | "about": "Ir a la página de acerca de", 5 | "mainHeading": "Plantilla de Inicio de Next.js", 6 | "subHeading": "Una plantilla mínima para impulsar tus proyectos de Next.js.", 7 | "getStartedButton": "Empezar", 8 | "githubButton": "Github" 9 | }, 10 | "localeSwitcher": { 11 | "label": "Language", 12 | "en": "English", 13 | "de": "German" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | import createNextIntlPlugin from "next-intl/plugin"; 4 | 5 | const nextConfig = { 6 | images: { 7 | remotePatterns: [ 8 | { hostname: "lh3.googleusercontent.com" }, 9 | { hostname: "platform-lookaside.fbsbx.com" }, 10 | ], 11 | }, 12 | compiler: { 13 | removeConsole: process.env.NODE_ENV === "production", 14 | }, 15 | }; 16 | 17 | const withNextIntl = createNextIntlPlugin(); 18 | 19 | export default withNextIntl(nextConfig); 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "dev:all": "concurrently \"pnpm dev --turbopack\" \"pnpm run dev:stripe\"", 8 | "dev:stripe": "stripe listen --forward-to localhost:3000/api/auth/stripe/webhook", 9 | "build": "next build", 10 | "format": "prettier --check --ignore-path .gitignore .", 11 | "format:fix": "prettier --write --ignore-path .gitignore .", 12 | "start": "next start", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "coverage": "jest --coverage", 16 | "cy:open": "cypress open", 17 | "lint": "next lint", 18 | "email:dev": "email dev --dir src/emails", 19 | "db:generate": "drizzle-kit generate", 20 | "db:migrate": "drizzle-kit migrate", 21 | "db:studio": "drizzle-kit studio", 22 | "db:push": "drizzle-kit push" 23 | }, 24 | "dependencies": { 25 | "@better-auth/stripe": "^1.2.8", 26 | "@hookform/resolvers": "^5.0.1", 27 | "@neondatabase/serverless": "^0.10.4", 28 | "@radix-ui/react-avatar": "^1.1.10", 29 | "@radix-ui/react-checkbox": "^1.3.2", 30 | "@radix-ui/react-dialog": "^1.1.14", 31 | "@radix-ui/react-dropdown-menu": "^2.1.15", 32 | "@radix-ui/react-label": "^2.1.7", 33 | "@radix-ui/react-navigation-menu": "^1.2.13", 34 | "@radix-ui/react-radio-group": "^1.3.7", 35 | "@radix-ui/react-select": "^2.2.5", 36 | "@radix-ui/react-separator": "^1.1.7", 37 | "@radix-ui/react-slot": "^1.2.3", 38 | "@radix-ui/react-switch": "^1.2.5", 39 | "@radix-ui/react-tabs": "^1.1.12", 40 | "@radix-ui/react-toast": "^1.2.14", 41 | "@radix-ui/react-tooltip": "^1.2.7", 42 | "@radix-ui/react-visually-hidden": "^1.2.3", 43 | "@react-email/components": "^0.0.33", 44 | "@react-email/tailwind": "^1.0.5", 45 | "@tanstack/react-query": "^5.77.2", 46 | "@trpc/client": "^11.1.3", 47 | "@trpc/react-query": "11.0.0-rc.768", 48 | "@trpc/server": "^11.1.3", 49 | "@trpc/tanstack-react-query": "^11.1.3", 50 | "@upstash/ratelimit": "^2.0.5", 51 | "@upstash/redis": "^1.34.9", 52 | "bcryptjs": "^2.4.3", 53 | "better-auth": "^1.2.8", 54 | "class-variance-authority": "^0.7.1", 55 | "client-only": "^0.0.1", 56 | "cloudinary": "^2.6.1", 57 | "clsx": "^2.1.1", 58 | "concurrently": "^9.1.2", 59 | "cypress": "^14.4.0", 60 | "depscan": "^0.4.1", 61 | "dotenv": "^16.5.0", 62 | "drizzle-orm": "^0.39.3", 63 | "drizzle-zod": "^0.7.1", 64 | "framer-motion": "^12.15.0", 65 | "input-otp": "^1.4.2", 66 | "lucide-react": "^0.479.0", 67 | "motion": "^12.15.0", 68 | "next": "15.2.0", 69 | "next-intl": "^4.1.0", 70 | "next-themes": "^0.4.6", 71 | "pg": "^8.16.0", 72 | "prettier-plugin-tailwindcss": "^0.6.11", 73 | "react": "19.0.0", 74 | "react-dom": "19.0.0", 75 | "react-email": "^2.1.6", 76 | "react-error-boundary": "^5.0.0", 77 | "react-hook-form": "^7.56.4", 78 | "react-icons": "^5.5.0", 79 | "react-use": "^17.6.0", 80 | "resend": "^4.5.1", 81 | "server-only": "^0.0.1", 82 | "sharp": "^0.33.5", 83 | "sonner": "^2.0.3", 84 | "stripe": "^17.7.0", 85 | "superjson": "^2.2.2", 86 | "tailwind-merge": "^3.3.0", 87 | "tailwindcss-animate": "^1.0.7", 88 | "vaul": "^1.1.2", 89 | "zod": "^3.25.32" 90 | }, 91 | "devDependencies": { 92 | "@tailwindcss/postcss": "^4.1.7", 93 | "@testing-library/dom": "^10.4.0", 94 | "@testing-library/jest-dom": "^6.6.3", 95 | "@testing-library/react": "^16.3.0", 96 | "@types/bcryptjs": "^2.4.6", 97 | "@types/jest": "^29.5.14", 98 | "@types/node": "^20.17.51", 99 | "@types/pg": "^8.15.2", 100 | "@types/react": "19.0.10", 101 | "@types/react-dom": "19.0.4", 102 | "@types/uuid": "^10.0.0", 103 | "drizzle-kit": "^0.30.6", 104 | "eslint": "^8.57.1", 105 | "eslint-config-next": "15.2.0", 106 | "jest": "^29.7.0", 107 | "jest-environment-jsdom": "^29.7.0", 108 | "prettier": "^3.5.3", 109 | "tailwindcss": "^4.1.7", 110 | "ts-node": "^10.9.2", 111 | "typescript": "^5.8.3" 112 | }, 113 | "pnpm": { 114 | "overrides": { 115 | "@types/react": "19.0.10", 116 | "@types/react-dom": "19.0.4" 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | '@tailwindcss/postcss': {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /public/images/next-js.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/images/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/2fa/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { ArrowLeft } from "lucide-react"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | 6 | import { TwoFactorToggle } from "@/modules/auth/ui/components/two-factor-toggle"; 7 | import { HydrateClient, trpc } from "@/trpc/server"; 8 | import { getSession } from "@/lib/session"; 9 | 10 | const TwoFactorPage = async () => { 11 | const sessionData = await getSession(); 12 | 13 | if (!sessionData) { 14 | redirect("/"); 15 | } 16 | 17 | if (sessionData.user.twoFactorEnabled) { 18 | redirect("/"); 19 | } 20 | 21 | void trpc.users.getUser.prefetch({ id: sessionData.user.id }); 22 | 23 | return ( 24 |
25 |
26 | 30 | 31 | Back to Home 32 | 33 |
34 | 35 |
36 |
37 | Logo 44 |
45 | 46 |
47 | 48 | 49 | 50 |

51 | Having trouble? 52 | 56 | Contact support 57 | 58 |

59 |
60 |
61 |
62 | ); 63 | }; 64 | 65 | export default TwoFactorPage; 66 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { AuthLayout } from "@/modules/auth/ui/layouts/auth-layout"; 2 | 3 | interface LayoutProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export default function Layout({ children }: LayoutProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import Link from "next/link"; 3 | 4 | import { UserAuthForm } from "@/modules/auth/ui/components/user-auth-form"; 5 | import { buttonVariants } from "@/components/ui/button"; 6 | import { Icons } from "@/components/shared/icons"; 7 | import { getSession } from "@/lib/session"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | export const metadata = { 11 | title: "Login", 12 | }; 13 | 14 | export default async function loginpage() { 15 | const session = await getSession(); 16 | 17 | if (session) { 18 | redirect("/"); 19 | } 20 | 21 | return ( 22 |
23 |
24 |
25 |

26 | Welcome back 27 |

28 |

29 | Enter your email to sign in to your account 30 |

31 |
32 | 33 |

34 | 38 | Don't have an account? Sign Up 39 | 40 |

41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/[locale]/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { Suspense } from "react"; 3 | import Link from "next/link"; 4 | 5 | import { UserAuthForm } from "@/modules/auth/ui/components/user-auth-form"; 6 | import { buttonVariants } from "@/components/ui/button"; 7 | import { getSession } from "@/lib/session"; 8 | import { cn } from "@/lib/utils"; 9 | 10 | export const metadata = { 11 | title: "Create an account", 12 | description: "Create an account to get started.", 13 | }; 14 | 15 | export default async function RegisterPage() { 16 | const session = await getSession(); 17 | 18 | if (session) { 19 | redirect("/"); 20 | } 21 | 22 | return ( 23 |
24 | 31 | Login 32 | 33 |
34 |
35 |
36 |
37 |

38 | Create an account 39 |

40 |

41 | Enter your email below to create your account 42 |

43 |
44 | 45 | 46 | 47 |

48 | By clicking continue, you agree to our 49 | 53 | Terms of Service 54 | {" "} 55 | and{" "} 56 | 60 | Privacy Policy 61 | 62 |

63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/layout.tsx: -------------------------------------------------------------------------------- 1 | import UserLayout from "@/modules/users/ui/layouts/user-layout"; 2 | 3 | interface DashboardLayoutProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | export default function Layout({ children }: DashboardLayoutProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/profile/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { UserProfileView } from "@/modules/users/ui/views/user-profile-view"; 4 | import { getSession } from "@/lib/session"; 5 | import { prefetch, trpc } from "@/trpc/server"; 6 | 7 | const ProfilePage = async () => { 8 | const session = await getSession(); 9 | 10 | if (!session) { 11 | redirect("/login"); 12 | } 13 | 14 | return ; 15 | }; 16 | 17 | export default ProfilePage; 18 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/profile/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { UserProfileSettingsView } from "@/modules/users/ui/views/user-profile-settings-view"; 4 | import { getSession } from "@/lib/session"; 5 | 6 | const SettingsPage = async () => { 7 | const session = await getSession(); 8 | 9 | if (!session) { 10 | redirect("/login"); 11 | } 12 | 13 | return ; 14 | }; 15 | 16 | export default SettingsPage; 17 | -------------------------------------------------------------------------------- /src/app/[locale]/(dashboard)/subscription/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { SubscriptionView } from "@/modules/subscription/ui/views/subscription-view"; 4 | import { getSession } from "@/lib/session"; 5 | 6 | const SubscriptionPage = async () => { 7 | const session = await getSession(); 8 | 9 | if (!session) { 10 | redirect("/login"); 11 | } 12 | 13 | return ; 14 | }; 15 | 16 | export default SubscriptionPage; 17 | -------------------------------------------------------------------------------- /src/app/[locale]/(error)/not-found.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AlertCircle, ArrowLeft } from "lucide-react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { useRouter } from "next/navigation"; 6 | 7 | export function NotFound() { 8 | const router = useRouter(); 9 | 10 | return ( 11 |
12 |
13 |
14 |

15 | 404 16 |

17 |
18 | 19 |
20 |
21 | 22 | {/* Message */} 23 |
24 |

25 | Page Not Found 26 |

27 |

28 | Oops! The page you're looking for doesn't exist or has 29 | been moved. 30 |

31 |
32 | 33 | {/* Buttons */} 34 |
35 | 43 |
44 |
45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/app/[locale]/(public)/about/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | 3 | // Users can customize these metadata values 4 | export const metadata: Metadata = { 5 | title: "About Us - NextSaaS", 6 | description: "Learn more about our company, mission and team.", 7 | }; 8 | 9 | // Main extensible About page component 10 | export default function AboutPage() { 11 | return ( 12 |
13 |
14 | {/* Section: Main About */} 15 |
16 |

17 | About Us 18 |

19 | 20 |

21 | {/* Users can replace this with their own company introduction */} 22 | Replace this with your company introduction. Describe who you are, 23 | your mission, and what makes your product or service unique. 24 |

25 | 26 |

27 | Founded in [YEAR], [COMPANY NAME] is dedicated to [MISSION 28 | STATEMENT]. 29 |

30 |
31 | 32 | {/* Section: Values - Users can modify these or add more */} 33 |
34 |

Our Values

35 |
36 | {/* Value 1 */} 37 |
38 |

Customer Focus

39 |

40 | We build solutions with our customers needs at the center of 41 | everything we do. 42 |

43 |
44 | 45 | {/* Value 2 */} 46 |
47 |

Innovation

48 |

49 | We constantly explore new ideas and technologies to solve 50 | challenging problems. 51 |

52 |
53 | 54 | {/* Value 3 */} 55 |
56 |

Transparency

57 |

58 | We believe in open and honest communication with our customers 59 | and team. 60 |

61 |
62 | 63 | {/* Value 4 */} 64 |
65 |

Quality

66 |

67 | We're committed to excellence in everything we create and 68 | deliver. 69 |

70 |
71 |
72 |
73 | 74 | {/* Section: Team - Easily extensible */} 75 |
76 |

Our Team

77 |

78 | Replace this section with information about your team members. 79 |

80 | 81 | {/* Example of how users can structure team members */} 82 |
83 | {/* Template for team member cards that users can duplicate */} 84 |
85 |
86 | {/* User can add: Team Member Name */} 87 |
88 |

Team Member Name

89 |

Position

90 |
91 |
92 |
93 | 94 | {/* Section: Contact - Users can customize this with their own contact info */} 95 |
96 |

Get in Touch

97 |

98 | Have questions? Contact us at{" "} 99 | 103 | example@yourcompany.com 104 | 105 |

106 |
107 |
108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/app/[locale]/(public)/layout.tsx: -------------------------------------------------------------------------------- 1 | import HomeLayout from "@/modules/home/ui/layouts/home-layout"; 2 | 3 | interface PagesLayoutProps { 4 | children: React.ReactNode; 5 | } 6 | 7 | const PagesLayout = ({ children }: PagesLayoutProps) => { 8 | return {children}; 9 | }; 10 | 11 | export default PagesLayout; 12 | -------------------------------------------------------------------------------- /src/app/[locale]/(public)/pricing/page.tsx: -------------------------------------------------------------------------------- 1 | import { PricingPlans } from "@/modules/pricing/ui/components/pricing-plans"; 2 | import { Metadata } from "next"; 3 | 4 | export const metadata: Metadata = { 5 | title: "Pricing - NextSaaS", 6 | description: "Simple and predictable pricing for all your SaaS needs.", 7 | }; 8 | 9 | export default function PricingPage() { 10 | return ( 11 |
12 |
13 |
14 | Simple, transparent pricing 15 |
16 |

17 | Choose the plan that's right for you and get started with your 18 | project today. 19 |

20 |
21 | 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/[locale]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/modules/home/ui/components/header"; 2 | import { NextIntlClientProvider } from "next-intl"; 3 | import { setRequestLocale } from "next-intl/server"; 4 | import { notFound } from "next/navigation"; 5 | 6 | interface LocaleLayoutProps { 7 | children: React.ReactNode; 8 | params: Promise<{ 9 | locale: string; 10 | }>; 11 | } 12 | 13 | export default async function LocaleLayout({ 14 | children, 15 | params, 16 | }: LocaleLayoutProps) { 17 | const { locale } = await params; 18 | 19 | if (!locale) { 20 | notFound(); 21 | } 22 | 23 | setRequestLocale(locale); 24 | 25 | return ( 26 | 27 |
28 | {children} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /src/app/[locale]/page.tsx: -------------------------------------------------------------------------------- 1 | import { Github } from "lucide-react"; 2 | import type React from "react"; 3 | import Link from "next/link"; 4 | 5 | import { Button } from "@/components/ui/button"; 6 | import { getTranslations } from "next-intl/server"; 7 | 8 | export default async function Home() { 9 | const t = await getTranslations("HomePage"); 10 | 11 | return ( 12 |
13 |
14 |

15 | {t("mainHeading")} 16 |

17 |

{t("subHeading")}

18 |
19 | 20 | 23 | 24 | 25 | 29 | 30 |
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { toNextJsHandler } from "better-auth/next-js"; 2 | 3 | import { auth } from "@/lib/auth"; 4 | 5 | export const { POST, GET } = toNextJsHandler(auth); 6 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | import { appRouter } from "@/trpc/routers/_app"; 3 | import { createTRPCContext } from "@/trpc/init"; 4 | 5 | const handler = (req: Request) => 6 | fetchRequestHandler({ 7 | endpoint: "/api/trpc", 8 | req, 9 | router: appRouter, 10 | createContext: createTRPCContext, 11 | }); 12 | 13 | export { handler as GET, handler as POST }; 14 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { getTranslations, setRequestLocale } from "next-intl/server"; 2 | import { hasLocale, NextIntlClientProvider } from "next-intl"; 3 | import { notFound } from "next/navigation"; 4 | import { Toaster } from "sonner"; 5 | 6 | import { ThemeProvider } from "@/components/providers/theme-provider"; 7 | import { SiteConfig } from "@/configs/site.config"; 8 | import { TRPCReactProvider } from "@/trpc/client"; 9 | import { 10 | OrganizationJsonLd, 11 | WebsiteSchemaJsonLd, 12 | } from "@/components/seo/structured-data"; 13 | import { routing } from "@/i18n/routing"; 14 | import { fonts } from "@/lib/fonts"; 15 | import { cn } from "@/lib/utils"; 16 | 17 | import { FooterComponent } from "@/modules/home/ui/components/footer"; 18 | 19 | import "./globals.css"; 20 | 21 | interface RootLayoutParams { 22 | children: React.ReactNode; 23 | } 24 | 25 | export default async function RootLayout({ 26 | children, 27 | }: Readonly) { 28 | return ( 29 | 30 | 31 | 34 | 40 | 41 | 42 | 48 | 49 | 50 | {children} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default function RootPage() { 4 | redirect("/en"); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next"; 2 | import { SiteConfig } from "@/configs/site.config"; 3 | 4 | export default function robots(): MetadataRoute.Robots { 5 | return { 6 | rules: { 7 | userAgent: "*", 8 | allow: ["/"], 9 | disallow: ["/search?q="], 10 | }, 11 | sitemap: [`${SiteConfig.metadataBase}/sitemap.xml`], 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { SiteConfig } from "@/configs/site.config"; 2 | import { MetadataRoute } from "next"; 3 | 4 | export default async function sitemap(): Promise { 5 | // You can fetch dynamic routes here from your API/database 6 | // const products = await getProducts(); 7 | 8 | // Example of static routes 9 | const routes = [ 10 | `${SiteConfig.openGraph?.url}`, 11 | `${SiteConfig.openGraph?.url}/about`, 12 | `${SiteConfig.openGraph?.url}/contact`, 13 | ].map((route) => ({ 14 | url: `${SiteConfig.openGraph?.url}${route}`, 15 | lastModified: new Date().toISOString().split("T")[0], 16 | })); 17 | 18 | return [ 19 | ...routes, 20 | // ...products.map((product) => ({ 21 | // url: `${siteConfig.url}/products/${product.slug}`, 22 | // lastModified: new Date(product.updatedAt).toISOString().split('T')[0], 23 | // })), 24 | ]; 25 | } 26 | -------------------------------------------------------------------------------- /src/components/lang-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname, useRouter } from "next/navigation"; 4 | import { useLocale, useTranslations } from "next-intl"; 5 | import { 6 | Select, 7 | SelectContent, 8 | SelectItem, 9 | SelectTrigger, 10 | SelectValue, 11 | } from "./ui/select"; 12 | 13 | export const LangSwitcher = () => { 14 | const t = useTranslations(); 15 | const locale = useLocale(); 16 | const router = useRouter(); 17 | const pathname = usePathname(); 18 | 19 | const onSelectChange = (value: string) => { 20 | const newPath = pathname.replace(`/${locale}`, `/${value}`); 21 | router.replace(newPath); 22 | }; 23 | 24 | return ( 25 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /src/components/providers/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import * as React from "react"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /src/components/responsive-modal.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactElement } from "react"; 2 | import { useMedia } from "react-use"; 3 | 4 | import { VisuallyHidden } from "@radix-ui/react-visually-hidden"; 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogHeader, 9 | DialogTitle, 10 | } from "@/components/ui/dialog"; 11 | import { 12 | Drawer, 13 | DrawerContent, 14 | DrawerHeader, 15 | DrawerTitle, 16 | } from "@/components/ui/drawer"; 17 | 18 | type ResponsiveModalProps = { 19 | children: React.ReactNode; 20 | open: boolean; 21 | onOpenChange: (open: boolean) => void; 22 | }; 23 | 24 | export default function ResponsiveModal({ 25 | children, 26 | open, 27 | onOpenChange, 28 | }: ResponsiveModalProps): ReactElement { 29 | const isDesktop = useMedia("(min-width: 1024px)", true); 30 | 31 | if (isDesktop) { 32 | return ( 33 | 34 | 35 | 36 | Title 37 | 38 | 39 | 40 | {children} 41 | 42 | 43 | ); 44 | } 45 | 46 | return ( 47 | 48 | 49 | 50 | 51 | Title 52 | 53 | 54 |
55 | {children} 56 |
57 |
58 |
59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /src/components/seo/structured-data.tsx: -------------------------------------------------------------------------------- 1 | import Script from "next/script"; 2 | 3 | export function WebsiteSchemaJsonLd({ siteUrl }: { siteUrl: string }) { 4 | return ( 5 |