├── pnpm-workspace.yaml ├── .eslintignore ├── .vscode ├── extensions.json └── settings.json ├── public ├── logo.png ├── og.png ├── favicon.ico ├── icon-192.png ├── icon-512.png ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── icon-192-maskable.png └── icon-512-maskable.png ├── app ├── (auth) │ ├── login │ │ ├── layout.tsx │ │ └── page.tsx │ └── api │ │ └── auth │ │ └── [...nextauth] │ │ └── route.ts ├── page.tsx ├── layout.tsx └── globals.css ├── i18n ├── config.ts └── request.ts ├── postcss.config.mjs ├── lib ├── utils.ts ├── auth.ts └── metadata.ts ├── db ├── drizzle.ts └── schema.ts ├── drizzle.config.ts ├── .env.example ├── components ├── logo.tsx ├── theme-provider.tsx ├── tailwind-indicator.tsx ├── header │ ├── theme-switcher.tsx │ ├── sign-in-button.tsx │ ├── header.tsx │ ├── language-switcher.tsx │ └── user-dropdown.tsx ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── button.tsx │ ├── card.tsx │ ├── dialog.tsx │ ├── form.tsx │ ├── toast.tsx │ ├── select.tsx │ └── dropdown-menu.tsx ├── auth │ ├── login-form-dialog.tsx │ └── login-form.tsx ├── hello-form.tsx └── footer.tsx ├── components.json ├── services └── locale.ts ├── actions └── hello-action.ts ├── Dockerfile ├── next.config.ts ├── biome.json ├── messages ├── zh.json └── en.json ├── .gitignore ├── config └── site.ts ├── tsconfig.json ├── package.json ├── README.zh-CN.md ├── README.md ├── hooks └── use-toast.ts └── pnpm-lock.yaml /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | onlyBuiltDependencies: 2 | - '@swc/core' 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/* 2 | .cache 3 | public 4 | node_modules 5 | .prettierrc.mjs -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"] 3 | } 4 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/og.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/icon-192.png -------------------------------------------------------------------------------- /public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/icon-512.png -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/icon-192-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/icon-192-maskable.png -------------------------------------------------------------------------------- /public/icon-512-maskable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Przeblysk/next-starter/HEAD/public/icon-512-maskable.png -------------------------------------------------------------------------------- /app/(auth)/login/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: { children: React.ReactNode }) { 2 | return
{children}
3 | } 4 | -------------------------------------------------------------------------------- /i18n/config.ts: -------------------------------------------------------------------------------- 1 | export type Locale = (typeof locales)[number] 2 | 3 | export const locales = ["en", "zh"] as const 4 | export const defaultLocale: Locale = "en" 5 | -------------------------------------------------------------------------------- /app/(auth)/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/lib/auth" // Referring to the auth.ts we just created 2 | 3 | export const { GET, POST } = handlers 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /db/drizzle.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/postgres-js" 2 | import postgres from "postgres" 3 | 4 | const client = postgres(process.env.AUTH_DRIZZLE_URL!, { prepare: false }) 5 | const db = drizzle({ client }) 6 | 7 | export default db 8 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config" 2 | 3 | import { defineConfig } from "drizzle-kit" 4 | 5 | export default defineConfig({ 6 | out: "./drizzle", 7 | schema: "./src/db/schema.ts", 8 | dialect: "postgresql", 9 | dbCredentials: { 10 | url: process.env.AUTH_DRIZZLE_URL! 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:3000 2 | AUTH_URL=http://localhost:3000 3 | AUTH_TRUST_HOST=true 4 | AUTH_SECRET="" # Added by `npx auth`. Read more: https://cli.authjs.dev 5 | AUTH_GITHUB_ID= 6 | AUTH_GITHUB_SECRET= 7 | AUTH_GOOGLE_ID= 8 | AUTH_GOOGLE_SECRET= 9 | AUTH_DRIZZLE_URL= 10 | 11 | -------------------------------------------------------------------------------- /app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "@/components/auth/login-form" 2 | 3 | export default function LoginPage() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /i18n/request.ts: -------------------------------------------------------------------------------- 1 | import { getUserLocale } from "@/services/locale" 2 | import { getRequestConfig } from "next-intl/server" 3 | 4 | export default getRequestConfig(async () => { 5 | const locale = await getUserLocale() 6 | 7 | return { 8 | locale, 9 | messages: (await import(`../messages/${locale}.json`)).default 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /components/logo.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | import Image from "next/image" 3 | 4 | export const Logo = ({ className }: { className?: string }) => { 5 | return ( 6 | Logo 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { ComponentProps } from 'react' 4 | import { ThemeProvider as NextThemesProvider } from 'next-themes' 5 | 6 | type ThemeProviderProps = ComponentProps 7 | 8 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 9 | return {children} 10 | } 11 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /services/locale.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { defaultLocale, Locale } from "@/i18n/config" 4 | import { cookies } from "next/headers" 5 | 6 | // In this example the locale is read from a cookie. You could alternatively 7 | // also read it from a database, backend service, or any other source. 8 | const COOKIE_NAME = "NEXT_LOCALE" 9 | 10 | export async function getUserLocale() { 11 | return (await cookies()).get(COOKIE_NAME)?.value || defaultLocale 12 | } 13 | 14 | export async function setUserLocale(locale: Locale) { 15 | ;(await cookies()).set(COOKIE_NAME, locale) 16 | } 17 | -------------------------------------------------------------------------------- /actions/hello-action.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { auth } from "@/lib/auth" 4 | import { getTranslations } from "next-intl/server" 5 | 6 | export const helloAction = async (message: string) => { 7 | const session = await auth() 8 | const t = await getTranslations() 9 | 10 | if (!session || !session.user) { 11 | return { 12 | message: t("HelloAction.not-logged-in-message", { 13 | message 14 | }) 15 | } 16 | } 17 | return { 18 | message: t("HelloAction.logged-in-message", { 19 | username: session.user.name ?? "", 20 | message 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.codeActionsOnSave": { 4 | "source.fixAll.biome": "explicit", 5 | "source.organizeImports.biome": "explicit" 6 | }, 7 | "[javascriptreact]": { 8 | "editor.defaultFormatter": "biomejs.biome" 9 | }, 10 | "[javascript]": { 11 | "editor.defaultFormatter": "biomejs.biome" 12 | }, 13 | "[typescriptreact]": { 14 | "editor.defaultFormatter": "biomejs.biome" 15 | }, 16 | "[typescript]": { 17 | "editor.defaultFormatter": "biomejs.biome" 18 | }, 19 | "[json]": { 20 | "editor.defaultFormatter": "biomejs.biome" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine AS base 2 | WORKDIR /app 3 | 4 | FROM base AS deps 5 | RUN corepack enable && corepack prepare pnpm@10.10.0 --activate 6 | COPY package.json pnpm-lock.yaml pnpm-workspace.yaml* ./ 7 | RUN pnpm install --frozen-lockfile 8 | 9 | FROM deps AS builder 10 | COPY . . 11 | RUN pnpm build 12 | 13 | FROM base AS runner 14 | ENV NODE_ENV=production 15 | ENV HOSTNAME=0.0.0.0 16 | ENV PORT=3000 17 | ENV NEXT_TELEMETRY_DISABLED=1 18 | COPY --from=builder /app/.next/standalone ./ 19 | COPY --from=builder /app/.next/static ./.next/static 20 | COPY --from=builder /app/public ./public 21 | EXPOSE 3000 22 | CMD ["node","server.js"] 23 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next" 2 | import createNextIntlPlugin from "next-intl/plugin" 3 | 4 | const withNextIntl = createNextIntlPlugin() 5 | 6 | const nextConfig: NextConfig = { 7 | output: "standalone", 8 | images: { 9 | remotePatterns: [ 10 | { 11 | protocol: "https", 12 | hostname: "avatars.githubusercontent.com", 13 | port: "", 14 | pathname: "/**" 15 | }, 16 | { 17 | protocol: "https", 18 | hostname: "*.googleusercontent.com", 19 | port: "", 20 | pathname: "/**" 21 | } 22 | ] 23 | } 24 | } 25 | 26 | export default withNextIntl(nextConfig) 27 | -------------------------------------------------------------------------------- /components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null; 3 | 4 | return ( 5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [ 11 | ".next", 12 | "node_modules", 13 | "dist", 14 | "build", 15 | "public" 16 | ] 17 | }, 18 | "formatter": { 19 | "enabled": true, 20 | "indentStyle": "space" 21 | }, 22 | "organizeImports": { 23 | "enabled": true 24 | }, 25 | "linter": { 26 | "enabled": true, 27 | "rules": { 28 | "recommended": true 29 | } 30 | }, 31 | "javascript": { 32 | "formatter": { 33 | "quoteStyle": "double" 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /messages/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "HomePage": { 3 | "title": "Next.js 启动模板!", 4 | "description": "Next.js 启动模板,带有 TypeScript, Tailwind CSS, Next-auth, i18n, Drizzle, Eslint, Prettier. 快速启动项目,高效工作。", 5 | "message-placeholder": "在这里输入你想说的话...", 6 | "submit": "提交", 7 | "hello-form-message-min-length": "你的消息必须至少有 3 个字符长。" 8 | }, 9 | "Auth": { 10 | "sign-in": "登录", 11 | "sign-out": "退出登录", 12 | "my-account": "我的账户", 13 | "welcome-back": "欢迎回来", 14 | "choose-sign-in-method": "选择登录方式" 15 | }, 16 | "HelloAction": { 17 | "not-logged-in-message": "来自服务器的消息。这是你的消息 👉 {message}", 18 | "logged-in-message": "你好 {username} 👋,来自服务器!!!这是你的消息 👉 {message}" 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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | .env.* 36 | !.env.example 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | -------------------------------------------------------------------------------- /components/header/theme-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { Moon, Sun } from "lucide-react" 5 | import { useTheme } from "next-themes" 6 | 7 | export const ThemeSwitcher = () => { 8 | const { theme, setTheme } = useTheme() 9 | return ( 10 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/header/sign-in-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { Loader2 } from "lucide-react" 5 | import { signIn } from "next-auth/react" 6 | import { useTranslations } from "next-intl" 7 | import { useTransition } from "react" 8 | 9 | export const SignInButton = () => { 10 | const t = useTranslations() 11 | const [isPending, startTransition] = useTransition() 12 | 13 | const handleSignIn = () => { 14 | startTransition(async () => { 15 | await signIn("github") 16 | }) 17 | } 18 | 19 | return ( 20 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | const SITE_URL = process.env.NEXT_PUBLIC_APP_URL! 2 | 3 | export const siteConfig = { 4 | url: SITE_URL, 5 | name: "Next Starter", 6 | description: 7 | "A Next.js starter template, packed with features like TypeScript, Tailwind CSS, Next-auth, i18n, Drizzle,Eslint, Prettier. Start your project quickly and efficiently.", 8 | keywords: [ 9 | "Next.js", 10 | "React", 11 | "Next.js starter", 12 | "Next.js boilerplate", 13 | "Starter Template", 14 | "Tailwind CSS", 15 | "TypeScript", 16 | "Shadcn/ui", 17 | "Next-auth", 18 | "Drizzle", 19 | "Next-intl", 20 | "i18n" 21 | ], 22 | author: "Przeblysk", 23 | ogImage: `${SITE_URL}/og.png`, 24 | links: { 25 | twitter: "https://twitter.com/vercel", 26 | github: "https://github.com/vercel/next.js" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "react-jsx", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts", 36 | ".next/dev/types/**/*.ts" 37 | ], 38 | "exclude": [ 39 | "node_modules" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Footer from "@/components/footer" 2 | import { Header } from "@/components/header/header" 3 | import { HelloForm } from "@/components/hello-form" 4 | import { useTranslations } from "next-intl" 5 | 6 | export default function Home() { 7 | const t = useTranslations() 8 | return ( 9 | <> 10 |
11 |
12 |
13 |

14 | {t("HomePage.title")} 15 |

16 |

17 | {t("HomePage.description")} 18 |

19 | 20 |
21 |
22 |