├── .eslintrc.js ├── .github ├── pull_request_template.md └── workflows │ ├── deploy-ota-on-release-merge.yml │ ├── pr-bot-suggestions-emoji-check.yml │ ├── pr-comment.yml │ └── renovate.yml ├── .gitignore ├── README.md ├── package.json ├── public └── index.html ├── renovate.json ├── src ├── README.md ├── components │ ├── App.tsx │ ├── Footer.tsx │ ├── Header.tsx │ ├── MainSection.tsx │ ├── TodoItem.tsx │ └── TodoTextInput.tsx └── index.js ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | ecmaFeatures: { 5 | jsx: true, 6 | }, 7 | ecmaVersion: 2020, 8 | sourceType: 'module', 9 | project: './tsconfig.json', 10 | }, 11 | globals: { 12 | browser: true, 13 | es2021: true, 14 | }, 15 | plugins: [ 16 | 'react', 17 | '@typescript-eslint', 18 | 'react-hooks', 19 | ], 20 | rules: { 21 | 'react/react-in-jsx-scope': 'off', 22 | 'react/prop-types': 'off', 23 | '@typescript-eslint/explicit-function-return-type': 'off', 24 | '@typescript-eslint/explicit-module-boundary-types': 'off', 25 | '@typescript-eslint/no-unused-vars': 'warn', 26 | 'no-console': 'warn', 27 | 'no-debugger': 'warn', 28 | '@typescript-eslint/no-explicit-any': 'warn', 29 | }, 30 | overrides: [ 31 | { 32 | files: ['**/*.ts', '**/*.tsx'], 33 | rules: { 34 | '@typescript-eslint/no-explicit-any': 'warn', 35 | }, 36 | }, 37 | ], 38 | }; 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 20 | 21 | ## In this PR 22 | 23 | - **Changes Implemented:** 24 | 25 | - _Detail the specific changes made in this pull request_ 26 | 27 | - **Reason for Change:** 28 | - _Explain why these changes were necessary or what problem it solves._ 29 | 30 | ## Screenshots 31 | 32 | | Before | After | 33 | | ------------------------- | ------------------------- | 34 | | | | 35 | 36 | ## Testing 37 | 38 | - **Local Testing Steps:** 39 | - _Describe the process for testing these changes locally_ 40 | - **Edge Cases:** 41 | - _Identify any edge cases that have not been tested or could potentially cause issues._ 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy-ota-on-release-merge.yml: -------------------------------------------------------------------------------- 1 | name: OTA Release Production 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | ota_release: 10 | name: EAS Update 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout repository 14 | uses: actions/checkout@v4 15 | 16 | - name: Log Commit Message 17 | run: echo "${{ github.event.head_commit.message }}" 18 | 19 | # - name: Get Version from package.json 20 | # id: get_version 21 | # run: | 22 | # VERSION=$(node -p -e "require('./package.json').version") 23 | # echo "version=$VERSION" >> $GITHUB_OUTPUT 24 | 25 | # - name: Create Git Tag 26 | # run: | 27 | # git tag v${{ steps.get_version.outputs.version }} 28 | # git push origin v${{ steps.get_version.outputs.version }} 29 | 30 | # - name: Install GitHub CLI 31 | # run: sudo apt-get install -y gh 32 | 33 | # - name: Create GitHub Release with Auto-Generated Notes 34 | # env: 35 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | # run: | 37 | # gh release create v${{ steps.get_version.outputs.version }} --generate-notes 38 | -------------------------------------------------------------------------------- /.github/workflows/pr-bot-suggestions-emoji-check.yml: -------------------------------------------------------------------------------- 1 | # This workflow ensures that PR authors have acknowledged and reviewed the code suggestions made by the PR agent 2 | # by checking if the author has marked a self-review checkbox in the comments and approves the PR if it is checked. 3 | 4 | name: Verify Author Self-Review! 5 | 6 | on: 7 | # Since `issue_comment` cannot directly trigger a PR workflow, this must be done via the main branch workflow. 8 | issue_comment: 9 | types: [edited] 10 | 11 | jobs: 12 | verify-author-self-review: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check for Author self-review or No Code Suggestions and Approve PR 17 | uses: actions/github-script@v7 18 | with: 19 | github-token: ${{ secrets.GITHUB_TOKEN }} 20 | script: | 21 | const issue_number = context.payload.issue.number; 22 | 23 | if (!context.payload.issue.pull_request) { 24 | console.log('This comment is not on a pull request.'); 25 | return; 26 | } 27 | 28 | console.log(`Pull Request Number: ${issue_number}`); 29 | 30 | const { data: comments } = await github.rest.issues.listComments({ 31 | owner: context.repo.owner, 32 | repo: context.repo.repo, 33 | issue_number: issue_number 34 | }); 35 | 36 | const foundChecked = comments.some(comment => 37 | comment.body.trim().includes('- [x] **Author self-review**:') 38 | ); 39 | 40 | const noSuggestionsFound = comments.some(comment => 41 | comment.user.login === 'github-actions[bot]' && comment.body.includes('No code suggestions found for the PR.') 42 | ); 43 | 44 | console.log(`foundChecked: ${foundChecked}`); 45 | console.log(`noSuggestionsFound: ${noSuggestionsFound}`); 46 | 47 | if (foundChecked || noSuggestionsFound) { 48 | // Check if the PR has already been approved by github-actions[bot] 49 | const { data: reviews } = await github.rest.pulls.listReviews({ 50 | owner: context.repo.owner, 51 | repo: context.repo.repo, 52 | pull_number: issue_number 53 | }); 54 | 55 | console.log('Reviewers:'); 56 | reviews.forEach(review => { 57 | console.log(`- ${review.user.login} (State: ${review.state})`); 58 | }); 59 | 60 | const botApproval = reviews.some(review => 61 | review.user.login === 'github-actions[bot]' && review.state === 'APPROVED' 62 | ); 63 | 64 | if (botApproval) { 65 | console.log('Pull request has already been approved by github-actions[bot]. Skipping approval.'); 66 | return; 67 | } 68 | 69 | // Approve the pull request if the self-review is checked or no suggestions found, and not already approved 70 | await github.rest.pulls.createReview({ 71 | owner: context.repo.owner, 72 | repo: context.repo.repo, 73 | pull_number: issue_number, 74 | event: 'APPROVE', 75 | }); 76 | console.log('Pull request approved.'); 77 | } 78 | -------------------------------------------------------------------------------- /.github/workflows/pr-comment.yml: -------------------------------------------------------------------------------- 1 | name: PR Comment 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | add-comment: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Add PR Comment 13 | uses: actions/github-script@v7 14 | with: 15 | script: | 16 | await github.rest.issues.createComment({ 17 | issue_number: context.issue.number, 18 | owner: context.repo.owner, 19 | repo: context.repo.repo, 20 | body: "## PR Code Suggestions ✨" 21 | }) 22 | -------------------------------------------------------------------------------- /.github/workflows/renovate.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/renovatebot/github-action/ 2 | name: Renovate 3 | on: 4 | schedule: 5 | - cron: "0 5 * * *" # Run daily at 5 AM 6 | workflow_dispatch: # Allow manual triggering 7 | 8 | jobs: 9 | renovate: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | 15 | - name: Self-hosted Renovate 16 | uses: renovatebot/github-action@v40.2.3 17 | with: 18 | configurationFile: renovate.json 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | env: 21 | LOG_LEVEL: "debug" 22 | RENOVATE_AUTODISCOVER: "true" 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-todoMVC 2 | 3 | This project is an implementation of the TodoMVC application using React with ES6/ES7 syntax, bootstrapped with create-react-app. 4 | 5 | ## Installation 6 | 7 | To get started with this project, clone the repository and install the dependencies: 8 | 9 | ```bash 10 | git clone https://github.com/ChrisWiles/React-todoMVC.git 11 | cd React-todoMVC 12 | yarn install 13 | ``` 14 | 15 | ## Running the Application 16 | 17 | To run the application in development mode: 18 | 19 | ```bash 20 | yarn start 21 | ``` 22 | 23 | This will start the development server and open the application in your default web browser. 24 | 25 | ## Building for Production 26 | 27 | To build the application for production: 28 | 29 | ```bash 30 | yarn build 31 | ``` 32 | 33 | This will bundle the React application in production mode and optimize the build for the best performance. 34 | 35 | ## Testing 36 | 37 | To run the test suite: 38 | 39 | ```bash 40 | yarn test 41 | ``` 42 | 43 | This will start the test runner in the interactive watch mode. 44 | 45 | ## Live Version 46 | 47 | You can view a live version of the application here: [React-todoMVC](https://chriswiles.github.io/React-todoMVC) 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-todomvc", 3 | "version": "0.0.5", 4 | "private": true, 5 | "devDependencies": { 6 | "@babel/plugin-proposal-private-property-in-object": "7.21.11", 7 | "@babel/plugin-transform-private-property-in-object": "7.24.1", 8 | "@types/prop-types": "15.7.12", 9 | "@types/react": "^18.2.79", 10 | "@typescript-eslint/eslint-plugin": "4.29.0", 11 | "@typescript-eslint/parser": "4.29.0", 12 | "browserify-zlib": "0.2.0", 13 | "buffer": "6.0.3", 14 | "crypto-browserify": "3.12.0", 15 | "eslint": "7.32.0", 16 | "eslint-plugin-react": "7.34.1", 17 | "eslint-plugin-react-hooks": "4.6.0", 18 | "https-browserify": "1.0.0", 19 | "prettier": "3.2.5", 20 | "react-scripts": "5.0.1", 21 | "stream-browserify": "3.0.0", 22 | "stream-http": "3.2.0", 23 | "typescript": "4.3.5", 24 | "util": "0.12.5" 25 | }, 26 | "dependencies": { 27 | "classnames": "2.5.1", 28 | "react": "18.2.0", 29 | "react-dom": "18.2.0", 30 | "todomvc-app-css": "2.4.3" 31 | }, 32 | "scripts": { 33 | "start": "BROWSER=none react-scripts start", 34 | "build": "react-scripts build", 35 | "eject": "react-scripts eject", 36 | "test": "react-scripts test", 37 | "format": "prettier --write .", 38 | "type-check": "tsc --noEmit", 39 | "lint": "eslint 'src/**/*.{js,jsx,ts,tsx}' --quiet" 40 | }, 41 | "browserslist": [ 42 | ">0.2%", 43 | "not dead", 44 | "not ie <= 11", 45 | "not op_mini all" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React TodoMVC Example 7 | 8 | 9 |
10 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:recommended"], 3 | "rangeStrategy": "pin", 4 | "packageRules": [ 5 | { 6 | "groupName": "all devDependencies", 7 | "matchDepTypes": ["devDependencies"], 8 | "matchUpdateTypes": ["minor", "patch", "major"], 9 | "schedule": ["before 5am on the first day of the month"], 10 | "automerge": false 11 | }, 12 | { 13 | "groupName": "all dependencies", 14 | "matchDepTypes": ["dependencies"], 15 | "matchUpdateTypes": ["minor", "patch", "major"], 16 | "schedule": ["every 3 months on the first day of the month"], 17 | "automerge": false 18 | }, 19 | { 20 | "groupName": "GitHub Actions", 21 | "matchManagers": ["github-actions"], 22 | "schedule": ["before 5am on the first day of the month"], 23 | "automerge": false 24 | } 25 | ], 26 | "ignoreDeps": [ 27 | "@intercom/intercom-react-native", 28 | "@shopify/restyle", 29 | "aws-amplify", 30 | "eslint", 31 | "eslint-plugin-sonarjs", 32 | "jwt-decode", 33 | "jest-expo", 34 | "react-native-image-viewing" 35 | ], 36 | "prCreation": "immediate", 37 | "automerge": false, 38 | "branchConcurrentLimit": 50, 39 | "dependencyDashboard": true 40 | } 41 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # React TodoMVC Example 2 | 3 | This project template was built with [Create React App](https://github.com/facebookincubator/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.
12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.
15 | You will also see any lint errors in the console. 16 | 17 | ### `npm run build` 18 | 19 | Builds the app for production to the `build` folder.
20 | It correctly bundles React in production mode and optimizes the build for the best performance. 21 | 22 | The build is minified and the filenames include the hashes.
23 | Your app is ready to be deployed! 24 | 25 | ### `npm run eject` 26 | 27 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 28 | 29 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 30 | 31 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 32 | 33 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 34 | -------------------------------------------------------------------------------- /src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Header from "./Header"; 3 | import MainSection from "./MainSection"; 4 | 5 | type Todo = { 6 | text: string; 7 | completed: boolean; 8 | id: number; 9 | }; 10 | 11 | const App = () => { 12 | const [todos, setTodos] = useState([ 13 | { 14 | text: "React ES6 TodoMVC", 15 | completed: false, 16 | id: 0, 17 | }, 18 | ]); 19 | 20 | const addTodo = (text: string) => { 21 | const newTodo: Todo = { 22 | id: todos.reduce((maxId, todo) => Math.max(todo.id, maxId), -1) + 1, 23 | completed: false, 24 | text: text, 25 | }; 26 | setTodos([newTodo, ...todos]); 27 | }; 28 | 29 | const deleteTodo = (id: number) => { 30 | setTodos(todos.filter((todo) => todo.id !== id)); 31 | }; 32 | 33 | const editTodo = (id: number, text: string) => { 34 | setTodos(todos.map((todo) => (todo.id === id ? { ...todo, text } : todo))); 35 | }; 36 | 37 | const completeTodo = (id: number) => { 38 | setTodos(todos.map((todo) => (todo.id === id ? { ...todo, completed: !todo.completed } : todo))); 39 | }; 40 | 41 | const completeAll = () => { 42 | const areAllMarked = todos.every((todo) => todo.completed); 43 | setTodos(todos.map((todo) => ({ ...todo, completed: !areAllMarked }))); 44 | }; 45 | 46 | const clearCompleted = () => { 47 | setTodos(todos.filter((todo) => !todo.completed)); 48 | }; 49 | 50 | const actions = { 51 | addTodo, 52 | deleteTodo, 53 | editTodo, 54 | completeTodo, 55 | completeAll, 56 | clearCompleted, 57 | }; 58 | 59 | return ( 60 |
61 |
62 | 63 |
64 | ); 65 | }; 66 | 67 | export default App; 68 | -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import classnames from "classnames"; 3 | 4 | type FilterType = 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED'; 5 | 6 | const FILTER_TITLES: Record = { 7 | SHOW_ALL: "All", 8 | SHOW_ACTIVE: "Active", 9 | SHOW_COMPLETED: "Completed", 10 | }; 11 | 12 | interface FooterProps { 13 | completedCount: number; 14 | activeCount: number; 15 | filter: FilterType; 16 | onClearCompleted: () => void; 17 | onShow: (filter: FilterType) => void; 18 | } 19 | 20 | const Footer = ({ completedCount, activeCount, filter: selectedFilter, onClearCompleted, onShow }: FooterProps) => { 21 | const renderTodoCount = () => { 22 | const itemWord = activeCount === 1 ? "item" : "items"; 23 | return ( 24 | 25 | {activeCount || "No"} {itemWord} left 26 | 27 | ); 28 | }; 29 | 30 | const renderFilterLink = (filter: FilterType) => { 31 | const title = FILTER_TITLES[filter]; 32 | return ( 33 | onShow(filter)} 37 | > 38 | {title} 39 | 40 | ); 41 | }; 42 | 43 | const renderClearButton = () => { 44 | if (completedCount > 0) { 45 | return ( 46 | 49 | ); 50 | } 51 | }; 52 | 53 | const renderFilterList = () => { 54 | return (["SHOW_ALL", "SHOW_ACTIVE", "SHOW_COMPLETED"] as FilterType[]).map((filter) => ( 55 |
  • {renderFilterLink(filter)}
  • 56 | )); 57 | }; 58 | 59 | return ( 60 |
    61 | {renderTodoCount()} 62 |
      {renderFilterList()}
    63 | {renderClearButton()} 64 |
    65 | ); 66 | }; 67 | 68 | export default Footer; 69 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import TodoTextInput from "./TodoTextInput"; 3 | 4 | interface HeaderProps { 5 | addTodo: (text: string) => void; 6 | } 7 | 8 | const Header = ({ addTodo }: HeaderProps) => { 9 | const handleSave = (text: string) => { 10 | if (text.length !== 0) { 11 | addTodo(text); 12 | } 13 | }; 14 | 15 | return ( 16 |
    17 |

    todos

    18 | 23 |
    24 | ); 25 | }; 26 | 27 | export default Header; 28 | -------------------------------------------------------------------------------- /src/components/MainSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import TodoItem from "./TodoItem"; 3 | import Footer from "./Footer"; 4 | 5 | type Todo = { 6 | text: string; 7 | completed: boolean; 8 | id: number; 9 | }; 10 | 11 | type TodoFilter = 'SHOW_ALL' | 'SHOW_ACTIVE' | 'SHOW_COMPLETED'; 12 | 13 | const TODO_FILTERS: Record boolean> = { 14 | SHOW_ALL: () => true, 15 | SHOW_ACTIVE: (todo) => !todo.completed, 16 | SHOW_COMPLETED: (todo) => todo.completed, 17 | }; 18 | 19 | interface MainSectionProps { 20 | todos: Todo[]; 21 | actions: { 22 | addTodo: (text: string) => void; 23 | deleteTodo: (id: number) => void; 24 | editTodo: (id: number, text: string) => void; 25 | completeTodo: (id: number) => void; 26 | completeAll: () => void; 27 | clearCompleted: () => void; 28 | }; 29 | } 30 | 31 | const MainSection = ({ todos, actions }: MainSectionProps) => { 32 | const [filter, setFilter] = useState("SHOW_ALL"); 33 | 34 | const handleClearCompleted = () => { 35 | actions.clearCompleted(); 36 | }; 37 | 38 | const handleShow = (newFilter: TodoFilter) => { 39 | setFilter(newFilter); 40 | }; 41 | 42 | const renderToggleAll = (completedCount: number) => { 43 | if (todos.length > 0) { 44 | return ( 45 | 51 | ); 52 | } 53 | }; 54 | 55 | const renderFooter = (completedCount: number) => { 56 | const activeCount = todos.length - completedCount; 57 | 58 | if (todos.length) { 59 | return ( 60 |