From ff0150d1eb0ce3778310e67bb4ce87d0666a37d7 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Thu, 23 Apr 2020 19:05:43 +0200
Subject: [PATCH] Replace news by FAQ

---
 TODO                               |   1 +
 client/src/App.vue                 |  19 +--
 client/src/router.js               |   6 +-
 client/src/translations/en.js      |   1 -
 client/src/translations/faq/en.pug | 158 +++++++++++++++++++
 client/src/translations/faq/es.pug | 177 +++++++++++++++++++++
 client/src/translations/faq/fr.pug | 171 ++++++++++++++++++++
 client/src/translations/fr.js      |   1 -
 client/src/variants/Teleport.js    |  11 +-
 client/src/views/Faq.vue           |  67 ++++++++
 client/src/views/News.vue          | 245 -----------------------------
 server/db/create.sql               |   8 -
 server/models/News.js              |  78 ---------
 server/routes/all.js               |   1 -
 server/routes/news.js              |  55 -------
 15 files changed, 585 insertions(+), 414 deletions(-)
 create mode 100644 client/src/translations/faq/en.pug
 create mode 100644 client/src/translations/faq/es.pug
 create mode 100644 client/src/translations/faq/fr.pug
 create mode 100644 client/src/views/Faq.vue
 delete mode 100644 client/src/views/News.vue
 delete mode 100644 server/models/News.js
 delete mode 100644 server/routes/news.js

diff --git a/TODO b/TODO
index c525e789..ec7d9756 100644
--- a/TODO
+++ b/TODO
@@ -1,5 +1,6 @@
 Issue: embedded rules language not updated when language is set (in Analyse, Game and Problems)
 Also: if new live game starts in background, "new game" notify OK but not first move (not too serious however)
+On smartphone for Teleport, Chakart, Weiqi and some others: wait re-click to confirm "touch" move (in Board.vue)
 
 https://www.chessvariants.com/crossover.dir/koopachess.html
 --> Can a stunned piece capture? Maybe not. ...recover? After 5 moves? Never?
diff --git a/client/src/App.vue b/client/src/App.vue
index 02693a62..5729c380 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -33,7 +33,7 @@
     .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
       footer
         router-link.menuitem(to="/about") {{ st.tr["About"] }}
-        router-link.menuitem#newsMenu(to="/news") {{ st.tr["News"] }}
+        router-link.menuitem(to="/faq") F.A.Q.
         a.menuitem(href="https://discord.gg/a9ZFKBe")
           span Discord
           img(src="/images/icons/discord.svg")
@@ -59,18 +59,6 @@ export default {
   data: function() {
     return { st: store.state };
   },
-  mounted: function() {
-    ajax(
-      "/newsts",
-      "GET",
-      {
-        success: (res) => {
-          if (this.st.user.newsRead < res.timestamp)
-            document.getElementById("newsMenu").classList.add("somenews");
-        }
-      }
-    );
-  },
   computed: {
     userName: function() {
       return (
@@ -297,9 +285,4 @@ footer
     height: 55px
     display: block
     padding: 5px 0
-
-.menuitem.somenews
-  color: red
-  &:link, &:visited, &:hover
-    color: red
 </style>
diff --git a/client/src/router.js b/client/src/router.js
index c5afb17d..c51c8862 100644
--- a/client/src/router.js
+++ b/client/src/router.js
@@ -62,9 +62,9 @@ const router = new Router({
       component: loadView("About")
     },
     {
-      path: "/news",
-      name: "news",
-      component: loadView("News")
+      path: "/faq",
+      name: "faq",
+      component: loadView("Faq")
     }
   ]
 });
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 6180f97b..909a3e35 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -84,7 +84,6 @@ export const translations = {
   "New correspondance game:": "New correspondance game:",
   "New game": "New game",
   "New problem": "New problem",
-  News: "News",
   "No challenges found :( Click on 'New game'!": "No challenges found :( Click on 'New game'!",
   "No games found :( Send a challenge!": "No games found :( Send a challenge!",
   "No more problems": "No more problems",
diff --git a/client/src/translations/faq/en.pug b/client/src/translations/faq/en.pug
new file mode 100644
index 00000000..0d5ae9ed
--- /dev/null
+++ b/client/src/translations/faq/en.pug
@@ -0,0 +1,158 @@
+.question.
+  Where should I start?
+.answer.
+  The "Variants" menu on top contains the list of all playable games.
+  If you know what you want, the "prefix" filter might be useful.
+  Then, read the rules, and optionally watch an example game or play a game
+  against the computer. Keep in mind that the "engine" is generally quite weak.
+  It's here for illustration and learning purpose only.
+
+.question.
+  The bot is too weak! Where should I continue?
+.answer
+  | I'm glad you ask. Since I believe that a place is more fun with nice people
+  | around, the next step is to play against humans.
+  | Go to main "Hall" (upper left menu), and first look if a live or
+  | correspondance challenge interests you. If yes just click on it. Challenges
+  | highlighted in orange are targeted: someone wants to play against you,
+  | specifically, optionally from a custom starting position.
+  | If there aren't any interesting challenge, then click on "New game" in the
+  | upper right corner. Select a variant, and
+  ul
+    li.
+      Adjust the cadence: format is
+      'main time + increment' with default units minutes and seconds.
+      If the main time is given in days then the game is considered by
+      correspondance.
+    li.
+      Select the randomness level: 'deterministic' always show the same setup,
+      the one you are generally used to.
+  | You can also click on "Who's there" and select a player to
+  | challenge. The "memorize" checkbox allows to store the challenge settings
+  | to re-issue it in one click later.
+
+.question.
+  I'm bored of being anonymous, I want a name!
+.answer.
+  Very good idea: click on "Login" in the upper right corner, and then switch
+  to the registration view (using the link appearing in dark red).
+  Only a username and an email are required.
+  Once signed up, you should log in within the next 24 hours or your 'account'
+  will be deleted (you can re-do this process anytime later of course).
+  This will also allow you to play correspondance games.
+  Note: the authentication is passwordless, as most websites should be.
+
+.question.
+  How can I talk to my opponent? Ask rematch?
+.answer.
+  Just click on the Chat icon on top. It turns purple when new messages arrive.
+  After the game ends, the Swords icon next to it is the rematch button:
+  it turns green when you receive a rematch offer.
+
+.question.
+  I made a bad move, I want to take it back!
+.answer
+  | It's not possible for now. However, there is a way to continue the game:
+  ol
+    li Click on the Analyze button, and go back just before the bad move.
+    li Copy the FEN displayed below the board (the weird long string).
+    li.
+      Go to main Hall, open "New game" window, select your opponent,
+      and then paste the FEN in the appropriate field.
+
+.question.
+  I'm streaming a game, and would like to use arrows and circles.
+.answer.
+  A right click on a square draws a purple circle inside, and a second right
+  click erases it.
+  'Drag and drop' with the right mouse button from a square to another to draw
+  a purple arrow. Click anywhere with the left mouse button to erase all
+  drawings.
+
+.question.
+  I want to start from a custom position!
+.answer.
+  If the desired position is on a board on the website, just click on the
+  Analyze button (microscope icon), and then copy the string appearing
+  below the board. If the position is in your mind or on a physical board,
+  then you can either try to understand the FEN format by trials and errors
+  from the Analysis mode, or ask me - using the Contact form or on Discord.
+  By the way, FEN are required to post in the "Problems" menu as well.
+
+.question.
+  I want a rating! I want tournaments! I want a new poney!
+.answer.
+  In my experience, caring about a rating only complicate your life,
+  so there won't be any around here. There probably won't be any tournaments
+  support on the website, but some may be organized anyway. Do the pairings
+  somewhere else and ask people to challenge each other.
+
+.question.
+  You said "Problems" menu? What's that?
+.answer.
+  An area where you can upload interesting positions, which could be studies,
+  (self-)mate in 3 and so on. For the moment there is no support for proof
+  games, but you could still provide the final diagram to let people search.
+  Start by looking at the existing problems, and try to solve them :-)
+
+.question.
+  I'm in Hall but cannot see my games!
+.answer.
+  Your games are gathered in "My games" menu on top.
+  If a game is highlighted in orange, then it's your turn: click on it and
+  play! You can have as many running games as you like in either live or
+  correspondance.
+
+.question.
+  Clocks in live games sometimes freeze.
+.answer
+  | The clocks in live games follow a non-standard logic - but sounded, I
+  | believe :-)
+  ul
+    li.
+      If you play a move and then quit the game, your clock will be
+      frozen at its current state until you come back in the game. After his move
+      your opponent will see your clock stopped; he could decide to abort or
+      resign, or
+    li.
+      If your opponent also quits the game after playing, then the time is
+      just frozen - and you can decide to continue it later.
+  | The rational behind this is that you don't know what the opponent played,
+  | so your clock shouldn't be running. Ans it also allows to take a break in
+  | long live games, like maybe 1h30 + 30s.
+
+.question.
+  I bookmarked a fascinating observed game, but the URL no longer works.
+.answer
+  | Live games are stored locally, on browsers only.
+  | If you are only an observer, your browser doesn't even store it, so later
+  | the game won't be reachable if the players and all observers leave the game.
+  | Consequently, the right way to go is
+  ol
+    li Download the game (see the icon on the right),
+    li Import it using the button in "Imported games" tab in "My Games" menu.
+
+.question.
+  I cleaned the website data, and all my live games are gone!
+.answer.
+  They are stored in your browser's database.
+  You just erased it, so the games are gone :-)
+  More flexible cleaning can be achieved: removing cookies is harmless (you'll
+  just need to re-login), as well as removing local storage data (your
+  preferences about board color and sounds will be reset). But touching
+  'IndexedDB' will remove all local games.
+
+.question.
+  But I would like to remove this specific game, and this one too!
+.answer.
+  Click on the game result area from "My Games" listings, and confirm deletion.
+  If the game is running this will abort it first.
+  Note that correspondance games deleted by only one player are still
+  reachable: they are just hidden for you.
+
+.question.
+  I've a question not answered here!
+.answer.
+  If the answer isn't 42 (are you really sure?), then use the contact form,
+  or ask on Discord :-)
+  See the links in the website footer.
diff --git a/client/src/translations/faq/es.pug b/client/src/translations/faq/es.pug
new file mode 100644
index 00000000..2344cabc
--- /dev/null
+++ b/client/src/translations/faq/es.pug
@@ -0,0 +1,177 @@
+.question.
+  ¿Por donde empiezo?
+.answer.
+  El menú "Variantes" en la parte superior contiene la lista de todos los
+  juegos disponibles.
+  Si sabe cuál quiere, el filtro "prefijo" puede ser útil.
+  Luego lea las reglas y posiblemente vea una partida de ejemplo o
+  juega uno contra la computadora. Tenga en cuenta que este oponente es
+  en general bastante débil.
+  Se utiliza solo para ilustrar y aprender las reglas.
+
+.question.
+  ¡La computadora está jugando demasiado mal! ¿Dónde debo continuar?
+.answer
+  | Me gusta esta pregunta. Para mí un lugar es más agradable con
+  | gente comprensivo, así que el siguiente paso es jugar contra
+  | humanos. Vaya al "Salón" principal (menú superior izquierda) y
+  | primero averigua si estás interesado en un desafío en vivo o por
+  | correspondencia. En caso afirmativo, simplemente haga clic en él.
+  | Los desafíos resaltados en naranja son apuntado: alguien quiere jugar
+  | contra ti, específicamente, tal vez desde una posición inicial particular.
+  | Si ningún desafío es interesante, haga clic en "Nueva partida" en
+  | la esquina superior derecha. Elija una variante, luego
+  ul
+    li.
+      Ajuste la cadencia: el formato es 'tiempo principal + incremento', por
+      defecto en minutos y segundos. Si el tiempo principal se da en días,
+      la parte se considera por correspondencia.
+    li.
+      Seleccione el nivel de aleatoriedad: 'determinista' para tener siempre
+      el misma configuración inicial, probablemente en la que estás
+      acostumbrado a.
+  | También puedes hacer clic en "Quién está allí" y elegir un jugador para
+  | desafiar. El cuadro "memorizar" permite almacenar los parámetros del
+  | desafío para el relanzar más tarde.
+
+.question.
+  Estoy cansado de ser anónimo, ¡quiero un nombre!
+.answer.
+  Gran idea: haga clic en "Login" en la esquina superior derecha, luego cambie
+  en la vista de registro (a través del enlace en rojo oscuro que aparece en
+  arriba). Solo se requiere su nombre de usuario y un correo electrónico.
+  Una vez registrado, debe iniciar sesión en las próximas 24 horas de lo
+  contrario su 'cuenta' se elimina (puede comenzar de nuevo en cualquier
+  momento tarde por supuesto).
+  Nota: la autenticación se realiza sin contraseña, como debería
+  sea el caso en la mayoría de los sitios.
+
+.question.
+  ¿Cómo puedo hablar con el oponente? Solicitar venganza?
+.answer.
+  Simplemente haga clic en el icono de Chat en la parte superior.
+  Se vuelve púrpura cuando llega un nuevo mensaje. Cuando la partida termina,
+  el ícono de Espadas al lado permite reiniciar un juego:
+  se vuelve verde cuando el oponente quiere repetición.
+
+.question.
+  Hice un mal movimiento, ¡quiero volver!
+.answer
+  | No es posible por el momento. Sin embargo, aquí hay una manera de
+  | continuar el juego:
+  ol
+    li.
+      Haga clic en el botón Análisis y regrese justo antes del movimiento
+      incorrecto.
+    li
+      Copie el FEN que se muestra debajo del tablero de ajedrez (la cadena
+      larga y extraña).
+    li.
+      Vaya al Salón, abra la ventana "Nueva partida", seleccione
+      su oponente y luego pegue el FEN en el campo apropiado.
+
+.question.
+  Estoy transmitiendo un juego en vivo, y me gustaría usar flechas y
+  círculos.
+.answer.
+  Haga clic derecho en un cuadro dibuja un círculo morado en el interior, y un
+  segundo clic derecho lo borra.
+  'Arrastra y suelta' con el botón derecho de un cuadro a otro dibujo
+  una flecha morada. Haga clic izquierdo en cualquier lugar para
+  eliminar dibujos.
+
+.question.
+  ¡Quiero comenzar desde una posición específica!
+.answer.
+  Si la posición deseada es en un tablero en algún lugar del sitio,
+  simplemente haga clic en el botón Análisis (el icono del microscopio) y
+  copie el cadena que aparece debajo del tablero. Si la posición está en tu
+  cabeza o en un tablero de ajedrez físico, entonces puedes buscar entender el
+  formato FEN por prueba y error en modo Análisis, o pregúntame a través del
+  formulario de contacto o en Discord. El pasaje,
+  estas cadenas FEN también son necesarias para publicar un problema.
+
+.question.
+  ¡Quiero un ranking! ¡Quiero torneos! ¡Quiero un nuevo pony!
+.answer.
+  En mi experiencia, preocuparse por un ranking solo te hace
+  complicar la vida, por lo que no habrá ninguno en este sitio.
+  Nada esta planeado ya no se trata de torneos, pero puedes organizarlos de
+  todos modos. Haga los emparejamientos en otro lugar y pídale a la gente que
+  desafío.
+
+.question.
+  ¿Mencionaste el menú "Problemas"? Que es eso ?
+.answer.
+  Un espacio donde puedes compartir posiciones interesantes, que podrían
+  ser estudios, mat (ayudado) en 3 etc. Actualmente no hay
+  soporte para análisis retrógrado, pero aún podría enviar
+  el diagrama final para permitir que las personas busquen.
+  Comience mirando los problemas existentes e intente resolverlos.
+  :-)
+
+.question.
+  ¡Estoy en el Salón pero no veo mis partidas!
+.answer.
+  Sus partidas se agrupan en el menú "Mis partidas" en la parte superior.
+  Si un juego está resaltado en naranja, entonces es tu turno: haga clic en
+  arriba y juega! El número de partidas en progreso en vivo o por
+  correspondencia no es limitada.
+
+.question.
+  En un juego en vivo, el péndulo a veces se detiene.
+.answer
+  | Los péndulos en las partidas en vivo siguen una lógica no estándar
+  | - pero consistente, creo :-)
+  ul
+    li.
+      Si haces un movimiento y luego sales del juego, tu péndulo se congelará
+      en su estado actual hasta que vuelvas al juego.
+      Despues su movimiento, tu oponente verá tu tiempo detenido;
+      él podría decidir para detener el juego o rendirse, o
+    li.
+      Si tu oponente también abandona el juego después de su movimiento,
+      entonces el tiempo está congelado y puede decidir reanudarlo más tarde.
+    | De hecho, ya que no sabes lo que jugó el oponente, es
+    | normal para que su péndulo se detenga. También te permite tomar un
+    | descanso durante una larga partida en vivo, como 1h30 + 30s, por ejemplo.
+
+.question.
+  Anoté la dirección de una partida fascinante, pero la URL ya no funciona.
+.answer
+  | Los juegos en vivo se almacenan localmente en el navegador
+  | solamente. Si eres un simple espectador, tu navegador no registra
+  | ni siquiera el juego, por lo que será inaccesible cuando los jugadores
+  | así como todos los observadores se habrán ido.
+  | La forma correcta de hacer esto es
+  ol
+    li Descargue la partida (vea el icono a la derecha),
+    li.
+      Importarlo usando el botón en la pestaña "Partidas importadas"
+      desde el menú "Mis partidas".
+
+.question.
+  ¡Eliminé los datos del sitio y mis partidas en vivo han desaparecido!
+.answer.
+  Se almacenan en la base de datos del navegador.
+  Simplemente lo eliminó, por lo que ya no están allí :-)
+  Se requiere una limpieza más fina: eliminar las cookies es inofensivo
+  (solo tendrá que iniciar sesión de nuevo), así como la eliminación de los
+  datos local (guardando sus preferencias como el color del tablero).
+  Sin embargo, tocar la base de datos 'IndexedDB' puede eliminar partidas.
+
+.question.
+  Pero me gustaría eliminar esta parte allí, ¡y esta también!
+.answer.
+  Haz clic en el área de resultados del juego en la lista "Mis partidas",
+  y confirme la eliminación. Si el juego está en progreso, será primero
+  detuvo. Tenga en cuenta que las partidas de correspondencia borradas por
+  solo uno de los dos jugadores siempre están accesibles: simplemente están
+  ocultos para ti.
+
+.question.
+  Tengo una pregunta sin respuesta aquí!
+.answer.
+  Si la respuesta no es 42 (¿estás seguro?), usa el formulario
+  de Contacta o pregunta en Discord :-)
+  Vea los enlaces al final de la página.
diff --git a/client/src/translations/faq/fr.pug b/client/src/translations/faq/fr.pug
new file mode 100644
index 00000000..e9ecd095
--- /dev/null
+++ b/client/src/translations/faq/fr.pug
@@ -0,0 +1,171 @@
+.question.
+  Par où dois-je commencer ?
+.answer.
+  Le menu "Variantes" en haut contient la liste de touts les jeux disponibles.
+  Si vous savez lequel vous voulez, le filtre "préfixe" peut être utile.
+  Ensuite, lisez les règles, et éventuellement regardez une partie exemple ou
+  jouez-en une contre l'ordinateur. Gardez à l'esprit que cet adversaire est
+  en général plutôt faible.
+  Il sert à illustrer et à apprendre les règles seulement.
+
+.question.
+  L'ordinateur joue trop mal ! Où dois-je continuer ?
+.answer
+  | Cette question me plaît. Pour moi un endroit est plus agréable avec des
+  | gens sympas autour, donc la prochaine étape est de jouer contre des
+  | humains. Allez dans le "Salon" principal (menu en haut à gauche), et
+  | regardez d'abord si un défi en direct ou par correspondance vous intéresse.
+  | Si oui cliquez simplement dessus. Les défis surlignés en orange sont
+  | ciblés : quelqu'un veut jouer contre vous, spécifiquement, peut-être
+  | depuis une position initiale particulière.
+  | Si aucun défi n'est intéressant, alors cliquez sur "Nouvelle partie" dans
+  | le coin en haut à droite. Choisissez une variante, puis
+  ul
+    li.
+      Ajustez la cadence : le format est 'temps principal + incrément', par
+      défaut en minutes et secondes. Si le temps principal est donnée en jours,
+      la partie est considérée par correspondance.
+    li.
+      Sélectionnez le niveau d'alea : 'déterministe' pour avoir toujours la
+      même configuration initiale, sans doute celle à laquelle vous êtes
+      habitués.
+  | Vous pouvez aussi cliquer sur "Qui est là" et choisir un joueur à défier.
+  | La case "mémoriser" permet de stocker les paramètres du défi pour le
+  | re-lancer plus tard.
+
+.question.
+  J'en ai marre d'être anonyme, je veux un nom !
+.answer.
+  Excellente idée : cliquez sur "Login" dans le coin haut droit, puis basculez
+  sur la vue d'enregistrement (via le lien en rouge foncé apparaissant en
+  haut). Seuls votre nom d'utilisateur ainsi qu'une adresse email sont requis.
+  Une fois inscrit, vous devez vous logger dans les prochaines 24 heures sinon
+  votre 'compte' est supprimé (vous pouvez recommencer n'importe quand plus
+  tard bien sûr).
+  Note : l'authentification s'effectue sans mot de passe, comme cela devrait
+  être le cas sur la plupart des sites.
+
+.question.
+  Comment puis-je parler à l'adversaire ? Demander une revanche ?
+.answer.
+  Cliquez simplement sur l'icône Chat en haut. Il devient violet quand un
+  nouveau message arrive. Une fois la partie terminée, l'icône Épées à côté
+  permet de relancer une partie : il devient vert lorsque l'adversaire veut
+  rejouer.
+
+.question.
+  J'ai joué un mauvais coup, je veux revenir en arrière !
+.answer
+  | Ce n'est pas possible pour le moment. Cependant, voici une façon de
+  | continuer la partie :
+  ol
+    li Cliquez sur le bouton Analyse, et revenez juste avant le mauvais coup.
+    li Copiez la FEN indiquée sous l'échiquier (la longue chaîne bizarre).
+    li.
+      Allez dans le Salon, ouvrez la fenêtre "Nouvelle partie", sélectionnez
+      votre adversaire et collez ensuite la FEN dans le champ approprié.
+
+.question.
+  Je diffuse une partie en direct, et aimerais utiliser des flèches et des
+  cercles.
+.answer.
+  Un clic droit sur une case dessine un cercle violet à l'intérieur, et un
+  second clic droit l'efface.
+  'Glisser déposer' avec le bouton droit depuis une case vers une autre dessine
+  une flèche violette. Cliquez n'importe où avec le bouton gauche pour
+  effacer les dessins.
+
+.question.
+  Je veux démarrer depuis une position spécifique !
+.answer.
+  SI la position souhaitée est sur un échiquier quelque part sur le site,
+  cliquez simplement sur le bouton Analyse (l'icône microscope), et copiez la
+  chaîne apparaissant sous l'échiquier. Si la position est dans votre tête ou
+  sur un échiquier physique, alors vous pouvez soit chercher à comprendre le
+  format FEN par essais et erreurs dans le mode Analyse, ou bien
+  me demander - via le formulaire de contact ou sur Discord. Au passage,
+  ces chaînes FEN sont nécessaires également pour poster un problème.
+
+.question.
+  Je veux un classement ! Je veux des tournois ! Je veux un nouveau poney !
+.answer.
+  D'après mon expérience, s'inquiéter pour un classement ne fait que vous
+  compliquer la vie, donc il n'y en aura pas sur ce site. Rien n'est prévu
+  non plus concernant les tournois, mais vous pouvez en organiser quand-même.
+  Effectuez les appariements quelque part ailleurs et demandez aux gens de se
+  défier.
+
+.question.
+  T'as mentionné le menu "Problèmes" ? C'est quoi ça ?
+.answer.
+  Un espace où vous pouvez partager des positions intéressantes, qui pourraient
+  être des études, des mat (aidés) en 3 etc. Actuellement il n'y a pas de
+  support pour l'analyse rétrograde, mais vous pourriez tout de même envoyer
+  le diagramme final pour permettre aux gens de chercher.
+  Commencez par regarder les problèmes existants, et essayez de les résoudre
+  :-)
+
+.question.
+  Je suis dans le Salon mais ne vois pas mes parties !
+.answer.
+  Vos parties sont rassemblées dans le menu "Mes parties" en haut.
+  Si une partie est surlignée en orange, alors c'est à vous de jouer : cliquez
+  dessus et jouez ! Le nombre de parties en cours en direct ou par
+  correspondance n'est pas limité.
+
+.question.
+  Dans une partie en direct, la pendule s'arrête parfois.
+.answer
+  | Les pendules dans les parties en direct suivent une logique non standard
+  | - mais cohérente, je crois :-)
+  ul
+    li.
+      Si vous jouez un coup puis quittez la partie, votre pendule sera figée
+      dans son état courant jusqu'à ce que vous reveniez dans le jeu. Après
+      son coup, votre adversaire verra votre temps arrêté ; il pourrait décider
+      d'arrêter la partie ou d'abandonner, ou
+    li.
+      Si votre adversaire quitte lui aussi la partie après son coup, alors le
+      temps est juste figé - et vous pouvez décider de reprendre plus tard.
+    | En effet puisque vous ne savez pas ce qu'a joué l'adversaire, il est
+    | normal que votre pendule s'arrête. Cela permet aussi de faire une pause
+    | pendant une longue partie en direct, comme 1h30 + 30s par exemple.
+
+.question.
+  J'ai noté l'adresse d'une partie fascinante, mais l'URL ne fonctionne plus.
+.answer
+  | Les parties en direct sont stockées localement, dans le navigateur
+  | seulement. Si vous êtes un simple spectateur, votre navigateur n'enregistre
+  | même pas la partie, donc celle-ci sera inaccessible lorsque les joueurs
+  | ainsi que tous les observateurs seront partis.
+  | La bonne manière de procéder consiste alors à
+  ol
+    li Télécharger la partie (cf. l'icône sur la droite),
+    li.
+      L'importer en utilisant le bouton dans l'onglet "Parties importées"
+      depuis le menu "Mes parties".
+
+.question.
+  J'ai effacé les données du site, et mes parties en direct ont disparu !
+.answer.
+  Elles sont stockées dans la base de données du navigateur.
+  Vous venez de l'effacer, donc elles ne sont plus là :-)
+  Un nettoyage plus fin est requis : supprimer les cookies est sans danger
+  (vous devrez juste vous re-logger), de même que la suppression des données
+  locales (enregistrant vos préférences comme la couleur de l'échiquier).
+  Cependant, toucher à la base 'IndexedDB' risque de supprimer des parties.
+
+.question.
+  Mais j'aimerais supprimer cette partie là, et celle-là aussi !
+.answer.
+  Cliquez sur la zone du résultat d'une partie depuis les listes "Mes parties",
+  et confirmez la suppression. Si la partie est en cours elle sera d'abord
+  arrêtée. Notez que les parties par correspondance supprimées par un seul des
+  deux joueurs sont toujours accessibles : elles vous sont juste cachées.
+
+.question.
+  J'ai une question sans réponse ici !
+.answer.
+  Si la réponse n'est pas 42 (êtes vous bien sûr ?), utilisez le formulaire de
+  Contact ou demandez sur Discord :-)
+  Cf. les liens en bas de la page.
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 9ee109f5..f14632e3 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -84,7 +84,6 @@ export const translations = {
   "New correspondance game:": "Nouvelle partie par corespondance :",
   "New game": "Nouvelle partie",
   "New problem": "Nouveau problème",
-  News: "Nouvelles",
   "No challenges found :( Click on 'New game'!": "Aucun défi trouvé :( Cliquez sur 'Nouvelle partie' !",
   "No games found :( Send a challenge!": "Aucune partie trouvée :( Envoyez un défi !",
   "No more problems": "Plus de problèmes",
diff --git a/client/src/variants/Teleport.js b/client/src/variants/Teleport.js
index 2379692a..93648c48 100644
--- a/client/src/variants/Teleport.js
+++ b/client/src/variants/Teleport.js
@@ -299,10 +299,13 @@ export class TeleportRules extends ChessRules {
         moves2.forEach(m2 => {
           this.play(m2);
           const score = this.getCurrentScore();
-          const mvEval =
-            ["1-0", "0-1"].includes(score)
-              ? (score == "1-0" ? 1 : -1) * maxeval
-              : (score == "1/2" ? 0 : initEval);
+          let mvEval = 0;
+          if (["1-0", "0-1"].includes(score))
+            mvEval = (score == "1-0" ? 1 : -1) * maxeval;
+          else if (score == "*")
+            // Add small fluctuations to avoid dropping pieces always on the
+            // first square available.
+            mvEval = initEval + 0.05 - Math.random() / 10;
           if (
             (color == 'w' && mvEval > m.eval) ||
             (color == 'b' && mvEval < m.eval)
diff --git a/client/src/views/Faq.vue b/client/src/views/Faq.vue
new file mode 100644
index 00000000..a87e6bd7
--- /dev/null
+++ b/client/src/views/Faq.vue
@@ -0,0 +1,67 @@
+<template lang="pug">
+main
+  .row
+    .col-sm-12.col-md-8.col-md-offset-2.col-lg-6.col-lg-offset-3
+      div#faqDiv(v-html="content")
+</template>
+
+<script>
+import { store } from "@/store";
+export default {
+  name: "my-faq",
+  data: function() {
+    return { st: store.state };
+  },
+  computed: {
+    content: function() {
+      // (AJAX) Request to get FAQ content (plain text, HTML)
+      return (
+        require("raw-loader!@/translations/faq/" + this.st.lang + ".pug")
+        // Next two lines fix a weird issue after last update (2019-11)
+        .replace(/\\n/g, " ")
+        .replace(/\\"/g, '"')
+        .replace('module.exports = "', "")
+        .replace(/"$/, "")
+      );
+    }
+  },
+  mounted: function() {
+    this.re_setListeners();
+  },
+  updated: function() {
+    this.re_setListeners();
+  },
+  methods: {
+    re_setListeners: function() {
+      document.querySelectorAll(".answer").forEach(a => {
+        a.style.display = "none";
+      });
+      document.querySelectorAll(".question").forEach(q => {
+        q.addEventListener("click", (e) => {
+          let answerDiv = e.target.nextSibling;
+          const answerVisible = (answerDiv.style.display == "block");
+          answerDiv.style.display = (answerVisible ? "none" : "block");
+        });
+      });
+    }
+  }
+};
+</script>
+
+<style lang="sass">
+#faqDiv
+  @media screen and (max-width: 767px)
+    margin-left: var(--universal-margin)
+    margin-right: var(--universal-margin)
+
+.question
+  color: darkblue
+  font-size: 1.1em
+  margin-top: 15px
+  cursor: pointer
+
+.answer
+  ol, ul
+    margin-top: 0
+    margin-bottom: 0
+</style>
diff --git a/client/src/views/News.vue b/client/src/views/News.vue
deleted file mode 100644
index 664b5957..00000000
--- a/client/src/views/News.vue
+++ /dev/null
@@ -1,245 +0,0 @@
-<template lang="pug">
-main
-  input#modalNews.modal(type="checkbox")
-  div#newnewsDiv(
-    role="dialog"
-    data-checkbox="modalNews"
-  )
-    .card#writeNews
-      label.modal-close(for="modalNews")
-      textarea#newsContent(
-        v-model="curnews.content"
-        :placeholder="st.tr['News go here']"
-        @input="adjustHeight"
-      )
-      button(@click="sendNews()") {{ st.tr["Send"] }}
-      #dialog.text-center {{ st.tr[infoMsg] }}
-  .row
-    .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
-      button#writeNewsBtn(
-        v-if="devTeam"
-        @click="showModalNews"
-      )
-        | {{ st.tr["Write news"] }}
-      .news(
-        v-for="n,idx in newsList"
-        :id="'n' + n.id"
-        :class="{margintop:idx>0}"
-      )
-        span.ndt {{ formatDatetime(n.added) }}
-        .dev-buttons(v-if="devTeam")
-          button(@click="editNews(n)") {{ st.tr["Edit"] }}
-          button(@click="deleteNews(n)") {{ st.tr["Delete"] }}
-        button(@click="gotoPrevNext(n, 1)") {{ st.tr["Previous_n"] }}
-        button(@click="gotoPrevNext(n, -1)") {{ st.tr["Next_n"] }}
-        .news-content(v-html="parseHtml(n.content)")
-      button#loadMoreBtn(
-        v-if="hasMore"
-        @click="loadMore()"
-      )
-        | {{ st.tr["Load more"] }}
-</template>
-
-<script>
-import { store } from "@/store";
-import { ajax } from "@/utils/ajax";
-import params from "@/parameters";
-import { getDate, getTime } from "@/utils/datetime";
-import { processModalClick } from "@/utils/modalClick";
-export default {
-  name: "my-news",
-  data: function() {
-    return {
-      st: store.state,
-      // timestamp of oldest showed news:
-      cursor: Number.MAX_SAFE_INTEGER,
-      // hasMore == TRUE: a priori there could be more news to load
-      hasMore: true,
-      curnews: { id: 0, content: "" },
-      newsList: [],
-      infoMsg: ""
-    };
-  },
-  computed: {
-    devTeam: function() {
-      return params.devs.includes(this.st.user.id);
-    }
-  },
-  created: function() {
-    ajax(
-      "/news",
-      "GET",
-      {
-        data: { cursor: this.cursor },
-        success: (res) => {
-          // The returned list is sorted from most recent to oldest
-          this.newsList = res.newsList;
-          const L = res.newsList.length;
-          if (L > 0) this.cursor = res.newsList[L - 1].added;
-          else this.hasMore = false;
-        }
-      }
-    );
-  },
-  mounted: function() {
-    // Mark that I've read the news:
-    localStorage.setItem("newsRead", Date.now());
-    if (this.st.user.id > 0) ajax("/newsread", "PUT");
-    document.getElementById("newsMenu").classList.remove("somenews");
-    document
-      .getElementById("newnewsDiv")
-      .addEventListener("click", processModalClick);
-  },
-  methods: {
-    formatDatetime: function(dt) {
-      const dtObj = new Date(dt);
-      const timePart = getTime(dtObj);
-      // Show minutes but not seconds:
-      return (
-        getDate(dtObj) + " " + timePart.substr(0, timePart.lastIndexOf(":"))
-      );
-    },
-    parseHtml: function(txt) {
-      return !txt.match(/<[/a-zA-Z]+>/)
-        ? txt.replace(/\n/g, "<br/>") //no HTML tag
-        : txt;
-    },
-    adjustHeight: function() {
-      const newsContent = document.getElementById("newsContent");
-      // https://stackoverflow.com/a/995374
-      newsContent.style.height = "1px";
-      newsContent.style.height = 10 + newsContent.scrollHeight + "px";
-    },
-    resetCurnews: function() {
-      this.curnews.id = 0;
-      this.curnews.content = "";
-      // No need for added and uid fields: never updated
-    },
-    gotoPrevNext: function(n, dir) {
-      document.getElementById("n" + n.id)[
-        (dir < 0 ? "previous" : "next") + "ElementSibling"].scrollIntoView();
-    },
-    showModalNews: function() {
-      this.resetCurnews();
-      window.doClick("modalNews");
-    },
-    sendNews: function() {
-      const edit = this.curnews.id > 0;
-      this.infoMsg = "Processing... Please wait";
-      ajax(
-        "/news",
-        edit ? "PUT" : "POST",
-        {
-          data: { news: this.curnews },
-          success: (res) => {
-            if (edit) {
-              let n = this.newsList.find(n => n.id == this.curnews.id);
-              if (!!n) n.content = this.curnews.content;
-            } else {
-              const newNews = {
-                content: this.curnews.content,
-                added: Date.now(),
-                uid: this.st.user.id,
-                id: res.id
-              };
-              this.newsList = [newNews].concat(this.newsList);
-            }
-            document.getElementById("modalNews").checked = false;
-            this.infoMsg = "";
-            this.resetCurnews();
-          }
-        }
-      );
-    },
-    editNews: function(n) {
-      this.curnews.content = n.content;
-      this.curnews.id = n.id;
-      // No need for added and uid fields: never updated
-      window.doClick("modalNews");
-    },
-    deleteNews: function(n) {
-      if (confirm(this.st.tr["Are you sure?"])) {
-        this.infoMsg = "Processing... Please wait";
-        ajax(
-          "/news",
-          "DELETE",
-          {
-            data: { id: n.id },
-            success: () => {
-              const nIdx = this.newsList.findIndex(nw => nw.id == n.id);
-              this.newsList.splice(nIdx, 1);
-              this.infoMsg = "";
-              document.getElementById("modalNews").checked = false;
-            }
-          }
-        );
-      }
-    },
-    loadMore: function() {
-      ajax(
-        "/news",
-        "GET",
-        {
-          data: { cursor: this.cursor },
-          success: (res) => {
-            const L = res.newsList.length;
-            if (L > 0) {
-              this.newsList = this.newsList.concat(res.newsList);
-              this.cursor = res.newsList[L - 1].added;
-            } else this.hasMore = false;
-          }
-        }
-      );
-    }
-  }
-};
-</script>
-
-<style lang="sass" scoped>
-[type="checkbox"].modal+div .card
-  max-width: 767px
-  max-height: 100%
-
-textarea#newsContent
-  margin: 0
-  width: 100%
-  min-height: 200px
-  max-height: 100%
-
-#dialog
-  padding: 5px
-  color: blue
-
-#writeNews
-  padding-top: 50px
-
-button#writeNewsBtn, button#loadMoreBtn
-  margin-top: 0
-  margin-bottom: 0
-
-span.ndt
-  color: darkblue
-  padding: 0 5px 0 var(--universal-margin)
-
-.margintop
-  margin-top: 25px
-  border-top: 1px solid grey
-@media screen and (max-width: 767px)
-  .margintop
-    margin-top: 10px
-</style>
-
-<style lang="sass">
-.news
-  padding-top: 10px
-  & > .dev-buttons
-    display: inline-block
-  & > .news-content
-    margin: var(--universal-margin)
-    & > p
-      margin: 10px 0
-    & > br
-      display: block
-      margin-top: 10px
-      content: " "
-</style>
diff --git a/server/db/create.sql b/server/db/create.sql
index 56ae4f19..84b72b99 100644
--- a/server/db/create.sql
+++ b/server/db/create.sql
@@ -31,14 +31,6 @@ create table Problems (
   foreign key (vid) references Variants(id)
 );
 
-create table News (
-  id integer primary key,
-  uid integer,
-  added datetime,
-  content text,
-  foreign key (uid) references Users(id)
-);
-
 create table Challenges (
   id integer primary key,
   added datetime,
diff --git a/server/models/News.js b/server/models/News.js
deleted file mode 100644
index c10f71f2..00000000
--- a/server/models/News.js
+++ /dev/null
@@ -1,78 +0,0 @@
-const db = require("../utils/database");
-
-/*
- * Structure:
- *   id: integer
- *   added: datetime
- *   uid: user id (int)
- *   content: text
- */
-
-const NewsModel =
-{
-  create: function(content, uid, cb)
-  {
-    db.serialize(function() {
-      const query =
-        "INSERT INTO News " +
-        "(added, uid, content) " +
-          "VALUES " +
-        "(" + Date.now() + "," + uid + ",?)";
-      db.run(query, content, function(err) {
-        cb(err, { id: this.lastID });
-      });
-    });
-  },
-
-  getNext: function(cursor, cb)
-  {
-    db.serialize(function() {
-      const query =
-        "SELECT * " +
-        "FROM News " +
-        "WHERE added < " + cursor + " " +
-        "ORDER BY added DESC " +
-        "LIMIT 10"; //TODO: 10 currently hard-coded
-      db.all(query, (err, newsList) => {
-        cb(err, newsList);
-      });
-    });
-  },
-
-  getTimestamp: function(cb)
-  {
-    db.serialize(function() {
-      const query =
-        "SELECT added " +
-        "FROM News " +
-        "ORDER BY added DESC " +
-        "LIMIT 1";
-      db.get(query, (err, ts) => {
-        cb(err, ts);
-      });
-    });
-  },
-
-  update: function(news)
-  {
-    db.serialize(function() {
-      let query =
-        "UPDATE News " +
-        "SET content = ? " +
-        "WHERE id = " + news.id;
-      db.run(query, news.content);
-    });
-  },
-
-  remove: function(id)
-  {
-    db.serialize(function() {
-      const query =
-        "DELETE FROM News " +
-        "WHERE id = " + id;
-      db.run(query);
-    });
-  },
-}
-
-module.exports = NewsModel;
diff --git a/server/routes/all.js b/server/routes/all.js
index 0c02129f..979b0d9c 100644
--- a/server/routes/all.js
+++ b/server/routes/all.js
@@ -7,6 +7,5 @@ router.use("/", require("./messages"));
 router.use("/", require("./users"));
 router.use("/", require("./variants"));
 router.use("/", require("./problems"));
-router.use("/", require("./news"));
 
 module.exports = router;
diff --git a/server/routes/news.js b/server/routes/news.js
deleted file mode 100644
index e78020ef..00000000
--- a/server/routes/news.js
+++ /dev/null
@@ -1,55 +0,0 @@
-let router = require("express").Router();
-const access = require("../utils/access");
-const params = require("../config/parameters");
-const NewsModel = require("../models/News");
-const sanitizeHtml = require('sanitize-html');
-
-router.post("/news", access.logged, access.ajax, (req,res) => {
-  if (params.devs.includes(req.userId)) {
-    const content = sanitizeHtml(req.body.news.content);
-    NewsModel.create(content, req.userId, (err, ret) => {
-      res.json(err || ret);
-    });
-  }
-});
-
-router.get("/news", access.ajax, (req,res) => {
-  const cursor = req.query["cursor"];
-  if (!!cursor && !!cursor.match(/^[0-9]+$/)) {
-    NewsModel.getNext(cursor, (err, newsList) => {
-      res.json(err || { newsList: newsList });
-    });
-  }
-});
-
-router.get("/newsts", access.ajax, (req,res) => {
-  // Special query for footer: just return timestamp of last news
-  NewsModel.getTimestamp((err, ts) => {
-    res.json(err || { timestamp: !!ts ? ts.added : 0 });
-  });
-});
-
-router.put("/news", access.logged, access.ajax, (req,res) => {
-  let news = req.body.news;
-  if (
-    params.devs.includes(req.userId) &&
-    news.id.toString().match(/^[0-9]+$/)
-  ) {
-    news.content = sanitizeHtml(news.content);
-    NewsModel.update(news);
-    res.json({});
-  }
-});
-
-router.delete("/news", access.logged, access.ajax, (req,res) => {
-  const nid = req.query.id;
-  if (
-    params.devs.includes(req.userId) &&
-    nid.toString().match(/^[0-9]+$/)
-  ) {
-    NewsModel.remove(nid);
-    res.json({});
-  }
-});
-
-module.exports = router;
-- 
2.44.0