├── .editorconfig ├── .gitattributes ├── .gitignore ├── README.md ├── Sample ├── TodoApi │ ├── Program.cs │ ├── TodoApi.csproj │ ├── TodoDbContext.cs │ └── TodoItem.cs └── TodoReact │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── bg.png │ ├── favicon.ico │ ├── index.css │ └── index.html │ └── src │ ├── App.js │ └── index.js ├── Tutorial └── TodoReact │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── bg.png │ ├── favicon.ico │ ├── index.css │ └── index.html │ └── src │ ├── App.js │ └── index.js └── nuget.config /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.props, *.targets, *.csproj] 4 | indent_style = space 5 | indent_size = 2 6 | 7 | [*.cs] 8 | indent_style = space 9 | indent_size = 4 -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Make sh files under the build directory always have LF as line endings 8 | ############################################################################### 9 | *.sh eol=lf 10 | 11 | ############################################################################### 12 | # Set default behavior for command prompt diff. 13 | # 14 | # This is need for earlier builds of msysgit that does not have it on by 15 | # default for csharp files. 16 | # Note: This is only used by command line 17 | ############################################################################### 18 | #*.cs diff=csharp 19 | 20 | ############################################################################### 21 | # Set the merge driver for project and solution files 22 | # 23 | # Merging from the command prompt will add diff markers to the files if there 24 | # are conflicts (Merging from VS is not affected by the settings below, in VS 25 | # the diff markers are never inserted). Diff markers may cause the following 26 | # file extensions to fail to load in VS. An alternative would be to treat 27 | # these files as binary and thus will always conflict and require user 28 | # intervention with every merge. To do so, just uncomment the entries below 29 | ############################################################################### 30 | #*.sln merge=binary 31 | #*.csproj merge=binary 32 | #*.vbproj merge=binary 33 | #*.vcxproj merge=binary 34 | #*.vcproj merge=binary 35 | #*.dbproj merge=binary 36 | #*.fsproj merge=binary 37 | #*.lsproj merge=binary 38 | #*.wixproj merge=binary 39 | #*.modelproj merge=binary 40 | #*.sqlproj merge=binary 41 | #*.wwaproj merge=binary 42 | 43 | ############################################################################### 44 | # behavior for image files 45 | # 46 | # image files are treated as binary by default. 47 | ############################################################################### 48 | #*.jpg binary 49 | #*.png binary 50 | #*.gif binary 51 | 52 | ############################################################################### 53 | # diff behavior for common document formats 54 | # 55 | # Convert binary document formats to text before diffing them. This feature 56 | # is only available from the command line. Turn it on by uncommenting the 57 | # entries below. 58 | ############################################################################### 59 | #*.doc diff=astextplain 60 | #*.DOC diff=astextplain 61 | #*.docx diff=astextplain 62 | #*.DOCX diff=astextplain 63 | #*.dot diff=astextplain 64 | #*.DOT diff=astextplain 65 | #*.pdf diff=astextplain 66 | #*.PDF diff=astextplain 67 | #*.rtf diff=astextplain 68 | #*.RTF diff=astextplain 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | artifacts/ 3 | bin/ 4 | obj/ 5 | .dotnet/ 6 | .nuget/ 7 | .packages/ 8 | .tools/ 9 | .vs/ 10 | .vscode/ 11 | node_modules/ 12 | BenchmarkDotNet.Artifacts/ 13 | .gradle/ 14 | src/SignalR/clients/**/dist/ 15 | modules/ 16 | dist/ 17 | .nuxt/ 18 | 19 | # File extensions 20 | *.aps 21 | *.binlog 22 | *.dll 23 | *.DS_Store 24 | *.exe 25 | *.idb 26 | *.lib 27 | *.log 28 | *.pch 29 | *.pdb 30 | *.pidb 31 | *.psess 32 | *.res 33 | *.snk 34 | *.so 35 | *.suo 36 | *.tlog 37 | *.user 38 | *.userprefs 39 | *.vspx 40 | 41 | # Specific files, typically generated by tools 42 | launchSettings.json 43 | msbuild.ProjectImports.zip 44 | StyleCop.Cache 45 | UpgradeLog.htm 46 | .idea -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutorial 2 | 3 | **Goal**: In this exercise, the participants will be asked to build the backend of a TodoReact App. The user will be exploring the functionality of FeatherHttp, a server-side framework. 4 | 5 | **What is FeatherHttp**: FeatherHttp makes it **easy** to write web applications. 6 | 7 | **Why FeatherHttp**: FeatherHttp is lightweight server-side framework designed to scale-up as your application grows in complexity. 8 | 9 | # Prerequisites 10 | 11 | 1. Install [.NET Core 5.0](https://dotnet.microsoft.com/download) 12 | 1. Install [Node.js](https://nodejs.org/en/) 13 | 14 | # Setup 15 | 16 | 1. Install the FeatherHttp template using the `dotnet CLI`. Copy the command below into a terminal or command prompt to install the template. 17 | ``` 18 | dotnet new -i FeatherHttp.Templates::0.1.67-alpha.g69b43bed72 --nuget-source https://f.feedz.io/featherhttp/framework/nuget/index.json 19 | ``` 20 | This will make the `FeatherHttp` templates available in the `dotnet new` command (more below). 21 | 22 | 1. Download this [repository](https://github.com/featherhttp/tutorial/archive/master.zip). Unzip it, and navigate to the Tutorial folder, this consists of the frontend application `TodoReact` app. 23 | > If using [Visual Studio Code](https://code.visualstudio.com/), install the [C# extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp) for C# support. 24 | 25 | **Task**: Build the backend portion using FeatherHttp 26 | ------------------------------------------------------- 27 | ## Tasks 28 | **Please Note: The completed exercise is available in the [samples folder](https://github.com/featherhttp/tutorial/tree/master/Sample). Feel free to reference it at any point during the tutorial.** 29 | ### Run the frontend application 30 | 31 | 1. Once you clone the Todo repo, navigate to the `TodoReact` folder inside of the `Tutorial` folder and run the following commands 32 | ``` 33 | TodoReact> npm i 34 | TodoReact> npm start 35 | ``` 36 | - The commands above 37 | - Restores packages `npm i ` 38 | - Starts the react app `npm start` 39 | 1. The app will load but have no functionality 40 | ![image](https://user-images.githubusercontent.com/2546640/75070087-86307c80-54c0-11ea-8012-c78813f1dfd6.png) 41 | 42 | > Keep this React app running as we'll need it once we build the back-end in the upcoming steps 43 | 44 | ### Build backend - FeatherHttp 45 | **Create a new project** 46 | 47 | 1. Create a new FeatherHttp application and add the necessary packages in the `TodoApi` folder 48 | 49 | ``` 50 | Tutorial>dotnet new feather -n TodoApi 51 | Tutorial> cd TodoApi 52 | TodoApi> dotnet add package Microsoft.EntityFrameworkCore.InMemory 53 | ``` 54 | - The commands above 55 | - create a new FeatherHttp application `dotnet new feather -n TodoApi` 56 | - Adds the NuGet packages required in the next section `dotnet add package Microsoft.EntityFrameworkCore.InMemory --version 5.0.0-preview.7.20365.15` 57 | 58 | 2. Open the `TodoApi` Folder in editor of your choice. 59 | 60 | ## Create the database model 61 | 62 | 1. Create a file called `TodoItem.cs` in the TodoApi folder. Add the content below: 63 | ```C# 64 | using System.Text.Json.Serialization; 65 | 66 | public class TodoItem 67 | { 68 | [JsonPropertyName("id")] 69 | public int Id { get; set; } 70 | 71 | [JsonPropertyName("name")] 72 | public string Name { get; set; } 73 | 74 | [JsonPropertyName("isComplete")] 75 | public bool IsComplete { get; set; } 76 | } 77 | ``` 78 | The above model will be used for reading in JSON and storing todo items into the database. 79 | 1. Create a file called `TodoDbContext.cs` with the following contents: 80 | ```C# 81 | using Microsoft.EntityFrameworkCore; 82 | 83 | public class TodoDbContext : DbContext 84 | { 85 | public DbSet Todos { get; set; } 86 | 87 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 88 | { 89 | optionsBuilder.UseInMemoryDatabase("Todos"); 90 | } 91 | } 92 | ``` 93 | This code does 2 things: 94 | - It exposes a `Todos` property which represents the list of todo items in the database. 95 | - The call to `UseInMemoryDatabase` wires up the in memory database storage. Data will only be persisted as long as the application is running. 96 | 1. Now we're going to use `dotnet watch` to run the server side application: 97 | ``` 98 | dotnet watch run 99 | ``` 100 | 101 | This will watch our application for source code changes and will restart the process as a result. 102 | 103 | ## Expose the list of todo items 104 | 105 | 1. Add the appropriate `usings` to the top of the `Program.cs` file. 106 | ``` 107 | using System.Threading.Tasks; 108 | using Microsoft.AspNetCore.Builder; 109 | using Microsoft.AspNetCore.Http; 110 | using Microsoft.EntityFrameworkCore; 111 | ``` 112 | This will import the required namespaces so that the application compiles successfully. 113 | 114 | 1. In `Program.cs`, create a method called `GetTodos` inside of the `Program` class: 115 | 116 | ```C# 117 | static async Task GetTodos(HttpContext http) 118 | { 119 | using var db = new TodoDbContext(); 120 | var todos = await db.Todos.ToListAsync(); 121 | 122 | await http.Response.WriteJsonAsync(todos); 123 | } 124 | ``` 125 | 126 | This method gets the list of todo items from the database and writes a JSON representation to the HTTP response. 127 | 128 | 1. Wire up `GetTodos` to the `api/todos` route by modifying the code in `Main` to the following: 129 | ```C# 130 | static async Task Main(string[] args) 131 | { 132 | var app = WebApplication.Create(args); 133 | 134 | app.MapGet("/api/todos", GetTodos); 135 | 136 | await app.RunAsync(); 137 | } 138 | ``` 139 | 1. Navigate to the URL http://localhost:5000/api/todos in the browser. It should return an empty JSON array. 140 | 141 | empty json array 142 | 143 | ## Adding a new todo item 144 | 145 | 1. In `Program.cs`, create another method called `CreateTodo` inside of the `Program` class: 146 | ```C# 147 | static async Task CreateTodo(HttpContext http) 148 | { 149 | var todo = await http.Request.ReadJsonAsync(); 150 | 151 | using var db = new TodoDbContext(); 152 | await db.Todos.AddAsync(todo); 153 | await db.SaveChangesAsync(); 154 | 155 | http.Response.StatusCode = 204; 156 | } 157 | ``` 158 | 159 | The above method reads the `TodoItem` from the incoming HTTP request and as a JSON payload and adds 160 | it to the database. 161 | 162 | 1. Wire up `CreateTodo` to the `api/todos` route by modifying the code in `Main` to the following: 163 | ```C# 164 | static async Task Main(string[] args) 165 | { 166 | var app = WebApplication.Create(args); 167 | 168 | app.MapGet("/api/todos", GetTodos); 169 | app.MapPost("/api/todos", CreateTodo); 170 | 171 | await app.RunAsync(); 172 | } 173 | ``` 174 | 1. Navigate to the `TodoReact` application which should be running on http://localhost:3000. The application should be able to add new todo items. Also, refreshing the page should show the stored todo items. 175 | ![image](https://user-images.githubusercontent.com/2546640/75119637-bc056a80-5652-11ea-81c8-71ea13d97a3c.png) 176 | 177 | ## Changing the state of todo items 178 | 1. In `Program.cs`, create another method called `UpdateCompleted` inside of the `Program` class: 179 | ```C# 180 | static async Task UpdateCompleted(HttpContext http) 181 | { 182 | if (!http.Request.RouteValues.TryGet("id", out int id)) 183 | { 184 | http.Response.StatusCode = 400; 185 | return; 186 | } 187 | 188 | using var db = new TodoDbContext(); 189 | var todo = await db.Todos.FindAsync(id); 190 | 191 | if (todo == null) 192 | { 193 | http.Response.StatusCode = 404; 194 | return; 195 | } 196 | 197 | var inputTodo = await http.Request.ReadJsonAsync(); 198 | todo.IsComplete = inputTodo.IsComplete; 199 | 200 | await db.SaveChangesAsync(); 201 | 202 | http.Response.StatusCode = 204; 203 | } 204 | ``` 205 | 206 | The above logic retrieves the id from the route parameter "id" and uses it to find the todo item in the database. It then reads the JSON payload from the incoming request, sets the `IsComplete` property and updates the todo item in the database. 207 | 1. Wire up `UpdateCompleted` to the `api/todos/{id}` route by modifying the code in `Main` to the following: 208 | ```C# 209 | static async Task Main(string[] args) 210 | { 211 | var app = WebApplication.Create(args); 212 | 213 | app.MapGet("/api/todos", GetTodos); 214 | app.MapPost("/api/todos", CreateTodo); 215 | app.MapPost("/api/todos/{id}", UpdateCompleted); 216 | 217 | await app.RunAsync(); 218 | } 219 | ``` 220 | 221 | ## Deleting a todo item 222 | 223 | 1. In `Program.cs` create another method called `DeleteTodo` inside of the `Program` class: 224 | ```C# 225 | static async Task DeleteTodo(HttpContext http) 226 | { 227 | if (!http.Request.RouteValues.TryGet("id", out int id)) 228 | { 229 | http.Response.StatusCode = 400; 230 | return; 231 | } 232 | 233 | using var db = new TodoDbContext(); 234 | var todo = await db.Todos.FindAsync(id); 235 | if (todo == null) 236 | { 237 | http.Response.StatusCode = 404; 238 | return; 239 | } 240 | 241 | db.Todos.Remove(todo); 242 | await db.SaveChangesAsync(); 243 | 244 | http.Response.StatusCode = 204; 245 | } 246 | ``` 247 | 248 | The above logic is very similar to `UpdateCompleted` but instead. it removes the todo item from the database after finding it. 249 | 250 | 1. Wire up `DeleteTodo` to the `api/todos/{id}` route by modifying the code in `Main` to the following: 251 | ```C# 252 | static async Task Main(string[] args) 253 | { 254 | var app = WebApplication.Create(args); 255 | 256 | app.MapGet("/api/todos", GetTodos); 257 | app.MapPost("/api/todos", CreateTodo); 258 | app.MapPost("/api/todos/{id}", UpdateCompleted); 259 | app.MapDelete("/api/todos/{id}", DeleteTodo); 260 | 261 | await app.RunAsync(); 262 | } 263 | ``` 264 | 265 | ## Test the application 266 | 267 | The application should now be fully functional. 268 | ![image](https://user-images.githubusercontent.com/2546640/75119891-08ea4080-5655-11ea-96be-adab4990ad65.png) 269 | 270 | -------------------------------------------------------------------------------- /Sample/TodoApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | class Program 7 | { 8 | static async Task Main(string[] args) 9 | { 10 | var app = WebApplication.Create(args); 11 | 12 | app.MapGet("/api/todos", GetTodos); 13 | app.MapGet("/api/todos/{id}", GetTodo); 14 | app.MapPost("/api/todos", CreateTodo); 15 | app.MapPost("/api/todos/{id}", UpdateCompleted); 16 | app.MapDelete("/api/todos/{id}", DeleteTodo); 17 | 18 | await app.RunAsync(); 19 | } 20 | 21 | static async Task GetTodos(HttpContext http) 22 | { 23 | using var db = new TodoDbContext(); 24 | var todos = await db.Todos.ToListAsync(); 25 | 26 | await http.Response.WriteJsonAsync(todos); 27 | } 28 | 29 | static async Task GetTodo(HttpContext http) 30 | { 31 | if (!http.Request.RouteValues.TryGet("id", out int id)) 32 | { 33 | http.Response.StatusCode = 400; 34 | return; 35 | } 36 | 37 | using var db = new TodoDbContext(); 38 | var todo = await db.Todos.FindAsync(id); 39 | if (todo == null) 40 | { 41 | http.Response.StatusCode = 404; 42 | return; 43 | } 44 | 45 | await http.Response.WriteJsonAsync(todo); 46 | } 47 | 48 | static async Task CreateTodo(HttpContext http) 49 | { 50 | var todo = await http.Request.ReadJsonAsync(); 51 | 52 | using var db = new TodoDbContext(); 53 | await db.Todos.AddAsync(todo); 54 | await db.SaveChangesAsync(); 55 | 56 | http.Response.StatusCode = 204; 57 | } 58 | 59 | static async Task UpdateCompleted(HttpContext http) 60 | { 61 | if (!http.Request.RouteValues.TryGet("id", out int id)) 62 | { 63 | http.Response.StatusCode = 400; 64 | return; 65 | } 66 | 67 | using var db = new TodoDbContext(); 68 | var todo = await db.Todos.FindAsync(id); 69 | 70 | if (todo == null) 71 | { 72 | http.Response.StatusCode = 404; 73 | return; 74 | } 75 | 76 | var inputTodo = await http.Request.ReadJsonAsync(); 77 | todo.IsComplete = inputTodo.IsComplete; 78 | 79 | await db.SaveChangesAsync(); 80 | 81 | http.Response.StatusCode = 204; 82 | } 83 | 84 | static async Task DeleteTodo(HttpContext http) 85 | { 86 | if (!http.Request.RouteValues.TryGet("id", out int id)) 87 | { 88 | http.Response.StatusCode = 400; 89 | return; 90 | } 91 | 92 | using var db = new TodoDbContext(); 93 | var todo = await db.Todos.FindAsync(id); 94 | if (todo == null) 95 | { 96 | http.Response.StatusCode = 404; 97 | return; 98 | } 99 | 100 | db.Todos.Remove(todo); 101 | await db.SaveChangesAsync(); 102 | 103 | http.Response.StatusCode = 204; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sample/TodoApi/TodoApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sample/TodoApi/TodoDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | public class TodoDbContext : DbContext 4 | { 5 | public DbSet Todos { get; set; } 6 | 7 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 8 | { 9 | optionsBuilder.UseInMemoryDatabase("Todos"); 10 | } 11 | } -------------------------------------------------------------------------------- /Sample/TodoApi/TodoItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | public class TodoItem 4 | { 5 | [JsonPropertyName("id")] 6 | public int Id { get; set; } 7 | [JsonPropertyName("name")] 8 | public string Name { get; set; } 9 | [JsonPropertyName("isComplete")] 10 | public bool IsComplete { get; set; } 11 | } -------------------------------------------------------------------------------- /Sample/TodoReact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-todos", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.12.0", 7 | "react-dom": "^16.12.0", 8 | "react-scripts": "3.4.0" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "proxy": "http://localhost:5000/" 32 | } 33 | -------------------------------------------------------------------------------- /Sample/TodoReact/public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/featherhttp/tutorial/721e5b8bb3a97c9d148489b036868e5006733d48/Sample/TodoReact/public/bg.png -------------------------------------------------------------------------------- /Sample/TodoReact/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/featherhttp/tutorial/721e5b8bb3a97c9d148489b036868e5006733d48/Sample/TodoReact/public/favicon.ico -------------------------------------------------------------------------------- /Sample/TodoReact/public/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea url('bg.png'); 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | button, 37 | input[type="checkbox"] { 38 | outline: none; 39 | } 40 | 41 | #todoapp { 42 | background: #fff; 43 | background: rgba(255, 255, 255, 0.9); 44 | margin: 130px 0 40px 0; 45 | border: 1px solid #ccc; 46 | position: relative; 47 | border-top-left-radius: 2px; 48 | border-top-right-radius: 2px; 49 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.15); 50 | } 51 | 52 | #todoapp:before { 53 | content: ''; 54 | border-left: 1px solid #f5d6d6; 55 | border-right: 1px solid #f5d6d6; 56 | width: 2px; 57 | position: absolute; 58 | top: 0; 59 | left: 40px; 60 | height: 100%; 61 | } 62 | 63 | #todoapp input::-webkit-input-placeholder { 64 | font-style: italic; 65 | } 66 | 67 | #todoapp input::-moz-placeholder { 68 | font-style: italic; 69 | color: #a9a9a9; 70 | } 71 | 72 | #todoapp h1 { 73 | position: absolute; 74 | top: -120px; 75 | width: 100%; 76 | font-size: 70px; 77 | font-weight: bold; 78 | text-align: center; 79 | color: #b3b3b3; 80 | color: rgba(255, 255, 255, 0.3); 81 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 82 | -webkit-text-rendering: optimizeLegibility; 83 | -moz-text-rendering: optimizeLegibility; 84 | -ms-text-rendering: optimizeLegibility; 85 | -o-text-rendering: optimizeLegibility; 86 | text-rendering: optimizeLegibility; 87 | } 88 | 89 | #header { 90 | padding-top: 15px; 91 | border-radius: inherit; 92 | } 93 | 94 | #header:before { 95 | content: ''; 96 | position: absolute; 97 | top: 0; 98 | right: 0; 99 | left: 0; 100 | height: 15px; 101 | z-index: 2; 102 | border-bottom: 1px solid #6c615c; 103 | background: #8d7d77; 104 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 105 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 106 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 107 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 108 | border-top-left-radius: 1px; 109 | border-top-right-radius: 1px; 110 | } 111 | 112 | #new-todo, 113 | .edit { 114 | position: relative; 115 | margin: 0; 116 | width: 100%; 117 | font-size: 24px; 118 | font-family: inherit; 119 | line-height: 1.4em; 120 | border: 0; 121 | outline: none; 122 | color: inherit; 123 | padding: 6px; 124 | border: 1px solid #999; 125 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 126 | -moz-box-sizing: border-box; 127 | -ms-box-sizing: border-box; 128 | -o-box-sizing: border-box; 129 | box-sizing: border-box; 130 | -webkit-font-smoothing: antialiased; 131 | -moz-font-smoothing: antialiased; 132 | -ms-font-smoothing: antialiased; 133 | -o-font-smoothing: antialiased; 134 | font-smoothing: antialiased; 135 | } 136 | 137 | #new-todo { 138 | padding: 16px 16px 16px 60px; 139 | border: none; 140 | background: rgba(0, 0, 0, 0.02); 141 | z-index: 2; 142 | box-shadow: none; 143 | } 144 | 145 | #main { 146 | position: relative; 147 | z-index: 2; 148 | border-top: 1px dotted #adadad; 149 | } 150 | 151 | label[for='toggle-all'] { 152 | display: none; 153 | } 154 | 155 | #toggle-all { 156 | position: absolute; 157 | top: -42px; 158 | left: -4px; 159 | width: 40px; 160 | text-align: center; 161 | /* Mobile Safari */ 162 | border: none; 163 | } 164 | 165 | #toggle-all:before { 166 | content: '»'; 167 | font-size: 28px; 168 | color: #d9d9d9; 169 | padding: 0 25px 7px; 170 | } 171 | 172 | #toggle-all:checked:before { 173 | color: #737373; 174 | } 175 | 176 | #todo-list { 177 | margin: 0; 178 | padding: 0; 179 | list-style: none; 180 | } 181 | 182 | #todo-list li { 183 | position: relative; 184 | font-size: 24px; 185 | border-bottom: 1px dotted #ccc; 186 | } 187 | 188 | #todo-list li:last-child { 189 | border-bottom: none; 190 | } 191 | 192 | #todo-list li.editing { 193 | border-bottom: none; 194 | padding: 0; 195 | } 196 | 197 | #todo-list li.editing .edit { 198 | display: block; 199 | width: 506px; 200 | padding: 13px 17px 12px 17px; 201 | margin: 0 0 0 43px; 202 | } 203 | 204 | #todo-list li.editing .view { 205 | display: none; 206 | } 207 | 208 | #todo-list li .toggle { 209 | text-align: center; 210 | width: 40px; 211 | /* auto, since non-WebKit browsers doesn't support input styling */ 212 | height: auto; 213 | position: absolute; 214 | top: 0; 215 | bottom: 0; 216 | margin: auto 0; 217 | /* Mobile Safari */ 218 | border: none; 219 | -webkit-appearance: none; 220 | -ms-appearance: none; 221 | -o-appearance: none; 222 | appearance: none; 223 | } 224 | 225 | #todo-list li .toggle:after { 226 | content: '✔'; 227 | /* 40 + a couple of pixels visual adjustment */ 228 | line-height: 43px; 229 | font-size: 20px; 230 | color: #d9d9d9; 231 | text-shadow: 0 -1px 0 #bfbfbf; 232 | } 233 | 234 | #todo-list li .toggle:checked:after { 235 | color: #85ada7; 236 | text-shadow: 0 1px 0 #669991; 237 | bottom: 1px; 238 | position: relative; 239 | } 240 | 241 | #todo-list li label { 242 | white-space: pre; 243 | word-break: break-word; 244 | padding: 15px 60px 15px 15px; 245 | margin-left: 45px; 246 | display: block; 247 | line-height: 1.2; 248 | -webkit-transition: color 0.4s; 249 | transition: color 0.4s; 250 | } 251 | 252 | #todo-list li.completed label { 253 | color: #a9a9a9; 254 | text-decoration: line-through; 255 | } 256 | 257 | #todo-list li .destroy { 258 | display: none; 259 | position: absolute; 260 | top: 0; 261 | right: 10px; 262 | bottom: 0; 263 | width: 40px; 264 | height: 40px; 265 | margin: auto 0; 266 | font-size: 22px; 267 | color: #a88a8a; 268 | -webkit-transition: all 0.2s; 269 | transition: all 0.2s; 270 | } 271 | 272 | #todo-list li .destroy:hover { 273 | text-shadow: 0 0 1px #000, 0 0 10px rgba(199, 107, 107, 0.8); 274 | -webkit-transform: scale(1.3); 275 | -ms-transform: scale(1.3); 276 | transform: scale(1.3); 277 | } 278 | 279 | #todo-list li .destroy:after { 280 | content: '✖'; 281 | } 282 | 283 | #todo-list li:hover .destroy { 284 | display: block; 285 | } 286 | 287 | #todo-list li .edit { 288 | display: none; 289 | } 290 | 291 | #todo-list li.editing:last-child { 292 | margin-bottom: -1px; 293 | } 294 | 295 | #footer { 296 | color: #777; 297 | padding: 0 15px; 298 | position: absolute; 299 | right: 0; 300 | bottom: -31px; 301 | left: 0; 302 | height: 20px; 303 | z-index: 1; 304 | text-align: center; 305 | } 306 | 307 | #footer:before { 308 | content: ''; 309 | position: absolute; 310 | right: 0; 311 | bottom: 31px; 312 | left: 0; 313 | height: 50px; 314 | z-index: -1; 315 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 6px 0 -3px rgba(255, 255, 255, 0.8), 0 7px 1px -3px rgba(0, 0, 0, 0.3), 0 43px 0 -6px rgba(255, 255, 255, 0.8), 0 44px 2px -6px rgba(0, 0, 0, 0.2); 316 | } 317 | 318 | #todo-count { 319 | float: left; 320 | text-align: left; 321 | } 322 | 323 | #filters { 324 | margin: 0; 325 | padding: 0; 326 | list-style: none; 327 | position: absolute; 328 | right: 0; 329 | left: 0; 330 | } 331 | 332 | #filters li { 333 | display: inline; 334 | } 335 | 336 | #filters li a { 337 | color: #83756f; 338 | margin: 2px; 339 | text-decoration: none; 340 | } 341 | 342 | #filters li a.selected { 343 | font-weight: bold; 344 | } 345 | 346 | #clear-completed { 347 | float: right; 348 | position: relative; 349 | line-height: 20px; 350 | text-decoration: none; 351 | background: rgba(0, 0, 0, 0.1); 352 | font-size: 11px; 353 | padding: 0 10px; 354 | border-radius: 3px; 355 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 356 | } 357 | 358 | #clear-completed:hover { 359 | background: rgba(0, 0, 0, 0.15); 360 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 361 | } 362 | 363 | #info { 364 | margin: 65px auto 0; 365 | color: #a6a6a6; 366 | font-size: 12px; 367 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 368 | text-align: center; 369 | } 370 | 371 | #info a { 372 | color: inherit; 373 | } 374 | 375 | /* 376 | Hack to remove background from Mobile Safari. 377 | Can't use it globally since it destroys checkboxes in Firefox and Opera 378 | */ 379 | 380 | @media screen and (-webkit-min-device-pixel-ratio:0) { 381 | #toggle-all, 382 | #todo-list li .toggle { 383 | background: none; 384 | } 385 | 386 | #todo-list li .toggle { 387 | height: 40px; 388 | } 389 | 390 | #toggle-all { 391 | top: -56px; 392 | left: -15px; 393 | width: 65px; 394 | height: 41px; 395 | -webkit-transform: rotate(90deg); 396 | -ms-transform: rotate(90deg); 397 | transform: rotate(90deg); 398 | -webkit-appearance: none; 399 | appearance: none; 400 | } 401 | } 402 | 403 | .hidden { 404 | display: none; 405 | } 406 | 407 | hr { 408 | margin: 20px 0; 409 | border: 0; 410 | border-top: 1px dashed #C5C5C5; 411 | border-bottom: 1px dashed #F7F7F7; 412 | } 413 | 414 | .learn a { 415 | font-weight: normal; 416 | text-decoration: none; 417 | color: #b83f45; 418 | } 419 | 420 | .learn a:hover { 421 | text-decoration: underline; 422 | color: #787e7e; 423 | } 424 | 425 | .learn h3, 426 | .learn h4, 427 | .learn h5 { 428 | margin: 10px 0; 429 | font-weight: 500; 430 | line-height: 1.2; 431 | color: #000; 432 | } 433 | 434 | .learn h3 { 435 | font-size: 24px; 436 | } 437 | 438 | .learn h4 { 439 | font-size: 18px; 440 | } 441 | 442 | .learn h5 { 443 | margin-bottom: 0; 444 | font-size: 14px; 445 | } 446 | 447 | .learn ul { 448 | padding: 0; 449 | margin: 0 0 30px 25px; 450 | } 451 | 452 | .learn li { 453 | line-height: 20px; 454 | } 455 | 456 | .learn p { 457 | font-size: 15px; 458 | font-weight: 300; 459 | line-height: 1.3; 460 | margin-top: 0; 461 | margin-bottom: 0; 462 | } 463 | 464 | .quote { 465 | border: none; 466 | margin: 20px 0 60px 0; 467 | } 468 | 469 | .quote p { 470 | font-style: italic; 471 | } 472 | 473 | .quote p:before { 474 | content: '“'; 475 | font-size: 50px; 476 | opacity: .15; 477 | position: absolute; 478 | top: -20px; 479 | left: 3px; 480 | } 481 | 482 | .quote p:after { 483 | content: '”'; 484 | font-size: 50px; 485 | opacity: .15; 486 | position: absolute; 487 | bottom: -42px; 488 | right: 3px; 489 | } 490 | 491 | .quote footer { 492 | position: absolute; 493 | bottom: -40px; 494 | right: 0; 495 | } 496 | 497 | .quote footer img { 498 | border-radius: 3px; 499 | } 500 | 501 | .quote footer a { 502 | margin-left: 5px; 503 | vertical-align: middle; 504 | } 505 | 506 | .speech-bubble { 507 | position: relative; 508 | padding: 10px; 509 | background: rgba(0, 0, 0, .04); 510 | border-radius: 5px; 511 | } 512 | 513 | .speech-bubble:after { 514 | content: ''; 515 | position: absolute; 516 | top: 100%; 517 | right: 30px; 518 | border: 13px solid transparent; 519 | border-top-color: rgba(0, 0, 0, .04); 520 | } 521 | 522 | .learn-bar > .learn { 523 | position: absolute; 524 | width: 272px; 525 | top: 8px; 526 | left: -300px; 527 | padding: 10px; 528 | border-radius: 5px; 529 | background-color: rgba(255, 255, 255, .6); 530 | -webkit-transition-property: left; 531 | transition-property: left; 532 | -webkit-transition-duration: 500ms; 533 | transition-duration: 500ms; 534 | } 535 | 536 | @media (min-width: 899px) { 537 | .learn-bar { 538 | width: auto; 539 | margin: 0 0 0 300px; 540 | } 541 | 542 | .learn-bar > .learn { 543 | left: 8px; 544 | } 545 | 546 | .learn-bar #todoapp { 547 | width: 550px; 548 | margin: 130px auto 40px auto; 549 | } 550 | } 551 | 552 | 553 | html, 554 | body { 555 | margin: 0; 556 | padding: 0; 557 | } 558 | 559 | button { 560 | margin: 0; 561 | padding: 0; 562 | border: 0; 563 | background: none; 564 | font-size: 100%; 565 | vertical-align: baseline; 566 | font-family: inherit; 567 | font-weight: inherit; 568 | color: inherit; 569 | -webkit-appearance: none; 570 | appearance: none; 571 | -webkit-font-smoothing: antialiased; 572 | -moz-osx-font-smoothing: grayscale; 573 | } 574 | 575 | body { 576 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 577 | line-height: 1.4em; 578 | background: #f5f5f5; 579 | color: #4d4d4d; 580 | min-width: 230px; 581 | max-width: 550px; 582 | margin: 0 auto; 583 | -webkit-font-smoothing: antialiased; 584 | -moz-osx-font-smoothing: grayscale; 585 | font-weight: 300; 586 | } 587 | 588 | :focus { 589 | outline: 0; 590 | } 591 | 592 | .hidden { 593 | display: none; 594 | } 595 | 596 | .todoapp { 597 | background: #fff; 598 | margin: 130px 0 40px 0; 599 | position: relative; 600 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); 601 | } 602 | 603 | .todoapp input::-webkit-input-placeholder { 604 | font-style: italic; 605 | font-weight: 300; 606 | color: #e6e6e6; 607 | } 608 | 609 | .todoapp input::-moz-placeholder { 610 | font-style: italic; 611 | font-weight: 300; 612 | color: #e6e6e6; 613 | } 614 | 615 | .todoapp input::input-placeholder { 616 | font-style: italic; 617 | font-weight: 300; 618 | color: #e6e6e6; 619 | } 620 | 621 | .todoapp h1 { 622 | position: absolute; 623 | top: -155px; 624 | width: 100%; 625 | font-size: 100px; 626 | font-weight: 100; 627 | text-align: center; 628 | color: rgba(175, 47, 47, 0.15); 629 | -webkit-text-rendering: optimizeLegibility; 630 | -moz-text-rendering: optimizeLegibility; 631 | text-rendering: optimizeLegibility; 632 | } 633 | 634 | .new-todo, 635 | .edit { 636 | position: relative; 637 | margin: 0; 638 | width: 100%; 639 | font-size: 24px; 640 | font-family: inherit; 641 | font-weight: inherit; 642 | line-height: 1.4em; 643 | border: 0; 644 | color: inherit; 645 | padding: 6px; 646 | border: 1px solid #999; 647 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 648 | box-sizing: border-box; 649 | -webkit-font-smoothing: antialiased; 650 | -moz-osx-font-smoothing: grayscale; 651 | } 652 | 653 | .new-todo { 654 | padding: 16px 16px 16px 60px; 655 | border: none; 656 | background: rgba(0, 0, 0, 0.003); 657 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 658 | } 659 | 660 | .main { 661 | position: relative; 662 | z-index: 2; 663 | border-top: 1px solid #e6e6e6; 664 | } 665 | 666 | .toggle-all { 667 | text-align: center; 668 | border: none; /* Mobile Safari */ 669 | opacity: 0; 670 | position: absolute; 671 | } 672 | 673 | .toggle-all + label { 674 | width: 60px; 675 | height: 34px; 676 | font-size: 0; 677 | position: absolute; 678 | top: -52px; 679 | left: -13px; 680 | -webkit-transform: rotate(90deg); 681 | transform: rotate(90deg); 682 | } 683 | 684 | .toggle-all + label:before { 685 | content: '❯'; 686 | font-size: 22px; 687 | color: #e6e6e6; 688 | padding: 10px 27px 10px 27px; 689 | } 690 | 691 | .toggle-all:checked + label:before { 692 | color: #737373; 693 | } 694 | 695 | .todo-list { 696 | margin: 0; 697 | padding: 0; 698 | list-style: none; 699 | } 700 | 701 | .todo-list li { 702 | position: relative; 703 | font-size: 24px; 704 | border-bottom: 1px solid #ededed; 705 | } 706 | 707 | .todo-list li:last-child { 708 | border-bottom: none; 709 | } 710 | 711 | .todo-list li.editing { 712 | border-bottom: none; 713 | padding: 0; 714 | } 715 | 716 | .todo-list li.editing .edit { 717 | display: block; 718 | width: 506px; 719 | padding: 12px 16px; 720 | margin: 0 0 0 43px; 721 | } 722 | 723 | .todo-list li.editing .view { 724 | display: none; 725 | } 726 | 727 | .todo-list li .toggle { 728 | text-align: center; 729 | width: 40px; 730 | /* auto, since non-WebKit browsers doesn't support input styling */ 731 | height: auto; 732 | position: absolute; 733 | top: 0; 734 | bottom: 0; 735 | margin: auto 0; 736 | border: none; /* Mobile Safari */ 737 | -webkit-appearance: none; 738 | appearance: none; 739 | } 740 | 741 | .todo-list li .toggle { 742 | opacity: 0; 743 | } 744 | 745 | .todo-list li .toggle + label { 746 | /* 747 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 748 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 749 | */ 750 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 751 | background-repeat: no-repeat; 752 | background-position: center left; 753 | } 754 | 755 | .todo-list li .toggle:checked + label { 756 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 757 | } 758 | 759 | .todo-list li label { 760 | word-break: break-all; 761 | padding: 15px 15px 15px 60px; 762 | display: block; 763 | line-height: 1.2; 764 | transition: color 0.4s; 765 | } 766 | 767 | .todo-list li.completed label { 768 | color: #d9d9d9; 769 | text-decoration: line-through; 770 | } 771 | 772 | .todo-list li .destroy { 773 | display: none; 774 | position: absolute; 775 | top: 0; 776 | right: 10px; 777 | bottom: 0; 778 | width: 40px; 779 | height: 40px; 780 | margin: auto 0; 781 | font-size: 30px; 782 | color: #cc9a9a; 783 | margin-bottom: 11px; 784 | transition: color 0.2s ease-out; 785 | } 786 | 787 | .todo-list li .destroy:hover { 788 | color: #af5b5e; 789 | } 790 | 791 | .todo-list li .destroy:after { 792 | content: '×'; 793 | } 794 | 795 | .todo-list li:hover .destroy { 796 | display: block; 797 | } 798 | 799 | .todo-list li .edit { 800 | display: none; 801 | } 802 | 803 | .todo-list li.editing:last-child { 804 | margin-bottom: -1px; 805 | } 806 | 807 | .footer { 808 | color: #777; 809 | padding: 10px 15px; 810 | height: 20px; 811 | text-align: center; 812 | border-top: 1px solid #e6e6e6; 813 | } 814 | 815 | .footer:before { 816 | content: ''; 817 | position: absolute; 818 | right: 0; 819 | bottom: 0; 820 | left: 0; 821 | height: 50px; 822 | overflow: hidden; 823 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); 824 | } 825 | 826 | .todo-count { 827 | float: left; 828 | text-align: left; 829 | } 830 | 831 | .todo-count strong { 832 | font-weight: 300; 833 | } 834 | 835 | .filters { 836 | margin: 0; 837 | padding: 0; 838 | list-style: none; 839 | position: absolute; 840 | right: 0; 841 | left: 0; 842 | } 843 | 844 | .filters li { 845 | display: inline; 846 | } 847 | 848 | .filters li a { 849 | color: inherit; 850 | margin: 3px; 851 | padding: 3px 7px; 852 | text-decoration: none; 853 | border: 1px solid transparent; 854 | border-radius: 3px; 855 | } 856 | 857 | .filters li a:hover { 858 | border-color: rgba(175, 47, 47, 0.1); 859 | } 860 | 861 | .filters li a.selected { 862 | border-color: rgba(175, 47, 47, 0.2); 863 | } 864 | 865 | .clear-completed, 866 | html .clear-completed:active { 867 | float: right; 868 | position: relative; 869 | line-height: 20px; 870 | text-decoration: none; 871 | cursor: pointer; 872 | } 873 | 874 | .clear-completed:hover { 875 | text-decoration: underline; 876 | } 877 | 878 | .info { 879 | margin: 65px auto 0; 880 | color: #bfbfbf; 881 | font-size: 10px; 882 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 883 | text-align: center; 884 | } 885 | 886 | .info p { 887 | line-height: 1; 888 | } 889 | 890 | .info a { 891 | color: inherit; 892 | text-decoration: none; 893 | font-weight: 400; 894 | } 895 | 896 | .info a:hover { 897 | text-decoration: underline; 898 | } 899 | 900 | /* 901 | Hack to remove background from Mobile Safari. 902 | Can't use it globally since it destroys checkboxes in Firefox 903 | */ 904 | @media screen and (-webkit-min-device-pixel-ratio:0) { 905 | .toggle-all, 906 | .todo-list li .toggle { 907 | background: none; 908 | } 909 | 910 | .todo-list li .toggle { 911 | height: 40px; 912 | } 913 | } 914 | 915 | @media (max-width: 430px) { 916 | .footer { 917 | height: 50px; 918 | } 919 | 920 | .filters { 921 | bottom: 10px; 922 | } 923 | } 924 | -------------------------------------------------------------------------------- /Sample/TodoReact/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Todos 9 | 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /Sample/TodoReact/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | function App() { 4 | const [newTodo, setNewTodo] = useState(""); 5 | const [todos, setTodos] = useState([]); 6 | 7 | async function getTodos() { 8 | const result = await fetch("/api/todos"); 9 | const todos = await result.json(); 10 | setTodos(todos); 11 | } 12 | 13 | async function createTodo(e) { 14 | e.preventDefault(); 15 | await fetch('/api/todos', { 16 | method: "POST", 17 | body: JSON.stringify({ name: newTodo }) 18 | }); 19 | setNewTodo(""); 20 | await getTodos(); 21 | } 22 | 23 | async function updateCompleted(todo, isComplete) { 24 | await fetch(`/api/todos/${todo.id}`, { 25 | method: "POST", 26 | body: JSON.stringify({ ...todo, isComplete: isComplete }) 27 | }); 28 | await getTodos(); 29 | } 30 | 31 | async function deleteTodo(id) { 32 | await fetch(`/api/todos/${id}`, { 33 | method: "DELETE" 34 | }); 35 | await getTodos(); 36 | } 37 | 38 | useEffect(() => { 39 | getTodos(); 40 | }, []); 41 | 42 | return ( 43 |
44 |
45 |

todos

46 |
47 | setNewTodo(e.target.value)} /> 48 |
49 |
50 |
51 |
    52 | {todos.map(todo => { 53 | return ( 54 |
  • 55 |
    56 | updateCompleted(todo, e.target.checked)} /> 57 | 58 | 59 |
    60 |
  • 61 | ); 62 | })} 63 |
64 |
65 |
66 | ); 67 | } 68 | 69 | export default App; -------------------------------------------------------------------------------- /Sample/TodoReact/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /Tutorial/TodoReact/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-todos", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.12.0", 7 | "react-dom": "^16.12.0", 8 | "react-scripts": "3.4.0" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "proxy": "http://localhost:5000/" 32 | } 33 | -------------------------------------------------------------------------------- /Tutorial/TodoReact/public/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/featherhttp/tutorial/721e5b8bb3a97c9d148489b036868e5006733d48/Tutorial/TodoReact/public/bg.png -------------------------------------------------------------------------------- /Tutorial/TodoReact/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/featherhttp/tutorial/721e5b8bb3a97c9d148489b036868e5006733d48/Tutorial/TodoReact/public/favicon.ico -------------------------------------------------------------------------------- /Tutorial/TodoReact/public/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | color: inherit; 16 | -webkit-appearance: none; 17 | -ms-appearance: none; 18 | -o-appearance: none; 19 | appearance: none; 20 | } 21 | 22 | body { 23 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #eaeaea url('bg.png'); 26 | color: #4d4d4d; 27 | width: 550px; 28 | margin: 0 auto; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-font-smoothing: antialiased; 31 | -ms-font-smoothing: antialiased; 32 | -o-font-smoothing: antialiased; 33 | font-smoothing: antialiased; 34 | } 35 | 36 | button, 37 | input[type="checkbox"] { 38 | outline: none; 39 | } 40 | 41 | #todoapp { 42 | background: #fff; 43 | background: rgba(255, 255, 255, 0.9); 44 | margin: 130px 0 40px 0; 45 | border: 1px solid #ccc; 46 | position: relative; 47 | border-top-left-radius: 2px; 48 | border-top-right-radius: 2px; 49 | box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.15); 50 | } 51 | 52 | #todoapp:before { 53 | content: ''; 54 | border-left: 1px solid #f5d6d6; 55 | border-right: 1px solid #f5d6d6; 56 | width: 2px; 57 | position: absolute; 58 | top: 0; 59 | left: 40px; 60 | height: 100%; 61 | } 62 | 63 | #todoapp input::-webkit-input-placeholder { 64 | font-style: italic; 65 | } 66 | 67 | #todoapp input::-moz-placeholder { 68 | font-style: italic; 69 | color: #a9a9a9; 70 | } 71 | 72 | #todoapp h1 { 73 | position: absolute; 74 | top: -120px; 75 | width: 100%; 76 | font-size: 70px; 77 | font-weight: bold; 78 | text-align: center; 79 | color: #b3b3b3; 80 | color: rgba(255, 255, 255, 0.3); 81 | text-shadow: -1px -1px rgba(0, 0, 0, 0.2); 82 | -webkit-text-rendering: optimizeLegibility; 83 | -moz-text-rendering: optimizeLegibility; 84 | -ms-text-rendering: optimizeLegibility; 85 | -o-text-rendering: optimizeLegibility; 86 | text-rendering: optimizeLegibility; 87 | } 88 | 89 | #header { 90 | padding-top: 15px; 91 | border-radius: inherit; 92 | } 93 | 94 | #header:before { 95 | content: ''; 96 | position: absolute; 97 | top: 0; 98 | right: 0; 99 | left: 0; 100 | height: 15px; 101 | z-index: 2; 102 | border-bottom: 1px solid #6c615c; 103 | background: #8d7d77; 104 | background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); 105 | background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 106 | background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); 107 | filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); 108 | border-top-left-radius: 1px; 109 | border-top-right-radius: 1px; 110 | } 111 | 112 | #new-todo, 113 | .edit { 114 | position: relative; 115 | margin: 0; 116 | width: 100%; 117 | font-size: 24px; 118 | font-family: inherit; 119 | line-height: 1.4em; 120 | border: 0; 121 | outline: none; 122 | color: inherit; 123 | padding: 6px; 124 | border: 1px solid #999; 125 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 126 | -moz-box-sizing: border-box; 127 | -ms-box-sizing: border-box; 128 | -o-box-sizing: border-box; 129 | box-sizing: border-box; 130 | -webkit-font-smoothing: antialiased; 131 | -moz-font-smoothing: antialiased; 132 | -ms-font-smoothing: antialiased; 133 | -o-font-smoothing: antialiased; 134 | font-smoothing: antialiased; 135 | } 136 | 137 | #new-todo { 138 | padding: 16px 16px 16px 60px; 139 | border: none; 140 | background: rgba(0, 0, 0, 0.02); 141 | z-index: 2; 142 | box-shadow: none; 143 | } 144 | 145 | #main { 146 | position: relative; 147 | z-index: 2; 148 | border-top: 1px dotted #adadad; 149 | } 150 | 151 | label[for='toggle-all'] { 152 | display: none; 153 | } 154 | 155 | #toggle-all { 156 | position: absolute; 157 | top: -42px; 158 | left: -4px; 159 | width: 40px; 160 | text-align: center; 161 | /* Mobile Safari */ 162 | border: none; 163 | } 164 | 165 | #toggle-all:before { 166 | content: '»'; 167 | font-size: 28px; 168 | color: #d9d9d9; 169 | padding: 0 25px 7px; 170 | } 171 | 172 | #toggle-all:checked:before { 173 | color: #737373; 174 | } 175 | 176 | #todo-list { 177 | margin: 0; 178 | padding: 0; 179 | list-style: none; 180 | } 181 | 182 | #todo-list li { 183 | position: relative; 184 | font-size: 24px; 185 | border-bottom: 1px dotted #ccc; 186 | } 187 | 188 | #todo-list li:last-child { 189 | border-bottom: none; 190 | } 191 | 192 | #todo-list li.editing { 193 | border-bottom: none; 194 | padding: 0; 195 | } 196 | 197 | #todo-list li.editing .edit { 198 | display: block; 199 | width: 506px; 200 | padding: 13px 17px 12px 17px; 201 | margin: 0 0 0 43px; 202 | } 203 | 204 | #todo-list li.editing .view { 205 | display: none; 206 | } 207 | 208 | #todo-list li .toggle { 209 | text-align: center; 210 | width: 40px; 211 | /* auto, since non-WebKit browsers doesn't support input styling */ 212 | height: auto; 213 | position: absolute; 214 | top: 0; 215 | bottom: 0; 216 | margin: auto 0; 217 | /* Mobile Safari */ 218 | border: none; 219 | -webkit-appearance: none; 220 | -ms-appearance: none; 221 | -o-appearance: none; 222 | appearance: none; 223 | } 224 | 225 | #todo-list li .toggle:after { 226 | content: '✔'; 227 | /* 40 + a couple of pixels visual adjustment */ 228 | line-height: 43px; 229 | font-size: 20px; 230 | color: #d9d9d9; 231 | text-shadow: 0 -1px 0 #bfbfbf; 232 | } 233 | 234 | #todo-list li .toggle:checked:after { 235 | color: #85ada7; 236 | text-shadow: 0 1px 0 #669991; 237 | bottom: 1px; 238 | position: relative; 239 | } 240 | 241 | #todo-list li label { 242 | white-space: pre; 243 | word-break: break-word; 244 | padding: 15px 60px 15px 15px; 245 | margin-left: 45px; 246 | display: block; 247 | line-height: 1.2; 248 | -webkit-transition: color 0.4s; 249 | transition: color 0.4s; 250 | } 251 | 252 | #todo-list li.completed label { 253 | color: #a9a9a9; 254 | text-decoration: line-through; 255 | } 256 | 257 | #todo-list li .destroy { 258 | display: none; 259 | position: absolute; 260 | top: 0; 261 | right: 10px; 262 | bottom: 0; 263 | width: 40px; 264 | height: 40px; 265 | margin: auto 0; 266 | font-size: 22px; 267 | color: #a88a8a; 268 | -webkit-transition: all 0.2s; 269 | transition: all 0.2s; 270 | } 271 | 272 | #todo-list li .destroy:hover { 273 | text-shadow: 0 0 1px #000, 0 0 10px rgba(199, 107, 107, 0.8); 274 | -webkit-transform: scale(1.3); 275 | -ms-transform: scale(1.3); 276 | transform: scale(1.3); 277 | } 278 | 279 | #todo-list li .destroy:after { 280 | content: '✖'; 281 | } 282 | 283 | #todo-list li:hover .destroy { 284 | display: block; 285 | } 286 | 287 | #todo-list li .edit { 288 | display: none; 289 | } 290 | 291 | #todo-list li.editing:last-child { 292 | margin-bottom: -1px; 293 | } 294 | 295 | #footer { 296 | color: #777; 297 | padding: 0 15px; 298 | position: absolute; 299 | right: 0; 300 | bottom: -31px; 301 | left: 0; 302 | height: 20px; 303 | z-index: 1; 304 | text-align: center; 305 | } 306 | 307 | #footer:before { 308 | content: ''; 309 | position: absolute; 310 | right: 0; 311 | bottom: 31px; 312 | left: 0; 313 | height: 50px; 314 | z-index: -1; 315 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 6px 0 -3px rgba(255, 255, 255, 0.8), 0 7px 1px -3px rgba(0, 0, 0, 0.3), 0 43px 0 -6px rgba(255, 255, 255, 0.8), 0 44px 2px -6px rgba(0, 0, 0, 0.2); 316 | } 317 | 318 | #todo-count { 319 | float: left; 320 | text-align: left; 321 | } 322 | 323 | #filters { 324 | margin: 0; 325 | padding: 0; 326 | list-style: none; 327 | position: absolute; 328 | right: 0; 329 | left: 0; 330 | } 331 | 332 | #filters li { 333 | display: inline; 334 | } 335 | 336 | #filters li a { 337 | color: #83756f; 338 | margin: 2px; 339 | text-decoration: none; 340 | } 341 | 342 | #filters li a.selected { 343 | font-weight: bold; 344 | } 345 | 346 | #clear-completed { 347 | float: right; 348 | position: relative; 349 | line-height: 20px; 350 | text-decoration: none; 351 | background: rgba(0, 0, 0, 0.1); 352 | font-size: 11px; 353 | padding: 0 10px; 354 | border-radius: 3px; 355 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); 356 | } 357 | 358 | #clear-completed:hover { 359 | background: rgba(0, 0, 0, 0.15); 360 | box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); 361 | } 362 | 363 | #info { 364 | margin: 65px auto 0; 365 | color: #a6a6a6; 366 | font-size: 12px; 367 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); 368 | text-align: center; 369 | } 370 | 371 | #info a { 372 | color: inherit; 373 | } 374 | 375 | /* 376 | Hack to remove background from Mobile Safari. 377 | Can't use it globally since it destroys checkboxes in Firefox and Opera 378 | */ 379 | 380 | @media screen and (-webkit-min-device-pixel-ratio:0) { 381 | #toggle-all, 382 | #todo-list li .toggle { 383 | background: none; 384 | } 385 | 386 | #todo-list li .toggle { 387 | height: 40px; 388 | } 389 | 390 | #toggle-all { 391 | top: -56px; 392 | left: -15px; 393 | width: 65px; 394 | height: 41px; 395 | -webkit-transform: rotate(90deg); 396 | -ms-transform: rotate(90deg); 397 | transform: rotate(90deg); 398 | -webkit-appearance: none; 399 | appearance: none; 400 | } 401 | } 402 | 403 | .hidden { 404 | display: none; 405 | } 406 | 407 | hr { 408 | margin: 20px 0; 409 | border: 0; 410 | border-top: 1px dashed #C5C5C5; 411 | border-bottom: 1px dashed #F7F7F7; 412 | } 413 | 414 | .learn a { 415 | font-weight: normal; 416 | text-decoration: none; 417 | color: #b83f45; 418 | } 419 | 420 | .learn a:hover { 421 | text-decoration: underline; 422 | color: #787e7e; 423 | } 424 | 425 | .learn h3, 426 | .learn h4, 427 | .learn h5 { 428 | margin: 10px 0; 429 | font-weight: 500; 430 | line-height: 1.2; 431 | color: #000; 432 | } 433 | 434 | .learn h3 { 435 | font-size: 24px; 436 | } 437 | 438 | .learn h4 { 439 | font-size: 18px; 440 | } 441 | 442 | .learn h5 { 443 | margin-bottom: 0; 444 | font-size: 14px; 445 | } 446 | 447 | .learn ul { 448 | padding: 0; 449 | margin: 0 0 30px 25px; 450 | } 451 | 452 | .learn li { 453 | line-height: 20px; 454 | } 455 | 456 | .learn p { 457 | font-size: 15px; 458 | font-weight: 300; 459 | line-height: 1.3; 460 | margin-top: 0; 461 | margin-bottom: 0; 462 | } 463 | 464 | .quote { 465 | border: none; 466 | margin: 20px 0 60px 0; 467 | } 468 | 469 | .quote p { 470 | font-style: italic; 471 | } 472 | 473 | .quote p:before { 474 | content: '“'; 475 | font-size: 50px; 476 | opacity: .15; 477 | position: absolute; 478 | top: -20px; 479 | left: 3px; 480 | } 481 | 482 | .quote p:after { 483 | content: '”'; 484 | font-size: 50px; 485 | opacity: .15; 486 | position: absolute; 487 | bottom: -42px; 488 | right: 3px; 489 | } 490 | 491 | .quote footer { 492 | position: absolute; 493 | bottom: -40px; 494 | right: 0; 495 | } 496 | 497 | .quote footer img { 498 | border-radius: 3px; 499 | } 500 | 501 | .quote footer a { 502 | margin-left: 5px; 503 | vertical-align: middle; 504 | } 505 | 506 | .speech-bubble { 507 | position: relative; 508 | padding: 10px; 509 | background: rgba(0, 0, 0, .04); 510 | border-radius: 5px; 511 | } 512 | 513 | .speech-bubble:after { 514 | content: ''; 515 | position: absolute; 516 | top: 100%; 517 | right: 30px; 518 | border: 13px solid transparent; 519 | border-top-color: rgba(0, 0, 0, .04); 520 | } 521 | 522 | .learn-bar > .learn { 523 | position: absolute; 524 | width: 272px; 525 | top: 8px; 526 | left: -300px; 527 | padding: 10px; 528 | border-radius: 5px; 529 | background-color: rgba(255, 255, 255, .6); 530 | -webkit-transition-property: left; 531 | transition-property: left; 532 | -webkit-transition-duration: 500ms; 533 | transition-duration: 500ms; 534 | } 535 | 536 | @media (min-width: 899px) { 537 | .learn-bar { 538 | width: auto; 539 | margin: 0 0 0 300px; 540 | } 541 | 542 | .learn-bar > .learn { 543 | left: 8px; 544 | } 545 | 546 | .learn-bar #todoapp { 547 | width: 550px; 548 | margin: 130px auto 40px auto; 549 | } 550 | } 551 | 552 | 553 | html, 554 | body { 555 | margin: 0; 556 | padding: 0; 557 | } 558 | 559 | button { 560 | margin: 0; 561 | padding: 0; 562 | border: 0; 563 | background: none; 564 | font-size: 100%; 565 | vertical-align: baseline; 566 | font-family: inherit; 567 | font-weight: inherit; 568 | color: inherit; 569 | -webkit-appearance: none; 570 | appearance: none; 571 | -webkit-font-smoothing: antialiased; 572 | -moz-osx-font-smoothing: grayscale; 573 | } 574 | 575 | body { 576 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 577 | line-height: 1.4em; 578 | background: #f5f5f5; 579 | color: #4d4d4d; 580 | min-width: 230px; 581 | max-width: 550px; 582 | margin: 0 auto; 583 | -webkit-font-smoothing: antialiased; 584 | -moz-osx-font-smoothing: grayscale; 585 | font-weight: 300; 586 | } 587 | 588 | :focus { 589 | outline: 0; 590 | } 591 | 592 | .hidden { 593 | display: none; 594 | } 595 | 596 | .todoapp { 597 | background: #fff; 598 | margin: 130px 0 40px 0; 599 | position: relative; 600 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); 601 | } 602 | 603 | .todoapp input::-webkit-input-placeholder { 604 | font-style: italic; 605 | font-weight: 300; 606 | color: #e6e6e6; 607 | } 608 | 609 | .todoapp input::-moz-placeholder { 610 | font-style: italic; 611 | font-weight: 300; 612 | color: #e6e6e6; 613 | } 614 | 615 | .todoapp input::input-placeholder { 616 | font-style: italic; 617 | font-weight: 300; 618 | color: #e6e6e6; 619 | } 620 | 621 | .todoapp h1 { 622 | position: absolute; 623 | top: -155px; 624 | width: 100%; 625 | font-size: 100px; 626 | font-weight: 100; 627 | text-align: center; 628 | color: #F27781; 629 | -webkit-text-rendering: optimizeLegibility; 630 | -moz-text-rendering: optimizeLegibility; 631 | text-rendering: optimizeLegibility; 632 | } 633 | 634 | .new-todo, 635 | .edit { 636 | position: relative; 637 | margin: 0; 638 | width: 100%; 639 | font-size: 24px; 640 | font-family: inherit; 641 | font-weight: inherit; 642 | line-height: 1.4em; 643 | border: 0; 644 | color: inherit; 645 | padding: 6px; 646 | border: 1px solid #999; 647 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 648 | box-sizing: border-box; 649 | -webkit-font-smoothing: antialiased; 650 | -moz-osx-font-smoothing: grayscale; 651 | } 652 | 653 | .new-todo { 654 | padding: 16px 16px 16px 60px; 655 | border: none; 656 | background: rgba(0, 0, 0, 0.003); 657 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03); 658 | } 659 | 660 | .main { 661 | position: relative; 662 | z-index: 2; 663 | border-top: 1px solid #e6e6e6; 664 | } 665 | 666 | .toggle-all { 667 | text-align: center; 668 | border: none; /* Mobile Safari */ 669 | opacity: 0; 670 | position: absolute; 671 | } 672 | 673 | .toggle-all + label { 674 | width: 60px; 675 | height: 34px; 676 | font-size: 0; 677 | position: absolute; 678 | top: -52px; 679 | left: -13px; 680 | -webkit-transform: rotate(90deg); 681 | transform: rotate(90deg); 682 | } 683 | 684 | .toggle-all + label:before { 685 | content: '❯'; 686 | font-size: 22px; 687 | color: #e6e6e6; 688 | padding: 10px 27px 10px 27px; 689 | } 690 | 691 | .toggle-all:checked + label:before { 692 | color: #737373; 693 | } 694 | 695 | .todo-list { 696 | margin: 0; 697 | padding: 0; 698 | list-style: none; 699 | } 700 | 701 | .todo-list li { 702 | position: relative; 703 | font-size: 24px; 704 | border-bottom: 1px solid #ededed; 705 | } 706 | 707 | .todo-list li:last-child { 708 | border-bottom: none; 709 | } 710 | 711 | .todo-list li.editing { 712 | border-bottom: none; 713 | padding: 0; 714 | } 715 | 716 | .todo-list li.editing .edit { 717 | display: block; 718 | width: 506px; 719 | padding: 12px 16px; 720 | margin: 0 0 0 43px; 721 | } 722 | 723 | .todo-list li.editing .view { 724 | display: none; 725 | } 726 | 727 | .todo-list li .toggle { 728 | text-align: center; 729 | width: 40px; 730 | /* auto, since non-WebKit browsers doesn't support input styling */ 731 | height: auto; 732 | position: absolute; 733 | top: 0; 734 | bottom: 0; 735 | margin: auto 0; 736 | border: none; /* Mobile Safari */ 737 | -webkit-appearance: none; 738 | appearance: none; 739 | } 740 | 741 | .todo-list li .toggle { 742 | opacity: 0; 743 | } 744 | 745 | .todo-list li .toggle + label { 746 | /* 747 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 748 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 749 | */ 750 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 751 | background-repeat: no-repeat; 752 | background-position: center left; 753 | } 754 | 755 | .todo-list li .toggle:checked + label { 756 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 757 | } 758 | 759 | .todo-list li label { 760 | word-break: break-all; 761 | padding: 15px 15px 15px 60px; 762 | display: block; 763 | line-height: 1.2; 764 | transition: color 0.4s; 765 | } 766 | 767 | .todo-list li.completed label { 768 | color: #d9d9d9; 769 | text-decoration: line-through; 770 | } 771 | 772 | .todo-list li .destroy { 773 | display: none; 774 | position: absolute; 775 | top: 0; 776 | right: 10px; 777 | bottom: 0; 778 | width: 40px; 779 | height: 40px; 780 | margin: auto 0; 781 | font-size: 30px; 782 | color: #af5b5e; 783 | margin-bottom: 11px; 784 | transition: color 0.2s ease-out; 785 | } 786 | 787 | .todo-list li .destroy:hover { 788 | color: #af5b5e; 789 | } 790 | 791 | .todo-list li .destroy:after { 792 | content: '×'; 793 | } 794 | 795 | .todo-list li:hover .destroy { 796 | display: block; 797 | } 798 | 799 | .todo-list li .edit { 800 | display: none; 801 | } 802 | 803 | .todo-list li.editing:last-child { 804 | margin-bottom: -1px; 805 | } 806 | 807 | .footer { 808 | color: #777; 809 | padding: 10px 15px; 810 | height: 20px; 811 | text-align: center; 812 | border-top: 1px solid #e6e6e6; 813 | } 814 | 815 | .footer:before { 816 | content: ''; 817 | position: absolute; 818 | right: 0; 819 | bottom: 0; 820 | left: 0; 821 | height: 50px; 822 | overflow: hidden; 823 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); 824 | } 825 | 826 | .todo-count { 827 | float: left; 828 | text-align: left; 829 | } 830 | 831 | .todo-count strong { 832 | font-weight: 300; 833 | } 834 | 835 | .filters { 836 | margin: 0; 837 | padding: 0; 838 | list-style: none; 839 | position: absolute; 840 | right: 0; 841 | left: 0; 842 | } 843 | 844 | .filters li { 845 | display: inline; 846 | } 847 | 848 | .filters li a { 849 | color: inherit; 850 | margin: 3px; 851 | padding: 3px 7px; 852 | text-decoration: none; 853 | border: 1px solid transparent; 854 | border-radius: 3px; 855 | } 856 | 857 | .filters li a:hover { 858 | border-color: rgba(175, 47, 47, 0.1); 859 | } 860 | 861 | .filters li a.selected { 862 | border-color: rgba(175, 47, 47, 0.2); 863 | } 864 | 865 | .clear-completed, 866 | html .clear-completed:active { 867 | float: right; 868 | position: relative; 869 | line-height: 20px; 870 | text-decoration: none; 871 | cursor: pointer; 872 | } 873 | 874 | .clear-completed:hover { 875 | text-decoration: underline; 876 | } 877 | 878 | .info { 879 | margin: 65px auto 0; 880 | color: #bfbfbf; 881 | font-size: 10px; 882 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 883 | text-align: center; 884 | } 885 | 886 | .info p { 887 | line-height: 1; 888 | } 889 | 890 | .info a { 891 | color: inherit; 892 | text-decoration: none; 893 | font-weight: 400; 894 | } 895 | 896 | .info a:hover { 897 | text-decoration: underline; 898 | } 899 | 900 | /* 901 | Hack to remove background from Mobile Safari. 902 | Can't use it globally since it destroys checkboxes in Firefox 903 | */ 904 | @media screen and (-webkit-min-device-pixel-ratio:0) { 905 | .toggle-all, 906 | .todo-list li .toggle { 907 | background: none; 908 | } 909 | 910 | .todo-list li .toggle { 911 | height: 40px; 912 | } 913 | } 914 | 915 | @media (max-width: 430px) { 916 | .footer { 917 | height: 50px; 918 | } 919 | 920 | .filters { 921 | bottom: 10px; 922 | } 923 | } 924 | -------------------------------------------------------------------------------- /Tutorial/TodoReact/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Todos 9 | 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /Tutorial/TodoReact/src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | function App() { 4 | const [newTodo, setNewTodo] = useState(""); 5 | const [todos, setTodos] = useState([]); 6 | 7 | async function getTodos() { 8 | const result = await fetch("/api/todos"); 9 | const todos = await result.json(); 10 | setTodos(todos); 11 | } 12 | 13 | async function createTodo(e) { 14 | e.preventDefault(); 15 | await fetch('/api/todos', { 16 | method: "POST", 17 | body: JSON.stringify({ name: newTodo }) 18 | }); 19 | setNewTodo(""); 20 | await getTodos(); 21 | } 22 | 23 | async function updateCompleted(todo, isComplete) { 24 | await fetch(`/api/todos/${todo.id}`, { 25 | method: "POST", 26 | body: JSON.stringify({ ...todo, isComplete: isComplete }) 27 | }); 28 | await getTodos(); 29 | } 30 | 31 | async function deleteTodo(id) { 32 | await fetch(`/api/todos/${id}`, { 33 | method: "DELETE" 34 | }); 35 | await getTodos(); 36 | } 37 | 38 | useEffect(() => { 39 | getTodos(); 40 | }, []); 41 | 42 | return ( 43 |
44 |
45 |

todos

46 |
47 | setNewTodo(e.target.value)} /> 48 |
49 |
50 |
51 |
    52 | {todos.map(todo => { 53 | return ( 54 |
  • 55 |
    56 | updateCompleted(todo, e.target.checked)} /> 57 | 58 | 59 |
    60 |
  • 61 | ); 62 | })} 63 |
64 |
65 |
66 | ); 67 | } 68 | 69 | export default App; -------------------------------------------------------------------------------- /Tutorial/TodoReact/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------