part 12 done

This commit is contained in:
QkoSad
2024-09-30 15:32:50 +03:00
parent 0a7a469d56
commit 33a5afd017
426 changed files with 46304 additions and 5 deletions
Regular → Executable
View File
Submodule Pt12/part12-containers-applications/other-app added at ba75516633
@@ -0,0 +1,47 @@
services:
app:
image: todo-frontend-dev
build:
context: ./todo-frontend
dockerfile: ./todo-frontend/dev.Dockerfile # This will simply tell which dockerfile to read
volumes:
- ./todo-frontend:/usr/src/app # The path can be relative, so ./ is enough to say "the same location as the docker-compose.yml"
container_name: todo-front
server:
image: todo-backend-dev
volumes:
- ./todo-backend:/usr/src/app
environment:
REDIS_URL: "redis://redis:6379"
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
mongo:
image: mongo
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
MONGO_INITDB_DATABASE: the_database
volumes:
- ./todo-backend/mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
- mongo_data:/data/db
redis:
image: redis
command: ['redis-server', '--appendonly', 'yes'] # Overwrite the CMD
volumes: # Declare the volume
- ./todo-backend/redis_data:/data
nginx:
image: nginx:1.20.1
volumes:
- ./nginx.dev.conf:/etc/nginx/nginx.conf:ro
ports:
- 8080:80
container_name: reverse-proxy-front-back
depends_on:
- app # wait for the frontend container to be started
- server
volumes:
mongo_data:
@@ -0,0 +1,22 @@
# events is required, but defaults are ok
events { }
# A http server, listening at port 80
http {
server {
listen 80;
# Requests starting with root (/) are handled
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_pass http://app:5173;
}
location /api/ {
proxy_pass http://server:3000/;
}
}
}
@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
@@ -0,0 +1,15 @@
# React application
This application is created from create-react-app.
Install dependencies with `npm install`
You can run the application in development mode with `npm start`
You can build static files for production release with `npm run build`
You can run tests with `npm run test`
## Environment variables
Use REACT_APP_BACKEND_URL to set where the backend for this application is.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
{
"name": "todo-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.3.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,12 @@
import './App.css';
import TodoView from './Todos/TodoView'
function App() {
return (
<div className="App">
<TodoView />
</div>
);
}
export default App;
@@ -0,0 +1,23 @@
import React, { useState } from 'react'
const TodoForm = ({ createTodo }) => {
const [text, setText] = useState('')
const onChange = ({ target }) => {
setText(target.value)
}
const handleSubmit = (e) => {
e.preventDefault()
createTodo({ text })
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="text" value={text} onChange={onChange} />
<button type="submit"> Submit </button>
</form>
)
}
export default TodoForm
@@ -0,0 +1,49 @@
import React from 'react'
const TodoList = ({ todos, deleteTodo, completeTodo }) => {
const onClickDelete = (todo) => () => {
deleteTodo(todo)
}
const onClickComplete = (todo) => () => {
completeTodo(todo)
}
return (
<>
{todos.map(todo => {
const doneInfo = (
<>
<span>This todo is done</span>
<span>
<button onClick={onClickDelete(todo)}> Delete </button>
</span>
</>
)
const notDoneInfo = (
<>
<span>
This todo is not done
</span>
<span>
<button onClick={onClickDelete(todo)}> Delete </button>
<button onClick={onClickComplete(todo)}> Set as done </button>
</span>
</>
)
return (
<div style={{ display: 'flex', justifyContent: 'space-between', maxWidth: '70%', margin: 'auto' }}>
<span>
{todo.text}
</span>
{todo.done ? doneInfo : notDoneInfo}
</div>
)
}).reduce((acc, cur) => [...acc, <hr />, cur], [])}
</>
)
}
export default TodoList
@@ -0,0 +1,46 @@
import React, { useEffect, useState } from 'react'
import axios from '../util/apiClient'
import List from './List'
import Form from './Form'
const TodoView = () => {
const [todos, setTodos] = useState([])
const refreshTodos = async () => {
const { data } = await axios.get('/todos')
setTodos(data)
}
useEffect(() => {
refreshTodos()
}, [])
const createTodo = async (todo) => {
const { data } = await axios.post('/todos', todo)
setTodos([...todos, data])
}
const deleteTodo = async (todo) => {
await axios.delete(`/todos/${todo._id}`)
refreshTodos()
}
const completeTodo = async (todo) => {
await axios.put(`/todos/${todo._id}`, {
text: todo.text,
done: true
})
refreshTodos()
}
return (
<>
<h1>Todos</h1>
<Form createTodo={createTodo} />
<List todos={todos} deleteTodo={deleteTodo} completeTodo={completeTodo} />
</>
)
}
export default TodoView
@@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<App />
);
@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
@@ -0,0 +1,7 @@
import axios from 'axios'
const apiClient = axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
})
export default apiClient
@@ -0,0 +1,61 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
@@ -0,0 +1,15 @@
FROM node:20
WORKDIR /usr/src/app
COPY --chown=node:node . .
RUN npm ci
ENV DEBUG=express:*
USER node
CMD npm start
@@ -0,0 +1,19 @@
# Express application
Install dependencies with `npm install`
Run with `npm start`
Or in development mode with `npm run dev`
# Visit counter
When running the server, visit http://localhost:3000 to see visit counter, or give environment variable `PORT` to change the port.
# MongoDB
The application has /todos crud which requires a MongoDB. Pass connection url with env `MONGO_URL`
# Redis
Pass connection url with env `REDIS_URL`
@@ -0,0 +1,20 @@
const express = require("express");
const logger = require("morgan");
const cors = require("cors");
const indexRouter = require("./routes/index");
const todosRouter = require("./routes/todos");
const statisticsRouter = require("./routes/statistics");
const app = express();
app.use(cors());
app.use(logger("dev"));
app.use(express.json());
app.use("/", indexRouter);
app.use("/todos", todosRouter);
app.use("/statistics", statisticsRouter);
module.exports = app;
@@ -0,0 +1,89 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('todo-express-backend:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
@@ -0,0 +1,15 @@
FROM node:20
WORKDIR /usr/src/app
COPY --chown=node:node . .
RUN npm i
ENV DEBUG=express:*
USER node
CMD ["npm", "run", "dev", "--", "--host"]
@@ -0,0 +1,38 @@
services:
server:
image: todo-backend-dev
build:
context: .
dockerfile: dev.Dockerfile
volumes:
- ./:/usr/src/app
ports:
- 3000:3000
environment:
REDIS_URL: "redis://redis:6379"
MONGO_URL: "mongodb://the_username:the_password@mongo:27017/the_database"
mongo:
image: mongo
ports:
- 3456:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
MONGO_INITDB_DATABASE: the_database
volumes:
- ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
- mongo_data:/data/db
redis:
image: redis
ports:
- 3457:6379
# Everything else
command: ['redis-server', '--appendonly', 'yes'] # Overwrite the CMD
volumes: # Declare the volume
- ./redis_data:/data
volumes:
mongo_data:
@@ -0,0 +1,6 @@
services:
app: # The name of the service, can be anything
image: todo-backend # Declares which image to use
build: . # Declares where to build if image is not found
ports: # Declares the ports to publish
- 3000:3000
@@ -0,0 +1,10 @@
const mongoose = require('mongoose')
const Todo = require('./models/Todo')
const { MONGO_URL } = require('../util/config')
if (MONGO_URL && !mongoose.connection.readyState) mongoose.connect(MONGO_URL, { useNewUrlParser: true, useUnifiedTopology: true })
module.exports = {
Todo
}
@@ -0,0 +1,8 @@
const mongoose = require('mongoose')
const todoSchema = new mongoose.Schema({
text: String,
done: Boolean
})
module.exports = mongoose.model('Todo', todoSchema)
@@ -0,0 +1,15 @@
db.createUser({
user: 'the_username',
pwd: 'the_password',
roles: [
{
role: 'dbOwner',
db: 'the_database',
},
],
});
db.createCollection('todos');
db.todos.insert({ text: 'Write code', done: true });
db.todos.insert({ text: 'Learn about containers', done: false });
@@ -0,0 +1,2 @@
WiredTiger
WiredTiger 11.2.0: (November 10, 2022)
@@ -0,0 +1 @@
WiredTiger lock file
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
{
"name": "todo-express-backend",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www",
"dev": "nodemon ./bin/www"
},
"dependencies": {
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"mongoose": "^5.13.2",
"morgan": "~1.9.1",
"redis": "^3.1.2"
},
"devDependencies": {
"nodemon": "^2.0.12"
}
}
@@ -0,0 +1,27 @@
const redis = require('redis')
const { promisify } = require('util')
const { REDIS_URL } = require('../util/config')
let getAsync
let setAsync
if (!REDIS_URL) {
const redisIsDisabled = () => {
console.log('No REDIS_URL set, Redis is disabled')
return null
}
getAsync = redisIsDisabled
setAsync = redisIsDisabled
} else {
const client = redis.createClient({
url: REDIS_URL
})
getAsync = promisify(client.get).bind(client)
setAsync = promisify(client.set).bind(client)
}
module.exports = {
getAsync,
setAsync
}
@@ -0,0 +1,36 @@
*2
$6
SELECT
$1
0
*3
$3
set
$11
added_todos
$1
1
*2
$6
SELECT
$1
0
*3
$3
set
$11
added_todos
$1
2
*2
$6
SELECT
$1
0
*3
$3
set
$11
added_todos
$1
3
@@ -0,0 +1,2 @@
file appendonly.aof.1.base.rdb seq 1 type b
file appendonly.aof.1.incr.aof seq 1 type i
@@ -0,0 +1,19 @@
const express = require("express");
const router = express.Router();
const redis = require("../redis");
const configs = require("../util/config");
let visits = 0;
/* GET index data. */
router.get("/", async (req, res) => {
visits++;
res.send({
...configs,
visits,
});
});
module.exports = router;
@@ -0,0 +1,11 @@
const express = require("express");
const router = express.Router();
const { getAsync } = require("../redis/index");
/* GET todos listing. */
router.get("/", async (_, res) => {
const statistics = await getAsync("added_todos");
res.send(statistics);
});
module.exports = router;
@@ -0,0 +1,54 @@
const express = require("express");
const { Todo } = require("../mongo");
const router = express.Router();
const { getAsync, setAsync } = require("../redis/index");
/* GET todos listing. */
router.get("/", async (_, res) => {
const todos = await Todo.find({});
res.send(todos);
});
/* POST todo to listing. */
router.post("/", async (req, res) => {
const todo = await Todo.create({
text: req.body.text,
done: false,
});
const todoCounter = await getAsync("added_todos");
setAsync("added_todos", Number(todoCounter) + 1);
res.send(todo);
});
const singleRouter = express.Router();
const findByIdMiddleware = async (req, res, next) => {
const { id } = req.params;
req.todo = await Todo.findById(id);
if (!req.todo) return res.sendStatus(404);
next();
};
/* DELETE todo. */
singleRouter.delete("/", async (req, res) => {
await req.todo.delete();
res.sendStatus(200);
});
/* GET todo. */
singleRouter.get("/", async (req, res) => {
res.send(req.todo);
});
/* PUT todo. */
singleRouter.put("/", async (req, res) => {
const todo = await Todo.findByIdAndUpdate(req.todo._id, {
done: true,
});
res.send(todo);
});
router.use("/:id", findByIdMiddleware, singleRouter);
module.exports = router;
@@ -0,0 +1,7 @@
const MONGO_URL = process.env.MONGO_URL || undefined
const REDIS_URL = process.env.REDIS_URL || undefined
module.exports = {
MONGO_URL,//: 'mongodb://the_username:the_password@localhost:3456/the_database',
REDIS_URL//: '//localhost:6378'
}
@@ -0,0 +1,21 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react/jsx-no-target-blank': 'off',
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
}
@@ -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?
@@ -0,0 +1,22 @@
# The first FROM is now a stage called build-stage
FROM node:20 AS build-stage
ENV VITE_BACKEND_URL=http://server:3000
WORKDIR /usr/src/app
COPY . .
RUN npm ci
RUN npm run build
# This is a new stage, everything before this is gone, except for the files that we want to COPY
FROM nginx:1.25-alpine
# COPY the directory dist from the build-stage to /usr/share/nginx/html
# The target location here was found from the Docker hub page
COPY --from=build-stage /usr/src/app/dist /usr/share/nginx/html
@@ -0,0 +1,13 @@
# React application
This application is created with [Vite](https://vitest.dev/).
Install dependencies with `npm install`
You can run the application in development mode with `npm run dev`
You can build static files for production release with `npm run build`
## Environment variables
Use env VITE_BACKEND_URL to set where the backend for this application is
@@ -0,0 +1,13 @@
FROM node:20 AS build-stage
ENV VITE_BACKEND_URL=http://localhost:8080/api/
WORKDIR /usr/src/app
COPY . .
RUN npm install
CMD ["npm", "run", "dev", "--", "--host"]
@@ -0,0 +1,11 @@
services:
app:
image: todo-front-dev
build:
context: . # The context will pick this directory as the "build context"
dockerfile: dev.Dockerfile # This will simply tell which dockerfile to read
volumes:
- ./:/usr/src/app # The path can be relative, so ./ is enough to say "the same location as the docker-compose.yml"
ports:
- 5173:5173
container_name: todo-front
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
{
"name": "todo-frontend",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.6.8",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"jest": "^29.7.0",
"vite": "^5.2.0"
}
}
@@ -0,0 +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>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@@ -0,0 +1,12 @@
import './App.css';
import TodoView from './Todos/TodoView'
function App() {
return (
<div className="App">
<TodoView />
</div>
);
}
export default App;
@@ -0,0 +1,23 @@
import React, { useState } from 'react'
const TodoForm = ({ createTodo }) => {
const [text, setText] = useState('')
const onChange = ({ target }) => {
setText(target.value)
}
const handleSubmit = (e) => {
e.preventDefault()
createTodo({ text })
}
return (
<form onSubmit={handleSubmit}>
<input type="text" name="text" value={text} onChange={onChange} />
<button type="submit"> Submit </button>
</form>
)
}
export default TodoForm
@@ -0,0 +1,49 @@
import React from 'react'
const TodoList = ({ todos, deleteTodo, completeTodo }) => {
const onClickDelete = (todo) => () => {
deleteTodo(todo)
}
const onClickComplete = (todo) => () => {
completeTodo(todo)
}
return (
<>
{todos.map(todo => {
const doneInfo = (
<>
<span>This todo is done</span>
<span>
<button onClick={onClickDelete(todo)}> Delete </button>
</span>
</>
)
const notDoneInfo = (
<>
<span>
This todo is not done
</span>
<span>
<button onClick={onClickDelete(todo)}> Delete </button>
<button onClick={onClickComplete(todo)}> Set as done </button>
</span>
</>
)
return (
<div style={{ display: 'flex', justifyContent: 'space-between', maxWidth: '70%', margin: 'auto' }}>
<span>
{todo.text}
</span>
{todo.done ? doneInfo : notDoneInfo}
</div>
)
}).reduce((acc, cur) => [...acc, <hr />, cur], [])}
</>
)
}
export default TodoList
@@ -0,0 +1,26 @@
import { useEffect, useState } from 'react'
import axios from '../util/apiClient'
const SingleTodoView = () => {
const [todo, setTodo] = useState([])
const refreshTodo = async () => {
const { data } = await axios.get('/todos/:id')
setTodo(data)
}
useEffect(() => {
refreshTodo()
}, [])
return (
<>
<h1>Todo</h1>
<div>{todo.text}</div>
<div>{todo.done}</div>
<div>{todo._id}</div>
</>
)
}
export default SingleTodoView
@@ -0,0 +1,46 @@
import { useEffect, useState } from 'react'
import axios from '../util/apiClient'
import List from './List'
import Form from './Form'
const TodoView = () => {
const [todos, setTodos] = useState([])
const refreshTodos = async () => {
const { data } = await axios.get('/todos')
setTodos(data)
}
useEffect(() => {
refreshTodos()
}, [])
const createTodo = async (todo) => {
const { data } = await axios.post('/todos', todo)
setTodos([...todos, data])
}
const deleteTodo = async (todo) => {
await axios.delete(`/todos/${todo._id}`)
refreshTodos()
}
const completeTodo = async (todo) => {
await axios.put(`/todos/${todo._id}`, {
text: todo.text,
done: true
})
refreshTodos()
}
return (
<>
<h1>Todos</h1>
<Form createTodo={createTodo} />
<List todos={todos} deleteTodo={deleteTodo} completeTodo={completeTodo} />
</>
)
}
export default TodoView
@@ -0,0 +1,7 @@
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<App />
);
@@ -0,0 +1,7 @@
import axios from 'axios'
const apiClient = axios.create({
baseURL: import.meta.env.VITE_BACKEND_URL,
})
export default apiClient
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
+24
View File
@@ -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?
+20
View File
@@ -0,0 +1,20 @@
# The first FROM is now a stage called build-stage
FROM node:20 AS build-stage
WORKDIR /usr/src/app
COPY . .
RUN npm ci
RUN npm run build
# This is a new stage, everything before this is gone, except for the files that we want to COPY
FROM nginx:1.25-alpine
# COPY the directory dist from the build-stage to /usr/share/nginx/html
# The target location here was found from the Docker hub page
COPY --from=build-stage /usr/src/app/dist /usr/share/nginx/html
+8
View File
@@ -0,0 +1,8 @@
# React + 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

Some files were not shown because too many files have changed in this diff Show More