diff --git a/members/NeilYeTAT/task1/React To-Do-List/.gitignore b/members/NeilYeTAT/task1/React To-Do-List/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/members/NeilYeTAT/task1/React To-Do-List/README.md b/members/NeilYeTAT/task1/React To-Do-List/README.md new file mode 100644 index 000000000..74872fd4a --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/README.md @@ -0,0 +1,50 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default tseslint.config({ + languageOptions: { + // other options... + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + }, +}) +``` + +- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` +- Optionally add `...tseslint.configs.stylisticTypeChecked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: + +```js +// eslint.config.js +import react from 'eslint-plugin-react' + +export default tseslint.config({ + // Set the react version + settings: { react: { version: '18.3' } }, + plugins: { + // Add the react plugin + react, + }, + rules: { + // other rules... + // Enable its recommended rules + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + }, +}) +``` diff --git a/members/NeilYeTAT/task1/React To-Do-List/eslint.config.js b/members/NeilYeTAT/task1/React To-Do-List/eslint.config.js new file mode 100644 index 000000000..092408a9f --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +) diff --git a/members/NeilYeTAT/task1/React To-Do-List/index.html b/members/NeilYeTAT/task1/React To-Do-List/index.html new file mode 100644 index 000000000..e4b78eae1 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/members/NeilYeTAT/task1/React To-Do-List/package.json b/members/NeilYeTAT/task1/React To-Do-List/package.json new file mode 100644 index 000000000..cb13a24de --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/package.json @@ -0,0 +1,34 @@ +{ + "name": "react-to-do-list", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "clsx": "^2.1.1", + "nanoid": "^5.0.7", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.11.1", + "@types/react": "^18.3.10", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.2", + "autoprefixer": "^10.4.20", + "eslint": "^9.11.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.12", + "globals": "^15.9.0", + "postcss": "^8.4.47", + "tailwindcss": "^3.4.14", + "typescript": "^5.5.3", + "typescript-eslint": "^8.7.0", + "vite": "^5.4.8" + } +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/postcss.config.js b/members/NeilYeTAT/task1/React To-Do-List/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/public/vite.svg b/members/NeilYeTAT/task1/React To-Do-List/public/vite.svg new file mode 100644 index 000000000..e7b8dfb1b --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/App.tsx b/members/NeilYeTAT/task1/React To-Do-List/src/App.tsx new file mode 100644 index 000000000..455cede2c --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/App.tsx @@ -0,0 +1,30 @@ +// import { useEffect, useState } from "react"; +import { useState } from "react"; +import Header from "./components/Header"; +import ToDoList from "./components/ToDoList"; + +function App() { + const [refresh, setRefresh] = useState(0); + + const handleClearStorage = () => { + localStorage.clear(); + setRefresh((refresh) => refresh + 1); + }; + + return ( +
+ +
+
+ +
+
+ ); +} + +export default App; diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/assets/react.svg b/members/NeilYeTAT/task1/React To-Do-List/src/assets/react.svg new file mode 100644 index 000000000..6c87de9bb --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/components/AddToDo.tsx b/members/NeilYeTAT/task1/React To-Do-List/src/components/AddToDo.tsx new file mode 100644 index 000000000..2148a006d --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/components/AddToDo.tsx @@ -0,0 +1,56 @@ +import { nanoid } from "nanoid"; +import { Dispatch, SetStateAction, useRef } from "react"; +import { ITodoType } from "../type/ITodoType"; + +export default function AddToDo({ + todoList, + setTodoList, +}: { + todoList: ITodoType[]; + setTodoList: Dispatch>; +}) { + const inputRef = useRef(null); + + const handleAddTodo = () => { + const todo = inputRef.current?.value.trim(); + if (todo) { + const newTodoList: ITodoType[] = [ + ...todoList, + { todo, isFinish: false, id: nanoid() }, + ]; + setTodoList(newTodoList); + localStorage.setItem("todo", JSON.stringify(newTodoList)); + } else { + inputRef.current?.focus(); + } + if (inputRef.current) { + inputRef.current.value = ""; + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleAddTodo(); + } + }; + + return ( + <> + + + ); +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/components/Header.tsx b/members/NeilYeTAT/task1/React To-Do-List/src/components/Header.tsx new file mode 100644 index 000000000..be7f3ff80 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/components/Header.tsx @@ -0,0 +1,3 @@ +export default function Header() { + return

Todo List Header

; +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/components/ToDoItem.tsx b/members/NeilYeTAT/task1/React To-Do-List/src/components/ToDoItem.tsx new file mode 100644 index 000000000..bbf5fbb45 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/components/ToDoItem.tsx @@ -0,0 +1,40 @@ +import clsx from "clsx"; +import type { ITodoType } from "../type/ITodoType"; + +export default function ToDoItem({ + todoItem, + toggleFinishState, + removeTodo, +}: { + todoItem: ITodoType; + toggleFinishState: (id: string) => void; + removeTodo: (id: string) => void; +}) { + return ( +
+ + +
+ ); +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/components/ToDoList.tsx b/members/NeilYeTAT/task1/React To-Do-List/src/components/ToDoList.tsx new file mode 100644 index 000000000..b5d8b0015 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/components/ToDoList.tsx @@ -0,0 +1,44 @@ +import { useEffect, useState } from "react"; +import AddToDo from "./AddToDo"; +import TodoItem from "./ToDoItem"; +import type { ITodoType } from "../type/ITodoType"; + +export default function ToDoList() { + const [todoList, setTodoList] = useState( + JSON.parse(localStorage.getItem("todo")!) || [] + ); + + useEffect(() => { + localStorage.setItem("todo", JSON.stringify(todoList)); + }, [todoList]); + + // 完成 和 未完成 + const toggleFinishState = (id: string) => { + setTodoList( + todoList.map((todoItem) => + todoItem.id === id + ? { ...todoItem, isFinish: !todoItem.isFinish } + : todoItem + ) + ); + }; + // 移出 todo + const removeTodo = (id: string) => { + setTodoList(todoList.filter((todoItem) => todoItem.id !== id)); + }; + + return ( +
+ + + {todoList.map((todoItem) => ( + + ))} +
+ ); +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/index.css b/members/NeilYeTAT/task1/React To-Do-List/src/index.css new file mode 100644 index 000000000..b5c61c956 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/main.tsx b/members/NeilYeTAT/task1/React To-Do-List/src/main.tsx new file mode 100644 index 000000000..6a7443737 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/main.tsx @@ -0,0 +1,10 @@ +// import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App.tsx"; + +createRoot(document.getElementById("root")!).render( + // + + // +); diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/type/ITodoType.ts b/members/NeilYeTAT/task1/React To-Do-List/src/type/ITodoType.ts new file mode 100644 index 000000000..0f5d400f9 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/type/ITodoType.ts @@ -0,0 +1,5 @@ +export interface ITodoType { + id: string; + todo: string; + isFinish: true | false; +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/src/vite-env.d.ts b/members/NeilYeTAT/task1/React To-Do-List/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/members/NeilYeTAT/task1/React To-Do-List/tailwind.config.js b/members/NeilYeTAT/task1/React To-Do-List/tailwind.config.js new file mode 100644 index 000000000..614c86b48 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/members/NeilYeTAT/task1/React To-Do-List/tsconfig.app.json b/members/NeilYeTAT/task1/React To-Do-List/tsconfig.app.json new file mode 100644 index 000000000..f0a235055 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/tsconfig.json b/members/NeilYeTAT/task1/React To-Do-List/tsconfig.json new file mode 100644 index 000000000..1ffef600d --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/tsconfig.node.json b/members/NeilYeTAT/task1/React To-Do-List/tsconfig.node.json new file mode 100644 index 000000000..0d3d71446 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/members/NeilYeTAT/task1/React To-Do-List/vite.config.ts b/members/NeilYeTAT/task1/React To-Do-List/vite.config.ts new file mode 100644 index 000000000..5a33944a9 --- /dev/null +++ b/members/NeilYeTAT/task1/React To-Do-List/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +})