Initial commit
This commit is contained in:
22
.claude/settings.json
Normal file
22
.claude/settings.json
Normal 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
4
.env.example
Normal 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
45
.gitignore
vendored
Normal 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
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
|
}
|
||||||
68
AGENTS.md
Normal file
68
AGENTS.md
Normal 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`.
|
||||||
94
README.md
Normal file
94
README.md
Normal 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
25
components.json
Normal 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
20
eslint.config.mjs
Normal 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
15
next.config.ts
Normal 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
51
package.json
Normal 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
8710
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
postcss.config.mjs
Normal file
7
postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
1
public/file.svg
Normal file
1
public/file.svg
Normal 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
1
public/globe.svg
Normal 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
1
public/next.svg
Normal 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
1
public/vercel.svg
Normal 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
1
public/window.svg
Normal 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
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
130
src/app/globals.css
Normal file
130
src/app/globals.css
Normal 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
44
src/app/layout.tsx
Normal 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
116
src/app/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/components/layout/header.tsx
Normal file
29
src/components/layout/header.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
60
src/components/ui/button.tsx
Normal file
60
src/components/ui/button.tsx
Normal 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
103
src/components/ui/card.tsx
Normal 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,
|
||||||
|
}
|
||||||
20
src/components/ui/input.tsx
Normal file
20
src/components/ui/input.tsx
Normal 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 }
|
||||||
20
src/components/ui/label.tsx
Normal file
20
src/components/ui/label.tsx
Normal 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 }
|
||||||
49
src/components/ui/sonner.tsx
Normal file
49
src/components/ui/sonner.tsx
Normal 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 }
|
||||||
48
src/hooks/use-example.query.ts
Normal file
48
src/hooks/use-example.query.ts
Normal 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()});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
18
src/hooks/use-media-query.ts
Normal file
18
src/hooks/use-media-query.ts
Normal 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
42
src/lib/axios.ts
Normal 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
26
src/lib/env.ts
Normal 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
34
src/lib/query-client.ts
Normal 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
6
src/lib/utils.ts
Normal 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
21
src/lib/validations.ts
Normal 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
17
src/providers/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
src/stores/use-app-store.ts
Normal file
27
src/stores/use-app-store.ts
Normal 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
21
src/types/index.ts
Normal 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
34
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user