diff --git a/R4.01_R4.A.10/td_tp/tp4/README.md b/R4.01_R4.A.10/td_tp/tp4/README.md
new file mode 100644
index 0000000..f28ac65
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/README.md
@@ -0,0 +1,47 @@
+# TP javascript : Ajax, promesses.
+
+Vous disposez tous, à la racine de votre compte, d'un répertoire `public_html` qui est servi (http/https) par le serveur dwarves.
+Pour accéder à vos pages, utilisez l'url :
+
+> http(s)://dwarves.[arda|iut-fbleau.fr]/~login
+
+Pour tester les différentes api http, vous pouvez utilisez la commande `curl`.
+
+
+#### Ex1
+Le but est de faire une recherche de film en utilisant l'api [OMDb](https://www.omdbapi.com/) (Open Movie Database).
+
+
+
+Son utilisation nécessite une clé. Vous pouvez utiliser la mienne `2fcb2848`. Si
+la limite de requêtes est atteinte, générez votre propre clé.
+
+
+Complétez le [code](./src/ex1).
+- J'ai volontairement utilisé des modules. Il vous faut donc tester l'application avec
+http.
+
+- Écrire la fonction du modèle
+ ```js
+ getMovies(search)
+ ```
+ Cette fonction renvoie une promesse qui permet de faire une requête ajax vers l'api OMDb. Pour ajax,
+ utilisez d'abord l'interface `fetch`, puis l'objet `XMLHttpRequest`.
+
+- Ecrire la fonction du contrôleur
+ ```js
+ async search(searchMovie)
+ ```
+ qui permet de faire une recherche. Cette fonction utilise évidemment la fonction précédente `getMovies`.
+
+- Ecrire la fonction de la vue
+ ```js
+ renderList(movies)
+ ```
+ quo construit la liste des films affichés. La structure attendue est
+ ```html
+
+ The Godfather 1972
+
+ ```
+
diff --git a/R4.01_R4.A.10/td_tp/tp4/aide.md b/R4.01_R4.A.10/td_tp/tp4/aide.md
new file mode 100644
index 0000000..80bb5dc
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/aide.md
@@ -0,0 +1,35 @@
+#### XMLHttpRequest
+```js
+let xhr = new XMLHttpRequest()
+xhr.open("GET","mon/url")
+
+xhr.onload = ...
+xhr.onerror = ...
+
+xhr.send()
+```
+#### Promise
+```js
+
+let promise = new Promise ((resolve,reject) => {
+ setTimeout(() => {
+ resolve("OK")
+ },1000)
+ })
+promise.then(data => ....)
+```
+
+#### async/await
+
+```js
+
+function getUser(email){
+ return new Promise(...)
+}
+
+async function updateUser(){
+
+ let user = await getUser(...)
+ console.log(user)
+}
+```
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/css/style.css b/R4.01_R4.A.10/td_tp/tp4/src/ex1/css/style.css
new file mode 100644
index 0000000..995cde0
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/css/style.css
@@ -0,0 +1,86 @@
+main {
+ position: absolute;
+ top: 2rem;
+ left: 50%;
+ transform: translate(-50%, 0);
+}
+.loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ margin: 6rem 0 0;
+}
+.error {
+ color: #FFFAAA;
+ margin: 1rem 0;
+}
+label {
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ font-size: 1.6rem;
+}
+label span {
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
+}
+input {
+ margin: 1rem 0 0;
+ font-size: 1.6rem;
+ font-weight: 300;
+ padding: 0.8rem 1rem;
+ color: white;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.05);
+ transition: all 0.3s;
+ box-shadow: 1px 1px 2px rgba(0,0,0, 0.3);
+ -moz-appearance:none;
+ -webkit-appearance:none;
+ outline: none;
+}
+input:focus {
+ border: 1px solid transparent;
+ background: rgba(255, 255, 255, 0.08);
+}
+ul {
+ padding: 0;
+ margin: 1rem 0 2rem;
+}
+ul li {
+ padding: 0.6rem 1rem;
+ margin: 1px 0;
+ line-height: 1.4rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ align-content: space-between;
+ text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
+ background: rgba(255, 255, 255, 0.08);
+ box-shadow: 0 0 2px rgba(0,0,0, 0.3);
+ box-sizing: border-box;
+}
+ul li:hover,
+ul li:active,
+ul li:focus {
+ background: rgba(255, 255, 255, 0.1);
+}
+ul li a {
+ margin: 0 0.6rem 0 0;
+ text-decoration: none;
+ color: white;
+}
+ul li span {
+ opacity: 0.5;
+}
+
+body {
+ margin: 0;
+ font-family: 'Helvetica Neue', Helvetica, Arial;
+ font-weight: 300;
+ background-size: cover;
+ background-attachment: fixed;
+ background-image: -webkit-radial-gradient(ellipse farthest-corner at top, #661141, #000000);
+ background-image: radial-gradient(ellipse farthest-corner at top, #661141, #000000);
+ color: white;
+}
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/img/puff.svg b/R4.01_R4.A.10/td_tp/tp4/src/ex1/img/puff.svg
new file mode 100644
index 0000000..d598440
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/img/puff.svg
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/index.html b/R4.01_R4.A.10/td_tp/tp4/src/ex1/index.html
new file mode 100644
index 0000000..ce5e1aa
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/index.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search a movie
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/app.js b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/app.js
new file mode 100644
index 0000000..e57610b
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/app.js
@@ -0,0 +1,7 @@
+
+import model from '../module/model.js'
+import Controller from '../module/controller.js'
+import View from '../module/view.js'
+
+const view = new View()
+const app = new Controller(view,model)
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/controller.js b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/controller.js
new file mode 100644
index 0000000..31a82f5
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/controller.js
@@ -0,0 +1,26 @@
+class Controller {
+ constructor(view,model){
+ this.view = view
+ this.model = model
+
+ this.loading = false
+ this.lastSearch = null
+ this.error = null
+ this.results = []
+
+ this.view.setLoading(false)
+ this.view.bindSearch(this.search.bind(this))
+ }
+ reset() {
+ this.loading = false
+ this.error = null
+ this.results = []
+ }
+
+ async search(searchMovie) {
+ // TODO
+ }
+}
+
+export default Controller
+
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/helpers.js b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/helpers.js
new file mode 100644
index 0000000..ddedc6c
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/helpers.js
@@ -0,0 +1,12 @@
+ // Returns a function, that, as long as it continues to be invoked, will not
+ // be triggered. The function will be called after it stops being called for
+ // N milliseconds.
+ function debounce(fn, wait) {
+ let timeout
+
+ return (...args) => {
+ clearTimeout(timeout)
+ timeout = setTimeout(() => fn(...args), wait)
+ }
+ }
+export default debounce
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/model.js b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/model.js
new file mode 100644
index 0000000..bce1c69
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/model.js
@@ -0,0 +1,9 @@
+let apiKey = '2fcb2848'
+
+let model = {
+ getMovies(search){
+ // TODO
+ }
+}
+
+export default model
diff --git a/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/view.js b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/view.js
new file mode 100644
index 0000000..b339bbb
--- /dev/null
+++ b/R4.01_R4.A.10/td_tp/tp4/src/ex1/module/view.js
@@ -0,0 +1,44 @@
+import debounce from "./helpers.js"
+
+class View {
+ constructor(){
+ this.listMovies = document.querySelector("#list-movies")
+ this.inputSearch = document.querySelector("input")
+ this.loader = document.querySelector(".loader")
+ this.message = document.querySelector("p.error")
+ }
+
+ #getInput(){
+ return this.inputSearch.value
+ }
+
+ setLoading(loading){
+ if (loading)
+ this.loader.style.display = "block"
+ else
+ this.loader.style.display = "none"
+ }
+
+ renderMessage(error){
+ this.message.style.display = "block"
+ this.message.textContent = error
+ }
+
+ renderList(movies){
+ let ul = document.createElement("ul")
+
+ movies.forEach((movie)=>{
+ // TODO
+ })
+
+ this.listMovies.replaceChildren(ul)
+ }
+
+ bindSearch(handler){
+ this.inputSearch.addEventListener("input",debounce((e)=>{
+ handler(this.#getInput())
+ },500))
+ }
+}
+
+export default View