Приложение на React c нуля до деплоя с помощью Cursor

Самое популярное приложение после Hello World на react — это личный планировщик задач Todolist и мы не будем сильно оригинальничать и напишем его с нуля на react. Разместим приложение в docker контейнере и поможет нам в этом Cursor AI IDE, а точнее сделает все за нас.

Разрабатывать приложение будем в ОС Windows 10, упакуем в docker контейнер и после разместим на хостинге.

Установим Node.js на Windows, для этого скачаем и установим Node.js LTS, проверим установку командой:

node -v

Если установка прошла успешно, в терминале отобразится установленная версия Node.js, теперь установим Create React App:

npm install -g create-react-app

При получении ошибки UnauthorizedAccess в терминале

  1. Откройте PowerShell от имени администратора

  2. Проверьте Текущую Политику Выполнения

    Get-ExecutionPolicy

    Если он показывает «Ограничено», вам нужно его изменить.

  3. Установите Политику выполнения на Неограниченную

    Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

  4. Проверьте изменение: запустите этот cmd

    enter code here

  5. Повторите попытку выполнения команды npx

    npx create-react-app .

Запустим приложение npm startв результате получим следующий вывод:

You can now view todolist in the browser.

Local: http://localhost:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

f2d1e10f8b66627859b18cb60feb3b88.png

Мы запустили простейшее приложение на react, а значит подготовили рабочее окружение для разработки собственного приложения на react.

Начнем и озадачим чат, написав ему следующее:

92a533cd31d67da27fe01d1b51850085.png

После попытки запустить приложение я получил кучу ошибок

Скрытый текст
ERROR in src/App.tsx:1:33
TS7016: Could not find a declaration file for module 'react'. 'C:/Разработка/todolist/node_modules/react/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react';`
  > 1 | import React, { useState } from 'react';
      |                                 ^^^^^^^
    2 | import './App.css';
    3 |
    4 | interface Todo {
ERROR in src/App.tsx:31:24
TS7006: Parameter 'todo' implicitly has an 'any' type.
    29 |
    30 |   const handleToggleTodo = (id: number) => {
  > 31 |     setTodos(todos.map(todo =>
       |                        ^^^^
    32 |       todo.id === id ? { ...todo, completed: !todo.completed } : todo
    33 |     ));
    34 |   };
ERROR in src/App.tsx:37:27
TS7006: Parameter 'todo' implicitly has an 'any' type.
    35 |
    36 |   const handleDeleteTodo = (id: number) => {
  > 37 |     setTodos(todos.filter(todo => todo.id !== id));
       |                           ^^^^
    38 |   };
    39 |
    40 |   const filteredTodos = todos.filter(todo => {
ERROR in src/App.tsx:40:38
TS7006: Parameter 'todo' implicitly has an 'any' type.
    38 |   };
    39 |
  > 40 |   const filteredTodos = todos.filter(todo => {
       |                                      ^^^^
    41 |     if (filter === 'active') return !todo.completed;
    42 |     if (filter === 'completed') return todo.completed;
    43 |     return true;
ERROR in src/App.tsx:46:39
TS7006: Parameter 'todo' implicitly has an 'any' type.
    44 |   });
    45 |
  > 46 |   const remainingTasks = todos.filter(todo => !todo.completed).length;
       |                                       ^^^^
    47 |
    48 |   return (
    49 |     
ERROR in src/App.tsx:49:5 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 47 | 48 | return ( > 49 |
| ^^^^^^^^^^^^^^^^^^^^^ 50 |
51 |

Todo List

52 |
ERROR in src/App.tsx:49:5 TS7016: Could not find a declaration file for module 'react/jsx-runtime'. 'C:/Разработка/todolist/node_modules/react/jsx-runtime.js' implicitly has an 'any' type. Try `npm i --save-dev @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react/jsx-runtime';` 47 | 48 | return ( > 49 |
| ^^^^^^^^^^^^^^^^^^^^^ > 50 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 51 |

Todo List

| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 52 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 53 | 54 | type="text" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 55 | value={inputValue} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 56 | onChange={(e) => setInputValue(e.target.value)} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 57 | placeholder="Add a new task..." | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 58 | className="todo-input" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 59 | /> | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 60 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 61 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 62 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 63 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 64 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 70 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 76 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 82 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 83 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 84 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 85 | {remainingTasks} {remainingTasks === 1 ? 'task' : 'tasks'} remaining | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 86 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 87 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 88 |
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 89 | {filteredTodos.map(todo => ( | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 90 |
  • | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 91 | 92 | onClick={() => handleToggleTodo(todo.id)} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 93 | className="todo-text" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 94 | > | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 95 | {todo.text} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 96 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 97 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 103 |
  • | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 104 | ))} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 105 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 106 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 107 |
| ^^^^^^^^^^^ 108 | ); 109 | } 110 | ERROR in src/App.tsx:50:7 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 48 | return ( 49 |
> 50 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 51 |

Todo List

52 |
53 | 50 |
> 51 |

Todo List

| ^^^^ 52 | 53 | 50 |
> 51 |

Todo List

| ^^^^^ 52 | 53 | 51 |

Todo List

> 52 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 53 | Todo List 52 | > 53 | 54 | type="text" | ^^^^^^^^^^^^^^^^^^^^^^^ > 55 | value={inputValue} | ^^^^^^^^^^^^^^^^^^^^^^^ > 56 | onChange={(e) => setInputValue(e.target.value)} | ^^^^^^^^^^^^^^^^^^^^^^^ > 57 | placeholder="Add a new task..." | ^^^^^^^^^^^^^^^^^^^^^^^ > 58 | className="todo-input" | ^^^^^^^^^^^^^^^^^^^^^^^ > 59 | /> | ^^^^^^^^^^^^^ 60 | 61 | 62 | ERROR in src/App.tsx:56:24 TS7006: Parameter 'e' implicitly has an 'any' type. 54 | type="text" 55 | value={inputValue} > 56 | onChange={(e) => setInputValue(e.target.value)} | ^ 57 | placeholder="Add a new task..." 58 | className="todo-input" 59 | /> ERROR in src/App.tsx:60:11 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 58 | className="todo-input" 59 | /> > 60 | | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 61 | 62 | 63 |
ERROR in src/App.tsx:60:59 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 58 | className="todo-input" 59 | /> > 60 | | ^^^^^^^^^ 61 | 62 | 63 |
ERROR in src/App.tsx:61:9 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 59 | /> 60 | > 61 | | ^^^^^^^ 62 | 63 |
64 | 70 | | ^^^^^^^^^ 70 | > 70 | 76 | | ^^^^^^^^^ 76 | > 76 | 82 |
ERROR in src/App.tsx:81:11 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 79 | > 80 | Completed > 81 | | ^^^^^^^^^ 82 |
83 | 84 |
ERROR in src/App.tsx:82:9 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 80 | Completed 81 | > 82 |
| ^^^^^^ 83 | 84 |
85 | {remainingTasks} {remainingTasks === 1 ? 'task' : 'tasks'} remaining ERROR in src/App.tsx:84:9 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 82 |
83 | > 84 |
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 85 | {remainingTasks} {remainingTasks === 1 ? 'task' : 'tasks'} remaining 86 |
87 | ERROR in src/App.tsx:86:9 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 84 |
85 | {remainingTasks} {remainingTasks === 1 ? 'task' : 'tasks'} remaining > 86 |
| ^^^^^^ 87 | 88 |
    89 | {filteredTodos.map(todo => ( ERROR in src/App.tsx:88:9 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 86 |
87 | > 88 |
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 89 | {filteredTodos.map(todo => ( 90 |
  • 91 | > 89 | {filteredTodos.map(todo => ( | ^^^^ 90 |
  • 91 | handleToggleTodo(todo.id)} ERROR in src/App.tsx:90:13 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 88 |
      89 | {filteredTodos.map(todo => ( > 90 |
    • | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 91 | handleToggleTodo(todo.id)} 93 | className="todo-text" ERROR in src/App.tsx:91:15 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 89 | {filteredTodos.map(todo => ( 90 |
    • > 91 | 92 | onClick={() => handleToggleTodo(todo.id)} | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 93 | className="todo-text" | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ > 94 | > | ^^^^^^^^^^^^^^^^ 95 | {todo.text} 96 | 97 | 103 |
    • ERROR in src/App.tsx:102:15 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 100 | > 101 | Delete > 102 | | ^^^^^^^^^ 103 | 104 | ))} 105 |
    ERROR in src/App.tsx:103:13 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 101 | Delete 102 | > 103 |
  • | ^^^^^ 104 | ))} 105 |
106 |
ERROR in src/App.tsx:105:9 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 103 | 104 | ))} > 105 | | ^^^^^ 106 |
107 |
108 | ); ERROR in src/App.tsx:106:7 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 104 | ))} 105 | > 106 |
| ^^^^^^ 107 |
108 | ); 109 | } ERROR in src/App.tsx:107:5 TS7026: JSX element implicitly has type 'any' because no interface 'JSX.IntrinsicElements' exists. 105 | 106 |
> 107 |
| ^^^^^^ 108 | ); 109 | } 110 | ERROR in src/index.tsx:1:19 TS7016: Could not find a declaration file for module 'react'. 'C:/Разработка/todolist/node_modules/react/index.js' implicitly has an 'any' type. Try `npm i --save-dev @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react';` > 1 | import React from 'react'; | ^^^^^^^ 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; ERROR in src/index.tsx:2:22 TS7016: Could not find a declaration file for module 'react-dom/client'. 'C:/Разработка/todolist/node_modules/react-dom/client.js' implicitly has an 'any' type. Try `npm i --save-dev @types/react-dom` if it exists or add a new declaration (.d.ts) file containing `declare module 'react-dom/client';` 1 | import React from 'react'; > 2 | import ReactDOM from 'react-dom/client'; | ^^^^^^^^^^^^^^^^^^ 3 | import './index.css'; 4 | import App from './App'; 5 | ERROR in src/index.tsx:11:3 TS7016: Could not find a declaration file for module 'react/jsx-runtime'. 'C:/Разработка/todolist/node_modules/react/jsx-runtime.js' implicitly has an 'any' type. Try `npm i --save-dev @types/react` if it exists or add a new declaration (.d.ts) file containing `declare module 'react/jsx-runtime';` 9 | 10 | root.render( > 11 | | ^^^^^^^^^^^^^^^^^^ > 12 | | ^^^^^^^^^^^ > 13 | | ^^^^^^^^^^^^^^^^^^^^^^ 14 | );

Не придумал ничего лучше, как скормить их все в чат Cursor

737b55e6571e7a2774e5b8d95f4c1c0d.png

и не поверите он все тут же поправил, после запуска открылось приложение:

a4d8292f212d66571ec859233323ae7a.png

и отчитался, что сделал

58096bd0d43aad4a1c5166438d1fecf6.png

На этом у нас есть готовое приложение, которое можно улучшать и дорабатывать, давая команды ИИ, но приложение работает локально. Останавливаем терминал и оно отключилось.

Теперь наша задача перенести его в интернет. Для того, чтобы запустить приложение мы разворачивали окружение, которое нужно переносить на другой сервер в интернете, чтобы упростить задачу, будем использовать docker на продакт сервере.

b919792b787fcba54edb4a075a726967.png

Попросим чат сформировать dockerfile для создания докер образа, содержащий все необходимые команды для развертки аналогичного окружения в один клик.

Скрытый текст
# Build stage
FROM node:18-alpine as build

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci

# Copy source code
COPY . .

# Build the application
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copy built files from build stage
COPY --from=build /app/build /usr/share/nginx/html

# Expose port 80
EXPOSE 80

# Start nginx
CMD ["nginx", "-g", "daemon off;"] 

ИИ подготовил dockerfile и конфиг nginx.conf, осталось загрузить весь проект на GitHub, зарегистрировать хостинг, сбилдить образ и запустить контейнер с приложением. Звучит сложно, на самом деле все просто.

Не буду заострять внимание, как отправить проект в github, на самом деле много информации в интернете и все сводится к установке git локально и подключении удаленного репозитория в Cursor

ec4b500012bbdbb864247c59181e5412.png

Запушим и получим код проекта в репозитории Github.

91356fb4d8f5ad51e6e402bdb40d8a49.png

Далее разворачиваем сервер, для пробы используем сервер за 1 рубль в dockerhosting.ru, на созданном инстасе уже установлен docker и portainer, не нужно тратить время на развертку.

40d00c9d06a00d3bcc471ce357755e11.png938d25c775805e77dffcd75a79ad3216.png

Выполним сборку образа и запуск контейнера. Можно все это сделать в portainer, но сделаем, как нам подсказал наш помощник и войдем на сервер по ssh и введм следующие команды по очереди:

6d59945f8332d71d9aa399f670d7dc11.png
# Build the image
docker build -t todolist .

# Run the container
docker run -p 80:80 todolist

Контейнер запущен:

60803748382068a85ef39e346b066433.png

Проверим в portainer собранный docker образ и контейнер:

c17434430ed93c29e9585d6cac189b27.png

Видим, что наш контейнер работает:

0308c20a5746837c1afdf093f9f91639.pngc02b70b71df2d91c9685119140841cd1.png

Проверим наше приложение по IP адресу сервера:

25616547216c49d4b8883c5e30fac27a.png

На выходе получил готовое приложение ни разу не работая с react. Если интересно, могу попробовать продолжить «разработку приложения», разработав бэк и подключить его к базе данных.

Если будут вопросы, мой телеграм.

© Habrahabr.ru