complete init
This commit is contained in:
14
index.tsx
Normal file
14
index.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './src/App';
|
||||
|
||||
// 使用React 18+的createRoot API
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
2112
package-lock.json
generated
2112
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -11,7 +11,8 @@
|
||||
"lint": "eslint src/**/*.{js,jsx}",
|
||||
"lint:fix": "eslint src/**/*.{js,jsx} --fix",
|
||||
"format": "prettier --write src/**/*.{js,jsx,json,css,md}",
|
||||
"dev": "webpack serve --mode development --open"
|
||||
"dev": "webpack serve --mode development --open",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.28.5",
|
||||
@@ -19,6 +20,7 @@
|
||||
"@babel/preset-react": "^7.28.5",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"babel-loader": "^10.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^9.38.0",
|
||||
@@ -26,10 +28,14 @@
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"globals": "^16.4.0",
|
||||
"mini-css-extract-plugin": "^2.9.4",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-loader": "^8.2.0",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.2",
|
||||
"sass-loader": "^16.0.6",
|
||||
"style-loader": "^4.0.0",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"ts-loader": "^9.5.4",
|
||||
"typescript": "^5.9.3",
|
||||
"webpack": "^5.102.1",
|
||||
@@ -37,9 +43,14 @@
|
||||
"webpack-dev-server": "^5.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.28.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@tailwindcss/postcss": "^4.1.16",
|
||||
"axios": "^1.13.1",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"zustand": "^5.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
6
postcss.config.mjs
Normal file
6
postcss.config.mjs
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
autoprefixer: {},
|
||||
}
|
||||
}
|
||||
14
src/App.tsx
Normal file
14
src/App.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
|
||||
import Demo from '@/pages/demo';
|
||||
import { Provider } from '@/components/ui/provider';
|
||||
import './styles/main.scss';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<Provider>
|
||||
<div className="app-container">
|
||||
<Demo />
|
||||
</div>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
108
src/components/ui/color-mode.tsx
Normal file
108
src/components/ui/color-mode.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
|
||||
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
|
||||
import { ThemeProvider, useTheme } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
import * as React from "react"
|
||||
import { LuMoon, LuSun } from "react-icons/lu"
|
||||
|
||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||
|
||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export type ColorMode = "light" | "dark"
|
||||
|
||||
export interface UseColorModeReturn {
|
||||
colorMode: ColorMode
|
||||
setColorMode: (colorMode: ColorMode) => void
|
||||
toggleColorMode: () => void
|
||||
}
|
||||
|
||||
export function useColorMode(): UseColorModeReturn {
|
||||
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
|
||||
const colorMode = forcedTheme || resolvedTheme
|
||||
const toggleColorMode = () => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
return {
|
||||
colorMode: colorMode as ColorMode,
|
||||
setColorMode: setTheme,
|
||||
toggleColorMode,
|
||||
}
|
||||
}
|
||||
|
||||
export function useColorModeValue<T>(light: T, dark: T) {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? dark : light
|
||||
}
|
||||
|
||||
export function ColorModeIcon() {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? <LuMoon /> : <LuSun />
|
||||
}
|
||||
|
||||
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||
|
||||
export const ColorModeButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ColorModeButtonProps
|
||||
>(function ColorModeButton(props, ref) {
|
||||
const { toggleColorMode } = useColorMode()
|
||||
return (
|
||||
<ClientOnly fallback={<Skeleton boxSize="9" />}>
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
aria-label="Toggle color mode"
|
||||
size="sm"
|
||||
ref={ref}
|
||||
{...props}
|
||||
css={{
|
||||
_icon: {
|
||||
width: "5",
|
||||
height: "5",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ColorModeIcon />
|
||||
</IconButton>
|
||||
</ClientOnly>
|
||||
)
|
||||
})
|
||||
|
||||
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||
function LightMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color="fg"
|
||||
display="contents"
|
||||
className="chakra-theme light"
|
||||
colorPalette="gray"
|
||||
colorScheme="light"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||
function DarkMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color="fg"
|
||||
display="contents"
|
||||
className="chakra-theme dark"
|
||||
colorPalette="gray"
|
||||
colorScheme="dark"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
15
src/components/ui/provider.tsx
Normal file
15
src/components/ui/provider.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||
import {
|
||||
ColorModeProvider,
|
||||
type ColorModeProviderProps,
|
||||
} from "./color-mode"
|
||||
|
||||
export function Provider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
||||
43
src/components/ui/toaster.tsx
Normal file
43
src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Toaster as ChakraToaster,
|
||||
Portal,
|
||||
Spinner,
|
||||
Stack,
|
||||
Toast,
|
||||
createToaster,
|
||||
} from "@chakra-ui/react"
|
||||
|
||||
export const toaster = createToaster({
|
||||
placement: "bottom-end",
|
||||
pauseOnPageIdle: true,
|
||||
})
|
||||
|
||||
export const Toaster = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||
{(toast) => (
|
||||
<Toast.Root width={{ md: "sm" }}>
|
||||
{toast.type === "loading" ? (
|
||||
<Spinner size="sm" color="blue.solid" />
|
||||
) : (
|
||||
<Toast.Indicator />
|
||||
)}
|
||||
<Stack gap="1" flex="1" maxWidth="100%">
|
||||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||
{toast.description && (
|
||||
<Toast.Description>{toast.description}</Toast.Description>
|
||||
)}
|
||||
</Stack>
|
||||
{toast.action && (
|
||||
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||
)}
|
||||
{toast.closable && <Toast.CloseTrigger />}
|
||||
</Toast.Root>
|
||||
)}
|
||||
</ChakraToaster>
|
||||
</Portal>
|
||||
)
|
||||
}
|
||||
46
src/components/ui/tooltip.tsx
Normal file
46
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||
showArrow?: boolean
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement | null>
|
||||
content: React.ReactNode
|
||||
contentProps?: ChakraTooltip.ContentProps
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
function Tooltip(props, ref) {
|
||||
const {
|
||||
showArrow,
|
||||
children,
|
||||
disabled,
|
||||
portalled = true,
|
||||
content,
|
||||
contentProps,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (disabled) return children
|
||||
|
||||
return (
|
||||
<ChakraTooltip.Root {...rest}>
|
||||
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraTooltip.Positioner>
|
||||
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||
{showArrow && (
|
||||
<ChakraTooltip.Arrow>
|
||||
<ChakraTooltip.ArrowTip />
|
||||
</ChakraTooltip.Arrow>
|
||||
)}
|
||||
{content}
|
||||
</ChakraTooltip.Content>
|
||||
</ChakraTooltip.Positioner>
|
||||
</Portal>
|
||||
</ChakraTooltip.Root>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './styles/main.scss';
|
||||
import { useCounterStore } from './store/useCounterStore';
|
||||
import { fetchTodos, type Todo } from './services/api';
|
||||
|
||||
function App() {
|
||||
const { count, increment, decrement, reset } = useCounterStore();
|
||||
const [todos, setTodos] = React.useState<Todo[]>([]);
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
// 加载示例数据
|
||||
React.useEffect(() => {
|
||||
const loadTodos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await fetchTodos();
|
||||
if (Array.isArray(data)) {
|
||||
setTodos(data.slice(0, 5)); // 只显示前5个
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load todos');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTodos();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="app-container">
|
||||
<h1>Hello TypeScript + React + Zustand + Axios!</h1>
|
||||
|
||||
{/* Zustand 计数器示例 */}
|
||||
<div className="counter-section">
|
||||
<h2>Counter: {count}</h2>
|
||||
<div className="counter-buttons">
|
||||
<button onClick={decrement}>-</button>
|
||||
<button onClick={reset}>Reset</button>
|
||||
<button onClick={increment}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Axios API 示例 */}
|
||||
<div className="todos-section">
|
||||
<h2>Sample Todos</h2>
|
||||
{loading && <p>Loading...</p>}
|
||||
{error && <p className="error">{error}</p>}
|
||||
{!loading && !error && (
|
||||
<ul className="todo-list">
|
||||
{todos.map((todo) => (
|
||||
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
|
||||
{todo.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 使用React 18+的createRoot API
|
||||
const rootElement = document.getElementById('root');
|
||||
if (rootElement) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
63
src/pages/demo.tsx
Normal file
63
src/pages/demo.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { useCounterStore } from '@/store/useCounterStore';
|
||||
import { fetchTodos, type Todo } from '@/services/api';
|
||||
|
||||
export default function Demo() {
|
||||
|
||||
const { count, increment, decrement, reset } = useCounterStore();
|
||||
const [todos, setTodos] = React.useState<Todo[]>([]);
|
||||
const [loading, setLoading] = React.useState<boolean>(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
// 加载示例数据
|
||||
React.useEffect(() => {
|
||||
const loadTodos = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const data = await fetchTodos();
|
||||
if (Array.isArray(data)) {
|
||||
setTodos(data.slice(0, 5)); // 只显示前5个
|
||||
}
|
||||
} catch (err) {
|
||||
setError('Failed to load todos');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTodos();
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello TypeScript + React + Zustand + Axios!</h1>
|
||||
|
||||
{/* Zustand 计数器示例 */}
|
||||
<div className="counter-section">
|
||||
<h2>Counter: {count}</h2>
|
||||
<div className="counter-buttons">
|
||||
<button onClick={decrement}>-</button>
|
||||
<button onClick={reset}>Reset</button>
|
||||
<button onClick={increment}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Axios API 示例 */}
|
||||
<div className="todos-section">
|
||||
<h2>Sample Todos</h2>
|
||||
{loading && <p>Loading...</p>}
|
||||
{error && <p className="error">{error}</p>}
|
||||
{!loading && !error && (
|
||||
<ul className="todo-list">
|
||||
{todos.map((todo) => (
|
||||
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
|
||||
{todo.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// 或者保留命名空间(更推荐,避免全局污染)
|
||||
@use "tailwindcss";
|
||||
|
||||
// 全局样式重置
|
||||
* {
|
||||
margin: 0;
|
||||
|
||||
10
tailwind.config.js
Normal file
10
tailwind.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{html,js,ts,jsx,tsx,scss}", // 包含你的 .scss 文件
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "node",
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
"strict": true,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist/types",
|
||||
"allowJs": true
|
||||
},
|
||||
"include": ["webpack.config.js"]
|
||||
}
|
||||
@@ -6,7 +6,7 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default {
|
||||
entry: './src/index.tsx',
|
||||
entry: './index.tsx',
|
||||
output: {
|
||||
filename: 'bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
directory: __dirname,
|
||||
},
|
||||
hot: true,
|
||||
open: true,
|
||||
open: false,
|
||||
port: 3000,
|
||||
historyApiFallback: true,
|
||||
},
|
||||
@@ -56,7 +56,7 @@ export default {
|
||||
},
|
||||
{
|
||||
test: /\.(scss|sass)$/,
|
||||
use: ['style-loader', 'css-loader', 'sass-loader'],
|
||||
use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
@@ -69,6 +69,9 @@ export default {
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
},
|
||||
extensions: ['.tsx', '.ts', '.jsx', '.js'],
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user