Initial commit

This commit is contained in:
测试2
2026-04-20 20:53:05 +08:00
commit 2a722f5383
39 changed files with 9939 additions and 0 deletions

22
.claude/settings.json Normal file
View File

@@ -0,0 +1,22 @@
{
"permissions": {
"allow":[
"Bash(pnpm dlx:*)",
"Bash(pnpm lint:*"
],
"deny": [
"Bash(pnpm add:*)",
"Bash(pnpm install:*)",
"Bash(npm install:*)",
"Bash(npm add:*)",
"Bash(yarn add:*)",
"CronCreate",
"CronDelete",
"CronList",
"EnterWorktree",
"ExitWorktree",
"LSP",
"PowerShell"
]
}
}

4
.env.example Normal file
View File

@@ -0,0 +1,4 @@
NEXT_PUBLIC_APP_NAME=Demo App
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_API_BASE_URL=http://localhost:3000/api
NEXT_PUBLIC_ALLOWED_DEV_DOMAIN=web2.guru.local

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"plugins": ["prettier-plugin-tailwindcss"],
}

68
AGENTS.md Normal file
View File

@@ -0,0 +1,68 @@
<!-- BEGIN:nextjs-agent-rules -->
# This is NOT the next.js you know
This version has breaking changes - APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
<!-- END:nextjs-agent-rules -->
# Idea Guru Template
Next.js 16 (App Router) starter template with Typescript, Tailwind CSS v4, and shadcn/ui (base-nova style).
# Commands
- `pnpm dev` - Start the development server
- `pnpm build` - Build the application for production
- `pnpm start` - Start the production server
- `pnpm lint` - Run ESLint to check for code issues
# Prejct Structure
- `src/app/` - Next.js App Router pages and layouts
- `src/components/ui/` - shadcn/ui components (managed by `pnpm dlx shadcn@latest add <component-name>`)
- `src/components/layouts/` - layout components (Header, Footer, etc.)
- `src/components/common/` - reuseable app-level components
- `src/hooks/` - custom React hooks
- `src/lib/` - utility modules (axios, query-client, validations, env, utils)
- `src/providers/` - React context providers (AuthProvider, ThemeProvider, QueryClientProvider, etc.)
- `src/stores/` - Zustand stores for global state management
- `src/types/` - TypeScript type definitions and interfaces
## Dependencies
Do NOT install new dependencies. Use only the libraries already in the package.json. Adding shadcn/ui components via `pnpm dlx shadcn-ui add <component-name>` is allowed since it generates code, not new dependencies.
## Conventions
- **Package manager**: pnpm (never use npm or yarn)
- **Imports**: always use the `@/*` path alias (maps to `src/*`)
- **Styling**: Tailwind CSS utility classes; avoid custom CSS. Use`cn()` from `@/lib/utils` fro conditional classes
- **Components**: use shadcn/ui as the base component library. shadcn uses `@base-ui/react` (not Radix) and does NOT support `asChild` - use `buttonVariant()` with `cn()` for link-styled buttons instead.
- **State**: Zustand for client-side global state; TanStack Query for server/async state
- **Theme**: use `next-themes` (`useTheme()`) for dark mode support. Do NOT manage theme in Zustand or any other global state.
- **Data fetching**: wrap Axios calls in TanStack Query hooks (see `src/hooks/use-example-query.ts`)
- **Forms**: React Hook Form + Zod Schemas from `@/lib/validations.ts`
- **Animation**: Framer Motion
- **Icons**: Lucide React
- **Dates**: date-fns
- **Authentication**: Auth.js (`next-auth`) for authentication flows
- **Internationalization**: `next-intl` for handling translations and locale-specific content
- **Error Monitoring**: Sentry (`@sentry/nextjs`) for error tracking and performance monitoring
- **Toasts**: Sonner (not the deprecated shadcn toast)
- **ESLint**: flat config (`eslint.config.mjs`) with next/core-web-vitals, next/typescript, and prettier
- **Prettier**: configred in `.prettierrc` with `prettier-plugin-tailwindcss`
## Key Files
- `src/lib/axios.ts` - Axios instance with interceptors for auth tokens and error handling
- `src/lib/query-client.ts` - TanStack Query client configuration
- `src/lib/env.ts` - Zod-validated environment variables (client + server)
- `src/providers/index.tsx` - app-wide providers (wrap children in layout.tsx)
- `src/stores/use-app-store.ts` - Zustand store with `persist` middlerware
- `.env.example` - template for environment variables
## Important Notes
- Although the server-side dependencies are included in `package.json`, this template is primarily focused on client-side development. Do NOT add any server-side code or API routes. The server-side dependencies are included for potential future use but are not intended to be used in this template.
- If there are any features that current dependencies cannot support, try to implement them within the constraints of the existing libraries. For example, if you need to implement authentication flows, use `next-auth` and do not add any new authentication libraries.
- If the feature is impossible to implement with the current dependencies, you can refuse the request and explain the limitations. However, try to find creative solutions within the existing tools before refusing any requests.
- All new files and directories must be created within the `src/` or `public/` directories and follow the established structure and conventions. Do NOT create any files or directories outside of these locations. This is to maintain a clean and organized project structure.
- Do NOT modify configuration files such as `package.json`, `next.config.js`, `eslint.config.mjs`, `prettier.config.mjs`, or `tsconfig.json`.

1
CLAUDE.md Normal file
View File

@@ -0,0 +1 @@
@AGENTS.md

94
README.md Normal file
View File

@@ -0,0 +1,94 @@
# Idea Guru Template
A modern Next.js starter template with a curated tech stack, designed for rapid development of full-featured web applications.
## Tech Stack
| Category | Technology |
| --- | --- |
| Framework | [Next.js 16](https://nextjs.org/) (App Router) |
| Language | [TypeScript 5](https://www.typescriptlang.org/) |
| Styling | [Tailwind CSS 4](https://tailwindcss.com/) |
| UI Components | [shadcn/ui](https://ui.shadcn.com/) (base-nova style, powered by `@base-ui/react`) |
| State Management | [Zustand 5](https://zustand.docs.pmnd.rs/) (client state) + [TanStack Query 5](https://tanstack.com/query) (server state) |
| Forms | [React Hook Form](https://react-hook-form.com/) + [Zod 4](https://zod.dev/) |
| HTTP Client | [Axios](https://axios-http.com/) with interceptors |
| Animation | [Framer Motion](https://motion.dev/) |
| Icons | [Lucide React](https://lucide.dev/) |
| Auth | [NextAuth.js v5](https://authjs.dev/) |
| i18n | [next-intl](https://next-intl.dev/) |
| Theming | [next-themes](https://github.com/pacocoursey/next-themes) |
| Toasts | [Sonner](https://sonner.emilkowal.dev/) |
| Dates | [date-fns](https://date-fns.org/) |
| Monitoring | [Sentry](https://sentry.io/) |
| Linting | ESLint (flat config) + Prettier + `prettier-plugin-tailwindcss` |
## Getting Started
```bash
# Install dependencies
pnpm install
# Start development server
pnpm dev
# Build for production
pnpm build
# Start production server
pnpm start
# Lint code
pnpm lint
```
Open [http://localhost:3000](http://localhost:3000) to view the app.
## Project Structure
```
src/
├── app/ # Next.js App Router (pages & layouts)
│ ├── globals.css # Global styles & shadcn/ui theme variables
│ ├── layout.tsx # Root layout with providers
│ └── page.tsx # Home page
├── components/
│ ├── ui/ # shadcn/ui components (Button, Card, Input, etc.)
│ ├── layout/ # Layout components (Header, Footer, etc.)
│ └── common/ # Reusable app-level components
├── hooks/ # Custom React hooks
│ ├── use-example.query.ts # Example TanStack Query hook
│ └── use-media-query.ts # Responsive media query hook
├── lib/ # Utility modules
│ ├── axios.ts # Axios instance with auth & error interceptors
│ ├── env.ts # Zod-validated environment variables
│ ├── query-client.ts # TanStack Query client configuration
│ ├── utils.ts # cn() utility for conditional classNames
│ └── validations.ts # Zod schemas for form validation
├── providers/ # React context providers (Theme, QueryClient)
│ └── index.tsx # Unified providers wrapper
├── stores/ # Zustand stores
│ └── use-app-store.ts # App store with persist middleware
└── types/ # Shared TypeScript type definitions
└── index.ts # ApiResponse, PaginatedResponse, etc.
```
## Conventions
- **Package manager** - Always use `pnpm`
- **Imports** - Use the `@/*` path alias (maps to `src/*`)
- **Styling** - Tailwind CSS utility classes; use `cn()` from `@/lib/utils` for conditional classes
- **Components** - shadcn/ui as the base component library. Add new components via `pnpm dlx shadcn@latest add <component-name>`
- **State** - Zustand for client-side global state; TanStack Query for server/async state
- **Data fetching** - Wrap Axios calls in TanStack Query hooks (see `src/hooks/use-example.query.ts`)
- **Forms** - React Hook Form + Zod schemas from `@/lib/validations.ts`
## Environment Variables
Copy `.env.example` to `.env.local` and fill in the values:
```bash
cp .env.example .env.local
```
Client-side variables (prefixed with `NEXT_PUBLIC_`) and server-side variables are validated at runtime using Zod schemas defined in `src/lib/env.ts`.

25
components.json Normal file
View File

@@ -0,0 +1,25 @@
{
"schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}

20
eslint.config.mjs Normal file
View File

@@ -0,0 +1,20 @@
import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";
import nextTs from "eslint-config-next/typescript";
import prettier from "eslint-config-prettier";
const eslintConfig = defineConfig([
...nextVitals,
...nextTs,
prettier,
// Override default ignores of eslint-config-next.
globalIgnores([
// Default ignores of eslint-config-next:
".next/**",
"out/**",
"build/**",
"next-env.d.ts",
]),
]);
export default eslintConfig;

15
next.config.ts Normal file
View File

@@ -0,0 +1,15 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
devIndicators: false,
allowedDevOrigins: [process.env.NEXT_PUBLIC_ALLOWED_DEV_DOMAIN || "http://localhost:3000"],
basePath: process.env.BASE_PATH || "",
trailingSlash: true,
images: {
remotePatterns: [
]
}
};
export default nextConfig;

51
package.json Normal file
View File

@@ -0,0 +1,51 @@
{
"name": "idea-guru-template",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
"@hookform/resolvers": "^5.2.2",
"@sentry/nextjs": "^10.46.0",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-query-devtools": "^5.95.2",
"axios": "^1.13.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
"jose": "^6.2.2",
"lucide-react": "^1.6.0",
"next": "16.2.1",
"next-auth":"^5.0.0-beta.30",
"next-intl": "^4.8.3",
"next-themes": "^0.4.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-hook-form": "^7.72.0",
"shadcn":"^4.1.0",
"sonner":"2.0.7",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0",
"zod": "^4.3.6",
"zustand": "^5.0.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.1",
"eslint-config-prettier": "^10.1.8",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^4",
"typescript": "^5"
}
}

8710
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

7
postcss.config.mjs Normal file
View File

@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;

1
public/file.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
public/globe.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
public/window.svg Normal file
View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

130
src/app/globals.css Normal file
View File

@@ -0,0 +1,130 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-heading: var(--font-sans);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);
--radius-3xl: calc(var(--radius) * 2.2);
--radius-4xl: calc(var(--radius) * 2.6);
}
:root {
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.87 0 0);
--chart-2: oklch(0.556 0 0);
--chart-3: oklch(0.439 0 0);
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
html {
@apply font-sans;
}
}

44
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Toaster } from "@/components/ui/sonner";
import { Providers } from "@/providers";
import { Header } from "@/components/layout/header";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Idea Guru",
description: "A Next.js starter template with a curated tech stack",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="en"
suppressHydrationWarning
className={`geistSans.variable geistMono.variable h-full antialiased`}
>
<body className="flex min-h-full flex-col">
<Providers>
<Header />
<main className="flex-1">{children}</main>
<Toaster />
</Providers>
</body>
</html>
);
}

116
src/app/page.tsx Normal file
View File

@@ -0,0 +1,116 @@
"use client";
import { motion } from "framer-motion";
import {
Layers,
Paintbrush,
Database,
FileCode,
Zap,
ArrowRight
} from 'lucide-react';
import {Button } from "@/components/ui/button";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
} from "@/components/ui/card";
const features = [
{
icon: Layers,
title: 'Next.js 16 + App Router',
description: 'Server components, layouts, and the latest React features'
},
{
icon: Paintbrush,
title:'Tailwind CSS + shadcn/ui',
description: 'Utility-first CSS framework with pre-designed components'
},
{
icon: Database,
title: 'Zustand + TanStack Query',
description: 'State management and data fetching solutions'
},
{
icon: FileCode,
title: 'React Hook Form + Zod',
description: 'Form handling and validation with TypeScript support'
},
{
icon: Zap,
title: 'Framer Motion + Lucide Icons',
description: 'Animation library and icon set for React'
},
{
icon: ArrowRight,
title: 'Axios + Interceptors',
description: 'HTTP client with request and response interceptors'
},
];
const containerVariants = {
hidden : { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
};
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 }
};
export default function Home() {
return (
<div className="mx-auto max-w-7xl px-4 py-16 sm:px-6 lg:px-8">
{/* Hero */}
<motion.section
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
className="text-center mb-20"
>
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl lg:text-6xl">
Welcome to Idea Guru
</h1>
<p className="mt-4 mx-auto max-w-2xl text-lg text-muted-foreground">
A Next.js starter template with a curated tech stack
</p>
<div className="mt-8 flex items-center justify-center gap-4">
<Button size="lg">
Get Started
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
<Button variant="outline" size="lg">
View on GitHub
</Button>
</div>
</motion.section>
{/* Features */}
<motion.section
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3"
>
{features.map((feature, index) => (
<motion.div key={index} variants={itemVariants}>
<Card className="h-full transition-shadow hover:shadow-md">
<CardHeader>
<feature.icon className="text-primary mb-2 h-8 w-8" />
<CardTitle className="text-lg">{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
</Card>
</motion.div>
))}
</motion.section>
</div>
);
}

View File

@@ -0,0 +1,29 @@
'use client';
import Link from "next/link";
import { Sparkles } from "lucide-react";
import { buttonVariants } from "@/components/ui/button";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
export function Header() {
return (
<header className="border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
<Link href="/" className="flex items-center gap-2 font-semibold">
<Sparkles className="h-5 w-5 text-primary" />
<span>Idea Guru</span>
</Link>
<nav className="flex items-center gap-2">
<Link
href="/"
className={cn(buttonVariants({ variant: "ghost", size: "sm" }))}
>
Home
</Link>
<Button size='sm'>Get Started</Button>
</nav>
</div>
</header>
);
}

View File

@@ -0,0 +1,60 @@
"use client"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
outline:
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
ghost:
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
destructive:
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default:
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3",
icon: "size-8",
"icon-xs":
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
"icon-sm":
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
"icon-lg": "size-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
function Button({
className,
variant = "default",
size = "default",
...props
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
return (
<ButtonPrimitive
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
}
export { Button, buttonVariants }

103
src/components/ui/card.tsx Normal file
View File

@@ -0,0 +1,103 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Card({
className,
size = "default",
...props
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
return (
<div
data-slot="card"
data-size={size}
className={cn(
"group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
className
)}
{...props}
/>
)
}
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-header"
className={cn(
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
className
)}
{...props}
/>
)
}
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-title"
className={cn(
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
className
)}
{...props}
/>
)
}
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
}
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-action"
className={cn(
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
className
)}
{...props}
/>
)
}
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-content"
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
{...props}
/>
)
}
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-footer"
className={cn(
"flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3",
className
)}
{...props}
/>
)
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
}

View File

@@ -0,0 +1,20 @@
import * as React from "react"
import { Input as InputPrimitive } from "@base-ui/react/input"
import { cn } from "@/lib/utils"
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<InputPrimitive
type={type}
data-slot="input"
className={cn(
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
className
)}
{...props}
/>
)
}
export { Input }

View File

@@ -0,0 +1,20 @@
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
function Label({ className, ...props }: React.ComponentProps<"label">) {
return (
<label
data-slot="label"
className={cn(
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
className
)}
{...props}
/>
)
}
export { Label }

View File

@@ -0,0 +1,49 @@
"use client"
import { useTheme } from "next-themes"
import { Toaster as Sonner, type ToasterProps } from "sonner"
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
icons={{
success: (
<CircleCheckIcon className="size-4" />
),
info: (
<InfoIcon className="size-4" />
),
warning: (
<TriangleAlertIcon className="size-4" />
),
error: (
<OctagonXIcon className="size-4" />
),
loading: (
<Loader2Icon className="size-4 animate-spin" />
),
}}
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
"--border-radius": "var(--radius)",
} as React.CSSProperties
}
toastOptions={{
classNames: {
toast: "cn-toast",
},
}}
{...props}
/>
)
}
export { Toaster }

View File

@@ -0,0 +1,48 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/axios';
// Types
interface ExampleItem {
id: string;
title: string;
completed: boolean;
}
interface CreateItemInput {
title: string;
}
// Query keys - centralized for cache management
export const exampleKeys = {
all: ['example'] as const,
lists: () => [...exampleKeys.all, 'lists'] as const,
detail: (id: string) => [...exampleKeys.all, 'detail', id] as const,
}
// GET - fetch single item
export function useExampleDetail(id: string) {
return useQuery({
queryKey: exampleKeys.detail(id),
queryFn: async () => {
const { data } = await api.get<ExampleItem>(`/example/id`);
return data;
},
enabled: !!id, // only run if id is provided
});
}
// POST - create new item
export function useCreateExample() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (input: CreateItemInput) => {
const { data } = await api.post<ExampleItem>('/example', input);
return data;
},
onSuccess: () => {
// Invalidate and refetch example lists after creating a new item
queryClient.invalidateQueries({queryKey: exampleKeys.lists()});
}
});
}

View File

@@ -0,0 +1,18 @@
import { useCallback, useSyncExternalStore } from "react";
export function useMediaQuery(query: string): boolean {
const subscribe = useCallback(
(callback: () => void) => {
const media = window.matchMedia(query);
media.addEventListener("change", callback);
return () => media.removeEventListener("change", callback);
},
[query]
);
const getSnapshot = () => window.matchMedia(query).matches;
const getServerSnapshot = () => false; // Default to false on server
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}

42
src/lib/axios.ts Normal file
View File

@@ -0,0 +1,42 @@
import axios from "axios";
export const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || "/api",
timeout: 30000,
headers: {
"Content-Type": "application/json",
},
});
// Request interceptor to add auth token
api.interceptors.request.use(
(config) => {
// Example: Add auth token from localStorage (replace with your auth logic)
const token = localStorage.getItem("authToken");
if (token) {
config.headers["Authorization"] = `Bearer token`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor for global error handling
api.interceptors.response.use(
(response) => response,
(error) => {
// Example: Handle 401 Unauthorized globally
if (error.response?.status === 401) {
// Optionally, you can redirect to login page or show a toast
console.error("Unauthorized - redirecting to login");
if (typeof window !== "undefined") {
localStorage.removeItem("authToken"); // Clear token on unauthorized
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);

26
src/lib/env.ts Normal file
View File

@@ -0,0 +1,26 @@
import { z } from "zod";
// Client-side environment variables (must be prefixed with NEXT_PUBLIC_)
export const clientEnvSchema = z.object({
NEXT_PUBLIC_APP_NAME: z.string().default("Demo App"),
NEXT_PUBLIC_APP_URL: z.string().url().default("http://localhost:3000"),
NEXT_PUBLIC_API_BASE_URL: z.string().url().default("http://localhost:3000/api"),
});
// Server-side environment variables (not prefixed)
export const serverEnvSchema = z.object({
NODE_ENV: z
.enum(["development", "production", "test"])
.default("development"),
});
// Only parse server variables on the server, and client variables on the client
export const serverEnv =
typeof window === "undefined" ?
serverEnvSchema.parse({NODE_ENV: process.env.NODE_ENV}) :
({} as z.infer<typeof serverEnvSchema>);
export const clientEnv =
typeof window !== "undefined" ?
clientEnvSchema.parse({NODE_ENV: process.env.NODE_ENV}) :
({} as z.infer<typeof clientEnvSchema>);

34
src/lib/query-client.ts Normal file
View File

@@ -0,0 +1,34 @@
import {
QueryClient,
defaultShouldDehydrateQuery,
isServer,
} from '@tanstack/react-query';
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
retry: 1, // Retry failed requests once
refetchOnWindowFocus: false, // Disable refetch on window focus
},
dehydrate: {
shouldDehydrateQuery: (query) =>
defaultShouldDehydrateQuery(query) || query.state.status === 'pending'
},
},
});
}
let browserQueryClient: QueryClient | undefined;
export function getQueryClient() {
if (isServer) {
return makeQueryClient();
}
// On the client, reuse the same QueryClient instance
if (!browserQueryClient) {
browserQueryClient = makeQueryClient();
}
return browserQueryClient;
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

21
src/lib/validations.ts Normal file
View File

@@ -0,0 +1,21 @@
import { z } from "zod";
// Example: login form validation schema
export const loginSchema = z.object({
email: z.email("Invalid email address"),
password: z.string().min(6, "Password must be at least 6 characters"),
});
export type LoginFormValues = z.infer<typeof loginSchema>;
// Example: Contact form validation schema
export const contactSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.email("Invalid email address"),
message: z
.string()
.min(10, "Message must be at least 10 characters")
.max(500, "Message must be less than 500 characters"),
});
export type ContactFormValues = z.infer<typeof contactSchema>;

17
src/providers/index.tsx Normal file
View File

@@ -0,0 +1,17 @@
'use client'
import { getQueryClient } from "@/lib/query-client";
import { QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from "next-themes";
export function Providers({ children }: { children: React.ReactNode }) {
const queryClient = getQueryClient();
return (
<ThemeProvider attribute="class" defaultTheme="light" enableSystem>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</ThemeProvider>
);
}

View File

@@ -0,0 +1,27 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
interface AppState {
// Example state properties
sidebarOpen: boolean;
toggleSidebar: () => void;
setSidebarOpen: (open: boolean) => void;
}
export const useAppStore = create<AppState>()(
persist(
(set) => ({
// Sidebar
sidebarOpen: true,
toggleSidebar: () =>
set((state) => ({ sidebarOpen: !state.sidebarOpen })),
setSidebarOpen: (open) => set({ sidebarOpen: open })
}),
{
name: "app-store", // unique name for localStorage key
partialize: (state) => ({
sidebarOpen: state.sidebarOpen
}),
}
)
);

21
src/types/index.ts Normal file
View File

@@ -0,0 +1,21 @@
// Shared typescript types and interfaces
export interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface ApiError {
message: string;
statusCode?: number;
errors?: Record<string, string[]>;
}

34
tsconfig.json Normal file
View File

@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts"
],
"exclude": ["node_modules"]
}