feat(ci): add CI/CD pipeline configuration and tests
This commit is contained in:
65
.gitea/workflows/ci.yml
Normal file
65
.gitea/workflows/ci.yml
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
name: CI/CD Pipeline
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
pull_request:
|
||||||
|
branches: [main, develop]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint-and-test:
|
||||||
|
name: Lint, Test & Build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run TypeScript type check
|
||||||
|
run: bun run type-check
|
||||||
|
|
||||||
|
- name: Run ESLint
|
||||||
|
run: bun run lint
|
||||||
|
|
||||||
|
- name: Check Prettier formatting
|
||||||
|
run: bun run format:check
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: bun run test:ci
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-files
|
||||||
|
path: .next/
|
||||||
|
|
||||||
|
security-audit:
|
||||||
|
name: Security Audit
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run security audit
|
||||||
|
run: bun audit
|
||||||
|
|
||||||
|
- name: Run dependency check
|
||||||
|
run: bunx audit-ci --moderate
|
43
.gitignore
vendored
43
.gitignore
vendored
@ -39,3 +39,46 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# IDEs and editors
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS generated files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
|
||||||
|
# eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Storybook build outputs
|
||||||
|
.out
|
||||||
|
.storybook-out
|
||||||
|
storybook-static/
|
||||||
|
|
||||||
|
# Temporary folders
|
||||||
|
tmp/
|
||||||
|
temp/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
lerna-debug.log*
|
||||||
|
31
.prettierignore
Normal file
31
.prettierignore
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# Lock files
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
bun.lock
|
21
.prettierrc.json
Normal file
21
.prettierrc.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss", "@trivago/prettier-plugin-sort-imports"],
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"jsxSingleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"printWidth": 100,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"embeddedLanguageFormatting": "auto",
|
||||||
|
"htmlWhitespaceSensitivity": "css",
|
||||||
|
"proseWrap": "preserve",
|
||||||
|
"quoteProps": "as-needed",
|
||||||
|
"importOrder": ["^react", "^next", "^@/(.*)$", "^[./]"],
|
||||||
|
"importOrderSeparation": true,
|
||||||
|
"importOrderSortSpecifiers": true
|
||||||
|
}
|
228
README.md
228
README.md
@ -1,36 +1,220 @@
|
|||||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
# Next.js Template with App Router
|
||||||
|
|
||||||
## Getting Started
|
A comprehensive, production-ready Next.js template featuring modern development tools, strict linting, testing setup, and CI/CD pipeline.
|
||||||
|
|
||||||
First, run the development server:
|
## 🚀 Features
|
||||||
|
|
||||||
```bash
|
- **Next.js 15** with App Router
|
||||||
npm run dev
|
- **TypeScript** with strict configuration
|
||||||
# or
|
- **Tailwind CSS** for styling
|
||||||
yarn dev
|
- **ESLint** with comprehensive rules
|
||||||
# or
|
- **Prettier** for code formatting
|
||||||
pnpm dev
|
- **Husky** for Git hooks
|
||||||
# or
|
- **lint-staged** for pre-commit checks
|
||||||
bun dev
|
- **Jest** and **React Testing Library** for testing
|
||||||
|
- **Commitizen** for conventional commits
|
||||||
|
- **Gitea Actions** CI/CD pipeline
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```treeview
|
||||||
|
├── .gitea/
|
||||||
|
│ └── workflows/ # Gitea Actions CI/CD
|
||||||
|
├── src/
|
||||||
|
│ ├── app/ # Next.js App Router pages
|
||||||
|
│ ├── components/ # Reusable React components
|
||||||
|
│ ├── hooks/ # Custom React hooks
|
||||||
|
│ ├── lib/ # Utility libraries
|
||||||
|
│ ├── types/ # TypeScript type definitions
|
||||||
|
│ └── utils/ # Utility functions
|
||||||
|
├── public/ # Static assets
|
||||||
|
├── __tests__/ # Test files
|
||||||
|
├── .eslintrc.js # ESLint configuration
|
||||||
|
├── .prettierrc.json # Prettier configuration
|
||||||
|
├── jest.config.js # Jest configuration
|
||||||
|
└── tsconfig.json # TypeScript configuration
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
## 🛠️ Getting Started
|
||||||
|
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
### Prerequisites
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
- Bun (latest version recommended)
|
||||||
|
- Node.js 18.x or higher (for compatibility)
|
||||||
|
|
||||||
## Learn More
|
### Installation
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
1. **Clone the repository:**
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
```bash
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
git clone <your-repo-url>
|
||||||
|
cd next-template-app-router
|
||||||
|
```
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
2. **Install dependencies:**
|
||||||
|
|
||||||
## Deploy on Vercel
|
```bash
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
3. **Set up Git hooks:**
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
```bash
|
||||||
|
bun run prepare
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Start the development server:**
|
||||||
|
```bash
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) to view the application.
|
||||||
|
|
||||||
|
## 📝 Available Scripts
|
||||||
|
|
||||||
|
| Script | Description |
|
||||||
|
| ---------------------- | --------------------------------------- |
|
||||||
|
| `bun run dev` | Start development server with Turbopack |
|
||||||
|
| `bun run build` | Build the application for production |
|
||||||
|
| `bun run start` | Start the production server |
|
||||||
|
| `bun run lint` | Run ESLint |
|
||||||
|
| `bun run lint:fix` | Run ESLint and fix issues |
|
||||||
|
| `bun run format` | Format code with Prettier |
|
||||||
|
| `bun run format:check` | Check code formatting |
|
||||||
|
| `bun run type-check` | Run TypeScript type checking |
|
||||||
|
| `bun run test` | Run tests |
|
||||||
|
| `bun run test:watch` | Run tests in watch mode |
|
||||||
|
| `bun run test:ci` | Run tests for CI |
|
||||||
|
| `bun run commit` | Make a conventional commit |
|
||||||
|
|
||||||
|
## 🔧 Configuration
|
||||||
|
|
||||||
|
### ESLint
|
||||||
|
|
||||||
|
The project uses a comprehensive ESLint configuration with:
|
||||||
|
|
||||||
|
- Next.js specific rules
|
||||||
|
- TypeScript support
|
||||||
|
- React and React Hooks rules
|
||||||
|
- Import sorting and organization
|
||||||
|
- Accessibility rules
|
||||||
|
- Testing library rules
|
||||||
|
|
||||||
|
### Prettier
|
||||||
|
|
||||||
|
Code formatting is handled by Prettier with:
|
||||||
|
|
||||||
|
- Consistent code style
|
||||||
|
- Tailwind CSS class sorting
|
||||||
|
- TypeScript support
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
|
||||||
|
Strict TypeScript configuration with:
|
||||||
|
|
||||||
|
- Strict mode enabled
|
||||||
|
- No unchecked indexed access
|
||||||
|
- Exact optional property types
|
||||||
|
- Path mapping for clean imports
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
Testing setup includes:
|
||||||
|
|
||||||
|
- Jest for unit testing
|
||||||
|
- React Testing Library for component testing
|
||||||
|
- Coverage reporting
|
||||||
|
- Mock utilities for common browser APIs
|
||||||
|
|
||||||
|
## 🔄 CI/CD Pipeline
|
||||||
|
|
||||||
|
The Gitea Actions workflow includes:
|
||||||
|
|
||||||
|
1. **Lint and Test Job:**
|
||||||
|
|
||||||
|
- Type checking
|
||||||
|
- ESLint validation
|
||||||
|
- Prettier formatting check
|
||||||
|
- Unit tests with coverage
|
||||||
|
- Build verification
|
||||||
|
|
||||||
|
2. **Security Audit:**
|
||||||
|
|
||||||
|
- Dependency vulnerability scanning
|
||||||
|
- Security audit reporting
|
||||||
|
|
||||||
|
## 📊 Code Quality
|
||||||
|
|
||||||
|
### Pre-commit Hooks
|
||||||
|
|
||||||
|
- **ESLint**: Catches code issues
|
||||||
|
- **Prettier**: Ensures consistent formatting
|
||||||
|
- **Type checking**: Validates TypeScript
|
||||||
|
|
||||||
|
### Conventional Commits
|
||||||
|
|
||||||
|
Use `bun run commit` to create conventional commits:
|
||||||
|
|
||||||
|
- `feat`: New features
|
||||||
|
- `fix`: Bug fixes
|
||||||
|
- `docs`: Documentation changes
|
||||||
|
- `style`: Code style changes
|
||||||
|
- `refactor`: Code refactoring
|
||||||
|
- `test`: Test changes
|
||||||
|
- `chore`: Maintenance tasks
|
||||||
|
|
||||||
|
### Coverage Requirements
|
||||||
|
|
||||||
|
- Minimum 70% coverage for branches, functions, lines, and statements
|
||||||
|
- Coverage reports are generated in CI/CD
|
||||||
|
|
||||||
|
## 🎨 Customization
|
||||||
|
|
||||||
|
### Path Aliases
|
||||||
|
|
||||||
|
The project includes TypeScript path aliases:
|
||||||
|
|
||||||
|
- `@/*` → `./src/*`
|
||||||
|
- `@/components/*` → `./src/components/*`
|
||||||
|
- `@/lib/*` → `./src/lib/*`
|
||||||
|
- `@/utils/*` → `./src/utils/*`
|
||||||
|
- `@/hooks/*` → `./src/hooks/*`
|
||||||
|
- `@/types/*` → `./src/types/*`
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Create a `.env.local` file for local environment variables:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example environment variables
|
||||||
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
||||||
|
DATABASE_URL=your-database-url
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contributing
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create a feature branch: `git checkout -b feature/new-feature`
|
||||||
|
3. Make your changes
|
||||||
|
4. Run tests: `bun test`
|
||||||
|
5. Commit changes: `bun run commit`
|
||||||
|
6. Push to the branch: `git push origin feature/new-feature`
|
||||||
|
7. Submit a pull request
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||||
|
|
||||||
|
## 🔗 Links
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs)
|
||||||
|
- [Bun Documentation](https://bun.sh/docs)
|
||||||
|
- [TypeScript Documentation](https://www.typescriptlang.org/docs)
|
||||||
|
- [Tailwind CSS Documentation](https://tailwindcss.com/docs)
|
||||||
|
- [Jest Documentation](https://jestjs.io/docs/getting-started)
|
||||||
|
- [React Testing Library](https://testing-library.com/docs/react-testing-library/intro)
|
||||||
|
- [Gitea Documentation](https://docs.gitea.io/)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Happy coding! 🎉**
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
import { dirname } from "path";
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
import { fileURLToPath } from "url";
|
import js from '@eslint/js';
|
||||||
import { FlatCompat } from "@eslint/eslintrc";
|
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||||
|
import typescriptParser from '@typescript-eslint/parser';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
import importPlugin from 'eslint-plugin-import';
|
||||||
|
import jestDom from 'eslint-plugin-jest-dom';
|
||||||
|
import jsxA11y from 'eslint-plugin-jsx-a11y';
|
||||||
|
import react from 'eslint-plugin-react';
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks';
|
||||||
|
import testingLibrary from 'eslint-plugin-testing-library';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@ -10,7 +20,113 @@ const compat = new FlatCompat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
js.configs.recommended,
|
||||||
|
...compat.extends('next/core-web-vitals', 'next/typescript'),
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
'@typescript-eslint': typescriptEslint,
|
||||||
|
react,
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'jsx-a11y': jsxA11y,
|
||||||
|
import: importPlugin,
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptParser,
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
jsx: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
'import/resolver': {
|
||||||
|
typescript: {
|
||||||
|
alwaysTryTypes: true,
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// TypeScript specific rules
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/prefer-as-const': 'error',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||||
|
|
||||||
|
// React specific rules
|
||||||
|
'react/react-in-jsx-scope': 'off', // Not needed in Next.js
|
||||||
|
'react/prop-types': 'off', // Using TypeScript
|
||||||
|
'react/jsx-uses-react': 'off', // Not needed in Next.js
|
||||||
|
'react/jsx-uses-vars': 'error',
|
||||||
|
'react/jsx-key': 'error',
|
||||||
|
'react/jsx-no-duplicate-props': 'error',
|
||||||
|
'react/jsx-no-undef': 'error',
|
||||||
|
'react/no-children-prop': 'error',
|
||||||
|
'react/no-danger-with-children': 'error',
|
||||||
|
'react/no-deprecated': 'error',
|
||||||
|
'react/no-direct-mutation-state': 'error',
|
||||||
|
'react/no-find-dom-node': 'error',
|
||||||
|
'react/no-is-mounted': 'error',
|
||||||
|
'react/no-render-return-value': 'error',
|
||||||
|
'react/no-string-refs': 'error',
|
||||||
|
'react/no-unescaped-entities': 'error',
|
||||||
|
'react/no-unknown-property': 'error',
|
||||||
|
'react/require-render-return': 'error',
|
||||||
|
'react/self-closing-comp': 'error',
|
||||||
|
|
||||||
|
// React Hooks rules
|
||||||
|
'react-hooks/rules-of-hooks': 'error',
|
||||||
|
'react-hooks/exhaustive-deps': 'warn',
|
||||||
|
|
||||||
|
// Import rules
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
groups: ['builtin', 'external', 'internal', ['parent', 'sibling'], 'index'],
|
||||||
|
'newlines-between': 'always',
|
||||||
|
alphabetize: {
|
||||||
|
order: 'asc',
|
||||||
|
caseInsensitive: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/no-duplicates': 'error',
|
||||||
|
'import/no-unresolved': 'error',
|
||||||
|
'import/no-cycle': 'error',
|
||||||
|
|
||||||
|
// General rules
|
||||||
|
'no-console': 'warn',
|
||||||
|
'no-debugger': 'error',
|
||||||
|
'no-alert': 'error',
|
||||||
|
'no-var': 'error',
|
||||||
|
'prefer-const': 'error',
|
||||||
|
'no-unused-expressions': 'error',
|
||||||
|
'no-duplicate-imports': 'error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/__tests__/**/*', '**/*.{test,spec}.{js,jsx,ts,tsx}'],
|
||||||
|
plugins: {
|
||||||
|
'testing-library': testingLibrary,
|
||||||
|
'jest-dom': jestDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...testingLibrary.configs.react.rules,
|
||||||
|
...jestDom.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettier, // Must be last to override other configs
|
||||||
];
|
];
|
||||||
|
|
||||||
export default eslintConfig;
|
export default eslintConfig;
|
||||||
|
38
jest.config.js
Normal file
38
jest.config.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
const nextJest = require('next/jest');
|
||||||
|
|
||||||
|
const createJestConfig = nextJest({
|
||||||
|
// Provide the path to your Next.js app to load next.config.js and .env files
|
||||||
|
dir: './',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add any custom config to be passed to Jest
|
||||||
|
const customJestConfig = {
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^@/(.*)$': '<rootDir>/src/$1',
|
||||||
|
'^@/components/(.*)$': '<rootDir>/src/components/$1',
|
||||||
|
'^@/lib/(.*)$': '<rootDir>/src/lib/$1',
|
||||||
|
'^@/utils/(.*)$': '<rootDir>/src/utils/$1',
|
||||||
|
'^@/hooks/(.*)$': '<rootDir>/src/hooks/$1',
|
||||||
|
'^@/types/(.*)$': '<rootDir>/src/types/$1',
|
||||||
|
},
|
||||||
|
collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}', '!src/**/*.d.ts', '!src/**/index.ts'],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
branches: 70,
|
||||||
|
functions: 70,
|
||||||
|
lines: 70,
|
||||||
|
statements: 70,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
testMatch: ['**/__tests__/**/*.(js|jsx|ts|tsx)', '**/*.(test|spec).(js|jsx|ts|tsx)'],
|
||||||
|
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/', '<rootDir>/e2e/'],
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
module.exports = createJestConfig(customJestConfig);
|
33
jest.setup.js
Normal file
33
jest.setup.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
// Optional: configure or set up a testing framework before each test
|
||||||
|
// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
|
||||||
|
|
||||||
|
// Global test utilities can be added here
|
||||||
|
global.ResizeObserver = jest.fn().mockImplementation(() => ({
|
||||||
|
observe: jest.fn(),
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock IntersectionObserver
|
||||||
|
global.IntersectionObserver = jest.fn().mockImplementation(() => ({
|
||||||
|
observe: jest.fn(),
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
disconnect: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock matchMedia
|
||||||
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn().mockImplementation((query) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addListener: jest.fn(), // deprecated
|
||||||
|
removeListener: jest.fn(), // deprecated
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
removeEventListener: jest.fn(),
|
||||||
|
dispatchEvent: jest.fn(),
|
||||||
|
})),
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
/* config options here */
|
||||||
|
59
package.json
59
package.json
@ -6,7 +6,19 @@
|
|||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"lint:fix": "next lint --fix",
|
||||||
|
"format": "prettier --write .",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:ci": "jest --ci --coverage --watchAll=false",
|
||||||
|
"prepare": "husky",
|
||||||
|
"commit": "cz",
|
||||||
|
"install": "bun install",
|
||||||
|
"add": "bun add",
|
||||||
|
"remove": "bun remove"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@ -14,14 +26,51 @@
|
|||||||
"next": "15.3.2"
|
"next": "15.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"@testing-library/jest-dom": "^6.5.0",
|
||||||
|
"@testing-library/react": "^16.0.1",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||||
"tailwindcss": "^4",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
|
"audit-ci": "^7.1.0",
|
||||||
|
"commitizen": "^4.3.0",
|
||||||
|
"cz-conventional-changelog": "^3.3.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.2",
|
"eslint-config-next": "15.3.2",
|
||||||
"@eslint/eslintrc": "^3"
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-import": "^2.31.0",
|
||||||
|
"eslint-plugin-jest-dom": "^5.4.0",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
|
"eslint-plugin-testing-library": "^6.2.2",
|
||||||
|
"husky": "^9.1.6",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "./node_modules/cz-conventional-changelog"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"**/*.{js,jsx,ts,tsx}": [
|
||||||
|
"eslint --fix",
|
||||||
|
"prettier --write"
|
||||||
|
],
|
||||||
|
"**/*.{json,css,md}": [
|
||||||
|
"prettier --write"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: ['@tailwindcss/postcss'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
25
src/app/__tests__/page.test.tsx
Normal file
25
src/app/__tests__/page.test.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import Home from '../page';
|
||||||
|
|
||||||
|
describe('Home Page', () => {
|
||||||
|
it('renders the Next.js logo', () => {
|
||||||
|
render(<Home />);
|
||||||
|
|
||||||
|
const logo = screen.getByAltText('Next.js logo');
|
||||||
|
expect(logo).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("contains the 'Get started' text", () => {
|
||||||
|
render(<Home />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/get started/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has the Vercel logomark', () => {
|
||||||
|
render(<Home />);
|
||||||
|
|
||||||
|
const vercelLogo = screen.getByAltText('Vercel logomark');
|
||||||
|
expect(vercelLogo).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,4 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import "./globals.css";
|
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: '--font-geist-sans',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: '--font-geist-mono',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: 'Create Next App',
|
||||||
description: "Generated by create next app",
|
description: 'Generated by create next app',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -23,12 +24,8 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang='en'>
|
||||||
<body
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,100 +1,80 @@
|
|||||||
import Image from "next/image";
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
<div className='grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]'>
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
<main className='flex flex-col gap-[32px] row-start-2 items-center sm:items-start'>
|
||||||
<Image
|
<Image
|
||||||
className="dark:invert"
|
className='dark:invert'
|
||||||
src="/next.svg"
|
src='/next.svg'
|
||||||
alt="Next.js logo"
|
alt='Next.js logo'
|
||||||
width={180}
|
width={180}
|
||||||
height={38}
|
height={38}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
<ol className='list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]'>
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
<li className='mb-2 tracking-[-.01em]'>
|
||||||
Get started by editing{" "}
|
Get started by editing{' '}
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
<code className='bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold'>
|
||||||
src/app/page.tsx
|
src/app/page.tsx
|
||||||
</code>
|
</code>
|
||||||
.
|
.
|
||||||
</li>
|
</li>
|
||||||
<li className="tracking-[-.01em]">
|
<li className='tracking-[-.01em]'>Save and see your changes instantly.</li>
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
<div className='flex gap-4 items-center flex-col sm:flex-row'>
|
||||||
<a
|
<a
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
className='rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto'
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href='https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
className="dark:invert"
|
className='dark:invert'
|
||||||
src="/vercel.svg"
|
src='/vercel.svg'
|
||||||
alt="Vercel logomark"
|
alt='Vercel logomark'
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Deploy now
|
Deploy now
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
className='rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]'
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href='https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
Read our docs
|
Read our docs
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
<footer className='row-start-3 flex gap-[24px] flex-wrap items-center justify-center'>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
className='flex items-center gap-2 hover:underline hover:underline-offset-4'
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href='https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<Image
|
<Image aria-hidden src='/file.svg' alt='File icon' width={16} height={16} />
|
||||||
aria-hidden
|
|
||||||
src="/file.svg"
|
|
||||||
alt="File icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Learn
|
Learn
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
className='flex items-center gap-2 hover:underline hover:underline-offset-4'
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href='https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<Image
|
<Image aria-hidden src='/window.svg' alt='Window icon' width={16} height={16} />
|
||||||
aria-hidden
|
|
||||||
src="/window.svg"
|
|
||||||
alt="Window icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Examples
|
Examples
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
className='flex items-center gap-2 hover:underline hover:underline-offset-4'
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
href='https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
rel="noopener noreferrer"
|
rel='noopener noreferrer'
|
||||||
>
|
>
|
||||||
<Image
|
<Image aria-hidden src='/globe.svg' alt='Globe icon' width={16} height={16} />
|
||||||
aria-hidden
|
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
/>
|
|
||||||
Go to nextjs.org →
|
Go to nextjs.org →
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
9
src/types/jest.d.ts
vendored
Normal file
9
src/types/jest.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
namespace jest {
|
||||||
|
interface Matchers<R> {
|
||||||
|
toBeInTheDocument(): R;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,13 +13,23 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"],
|
||||||
|
"@/components/*": ["./src/components/*"],
|
||||||
|
"@/lib/*": ["./src/lib/*"],
|
||||||
|
"@/utils/*": ["./src/utils/*"],
|
||||||
|
"@/hooks/*": ["./src/hooks/*"],
|
||||||
|
"@/types/*": ["./src/types/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
Reference in New Issue
Block a user