create-jsc-vite-react-ts 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +21 -0
- package/README.md +83 -0
- package/bin/cli.js +113 -0
- package/package.json +47 -0
- package/template/README.md +97 -0
- package/template/eslint.config.js +23 -0
- package/template/index.html +16 -0
- package/template/package.json +43 -0
- package/template/public/vite.svg +1 -0
- package/template/src/App.test.tsx +29 -0
- package/template/src/App.tsx +18 -0
- package/template/src/app.css +7 -0
- package/template/src/components/Footer.test.tsx +17 -0
- package/template/src/components/Footer.tsx +7 -0
- package/template/src/components/ThemeToggle.test.tsx +53 -0
- package/template/src/components/ThemeToggle.tsx +15 -0
- package/template/src/contexts/ThemeContext.tsx +30 -0
- package/template/src/contexts/theme.ts +10 -0
- package/template/src/hooks/useTheme.test.tsx +34 -0
- package/template/src/hooks/useTheme.ts +10 -0
- package/template/src/main.tsx +13 -0
- package/template/src/test/setup.ts +1 -0
- package/template/tsconfig.app.json +28 -0
- package/template/tsconfig.json +7 -0
- package/template/tsconfig.node.json +26 -0
- package/template/vite.config.ts +13 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Cédric Rochefolle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# create-jsc-vite-react-ts
|
|
2
|
+
|
|
3
|
+
A CLI tool to scaffold modern React applications with TypeScript, Vite, Tailwind CSS, and Vitest pre-configured.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
Create a new project using npm, yarn, or pnpm:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# npm
|
|
11
|
+
npm create jsc-vite-react-ts my-app
|
|
12
|
+
|
|
13
|
+
# yarn
|
|
14
|
+
yarn create jsc-vite-react-ts my-app
|
|
15
|
+
|
|
16
|
+
# pnpm
|
|
17
|
+
pnpm create jsc-vite-react-ts my-app
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then navigate to your project and start developing:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd my-app
|
|
24
|
+
yarn dev
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## What's Included
|
|
28
|
+
|
|
29
|
+
The generated project includes:
|
|
30
|
+
|
|
31
|
+
- **React 19** with TypeScript
|
|
32
|
+
- **Vite 7** for blazing-fast development and builds
|
|
33
|
+
- **Tailwind CSS 4** for utility-first styling
|
|
34
|
+
- **Vitest** for unit testing with React Testing Library
|
|
35
|
+
- **ESLint** with TypeScript and React rules
|
|
36
|
+
- **Dark mode** implementation example with theme context
|
|
37
|
+
- **Arrow function** components by default
|
|
38
|
+
- **100% test coverage** example with all components tested
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
### Pre-configured Testing
|
|
43
|
+
|
|
44
|
+
- Vitest with jsdom environment
|
|
45
|
+
- React Testing Library setup
|
|
46
|
+
- Example tests for all components and hooks
|
|
47
|
+
- Coverage reporting with `yarn test:coverage`
|
|
48
|
+
|
|
49
|
+
### Dark Mode Ready
|
|
50
|
+
|
|
51
|
+
Complete dark mode implementation using React Context and Tailwind CSS, including localStorage persistence.
|
|
52
|
+
|
|
53
|
+
### TypeScript First
|
|
54
|
+
|
|
55
|
+
Strict TypeScript configuration with project references for optimal type checking and IDE performance.
|
|
56
|
+
|
|
57
|
+
### Modern Stack
|
|
58
|
+
|
|
59
|
+
All dependencies are up-to-date with the latest stable versions of React, Vite, Tailwind CSS, and testing libraries.
|
|
60
|
+
|
|
61
|
+
## Available Scripts
|
|
62
|
+
|
|
63
|
+
In the generated project, you can run:
|
|
64
|
+
|
|
65
|
+
- `yarn dev` - Start development server
|
|
66
|
+
- `yarn build` - Build for production
|
|
67
|
+
- `yarn preview` - Preview production build
|
|
68
|
+
- `yarn lint` - Run ESLint
|
|
69
|
+
- `yarn test` - Run tests in watch mode
|
|
70
|
+
- `yarn test:ui` - Run tests with interactive UI
|
|
71
|
+
- `yarn test:coverage` - Run tests with coverage report
|
|
72
|
+
|
|
73
|
+
## Requirements
|
|
74
|
+
|
|
75
|
+
- Node.js 18.0.0 or higher
|
|
76
|
+
|
|
77
|
+
## Contributing
|
|
78
|
+
|
|
79
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
import fs from 'fs-extra'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import { fileURLToPath } from 'url'
|
|
7
|
+
import prompts from 'prompts'
|
|
8
|
+
import chalk from 'chalk'
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
11
|
+
const __dirname = path.dirname(__filename)
|
|
12
|
+
|
|
13
|
+
const run = async () => {
|
|
14
|
+
console.log(chalk.bold.cyan('\n🚀 Create React + TypeScript + Vite App\n'))
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2)
|
|
17
|
+
let projectName = args[0]
|
|
18
|
+
|
|
19
|
+
if (!projectName) {
|
|
20
|
+
const response = await prompts({
|
|
21
|
+
type: 'text',
|
|
22
|
+
name: 'projectName',
|
|
23
|
+
message: 'What is your project named?',
|
|
24
|
+
initial: 'my-app',
|
|
25
|
+
validate: (value) => {
|
|
26
|
+
if (!value) return 'Project name is required'
|
|
27
|
+
if (!/^[a-z0-9-_]+$/.test(value)) {
|
|
28
|
+
return 'Project name can only contain lowercase letters, numbers, hyphens, and underscores'
|
|
29
|
+
}
|
|
30
|
+
return true
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
if (!response.projectName) {
|
|
35
|
+
console.log(chalk.red('\n✖ Project creation cancelled\n'))
|
|
36
|
+
process.exit(1)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
projectName = response.projectName
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const targetDir = path.resolve(process.cwd(), projectName)
|
|
43
|
+
|
|
44
|
+
if (fs.existsSync(targetDir)) {
|
|
45
|
+
console.log(chalk.red(`\n✖ Directory ${projectName} already exists\n`))
|
|
46
|
+
process.exit(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(chalk.blue(`\n📁 Creating project in ${targetDir}...\n`))
|
|
50
|
+
|
|
51
|
+
const templateDir = path.resolve(__dirname, '../template')
|
|
52
|
+
fs.copySync(templateDir, targetDir, {
|
|
53
|
+
filter: (src) => {
|
|
54
|
+
const basename = path.basename(src)
|
|
55
|
+
return !['node_modules', 'dist', 'coverage', '.claude', 'plan.md', 'CLAUDE.md', 'yarn.lock'].includes(basename)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const packageJsonPath = path.join(targetDir, 'package.json')
|
|
60
|
+
const packageJson = fs.readJsonSync(packageJsonPath)
|
|
61
|
+
packageJson.name = projectName
|
|
62
|
+
fs.writeJsonSync(packageJsonPath, packageJson, { spaces: 2 })
|
|
63
|
+
|
|
64
|
+
console.log(chalk.green('✓ Project files created'))
|
|
65
|
+
|
|
66
|
+
const packageManager = await detectPackageManager()
|
|
67
|
+
|
|
68
|
+
console.log(chalk.blue(`\n📦 Installing dependencies with ${packageManager}...\n`))
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
execSync(`${packageManager} install`, {
|
|
72
|
+
cwd: targetDir,
|
|
73
|
+
stdio: 'inherit',
|
|
74
|
+
})
|
|
75
|
+
console.log(chalk.green('\n✓ Dependencies installed'))
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.log(chalk.yellow('\n⚠ Failed to install dependencies automatically'))
|
|
78
|
+
console.log(chalk.yellow(` Please run "${packageManager} install" manually\n`))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log(chalk.bold.green('\n✨ Success! Your project is ready.\n'))
|
|
82
|
+
console.log(chalk.bold('Next steps:\n'))
|
|
83
|
+
console.log(chalk.cyan(` cd ${projectName}`))
|
|
84
|
+
console.log(chalk.cyan(` ${packageManager} dev`))
|
|
85
|
+
console.log()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const detectPackageManager = async () => {
|
|
89
|
+
const userAgent = process.env.npm_config_user_agent || ''
|
|
90
|
+
|
|
91
|
+
if (userAgent.startsWith('yarn')) return 'yarn'
|
|
92
|
+
if (userAgent.startsWith('pnpm')) return 'pnpm'
|
|
93
|
+
|
|
94
|
+
const response = await prompts({
|
|
95
|
+
type: 'select',
|
|
96
|
+
name: 'packageManager',
|
|
97
|
+
message: 'Which package manager do you want to use?',
|
|
98
|
+
choices: [
|
|
99
|
+
{ title: 'npm', value: 'npm' },
|
|
100
|
+
{ title: 'yarn', value: 'yarn' },
|
|
101
|
+
{ title: 'pnpm', value: 'pnpm' },
|
|
102
|
+
],
|
|
103
|
+
initial: 0,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
return response.packageManager || 'npm'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
run().catch((error) => {
|
|
110
|
+
console.error(chalk.red('\n✖ An error occurred:\n'))
|
|
111
|
+
console.error(error)
|
|
112
|
+
process.exit(1)
|
|
113
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-jsc-vite-react-ts",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Create a modern React app with TypeScript, Vite, Tailwind CSS, and Vitest",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-create-jsc-vite-react-ts": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"template"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"react",
|
|
15
|
+
"vite",
|
|
16
|
+
"typescript",
|
|
17
|
+
"tailwindcss",
|
|
18
|
+
"vitest",
|
|
19
|
+
"starter",
|
|
20
|
+
"template",
|
|
21
|
+
"boilerplate",
|
|
22
|
+
"scaffolding",
|
|
23
|
+
"create-app"
|
|
24
|
+
],
|
|
25
|
+
"author": "JScoobyCed",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/jscoobyced/create-jsc-vite-react-ts.git"
|
|
30
|
+
},
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/jscoobyced/create-jsc-vite-react-ts/issues"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://github.com/jscoobyced/create-jsc-vite-react-ts#readme",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"chalk": "^5.4.1",
|
|
40
|
+
"fs-extra": "^11.2.0",
|
|
41
|
+
"prompts": "^2.4.2"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/fs-extra": "^11.0.4",
|
|
45
|
+
"@types/prompts": "^2.4.9"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# React + TypeScript + Vite Starter
|
|
2
|
+
|
|
3
|
+
A modern React application scaffolding with TypeScript, Vite, Tailwind CSS, and testing pre-configured.
|
|
4
|
+
|
|
5
|
+
## What's Included
|
|
6
|
+
|
|
7
|
+
- **React 19** with TypeScript
|
|
8
|
+
- **Vite 7** for blazing-fast development and builds
|
|
9
|
+
- **Tailwind CSS 4** for utility-first styling
|
|
10
|
+
- **Vitest** for unit testing with React Testing Library
|
|
11
|
+
- **ESLint** with TypeScript and React rules
|
|
12
|
+
- **Dark mode** implementation example with theme context
|
|
13
|
+
- **Arrow function** components by default
|
|
14
|
+
|
|
15
|
+
## Getting Started
|
|
16
|
+
|
|
17
|
+
### Install Dependencies
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
yarn install
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Development
|
|
24
|
+
|
|
25
|
+
Start the development server with hot module replacement:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
yarn dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Open [http://localhost:5173](http://localhost:5173) to view your app.
|
|
32
|
+
|
|
33
|
+
### Build
|
|
34
|
+
|
|
35
|
+
Create a production build:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
yarn build
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Preview the production build locally:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
yarn preview
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Available Scripts
|
|
48
|
+
|
|
49
|
+
- `yarn dev` - Start development server
|
|
50
|
+
- `yarn build` - Build for production
|
|
51
|
+
- `yarn preview` - Preview production build
|
|
52
|
+
- `yarn lint` - Run ESLint
|
|
53
|
+
- `yarn test` - Run tests in watch mode
|
|
54
|
+
- `yarn test:ui` - Run tests with interactive UI
|
|
55
|
+
- `yarn test:coverage` - Run tests with coverage report
|
|
56
|
+
|
|
57
|
+
## Project Structure
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
src/
|
|
61
|
+
├── components/ # React components
|
|
62
|
+
├── contexts/ # React contexts and providers
|
|
63
|
+
├── hooks/ # Custom React hooks
|
|
64
|
+
├── test/ # Test setup and utilities
|
|
65
|
+
├── App.tsx # Main application component
|
|
66
|
+
└── main.tsx # Application entry point
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Key Features
|
|
70
|
+
|
|
71
|
+
### Dark Mode
|
|
72
|
+
|
|
73
|
+
The template includes a complete dark mode implementation using React Context and Tailwind CSS. Toggle between light and dark themes with the button in the top-right corner.
|
|
74
|
+
|
|
75
|
+
### Testing
|
|
76
|
+
|
|
77
|
+
All components and hooks include example tests demonstrating best practices with Vitest and React Testing Library. Run `yarn test:coverage` to see your test coverage.
|
|
78
|
+
|
|
79
|
+
### TypeScript
|
|
80
|
+
|
|
81
|
+
Strict TypeScript configuration with project references for optimal type checking and IDE performance.
|
|
82
|
+
|
|
83
|
+
## Next Steps
|
|
84
|
+
|
|
85
|
+
1. Update `package.json` with your project name and details
|
|
86
|
+
2. Customize the theme colors in your Tailwind configuration
|
|
87
|
+
3. Replace the example content in `App.tsx` with your application
|
|
88
|
+
4. Add your routes, state management, and API integration
|
|
89
|
+
5. Update this README with your project-specific information
|
|
90
|
+
|
|
91
|
+
## Learn More
|
|
92
|
+
|
|
93
|
+
- [React Documentation](https://react.dev)
|
|
94
|
+
- [Vite Documentation](https://vite.dev)
|
|
95
|
+
- [Tailwind CSS Documentation](https://tailwindcss.com)
|
|
96
|
+
- [Vitest Documentation](https://vitest.dev)
|
|
97
|
+
- [TypeScript Documentation](https://www.typescriptlang.org)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import js from '@eslint/js'
|
|
2
|
+
import globals from 'globals'
|
|
3
|
+
import reactHooks from 'eslint-plugin-react-hooks'
|
|
4
|
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
|
5
|
+
import tseslint from 'typescript-eslint'
|
|
6
|
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
|
7
|
+
|
|
8
|
+
export default defineConfig([
|
|
9
|
+
globalIgnores(['dist']),
|
|
10
|
+
{
|
|
11
|
+
files: ['**/*.{ts,tsx}'],
|
|
12
|
+
extends: [
|
|
13
|
+
js.configs.recommended,
|
|
14
|
+
tseslint.configs.recommended,
|
|
15
|
+
reactHooks.configs.flat.recommended,
|
|
16
|
+
reactRefresh.configs.vite,
|
|
17
|
+
],
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: 2020,
|
|
20
|
+
globals: globals.browser,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
])
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet" />
|
|
10
|
+
<title>hello-world</title>
|
|
11
|
+
</head>
|
|
12
|
+
<body>
|
|
13
|
+
<div id="root"></div>
|
|
14
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
15
|
+
</body>
|
|
16
|
+
</html>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "tsc -b && vite build",
|
|
9
|
+
"lint": "eslint .",
|
|
10
|
+
"preview": "vite preview",
|
|
11
|
+
"test": "vitest",
|
|
12
|
+
"test:ui": "vitest --ui",
|
|
13
|
+
"test:coverage": "vitest --coverage"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"react": "^19.2.0",
|
|
17
|
+
"react-dom": "^19.2.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@eslint/js": "^9.39.1",
|
|
21
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
22
|
+
"@testing-library/dom": "^10.4.1",
|
|
23
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
24
|
+
"@testing-library/react": "^16.3.1",
|
|
25
|
+
"@testing-library/user-event": "^14.6.1",
|
|
26
|
+
"@types/node": "^24.10.1",
|
|
27
|
+
"@types/react": "^19.2.5",
|
|
28
|
+
"@types/react-dom": "^19.2.3",
|
|
29
|
+
"@vitejs/plugin-react": "^5.1.1",
|
|
30
|
+
"@vitest/coverage-v8": "^4.0.16",
|
|
31
|
+
"@vitest/ui": "^4.0.16",
|
|
32
|
+
"eslint": "^9.39.1",
|
|
33
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
34
|
+
"eslint-plugin-react-refresh": "^0.4.24",
|
|
35
|
+
"globals": "^16.5.0",
|
|
36
|
+
"jsdom": "^27.3.0",
|
|
37
|
+
"tailwindcss": "^4.0.0",
|
|
38
|
+
"typescript": "~5.9.3",
|
|
39
|
+
"typescript-eslint": "^8.46.4",
|
|
40
|
+
"vite": "^7.2.4",
|
|
41
|
+
"vitest": "^4.0.16"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import App from './App'
|
|
4
|
+
import { ThemeProvider } from './contexts/ThemeContext'
|
|
5
|
+
|
|
6
|
+
const renderWithTheme = (component: React.ReactElement) => {
|
|
7
|
+
return render(<ThemeProvider>{component}</ThemeProvider>)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('App', () => {
|
|
11
|
+
it('renders Hello World heading', () => {
|
|
12
|
+
renderWithTheme(<App />)
|
|
13
|
+
const heading = screen.getByText('Hello World')
|
|
14
|
+
expect(heading).toBeInTheDocument()
|
|
15
|
+
expect(heading).toHaveClass('text-4xl')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders ThemeToggle button', () => {
|
|
19
|
+
renderWithTheme(<App />)
|
|
20
|
+
const button = screen.getByRole('button', { name: /toggle theme/i })
|
|
21
|
+
expect(button).toBeInTheDocument()
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('renders Footer', () => {
|
|
25
|
+
renderWithTheme(<App />)
|
|
26
|
+
const footer = screen.getByText(/built with ❤️/i)
|
|
27
|
+
expect(footer).toBeInTheDocument()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ThemeToggle } from './components/ThemeToggle'
|
|
2
|
+
import { Footer } from './components/Footer'
|
|
3
|
+
|
|
4
|
+
const App = () => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="min-h-screen flex flex-col bg-white dark:bg-gray-900">
|
|
7
|
+
<ThemeToggle />
|
|
8
|
+
<main className="grow flex items-center justify-center">
|
|
9
|
+
<h1 className="text-4xl text-gray-900 dark:text-white font-family-sans">
|
|
10
|
+
Hello World
|
|
11
|
+
</h1>
|
|
12
|
+
</main>
|
|
13
|
+
<Footer />
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default App
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { Footer } from './Footer'
|
|
4
|
+
|
|
5
|
+
describe('Footer', () => {
|
|
6
|
+
it('renders footer text', () => {
|
|
7
|
+
render(<Footer />)
|
|
8
|
+
const footerText = screen.getByText(/built with ❤️ by IndyTheDog/i)
|
|
9
|
+
expect(footerText).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('has correct styling classes', () => {
|
|
13
|
+
render(<Footer />)
|
|
14
|
+
const footer = screen.getByRole('contentinfo')
|
|
15
|
+
expect(footer).toHaveClass('text-center')
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { userEvent } from '@testing-library/user-event'
|
|
3
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
4
|
+
import { ThemeToggle } from './ThemeToggle'
|
|
5
|
+
import { ThemeProvider } from '../contexts/ThemeContext'
|
|
6
|
+
|
|
7
|
+
const renderWithTheme = (component: React.ReactElement) => {
|
|
8
|
+
return render(<ThemeProvider>{component}</ThemeProvider>)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('ThemeToggle', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
localStorage.clear()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders theme toggle button', () => {
|
|
17
|
+
renderWithTheme(<ThemeToggle />)
|
|
18
|
+
const button = screen.getByRole('button', { name: /toggle theme/i })
|
|
19
|
+
expect(button).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('displays moon icon for light theme', () => {
|
|
23
|
+
renderWithTheme(<ThemeToggle />)
|
|
24
|
+
const button = screen.getByRole('button', { name: /toggle theme/i })
|
|
25
|
+
expect(button).toHaveTextContent('🌙')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('toggles theme when clicked', async () => {
|
|
29
|
+
const user = userEvent.setup()
|
|
30
|
+
renderWithTheme(<ThemeToggle />)
|
|
31
|
+
const button = screen.getByRole('button', { name: /toggle theme/i })
|
|
32
|
+
|
|
33
|
+
expect(button).toHaveTextContent('🌙')
|
|
34
|
+
|
|
35
|
+
await user.click(button)
|
|
36
|
+
|
|
37
|
+
expect(button).toHaveTextContent('☀️')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('toggles theme back and forth', async () => {
|
|
41
|
+
const user = userEvent.setup()
|
|
42
|
+
renderWithTheme(<ThemeToggle />)
|
|
43
|
+
const button = screen.getByRole('button', { name: /toggle theme/i })
|
|
44
|
+
|
|
45
|
+
expect(button).toHaveTextContent('🌙')
|
|
46
|
+
|
|
47
|
+
await user.click(button)
|
|
48
|
+
expect(button).toHaveTextContent('☀️')
|
|
49
|
+
|
|
50
|
+
await user.click(button)
|
|
51
|
+
expect(button).toHaveTextContent('🌙')
|
|
52
|
+
})
|
|
53
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useTheme } from '../hooks/useTheme'
|
|
2
|
+
|
|
3
|
+
export const ThemeToggle = () => {
|
|
4
|
+
const { theme, toggleTheme } = useTheme()
|
|
5
|
+
|
|
6
|
+
return (
|
|
7
|
+
<button
|
|
8
|
+
onClick={toggleTheme}
|
|
9
|
+
className="fixed top-4 right-4 p-3 rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-700 transition-colors"
|
|
10
|
+
aria-label="Toggle theme"
|
|
11
|
+
>
|
|
12
|
+
{theme === 'light' ? '🌙' : '☀️'}
|
|
13
|
+
</button>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from 'react'
|
|
2
|
+
import { ThemeContext, type Theme } from './theme'
|
|
3
|
+
|
|
4
|
+
export const ThemeProvider = ({ children }: { children: ReactNode }) => {
|
|
5
|
+
const [theme, setTheme] = useState<Theme>(() => {
|
|
6
|
+
const saved = localStorage.getItem('theme')
|
|
7
|
+
return (saved as Theme) || 'light'
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const root = document.documentElement
|
|
12
|
+
if (theme === 'dark') {
|
|
13
|
+
root.classList.add('dark')
|
|
14
|
+
} else {
|
|
15
|
+
root.classList.remove('dark')
|
|
16
|
+
}
|
|
17
|
+
localStorage.setItem('theme', theme)
|
|
18
|
+
}, [theme])
|
|
19
|
+
|
|
20
|
+
const toggleTheme = () => {
|
|
21
|
+
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ThemeContext.Provider value={{ theme, toggleTheme }}>
|
|
26
|
+
{children}
|
|
27
|
+
</ThemeContext.Provider>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { renderHook } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect, beforeEach } from 'vitest'
|
|
3
|
+
import { useTheme } from './useTheme'
|
|
4
|
+
import { ThemeProvider } from '../contexts/ThemeContext'
|
|
5
|
+
|
|
6
|
+
describe('useTheme', () => {
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
localStorage.clear()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('throws error when used outside ThemeProvider', () => {
|
|
12
|
+
expect(() => {
|
|
13
|
+
renderHook(() => useTheme())
|
|
14
|
+
}).toThrow('useTheme must be used within a ThemeProvider')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns theme and toggleTheme function', () => {
|
|
18
|
+
const { result } = renderHook(() => useTheme(), {
|
|
19
|
+
wrapper: ThemeProvider,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
expect(result.current).toHaveProperty('theme')
|
|
23
|
+
expect(result.current).toHaveProperty('toggleTheme')
|
|
24
|
+
expect(typeof result.current.toggleTheme).toBe('function')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('starts with light theme by default', () => {
|
|
28
|
+
const { result } = renderHook(() => useTheme(), {
|
|
29
|
+
wrapper: ThemeProvider,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(result.current.theme).toBe('light')
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { useContext } from 'react'
|
|
2
|
+
import { ThemeContext } from '../contexts/theme'
|
|
3
|
+
|
|
4
|
+
export const useTheme = () => {
|
|
5
|
+
const context = useContext(ThemeContext)
|
|
6
|
+
if (context === undefined) {
|
|
7
|
+
throw new Error('useTheme must be used within a ThemeProvider')
|
|
8
|
+
}
|
|
9
|
+
return context
|
|
10
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './app.css'
|
|
4
|
+
import App from './App.tsx'
|
|
5
|
+
import { ThemeProvider } from './contexts/ThemeContext'
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<ThemeProvider>
|
|
10
|
+
<App />
|
|
11
|
+
</ThemeProvider>
|
|
12
|
+
</StrictMode>,
|
|
13
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "ES2022",
|
|
5
|
+
"useDefineForClassFields": true,
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"types": ["vite/client"],
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true,
|
|
25
|
+
"noUncheckedSideEffectImports": true
|
|
26
|
+
},
|
|
27
|
+
"include": ["src"]
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "ES2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
|
|
10
|
+
/* Bundler mode */
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"allowImportingTsExtensions": true,
|
|
13
|
+
"verbatimModuleSyntax": true,
|
|
14
|
+
"moduleDetection": "force",
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
/* Linting */
|
|
18
|
+
"strict": true,
|
|
19
|
+
"noUnusedLocals": true,
|
|
20
|
+
"noUnusedParameters": true,
|
|
21
|
+
"erasableSyntaxOnly": true,
|
|
22
|
+
"noFallthroughCasesInSwitch": true,
|
|
23
|
+
"noUncheckedSideEffectImports": true
|
|
24
|
+
},
|
|
25
|
+
"include": ["vite.config.ts"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config'
|
|
2
|
+
import react from '@vitejs/plugin-react'
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
4
|
+
|
|
5
|
+
// https://vite.dev/config/
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [react(), tailwindcss()],
|
|
8
|
+
test: {
|
|
9
|
+
globals: true,
|
|
10
|
+
environment: 'jsdom',
|
|
11
|
+
setupFiles: './src/test/setup.ts',
|
|
12
|
+
},
|
|
13
|
+
})
|