Compare commits
39 Commits
205067b9df
...
task/updat
Author | SHA1 | Date | |
---|---|---|---|
fb81e0a7c0
|
|||
542aaf9c6f
|
|||
e7066e7bfe
|
|||
6682cb335a
|
|||
1c2274c118
|
|||
488ae383a7
|
|||
4809ea810c
|
|||
4531c82718
|
|||
2f71aacae4
|
|||
3e76bb6d33
|
|||
2ab7e221e2
|
|||
ed07222957
|
|||
5dbf4f67c2
|
|||
a0af281ace
|
|||
892ecf8702
|
|||
82f081321c
|
|||
0b5f1dbfe4
|
|||
bb69f71790
|
|||
954b034d68
|
|||
fbfa5a4b6a
|
|||
1ae4872428
|
|||
29554d2952
|
|||
bd0ec61189
|
|||
a644fb9c2a
|
|||
1e1ace8186
|
|||
bc5ff8e7fc
|
|||
d4859f686a
|
|||
d464e93395
|
|||
9f37bf2194
|
|||
95823a866f
|
|||
9cb7cf000f
|
|||
8876fb68a2
|
|||
0d6e5b08ed
|
|||
a4a88c1efb
|
|||
9498026e71
|
|||
76dd9cd838
|
|||
7093f4845e
|
|||
89cf3510bc
|
|||
79a84fd255
|
@ -6,62 +6,155 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main, develop]
|
branches: [main, develop]
|
||||||
|
|
||||||
|
# Concurrency control to cancel previous runs
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
BUN_VERSION: '1.2.14' # Pin version for consistency
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint-and-test:
|
# Job 1: Quick checks that can fail fast
|
||||||
name: Lint, Test & Build
|
quick-checks:
|
||||||
|
name: Quick Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Generate cache key
|
||||||
|
id: cache-key
|
||||||
|
run: echo "key=bun-${{ hashFiles('bun.lock') }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: ${{ env.BUN_VERSION }}
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v4
|
||||||
|
id: cache-deps
|
||||||
|
with:
|
||||||
|
path: ~/.bun/install/cache
|
||||||
|
key: ${{ steps.cache-key.outputs.key }}
|
||||||
|
restore-keys: bun-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: bun install --frozen-lockfile
|
run: bun install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Cache node_modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ steps.cache-key.outputs.key }}
|
||||||
|
|
||||||
- name: Run TypeScript type check
|
- name: Run TypeScript type check
|
||||||
run: bun run type-check
|
run: bun run type-check
|
||||||
|
|
||||||
- name: Run ESLint
|
|
||||||
run: bun run lint
|
|
||||||
|
|
||||||
- name: Check Prettier formatting
|
- name: Check Prettier formatting
|
||||||
run: bun run format:check
|
run: bun run format:check
|
||||||
|
|
||||||
- name: Run tests
|
# Job 2: Linting (can run in parallel with type checking)
|
||||||
run: bun run test:ci
|
lint:
|
||||||
|
name: ESLint
|
||||||
- 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
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
needs: quick-checks
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Setup Bun
|
||||||
uses: oven-sh/setup-bun@v1
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
bun-version: ${{ env.BUN_VERSION }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Restore node_modules cache
|
||||||
run: bun install --frozen-lockfile
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ needs.quick-checks.outputs.cache-key }}
|
||||||
|
|
||||||
- name: Run security audit
|
- name: Install dependencies (if cache miss)
|
||||||
run: bun audit
|
run: bun install
|
||||||
|
|
||||||
- name: Run dependency check
|
- name: Run ESLint
|
||||||
run: bunx audit-ci --moderate
|
run: bun run lint
|
||||||
|
|
||||||
|
# Job 3: Testing with optimizations
|
||||||
|
test:
|
||||||
|
name: Test & Coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: quick-checks
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: ${{ env.BUN_VERSION }}
|
||||||
|
|
||||||
|
- name: Restore node_modules cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ needs.quick-checks.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Install dependencies (if cache miss)
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Cache Jest cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: .jest-cache
|
||||||
|
key: jest-cache-${{ hashFiles('jest.config.js', 'src/**/*.{ts,tsx}') }}
|
||||||
|
restore-keys: jest-cache-
|
||||||
|
|
||||||
|
- name: Run tests with optimizations
|
||||||
|
run: bun run test:ci --maxWorkers=2 --cacheDirectory=.jest-cache
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max_old_space_size=4096
|
||||||
|
|
||||||
|
# Job 4: Build (depends on tests passing)
|
||||||
|
build:
|
||||||
|
name: Build Application
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [quick-checks, lint, test]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Bun
|
||||||
|
uses: oven-sh/setup-bun@v2
|
||||||
|
with:
|
||||||
|
bun-version: ${{ env.BUN_VERSION }}
|
||||||
|
|
||||||
|
- name: Restore node_modules cache
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: node_modules
|
||||||
|
key: node-modules-${{ needs.quick-checks.outputs.cache-key }}
|
||||||
|
|
||||||
|
- name: Install dependencies (if cache miss)
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: Cache Next.js build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
.next/cache
|
||||||
|
.next/static
|
||||||
|
key: nextjs-${{ hashFiles('next.config.ts', 'src/**/*.{ts,tsx}', 'public/**/*') }}
|
||||||
|
restore-keys: nextjs-
|
||||||
|
|
||||||
|
- name: Build application
|
||||||
|
run: bun run build
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max_old_space_size=4096
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
.jest-cache
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
@ -31,7 +32,7 @@ yarn-error.log*
|
|||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
@ -138,11 +138,6 @@ The Gitea Actions workflow includes:
|
|||||||
- Unit tests with coverage
|
- Unit tests with coverage
|
||||||
- Build verification
|
- Build verification
|
||||||
|
|
||||||
2. **Security Audit:**
|
|
||||||
|
|
||||||
- Dependency vulnerability scanning
|
|
||||||
- Security audit reporting
|
|
||||||
|
|
||||||
## 📊 Code Quality
|
## 📊 Code Quality
|
||||||
|
|
||||||
### Pre-commit Hooks
|
### Pre-commit Hooks
|
||||||
|
@ -32,6 +32,20 @@ const customJestConfig = {
|
|||||||
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
|
'^.+\\.(js|jsx|ts|tsx)$': ['babel-jest', { presets: ['next/babel'] }],
|
||||||
},
|
},
|
||||||
transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'],
|
transformIgnorePatterns: ['/node_modules/', '^.+\\.module\\.(css|sass|scss)$'],
|
||||||
|
// Performance optimizations
|
||||||
|
maxWorkers: '50%',
|
||||||
|
cache: true,
|
||||||
|
cacheDirectory: '.jest-cache',
|
||||||
|
clearMocks: true,
|
||||||
|
collectCoverage: false, // Only collect coverage when explicitly requested
|
||||||
|
coverageReporters: ['text', 'lcov'],
|
||||||
|
errorOnDeprecated: true,
|
||||||
|
// Reduce memory usage
|
||||||
|
logHeapUsage: true,
|
||||||
|
// Faster test discovery
|
||||||
|
testLocationInResults: true,
|
||||||
|
// Skip coverage for faster runs in watch mode
|
||||||
|
watchPathIgnorePatterns: ['<rootDir>/coverage/', '<rootDir>/.next/'],
|
||||||
};
|
};
|
||||||
|
|
||||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||||
|
21
package.json
21
package.json
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "next-template-app-router",
|
"name": "next-template-app-router",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
@ -13,13 +13,16 @@
|
|||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:ci": "jest --ci --coverage --watchAll=false",
|
"test:ci": "jest --ci --coverage --watchAll=false --maxWorkers=2",
|
||||||
|
"test:fast": "jest --watchAll=false --maxWorkers=50%",
|
||||||
|
"test:coverage": "jest --coverage --watchAll=false",
|
||||||
|
"ci:all": "bun run type-check && bun run lint && bun run test:ci && bun run build",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"next": "15.3.2"
|
"next": "15.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -29,25 +32,25 @@
|
|||||||
"@testing-library/user-event": "^14.5.2",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20",
|
"@types/node": "^22.0.0",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
"@typescript-eslint/eslint-plugin": "^8.32.1",
|
||||||
"@typescript-eslint/parser": "^8.32.1",
|
"@typescript-eslint/parser": "^8.32.1",
|
||||||
"audit-ci": "^7.1.0",
|
"audit-ci": "^7.1.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.2",
|
"eslint-config-next": "15.3.3",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"eslint-plugin-import": "^2.31.0",
|
||||||
"eslint-plugin-jest-dom": "^5.4.0",
|
"eslint-plugin-jest-dom": "^5.4.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"eslint-plugin-react": "^7.37.5",
|
"eslint-plugin-react": "^7.37.5",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-testing-library": "^7.0.0",
|
||||||
"eslint-plugin-testing-library": "^6.2.2",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"husky": "^9.1.6",
|
"husky": "^9.1.6",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"lint-staged": "^15.2.10",
|
"lint-staged": "^16.0.0",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.6",
|
"prettier-plugin-tailwindcss": "^0.6.6",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
|
3
renovate.json
Normal file
3
renovate.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
49
src/app/__tests__/layout.test.tsx
Normal file
49
src/app/__tests__/layout.test.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import RootLayout, { metadata } from '../layout';
|
||||||
|
|
||||||
|
// Mock next/font/google since it's not available in the test environment
|
||||||
|
jest.mock('next/font/google', () => ({
|
||||||
|
Geist: () => ({
|
||||||
|
variable: '--font-geist-sans',
|
||||||
|
}),
|
||||||
|
Geist_Mono: () => ({
|
||||||
|
variable: '--font-geist-mono',
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock globals.css import
|
||||||
|
jest.mock('../globals.css', () => ({}));
|
||||||
|
|
||||||
|
describe('RootLayout', () => {
|
||||||
|
it('renders children correctly', () => {
|
||||||
|
const testContent = <div data-testid='test-content'>Test content</div>;
|
||||||
|
render(<RootLayout>{testContent}</RootLayout>);
|
||||||
|
|
||||||
|
expect(screen.getByTestId('test-content')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('test-content')).toHaveTextContent('Test content');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates proper HTML structure', () => {
|
||||||
|
const testContent = <div data-testid='layout-child'>Test</div>;
|
||||||
|
render(<RootLayout>{testContent}</RootLayout>);
|
||||||
|
|
||||||
|
// Verify the child component is rendered correctly
|
||||||
|
expect(screen.getByTestId('layout-child')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has correct metadata export', () => {
|
||||||
|
expect(metadata).toBeDefined();
|
||||||
|
expect(metadata.title).toBe('Create Next App');
|
||||||
|
expect(metadata.description).toBe('Generated by create next app');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders without errors', () => {
|
||||||
|
const testContent = <div data-testid='render-test'>Test</div>;
|
||||||
|
const view = render(<RootLayout>{testContent}</RootLayout>);
|
||||||
|
|
||||||
|
// Verify the component renders successfully
|
||||||
|
expect(screen.getByTestId('render-test')).toBeInTheDocument();
|
||||||
|
expect(view.container).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,18 +1,23 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
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 text-center justify-center'>
|
||||||
<Image
|
<Image
|
||||||
className='dark:invert'
|
className='dark:invert m-auto'
|
||||||
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)]'>
|
<p className='text-lg text-gray-600 dark:text-gray-300'>
|
||||||
|
Welcome to the Next.JS App Router Template. If you like this please consider starring the
|
||||||
|
repository.
|
||||||
|
</p>
|
||||||
|
<ol className='list-inside list-decimal text-sm/6 text-center font-[family-name:var(--font-geist-mono)] m-auto'>
|
||||||
<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'>
|
||||||
@ -23,8 +28,8 @@ export default function Home() {
|
|||||||
<li className='tracking-[-.01em]'>Save and see your changes instantly.</li>
|
<li className='tracking-[-.01em]'>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 m-auto'>
|
||||||
<a
|
<Link
|
||||||
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'
|
||||||
@ -38,19 +43,19 @@ export default function Home() {
|
|||||||
height={20}
|
height={20}
|
||||||
/>
|
/>
|
||||||
Deploy now
|
Deploy now
|
||||||
</a>
|
</Link>
|
||||||
<a
|
<Link
|
||||||
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>
|
</Link>
|
||||||
</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
|
<Link
|
||||||
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'
|
||||||
@ -58,8 +63,8 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
<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
|
Learn
|
||||||
</a>
|
</Link>
|
||||||
<a
|
<Link
|
||||||
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'
|
||||||
@ -67,8 +72,8 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
<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
|
Examples
|
||||||
</a>
|
</Link>
|
||||||
<a
|
<Link
|
||||||
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'
|
||||||
@ -76,8 +81,20 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
<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 →
|
Go to nextjs.org →
|
||||||
</a>
|
</Link>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
|
<p className='text-sm text-gray-600 dark:text-gray-300'>
|
||||||
|
Made with <span className='text-red-500'>♥</span> by{' '}
|
||||||
|
<Link
|
||||||
|
href='https://jleibl.net'
|
||||||
|
target='_blank'
|
||||||
|
rel='noopener noreferrer'
|
||||||
|
className='font-bold hover:underline hover:underline-offset-4'
|
||||||
|
>
|
||||||
|
Jan-Marlon Leibl
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user