feat(ci): add CI/CD pipeline configuration and tests

This commit is contained in:
2025-05-27 12:42:51 +02:00
parent 1688849521
commit 9b7d001611
18 changed files with 1841 additions and 127 deletions

65
.gitea/workflows/ci.yml Normal file
View 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
View File

@ -39,3 +39,46 @@ yarn-error.log*
# typescript
*.tsbuildinfo
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
View 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
View 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
View File

@ -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
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
- **Next.js 15** with App Router
- **TypeScript** with strict configuration
- **Tailwind CSS** for styling
- **ESLint** with comprehensive rules
- **Prettier** for code formatting
- **Husky** for Git hooks
- **lint-staged** for pre-commit checks
- **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.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
```bash
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! 🎉**

1149
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
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 __dirname = dirname(__filename);
@ -10,7 +20,113 @@ const compat = new FlatCompat({
});
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;

38
jest.config.js Normal file
View 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
View 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(),
})),
});

View File

@ -1,4 +1,4 @@
import type { NextConfig } from "next";
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
/* config options here */

View File

@ -6,7 +6,19 @@
"dev": "next dev --turbopack",
"build": "next build",
"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": {
"react": "^19.0.0",
@ -14,14 +26,51 @@
"next": "15.3.2"
},
"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/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4",
"@typescript-eslint/eslint-plugin": "^8.32.1",
"@typescript-eslint/parser": "^8.32.1",
"audit-ci": "^7.1.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^9",
"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"
]
}
}

View File

@ -1,5 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
plugins: ['@tailwindcss/postcss'],
};
export default config;

View 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();
});
});

View File

@ -1,4 +1,4 @@
@import "tailwindcss";
@import 'tailwindcss';
:root {
--background: #ffffff;

View File

@ -1,20 +1,21 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import type { Metadata } from 'next';
import { Geist, Geist_Mono } from 'next/font/google';
import './globals.css';
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
variable: '--font-geist-sans',
subsets: ['latin'],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
variable: '--font-geist-mono',
subsets: ['latin'],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: 'Create Next App',
description: 'Generated by create next app',
};
export default function RootLayout({
@ -23,12 +24,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
<html lang='en'>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
</html>
);
}

View File

@ -1,100 +1,80 @@
import Image from "next/image";
import Image from 'next/image';
export default function Home() {
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)]">
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
<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'>
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
className='dark:invert'
src='/next.svg'
alt='Next.js logo'
width={180}
height={38}
priority
/>
<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]">
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">
<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]'>
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'>
src/app/page.tsx
</code>
.
</li>
<li className="tracking-[-.01em]">
Save and see your changes instantly.
</li>
<li className='tracking-[-.01em]'>Save and see your changes instantly.</li>
</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
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"
target="_blank"
rel="noopener noreferrer"
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'
target='_blank'
rel='noopener noreferrer'
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
className='dark:invert'
src='/vercel.svg'
alt='Vercel logomark'
width={20}
height={20}
/>
Deploy now
</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]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
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'
target='_blank'
rel='noopener noreferrer'
>
Read our docs
</a>
</div>
</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
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"
target="_blank"
rel="noopener noreferrer"
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'
target='_blank'
rel='noopener noreferrer'
>
<Image
aria-hidden
src="/file.svg"
alt="File icon"
width={16}
height={16}
/>
<Image aria-hidden src='/file.svg' alt='File icon' width={16} height={16} />
Learn
</a>
<a
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"
target="_blank"
rel="noopener noreferrer"
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'
target='_blank'
rel='noopener noreferrer'
>
<Image
aria-hidden
src="/window.svg"
alt="Window icon"
width={16}
height={16}
/>
<Image aria-hidden src='/window.svg' alt='Window icon' width={16} height={16} />
Examples
</a>
<a
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"
target="_blank"
rel="noopener noreferrer"
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'
target='_blank'
rel='noopener noreferrer'
>
<Image
aria-hidden
src="/globe.svg"
alt="Globe icon"
width={16}
height={16}
/>
<Image aria-hidden src='/globe.svg' alt='Globe icon' width={16} height={16} />
Go to nextjs.org
</a>
</footer>

9
src/types/jest.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
import '@testing-library/jest-dom';
declare global {
namespace jest {
interface Matchers<R> {
toBeInTheDocument(): R;
}
}
}

View File

@ -13,13 +13,23 @@
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": true,
"plugins": [
{
"name": "next"
}
],
"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"],