From 09bea09c8157ff45279f0d06aa9d313448c0bec5 Mon Sep 17 00:00:00 2001 From: polo Date: Mon, 7 Apr 2025 07:46:27 +0200 Subject: nouvelle "news" --- public/assets/logo-fond-bleu.png | Bin 61236 -> 0 bytes public/js/main.js | 96 +++++++++++++--------- public/js/tinymce.js | 166 ++++++++++++++++++++++++--------------- src/controller/Director.php | 2 +- src/controller/ajax.php | 46 ++++++++--- src/model/Path.php | 6 -- src/model/entities/Node.php | 10 +-- src/view/GridBuilder.php | 4 +- src/view/MainBuilder.php | 18 ++++- src/view/NewBuilder.php | 63 +++++++++------ 10 files changed, 257 insertions(+), 154 deletions(-) delete mode 100644 public/assets/logo-fond-bleu.png diff --git a/public/assets/logo-fond-bleu.png b/public/assets/logo-fond-bleu.png deleted file mode 100644 index f51ac9c..0000000 Binary files a/public/assets/logo-fond-bleu.png and /dev/null differ diff --git a/public/js/main.js b/public/js/main.js index c05eb2f..cb76ea8 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -90,13 +90,8 @@ function changeDate(id_date) const real_id = 'i' + id_date.slice(1); const date_span = document.getElementById(id_date); // = var old_date = date_span.innerHTML; - - // changer "le 28-12-2024 à 23h14" en "2024-12-28T23:14" - let values = old_date.split(" à "); // 2 parties: date et heure - values[1] = values[1].replace('h', ':'); - values[0] = values[0].replace("le ", ""); - let date = values[0].split("-"); // tableau jj-mm-aaaa - old_date = date[2] + '-' + date[1] + "-" + date[0] + "T" + values[1]; + + old_date = dateToISO(old_date); var label = document.createElement('label'); label.textContent = 'Choisir une date: '; @@ -117,6 +112,15 @@ function changeDate(id_date) document.querySelector(`#submit-${id_date}`).classList.remove('hidden'); } +function dateToISO(date){ + // changer "le 28-12-2024 à 23h14" en "2024-12-28T23:14" + let values = date.split(" à "); // 2 parties: date et heure + values[1] = values[1].replace('h', ':'); + values[0] = values[0].replace("le ", ""); + let date_array = values[0].split("-"); // tableau jj-mm-aaaa + return date_array[2] + '-' + date_array[1] + "-" + date_array[0] + "T" + values[1]; +} + function closeInput(id) { const date_span = document.getElementById(id); @@ -133,40 +137,56 @@ function closeInput(id) function submitDate(id_date) { - const date_input = document.getElementById('input-' + id_date); + var date_input = document.getElementById('input-' + id_date); + + // cas des nouvelles "news" + const params = new URL(document.location).searchParams; // "search" = ? et paramètres, searchParams = objet avec des getters + if(params != null && params.get("id")[0] === 'n') + { + // modifier la date dans le caché + date_input = updateDate(id_date, date_input); + closeInput(id_date); + return; + } + else{ + fetch('index.php?action=date_submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({id: id_date, date: date_input.value}) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // modifier la date dans le caché + date_input = updateDate(id_date, date_input); + closeInput(id_date); + } + else { + console.error('Erreur lors de la sauvegarde de la date.'); + } + }) + .catch(error => { + console.error('Erreur:', error); + }); + } +} - fetch('index.php?action=date_submit', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({id: id_date, date: date_input.value}) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - // modifier la date dans le caché - const date_span = document.getElementById(id_date); - let date = new Date(date_input.value); - date_span.innerHTML = - 'le ' + String(date.getDate()).padStart(2, '0') + '-' + - String(date.getMonth() + 1).padStart(2, '0') + '-' + - String(date.getFullYear()).padStart(4, '0') + ' à ' + - String(date.getHours()).padStart(2, '0') + 'h' + - String(date.getMinutes()).padStart(2, '0'); - - closeInput(id_date); - } - else { - console.error('Erreur lors de la sauvegarde de la date.'); - } - }) - .catch(error => { - console.error('Erreur:', error); - }); +function updateDate(id_date, date_input){ + var date_span = document.getElementById(id_date); + let date = new Date(date_input.value); + date_span.innerHTML = + 'le ' + String(date.getDate()).padStart(2, '0') + '-' + + String(date.getMonth() + 1).padStart(2, '0') + '-' + + String(date.getFullYear()).padStart(4, '0') + ' à ' + + String(date.getHours()).padStart(2, '0') + 'h' + + String(date.getMinutes()).padStart(2, '0'); + + return date_input; } -function findParent(element, tag_name) { +function findParent(element, tag_name){ while (element !== null) { if (element.tagName === tag_name.toUpperCase()) // tagName est en majuscules { diff --git a/public/js/tinymce.js b/public/js/tinymce.js index 1e40f51..00868ee 100644 --- a/public/js/tinymce.js +++ b/public/js/tinymce.js @@ -38,10 +38,10 @@ function openEditor(id, page = '') { document.querySelector(`#submit-${id}`).classList.remove('hidden'); if(creation_mode === false){ document.querySelector(`#edit-${id}`).classList.add('hidden'); - document.querySelector(`#delete-${real_id}`).classList.add('hidden'); if(page != 'article'){ document.querySelector(`#position_up-${id}`).classList.add('hidden'); document.querySelector(`#position_down-${id}`).classList.add('hidden'); + document.querySelector(`#delete-${real_id}`).classList.add('hidden'); } } else{ @@ -175,37 +175,75 @@ function closeEditor(id, page = '', restore_old = true) } -function submitArticle(id, page = '', clone = null) { - //var creation_mode; - if(id[0] === 'n'){ - //creation_mode = true; +function submitArticle(id, page = '', clone = null) +{ + /*if(id[0] === 'n' && clone == null){ + return; // sécurité + }*/ + var editor; + const params = new URL(document.location).searchParams; // "search" = ? et paramètres, searchParams = objet avec des getters - // sécurité - if(clone == null){ - return; - } - } - else{ - //creation_mode = false; - } + // clic sur "tout enregistrer" + if(id[0] === 'n' && page === 'article'){ + const prefixes = ['t', 'p', 'i', 'd']; + const allElemsWithId = document.querySelectorAll('[id]'); + var content = {}; - // Récupérer l'éditeur correspondant à l'article - const editor = editors[id]; - if(!editor) { - console.error('Éditeur non trouvé pour l\'article:', id); + allElemsWithId.forEach(element => { + const first_letter = element.id.charAt(0).toLowerCase(); + if(prefixes.includes(first_letter)){ + content[first_letter] = element.innerHTML; + } + }) + content['d'] = dateToISO(content['d']); + + // Envoi AJAX au serveur + fetch('index.php?action=editor_submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({id: id, content: content}) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log('données envoyées au serveur avec succès.'); + + // remplacer les boutons (Enregistrer => Supprimer) + } + else { + alert('Erreur lors de la sauvegarde de l\'article.'); + } + }) + .catch(error => { + console.error('Erreur:', error); + }); return; } + // champs à remplir des nouvelles "news" + else if(page === 'article' && params != null && params.get("id")[0] === 'n'){ + closeEditor(id, page, false); + //makeNewArticleButtons(id, id, clone); + return; + } + // dans les autres cas, on doit pouvoir récupérer l'éditeur + else{ + // l'éditeur correspond à l'article OU page "article" à un élément: titre, aperçu, article + editor = editors[id]; + if(!editor) { + console.error('Éditeur non trouvé pour l\'article:', id); + return; + } + } - // Récupérer le contenu de l'éditeur - const html = editor.getContent(); - // Envoi AJAX au serveur fetch('index.php?action=editor_submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({id: id, content: html}) + body: JSON.stringify({id: id, content: editor.getContent()}) }) .then(response => response.json()) .then(data => { @@ -213,48 +251,7 @@ function submitArticle(id, page = '', clone = null) { // Fermer l'éditeur et mettre à jour le contenu de l'article closeEditor(id, page, false); if(id[0] === 'n'){ - var share_btn = document.querySelector(`.share.hidden`); // combinaison de deux classes - var new_btn = document.querySelector(`#new-${id}`); - var edit_btn = document.querySelector(`#edit-${id}`); - var pos_up_btn = document.querySelector(`#position_up-${id}`); - var pos_down_btn = document.querySelector(`#position_down-${id}`); - var delete_btn = document.querySelector(`#delete-${id}`); - var cancel_btn = document.querySelector(`#cancel-${id}`); - var submit_btn = document.querySelector(`#submit-${id}`); - - share_btn.classList.remove('hidden') - new_btn.classList.add('hidden'); - edit_btn.classList.remove('hidden'); - pos_up_btn.classList.remove('hidden'); - pos_down_btn.classList.remove('hidden'); - delete_btn.classList.remove('hidden'); - //cancel_btn.classList.add('hidden'); - //submit_btn.classList.add('hidden'); - - var article = document.getElementById(id); - var parent = findParent(article, 'article'); - //share_btn.setAttribute('href', '#' + data.article_id); - share_btn.setAttribute('onclick', "copyInClipBoard('" + window.location.href + data.article_id + "')"); // # de l'ancre ajouté au clic sur le lien ouvrant l'éditeur - article.id = data.article_id; - edit_btn.id = 'edit-' + data.article_id; - edit_btn.querySelector('.action_icon').setAttribute('onclick', "openEditor('" + data.article_id + "')"); - pos_up_btn.id = 'position_up-' + data.article_id; - pos_up_btn.querySelector('.action_icon').setAttribute('onclick', "switchPositions('" + data.article_id + "', 'up')"); - pos_down_btn.id = 'position_down-' + data.article_id; - pos_down_btn.querySelector('.action_icon').setAttribute('onclick', "switchPositions('" + data.article_id + "', 'down')"); - delete_btn.id = 'delete-' + data.article_id; - delete_btn.querySelector('.action_icon').setAttribute('onclick', "deleteArticle('" + data.article_id + "')"); - cancel_btn.id = 'cancel-' + data.article_id; - cancel_btn.querySelector('button').setAttribute('onclick', "closeEditor('" + data.article_id + "')"); - submit_btn.id = 'submit-' + data.article_id; - submit_btn.querySelector('button').setAttribute('onclick', "submitArticle('" + data.article_id + "')"); - - var next_div = parent.nextElementSibling.nextElementSibling; - parent.parentNode.replaceChild(clone.cloneNode(true), parent); // clone du squelette pour le garder intact - next_div.appendChild(parent); - } - else{ - //document.getElementById(id).innerHTML = html; + makeNewArticleButtons(id, data.article_id, clone); } } else { @@ -264,4 +261,47 @@ function submitArticle(id, page = '', clone = null) { .catch(error => { console.error('Erreur:', error); }); +} + +function makeNewArticleButtons(id, article_id, clone) +{ + var share_btn = document.querySelector(`.share.hidden`); // combinaison de deux classes + var new_btn = document.querySelector(`#new-${id}`); + var edit_btn = document.querySelector(`#edit-${id}`); + var pos_up_btn = document.querySelector(`#position_up-${id}`); + var pos_down_btn = document.querySelector(`#position_down-${id}`); + var delete_btn = document.querySelector(`#delete-${id}`); + var cancel_btn = document.querySelector(`#cancel-${id}`); + var submit_btn = document.querySelector(`#submit-${id}`); + + share_btn.classList.remove('hidden') + new_btn.classList.add('hidden'); + edit_btn.classList.remove('hidden'); + pos_up_btn.classList.remove('hidden'); + pos_down_btn.classList.remove('hidden'); + delete_btn.classList.remove('hidden'); + //cancel_btn.classList.add('hidden'); + //submit_btn.classList.add('hidden'); + + var article = document.getElementById(id); + var parent = findParent(article, 'article'); + + share_btn.setAttribute('onclick', "copyInClipBoard('" + window.location.href + article_id + "')"); // # de l'ancre ajouté au clic sur le lien ouvrant l'éditeur + article.id = article_id; + edit_btn.id = 'edit-' + article_id; + edit_btn.querySelector('.action_icon').setAttribute('onclick', "openEditor('" + article_id + "')"); + pos_up_btn.id = 'position_up-' + article_id; + pos_up_btn.querySelector('.action_icon').setAttribute('onclick', "switchPositions('" + article_id + "', 'up')"); + pos_down_btn.id = 'position_down-' + article_id; + pos_down_btn.querySelector('.action_icon').setAttribute('onclick', "switchPositions('" + article_id + "', 'down')"); + delete_btn.id = 'delete-' + article_id; + delete_btn.querySelector('.action_icon').setAttribute('onclick', "deleteArticle('" + article_id + "')"); + cancel_btn.id = 'cancel-' + article_id; + cancel_btn.querySelector('button').setAttribute('onclick', "closeEditor('" + article_id + "')"); + submit_btn.id = 'submit-' + article_id; + submit_btn.querySelector('button').setAttribute('onclick', "submitArticle('" + article_id + "')"); + + var next_div = parent.nextElementSibling.nextElementSibling; + parent.parentNode.replaceChild(clone.cloneNode(true), parent); // clone du squelette pour le garder intact + next_div.appendChild(parent); } \ No newline at end of file diff --git a/src/controller/Director.php b/src/controller/Director.php index db84661..b7be9b8 100644 --- a/src/controller/Director.php +++ b/src/controller/Director.php @@ -87,7 +87,7 @@ class Director } } if(isset($new)){ - $main->setTempChild($new); + $main->setAdoptedChild($new); } } diff --git a/src/controller/ajax.php b/src/controller/ajax.php index b5c2e51..4863681 100644 --- a/src/controller/ajax.php +++ b/src/controller/ajax.php @@ -18,32 +18,54 @@ if($_SERVER['CONTENT_TYPE'] === 'application/json' && isset($_GET['action'])) if(json_last_error() === JSON_ERROR_NONE) { $id = $json['id']; - $content = Security::secureString($json['content']); $director = new Director($entityManager); - // nouvel article - if($id[0] === 'n') - { + // cas d'une nouvelle "news" + if(is_array($json['content'])){ + foreach($json['content'] as $one_input){ + $one_input = Security::secureString($one_input); + } + $content = $json['content']; + } + else{ + $content = Security::secureString($json['content']); if($content === ''){ echo json_encode(['success' => false, 'message' => 'pas de données à sauvegarder']); die; } + } + + // nouvel article + if($id[0] === 'n') + { $section_id = (int)substr($id, 1); // id du bloc
$director->makeSectionNode($section_id); $node = $director->getNode(); // =
- $timestamp = time(); - $date = new \DateTime; - $date->setTimestamp($timestamp); + if(is_array($content)){ + // + //$timestamp = time(); // int + $date = new \DateTime($content['d']); + + //echo substr($content['i'], 1) . ' '; + //echo $article_id;die; + $article = new Article($content['i'], $date, $content['t'], $content['p']); + $article_node = new Node('new', 'i' . (string)$date->getTimestamp(), [], count($node->getChildren()) + 1, $node, $node->getPage(), $article); - $article = new Article($content, $date); // le "current" timestamp est obtenu par la BDD - $article_node = new Node('article', 'i' . (string)$timestamp, [], count($node->getChildren()) + 1, $node, $node->getPage(), $article); + // id_node tout juste généré + //$article_node->getId(); + } + else{ + $timestamp = time(); + $date = new \DateTime; + $date->setTimestamp($timestamp); + + $article = new Article($content, $date); // le "current" timestamp est obtenu par la BDD + $article_node = new Node('article', 'i' . (string)$timestamp, [], count($node->getChildren()) + 1, $node, $node->getPage(), $article); + } $entityManager->persist($article_node); $entityManager->flush(); - - // id_node tout juste généré - //$article_node->getId(); echo json_encode(['success' => true, 'article_id' => $article_node->getArticleTimestamp()]); die; diff --git a/src/model/Path.php b/src/model/Path.php index 6faadfd..11be6fe 100644 --- a/src/model/Path.php +++ b/src/model/Path.php @@ -17,12 +17,6 @@ class Path extends Page $this->findPage(Director::$menu_data, $path_array); // remplit $this->current_page } catch(Exception $e){} - /*echo "nb d'autres pages: " . count(Director::$menu_data->getOtherPages()) . '
'; - echo 'longueur du chemin: ' . count($this->current_page) . '
'; - foreach($this->current_page as $current){ - echo $current->getEndOfPath() . ' '; - } - die;*/ } // produit un tableau de Page en comparant le chemin demandé avec les données dans Menu diff --git a/src/model/entities/Node.php b/src/model/entities/Node.php index c9b310a..a52a7e6 100644 --- a/src/model/entities/Node.php +++ b/src/model/entities/Node.php @@ -52,7 +52,7 @@ class Node // -- fin des attributs destinés à doctrine, début du code utilisateur -- private array $children = []; // tableau de Node - private ?self $temp_child = null; // = "new" est l'enfant de "main" lorsque la page est "article" + private ?self $adopted = null; // = "new" est un enfant de "main" lorsque la page est "article" public function __construct(string $name = '', ?string $article_timestamp = null, array $attributes = [], int $position = 0, ?self $parent = null, ?Page $page = null, ?Article $article = null) { @@ -176,12 +176,12 @@ class Node $this->children = array_values($this->children); // réindexer pour supprimer la case vide } - public function getTempChild(): ?self // peut renvoyer null + public function getAdoptedChild(): ?self // peut renvoyer null { - return $this->temp_child; + return $this->adopted; } - public function setTempChild(self $child): void + public function setAdoptedChild(self $child): void { - $this->temp_child = $child; + $this->adopted = $child; } } diff --git a/src/view/GridBuilder.php b/src/view/GridBuilder.php index e183e6f..fdc5426 100644 --- a/src/view/GridBuilder.php +++ b/src/view/GridBuilder.php @@ -27,9 +27,9 @@ class GridBuilder extends AbstractBuilder $share_button = ''; $html = ''; - if(CURRENT_PAGE === 'accueil'){ + if(Director::$page_path->getLast()->getEndOfPath() === 'accueil'){ $new_button = '

-

'; +

'; } else{ $new_button = '

' . "\n" . diff --git a/src/view/MainBuilder.php b/src/view/MainBuilder.php index a44570b..ea7b8c3 100644 --- a/src/view/MainBuilder.php +++ b/src/view/MainBuilder.php @@ -1,6 +1,7 @@ html .= "

\n"; if(Director::$page_path->getLast()->getEndOfPath() === 'article'){ - if($node->getTempChild() == null){ - $new = new Node; + // pas censé arriver + if(!isset($_GET['id'])){ + header('Location: ' . new URL); + die; + } + + if($node->getAdoptedChild() == null){ + // on pourrait raccourcir ça + $timestamp = time(); // int + $date = new \DateTime; + $date->setTimestamp($timestamp); // \DateTime + $article = new Article('', $date); + $new = new Node('new', 'i' . (string)$timestamp, [], 0, null, null, $article); } else{ - $new = $node->getTempChild(); + $new = $node->getAdoptedChild(); } //$builder_name = $this->snakeToPascalCase($new->getName()) . 'Builder'; $builder_name = 'NewBuilder'; diff --git a/src/view/NewBuilder.php b/src/view/NewBuilder.php index acde8b4..a862a08 100644 --- a/src/view/NewBuilder.php +++ b/src/view/NewBuilder.php @@ -5,7 +5,7 @@ use App\Entity\Node; class NewBuilder extends AbstractBuilder { - public function __construct(Node $node) + public function __construct(Node $node, ) { $viewFile = self::VIEWS_PATH . $node->getName() . '.php'; @@ -50,10 +50,15 @@ class NewBuilder extends AbstractBuilder //$date = str_replace(':', 'h', $date_object->format('d-m-Y à H:i')); // partage - $share_link = new URL(['page' => CURRENT_PAGE], $id); - isset($_GET['id']) ? $share_link->addParams(['id' => $_GET['id']]) : ''; + $share_link = new URL(['page' => 'article', 'id' => $id]); $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"'; - $share_button = '' . "\n"; + if(isset($_GET['id']) && $_GET['id'][0] === 'n'){ + $class = 'class="share hidden"'; + } + else{ + $class = 'class="share"'; + } + $share_button = '

' . "\n"; // modifier un article $title_buttons = ''; @@ -65,53 +70,64 @@ class NewBuilder extends AbstractBuilder { if(Director::$page_path->getLast()->getEndOfPath() === 'article'){ $title_js = 'onclick="openEditor(\'' . $id_title . '\', \'article\')"'; - $modify_title = '

' . "\n"; + $modify_title = '

' . "\n"; $close_js_title = 'onclick="closeEditor(\'' . $id_title . '\', \'article\', \'preview\')"'; - $close_editor_title = ''; + $close_editor_title = ''; $submit_js_title = 'onclick="submitArticle(\'' . $id_title . '\', \'article\')"'; - $submit_title = ''; + $submit_title = ''; $title_buttons = '
' . $modify_title . $close_editor_title . $submit_title . '
'; $preview_js = 'onclick="openEditor(\'' . $id_preview . '\', \'article\')"'; - $modify_preview = '

' . "\n"; + $modify_preview = '

' . "\n"; $close_js_preview = 'onclick="closeEditor(\'' . $id_preview . '\', \'article\', \'preview\')"'; - $close_editor_preview = ''; + $close_editor_preview = ''; $submit_js_preview = 'onclick="submitArticle(\'' . $id_preview . '\', \'article\')"'; - $submit_preview = ''; + $submit_preview = ''; $preview_buttons = '
' . $modify_preview . $close_editor_preview . $submit_preview . '
'; $article_js = 'onclick="openEditor(\'' . $id . '\', \'article\')"'; - $modify_article = '

' . "\n"; + $modify_article = '

' . "\n"; $close_js_article = 'onclick="closeEditor(\'' . $id . '\', \'article\')"'; - $close_editor_article = ''; + $close_editor_article = ''; $submit_js_article = 'onclick="submitArticle(\'' . $id . '\', \'article\')"'; - $submit_article = ''; + $submit_article = ''; $article_buttons = '
' . $modify_article . $close_editor_article . $submit_article . '
'; $date_js = 'onclick="changeDate(\'' . $id_date . '\', \'article\');'; - $modify_date = '

' . "\n"; + $modify_date = '

' . "\n"; $close_js_date = 'onclick="closeInput(\'' . $id_date . '\')"'; - $close_editor_date = ''; + $close_editor_date = ''; $submit_js_date = 'onclick="submitDate(\'' . $id_date . '\')"'; - $submit_date = ''; + $submit_date = ''; $date_buttons = '
' . $modify_date . $close_editor_date . $submit_date . '
'; - $delete_js = 'onclick="deleteArticle(\'' . $id . '\', \'' . CURRENT_PAGE . '\')"'; - $delete_article = '

' . "\n"; - - $admin_buttons = $delete_article; + $delete_article = ''; + $submit_article = ''; + // modification: bouton "supprimer" + if($_GET['id'][0] === 'i'){ + $delete_js = 'onclick="deleteArticle(\'' . $id . '\', \'' . Director::$page_path->getLast()->getEndOfPath() . '\')"'; + $delete_article = '

' . "\n"; + } + // nouvel article: bouton pour valider la création d'un nouvel article + else{ + $submit_js = 'onclick="submitArticle(\'' . $_GET['id'] . '\', \'' . Director::$page_path->getLast()->getEndOfPath() . '\')"'; + $submit_article = '

' . "\n"; + } + + $admin_buttons = $delete_article . $submit_article; } + // page d'accueil else{ $modify_article = '

' . "\n"; $up_js = 'onclick="switchPositions(\'' . $id . '\', \'up\')"'; - $up_button = '

' . "\n"; + $up_button = '

' . "\n"; $down_js = 'onclick="switchPositions(\'' . $id . '\', \'down\')"'; - $down_button = '

' . "\n"; + $down_button = '

' . "\n"; $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; - $delete_article = '

' . "\n"; + $delete_article = '

' . "\n"; $close_editor = ''; $submit_article = ''; @@ -120,7 +136,6 @@ class NewBuilder extends AbstractBuilder $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; } - } ob_start(); -- cgit v1.2.3