Compare commits

..

40 Commits

Author SHA1 Message Date
Sergiotarxz a2999657a8 Fixing unclosed tag. 2024-02-10 21:14:14 +01:00
Sergiotarxz 003f4972cb Adding inital select battle support. 2024-01-24 01:09:33 +01:00
Sergiotarxz 34723db31c Adding initial battle bar. 2024-01-23 20:19:26 +01:00
Sergiotarxz 58c30c1059 Addding initial support for listing enemies. 2024-01-22 01:08:28 +01:00
Sergiotarxz 08f539bf42 Improving navigation and using an arrow icon instead of a circle for the player. 2024-01-15 20:10:57 +01:00
Sergiotarxz c945cc453b Adding initial arrow player support. 2024-01-15 16:24:00 +01:00
Sergiotarxz 40e392e003 Improving the number of requests for team. 2024-01-14 22:17:54 +01:00
Sergiotarxz 278c7c5112 Adding refreshLayers after betraying your team. 2024-01-14 22:07:59 +01:00
Sergiotarxz 389f325618 Adding initial support for conquering nodes. 2024-01-14 22:02:49 +01:00
Sergiotarxz 16888b9fdb Migrating to postgis. 2024-01-14 05:45:56 +01:00
Sergiotarxz 5f70116da2 Splitting templates in files. 2024-01-14 04:36:36 +01:00
Sergiotarxz 30188a1a76 Completed ability to choose a node as player. 2024-01-14 04:18:14 +01:00
Sergiotarxz 35a65cfa1f Adding the UI to select a team. 2024-01-13 23:14:14 +01:00
Sergiotarxz e3708066e6 Finished create team endpoint and interface. 2024-01-13 20:29:40 +01:00
Sergiotarxz ed48de1c38 Creating the create team interface. 2024-01-13 19:59:11 +01:00
Sergiotarxz 589782365b Adding initial support to looking at you team data on profile. 2024-01-13 19:10:40 +01:00
Sergiotarxz 9279e6388a Adding initial team support. 2024-01-13 01:17:57 +01:00
Sergiotarxz c38474614d Adding initial support for showing node contents. 2024-01-01 01:36:10 +01:00
Sergiotarxz 7994119d66 First bugfix of the year, actually removing free move from status. 2024-01-01 00:53:37 +01:00
Sergiotarxz ac21ac1387 Adding the ability to free move in the map. 2023-12-31 21:48:11 +01:00
Sergiotarxz d9e6e664f2 Splitting templates to different files. 2023-12-31 20:56:07 +01:00
Sergiotarxz 8c27095ad1 Adding the capability of create nodes. 2023-12-31 20:43:53 +01:00
Sergiotarxz 85a104caa5 Adding initial js support of protobuf. 2023-12-02 17:22:27 +01:00
Sergiotarxz 24b4f7db9f Merge branch 'main' of git.owlcode.tech:sergiotarxz/burguillos.info into conquer 2023-12-02 15:45:59 +01:00
Sergiotarxz 711f1dc845 Adding tile cache and preprocess. 2023-11-29 19:43:56 +01:00
Sergiotarxz 598dda2aae Merge branch 'main' of git.owlcode.tech:sergiotarxz/burguillos.info into conquer
Conflicts:
      js-src/index.js
      public/js/bundle.js
2023-11-29 18:09:14 +01:00
Sergiotarxz acec248f4d Adding initial create node support. 2023-11-28 21:10:12 +01:00
Sergiotarxz e5d9230a74 Adding files I forgot to commit. 2023-11-23 00:41:10 +01:00
Sergiotarxz d4927e2e1b Improving login workflow to make extensible UI. 2023-11-23 00:35:28 +01:00
Sergiotarxz d73ff6692a Adding semi-opaque overlay to avoid interation with the app without login. 2023-11-21 19:23:51 +01:00
Sergiotarxz 90d85ed4af Splitting login logic to a separated file to make the code easier to work with. 2023-11-21 19:16:30 +01:00
Sergiotarxz 064ec75ed3 Adding the ability to click on things. 2023-11-21 12:53:58 +01:00
Sergiotarxz f3f111060b Improving readbility of the javascript. 2023-11-20 21:21:50 +01:00
Sergiotarxz d6d827fe8d Working login. 2023-11-20 20:20:04 +01:00
Sergiotarxz 1447b2fa6e Adding initial login support. 2023-11-19 23:14:02 +01:00
Sergiotarxz 61b0066f0a Improving the serialize to owner method. 2023-11-19 19:44:09 +01:00
Sergiotarxz 3396c36529 Adding first working sign up. 2023-11-19 19:26:59 +01:00
Sergiotarxz 21d9f46d03 Initial create user support. 2023-11-17 23:16:54 +01:00
Sergiotarxz 2d1430ca87 Multiple bug fixes and adding initial UI support for register and login. 2023-11-13 21:13:21 +01:00
Sergiotarxz dd2ca2f786 Adding initial conquer support. 2023-11-13 17:32:12 +01:00
460 changed files with 7819 additions and 427231 deletions

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "conversejs"]
path = conversejs
url = https://github.com/conversejs/converse.js/

View File

@ -27,6 +27,13 @@ my $build = Module::Build->new(
'Module::Pluggable' => 0,
'List::AllUtils' => 0,
'Lingua::Stem::Snowball' => 0,
'Mojo::Redis' => 0,
'DBIx::Class' => 0,
'UUID::URandom' => 0,
'Crypt::Bcrypt' => 0,
'DBIx::Class::TimeStamp' => 0,
'DateTime::Format::HTTP' => 0,
'GIS::Distance' => 0,
},
);
$build->create_build_script;

View File

@ -1,14 +0,0 @@
<category>
<title>Dentistas en Burguillos.</title>
<description>
<p>Te traemos una lista de dentistas en Burguillos para que puedas cuidar de la salud y estética de tu boca con los mejores profesionales.</p>
<p>¿Quieres que tu comercio aparezca aquí? Contacta con <a href="mailto:contact@owlcode.tech">contact@owlcode.tech</a></p>
<p><a rel="noreferrer nofollow" href="https://www.freepik.es/vector-gratis/mujer-dentista-examinando-dientes-paciente-sobre-fondo-blanco_24553482.htm">Imagen de preview cortesía de Brgfx</a>.</p>
</description>
<priority>2</priority>
<img src="/img/dentista.webp" bottom-preview="530"/>
<menu_text>Dentistas</menu_text>
<slug>dentistas</slug>
<parent>comercios</parent>
</category>

View File

@ -3,7 +3,7 @@
<date>2022-11-19T18:03+00:00</date>
<title>Centro Médico Juan Manuel Pérez Sanchez - Datos de Contacto - Pedir Cita</title>
<ogdesc>Centro Médico Juan Manuel Pérez Sanchez - Datos de Contacto - Pedir Cita</ogdesc>
<last_modification_date>2023-05-03T00:59+00:00</last_modification_date>
<last_modification_date>2023-05-03T00:59+00:00</last_modification_date>
<category>comercios</category>
<slug>centro-medico-juan-manuel-perez-sanchez</slug>
<img src="/img/policlinica-burguillos-preview.webp"/>
@ -13,7 +13,7 @@
<p>La clínica Juan Manuel Pérez Sanchez es un centro de atención a la salud dedicado a las siguientes especialidades:</p>
<ul>
<li>Odontologia general, Ortodoncia, Ortodoncia Invisible, Odontopediatría, Estetica Dental y Labios, Protesis y Aparatos Dentales, Implantes...</li>
<li>Odontologia general, Ortodoncia, Ortodoncia Invisible, Odontopediatria, Estetica Dental y Labios, Protesis y Aparatos Dentales, Implantes...</li>
<li>Clínica concertada del plan de atención infantil de la Junta de Andalucía.</li>
<li>Radiografías Panorex-Teleradiografía.</li>
<li>Pedagogia, Especialista en trastornos de lenguaje y audicion, Clases de apoyo, Talleres, Tramitacion de becas escolares.</li>

View File

@ -27,109 +27,400 @@
<a href="tel:+34621210460">621 210 460</a>.</p>
<p>A continuación procedemos a dejar la carta para que podáis
realizar el pedido que deseeis:</p>
<details>
<summary><h2>Entrantes</h2></summary>
<ul>
<li>Ensaladilla -- Tapa: <b>2.50€</b> 1/2 Ración: <b>5.00€</b> Ración: <b>10.00€</b></li>
<li>Aliño de Pimientos -- Tapa: <b>2.50€</b> 1/2 Ración: <b>5.00€</b> Ración: <b>10.00€</b></li>
<li>Aliño de Pulpo -- Tapa: <b>2.50€</b> 1/2 Ración: <b>5.00€</b> Ración: <b>10.00€</b></li>
<li>Aliño de Huevas -- Tapa: <b>2.50€</b> 1/2 Ración: <b>5.00€</b> Ración: <b>10.00€</b></li>
<li>Huevas con Mayonesa -- Tapa: <b>2.50€</b> 1/2 Ración: <b>5.00€</b> Ración: <b>10.00€</b></li>
<li>Ensalada Mixta -- Ración: <b>4.00€</b></li>
<li>Ensalada Normal -- Ración: <b>3.00€</b></li>
</ul>
</details>
<details>
<summary><h2>Aperitivos</h2></summary>
<ul>
<li>Papas Bravas -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Papas Alioli Calientes -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Papas Alioli Frías -- Tapa: <b>2.50€</b> 1/2 Ración: <b>5.00€</b> Ración: <b>10.00€</b></li>
<li>Croquetas de Jamón -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Croquetas de Cola de Toro -- Tapa: <b>3.50€</b> 1/2 Ración: <b>7.00€</b> Ración: <b>14.00€</b></li>
<li>Nugget de Pollo -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Cachopo -- Ración: <b>8.00€</b></li>
</ul>
</details>
<details>
<summary><h2>En Temporada</h2></summary>
<ul>
<li>Cabrillas -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Caracoles -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
</ul>
</details>
<details>
<summary><h2>Ibéricos</h2></summary>
<ul>
<li>Secreto -- Ración: <b>S-P</b></li>
<li>Lagrimitas -- Tapa: <b>3.50€</b> 1/2 Ración: <b>7.00€</b> Ración: <b>14.00€</b></li>
<li>Lagarto -- Tapa: <b>3.50€</b> 1/2 Ración: <b>7.00€</b> Ración: <b>14.00€</b></li>
<li>Tocinito -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
</ul>
</details>
<details>
<summary><h2>Carnes</h2></summary>
<ul>
<li>Carne Asá -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Solomillo al Whisky -- Tapa: <b>3.50€</b> 1/2 Ración: <b>8.00€</b> Ración: <b>16.00€</b></li>
<li>Solomillo a la Pimienta -- Tapa: <b>3.50€</b> 1/2 Ración: <b>8.00€</b> Ración: <b>16.00€</b></li>
<li>Solomillo al Roquefort -- Tapa: <b>3.50€</b> 1/2 Ración: <b>8.00€</b> Ración: <b>16.00€</b></li>
<li>Churrasco de Pollo/Cerdo -- Ración: <b>5.50€</b></li>
<li>Mini Serranito de Pollo/Cerdo -- Ración: <b>3.00€</b></li>
<li>Serranito de Pollo/Cerdo -- Ración: <b>5.00€</b></li>
<li>Pechuga de Pollo -- Ración: <b>5.50€</b></li>
<li>Pinchito de Pollo/Cerdo -- Ración: <b>3.00€</b></li>
<li>Brocheta de Solomillo -- Ración: <b>7.00€</b></li>
<li>Hamburguesa Simple -- Ración: <b>2.50€</b></li>
<li>Hamburguesa Completa -- Ración: <b>3.00€</b></li>
<li>Hamburguesa de Buey -- Ración: <b>5.00€</b></li>
</ul>
</details>
<details>
<summary><h2>Montaditos</h2></summary>
<ul>
<li>Montadito de Pollo/Cerdo -- Precio: <b>2.50€</b></li>
<li>Mantecadito de Pollo/Cerdo -- Precio: <b>3.00€</b></li>
<li>Montadito de Gambas -- Precio: <b>3.00€</b></li>
</ul>
</details>
<details>
<summary><h2>Cazuelitas</h2></summary>
<ul>
<li>Carne con tomate -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Carrillada Ibérica -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Atún Encebollado -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Bacalao con Tomate -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Espinacas -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
</ul>
</details>
<details>
<summary><h2>Pescados</h2></summary>
<ul>
<li>Chipirón a la Plancha/Frito -- Tapa: <b>3.50€</b> 1/2 Ración: <b>8.00€</b> Ración: <b>16.00€</b></li>
<li>Calamares Fritos -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Choco Frito -- Tapa: <b>3.50€</b> 1/2 Ración: <b>7.00€</b> Ración: <b>14.00€</b></li>
<li>Boquerones -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Acedias -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Puntillitas -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Adobo -- Tapa: <b>3.00€</b> 1/2 Ración: <b>6.00€</b> Ración: <b>12.00€</b></li>
<li>Brocheta de Gambas -- Tapa: <b>3.00€</b> 1/2 Ración: <b>8.00€</b> Ración: <b>14.00€</b></li>
<li>Pescado Variado -- 1/2 Ración: <b>7.00€</b> Ración: <b>14.00€</b></li>
<li>Pez Espada -- Ración: <b>8.00€</b></li>
<li>Merluza -- Ración: <b>6.00€</b></li>
<li>Dorada -- Ración: <b>6.00€</b></li>
</ul>
</details>
<details>
<summary><h2>Postres</h2></summary>
<ol>
<li>Arroz con leche <b>2.50€</b></li>
<li>Natillas <b>2.50€</b></li>
<li>Flan de Huevo <b>2.50€</b></li>
<li>Helados Nestle <b>3.00€</b></li>
</ol>
</details>
<h2>Entrantes</h2>
<table>
<tr>
<th></th>
<th>Tapa</th>
<th>1/2 Ración</th>
<th>Ración</th>
</tr>
<tr>
<td>Ensaladilla</td>
<td>2.50€</td>
<td>5.00€</td>
<td>10.00€</td>
</tr>
<tr>
<td>Aliño de Pimientos</td>
<td>2.50€</td>
<td>5.00€</td>
<td>10.00€</td>
</tr>
<tr>
<td>Aliño de Pulpo</td>
<td>2.50€</td>
<td>5.00€</td>
<td>10.00€</td>
</tr>
<tr>
<td>Aliño de Huevas</td>
<td>2.50€</td>
<td>5.00€</td>
<td>10.00€</td>
</tr>
<tr>
<td>Huevas con Mayonesa</td>
<td>2.50€</td>
<td>5.00€</td>
<td>10.00€</td>
</tr>
<tr>
<td>Ensalada Mixta</td>
<td>-</td>
<td>-</td>
<td>4.00€</td>
</tr>
<tr>
<td>Ensalada Normal</td>
<td>-</td>
<td>-</td>
<td>3.00€</td>
</tr>
</table>
<h2>Aperitivos</h2>
<table>
<tr>
<th></th>
<th>Tapa</th>
<th>1/2 Ración</th>
<th>Ración</th>
</tr>
<tr>
<td>Papas Bravas</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Papas Alioli Calientes</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Papas Alioli Frías</td>
<td>2.50€</td>
<td>5.00€</td>
<td>10.00€</td>
</tr>
<tr>
<td>Croquetas de Jamón</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Croquetas de Cola de Toro</td>
<td>3.50€</td>
<td>7.00€</td>
<td>14.00€</td>
</tr>
<tr>
<td>Nugget de Pollo</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Cachopo</td>
<td>--</td>
<td>--</td>
<td>8.00€</td>
</tr>
</table>
<h2>En Temporada</h2>
<table>
<tr>
<th></th>
<th>Tapa</th>
<th>1/2 Ración</th>
<th>Ración</th>
</tr>
<tr>
<td>Cabrillas</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Caracoles</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
</table>
<h2>Ibéricos</h2>
<table>
<tr>
<th></th>
<th>Tapa</th>
<th>1/2 Ración</th>
<th>Ración</th>
</tr>
<tr>
<td>Secreto</td>
<td>--</td>
<td>--</td>
<td>S-P</td>
</tr>
<tr>
<td>Lagrimitas</td>
<td>3.50€</td>
<td>7.00€</td>
<td>14.00€</td>
</tr>
<tr>
<td>Lagarto</td>
<td>3.50€</td>
<td>7.00€</td>
<td>14.00€</td>
</tr>
<tr>
<td>Tocinito</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
</table>
<h2>Carnes</h2>
<table>
<tr>
<th></th>
<th>Tapa</th>
<th>1/2 Ración</th>
<th>Ración</th>
</tr>
<tr>
<td>Carne Asá</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Solomillo al Whisky</td>
<td>3.50€</td>
<td>8.00€</td>
<td>16.00€</td>
</tr>
<tr>
<td>Solomillo a la Pimienta</td>
<td>3.50€</td>
<td>8.00€</td>
<td>16.00€</td>
</tr>
<tr>
<td>Solomillo al Roquefort</td>
<td>3.50€</td>
<td>8.00€</td>
<td>16.00€</td>
</tr>
<tr>
<td>Churrasco de Pollo/Cerdo</td>
<td>--</td>
<td>--</td>
<td>5.50€</td>
</tr>
<tr>
<td>Mini Serranito de Pollo/Cerdo</td>
<td>--</td>
<td>--</td>
<td>3.00€</td>
</tr>
<tr>
<td>Serranito de Pollo/Cerdo</td>
<td>--</td>
<td>--</td>
<td>5.00€</td>
</tr>
<tr>
<td>Pechuga de Pollo</td>
<td>--</td>
<td>--</td>
<td>5.50€</td>
</tr>
<tr>
<td>Pinchito de Pollo/Cerdo</td>
<td>--</td>
<td>--</td>
<td>3.00€</td>
</tr>
<tr>
<td>Brocheta de Solomillo</td>
<td>--</td>
<td>--</td>
<td>7.00€</td>
</tr>
<tr>
<td>Hamburguesa Simple</td>
<td>--</td>
<td>--</td>
<td>2.50€</td>
</tr>
<tr>
<td>Hamburguesa Completa</td>
<td>--</td>
<td>--</td>
<td>3.00€</td>
</tr>
<tr>
<td>Hamburguesa de Buey</td>
<td>--</td>
<td>--</td>
<td>5.00€</td>
</tr>
</table>
<h2>Montaditos</h2>
<table>
<tr>
<th></th>
<th>Precio</th>
</tr>
<tr>
<td>Montadito de Pollo/Cerdo</td>
<td>2.50€</td>
</tr>
<tr>
<td>Mantecadito de Pollo/Cerdo</td>
<td>3.00€</td>
</tr>
<tr>
<td>Montadito de Gambas</td>
<td>3.00€</td>
</tr>
</table>
<h2>Cazuelitas</h2>
<table>
<tr>
<th></th>
<th>Tapa</th>
<th>1/2 Ración</th>
<th>Ración</th>
</tr>
<tr>
<td>Carne con tomate</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Carrillada Ibérica</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Atún Encebollado</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Bacalao con Tomate</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Espinacas</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
</table>
<h2>Pescados</h2>
<table>
<tr>
<th></th>
<th>Tapa</th>
<th>1/2 Ración</th>
<th>Ración</th>
</tr>
<tr>
<td>Chipirón a la Plancha/Frito</td>
<td>3.50€</td>
<td>8.00€</td>
<td>16.00€</td>
</tr>
<tr>
<td>Calamares Fritos</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Choco Frito</td>
<td>3.50€</td>
<td>7.00€</td>
<td>14.00€</td>
</tr>
<tr>
<td>Boquerones</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Acedias</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Puntillitas</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Adobo</td>
<td>3.00€</td>
<td>6.00€</td>
<td>12.00€</td>
</tr>
<tr>
<td>Brocheta de Gambas</td>
<td>3.00€</td>
<!-- Precio raro preguntar. -->
<td>8.00€</td>
<td>14.00€</td>
</tr>
<tr>
<td>Pescado Variado</td>
<td>--</td>
<td>7.00€</td>
<td>14.00€</td>
</tr>
<tr>
<td>Pez Espada</td>
<td>--</td>
<td>--</td>
<td>8.00€</td>
</tr>
<tr>
<td>Merluza.</td>
<td>--</td>
<td>--</td>
<td>6.00€</td>
</tr>
<tr>
<td>Dorada.</td>
<td>--</td>
<td>--</td>
<td>6.00€</td>
</tr>
</table>
<h2>Postres</h2>
<ol>
<li>Arroz con leche
<b>2.50€</b></li>
<li>Natillas
<b>2.50€</b></li>
<li>Flan de Huevo
<b>2.50€</b></li>
<li>Helados Nestle
<b>3.00€</b></li>
</ol>
<p>Por último desde Burguillos.info os indicamos que el precio
por el servicio de pan y picos por comensal es de 0.50€ y que
tenéis la posibilidad de pedir un extra en salsas por

View File

@ -14,6 +14,7 @@
</attributes>
<slug>cafe-bar-beluche</slug>
<content>
<h2 style="background: black; color: red;">Menú actualizado con nuevos precios y productos 2023-09-13</h2>
<img width="50%" alt="Front door of 'Café-Bar Beluche'" style="border: solid 1px black;" src="/img/beluche.webp"/>
<h2>Información de contacto para preparación de pedidos y envío a domicilio.</h2>
@ -22,119 +23,402 @@
<p>Su ubicación en calle Albahaca número 13 es inmejorable, ofreciendo terrazas a parte de mesas en el interior.</p>
<p>El teléfono de contacto es <a href="tel:+34691492054">691 492 054</a>, puedes usarlo para reservar, pedir comida a domicilio o pedir que te preparen platos para llevar.</p>
<p>Los teléfonos de contacto son <a href="tel:+34694200713">694 200 713</a> y <a href="tel:+34691492054">691 492 054</a>, puedes usarlos para reservar, pedir comida a domicilio o pedir que te preparen platos para llevar.</p>
<p>Procedemos a transcribir la carta a continuación:</p>
<details>
<summary><h3>Ensaladas</h3></summary>
<ul>
<li>Mixta -- Precio: <b>6.00€</b></li>
<li>César -- Precio: <b>6.50€</b></li>
<li>Trópical -- Precio: <b>N/A</b></li>
</ul>
</details>
<details>
<summary><h3>Revueltos</h3></summary>
<ul>
<li>Gula langostinos -- Precio: <b>7.50€</b></li>
<li>Bacalao dorado -- Precio: <b>7.50€</b></li>
<li>Morcilla de arroz -- Precio: <b>7.50€</b></li>
</ul>
</details>
<details>
<summary><h3>Tapas frías.</h3></summary>
<ul>
<li>Ensaladilla rusa -- Tapa: <b>3.00€</b> Plato: <b>6.00€</b></li>
<li>Aliños del día (Aliño Melva/Salpicón de marisco) -- Tapa: <b>3.00€</b> Plato: <b>6.00€</b></li>
<li>Cóctel de mariscos -- Plato: <b>4.00€</b></li>
<li>Ensaladilla de cangrejo -- Tapa: <b>3.50€</b> Plato: <b>7.00€</b></li>
</ul>
</details>
<details>
<summary><h3>Para compartir</h3></summary>
<ul>
<li>Papas bravas -- Precio tapa: <b>3.50€</b> Precio plato: <b>6.00€</b></li>
<li>Papas de mi prima -- Precio tapa: <b>3.50€</b> Precio plato: <b>6.00€</b></li>
<li>Papas arrieras -- Precio tapa: <b>3.80€</b> Precio plato: <b>7.00€</b></li>
<li>Bartolitos. (Langostinos con bacon) -- Precio tapa: <b>3.80€</b> Precio plato: <b>6.00€</b></li>
<li>Queso rulo con bacon, nueces y miel de caña -- Precio tapa: <b>3.80€</b> Precio plato: <b>6.00€</b></li>
<li>Morcilla crocanti -- Precio tapa: <b>3.50€</b> Precio plato: <b>6.00€</b></li>
<li>Muss de pato -- Precio tapa: <b>4.00€</b> Precio plato: <b>8.00€</b></li>
<li>Duo de rulo y muss de pato -- Precio plato: <b>7.00€</b></li>
<li>Talegitas de queso -- Precio tapa: <b>3.50€</b> Precio plato: <b>6.00€</b></li>
<li>Champiñones con alioli y jamón -- Precio tapa: <b>3.50€</b> Precio plato: <b>6.50€</b></li>
<li>Fideos tostados -- Precio plato: <b>4.00€</b></li>
<li>Variado de croquetas -- Precio plato: <b>11.00€</b></li>
<li>Croquetas de secreto y miel -- Precio tapa: <b>4.00€</b> Precio plato: <b>8.00€</b></li>
</ul>
</details>
<details>
<summary><h3>Rico rico</h3></summary>
<ul>
<li>Pollo kentucky -- Precio tapa: <b>3.50€</b> Precio plato: <b>7.00€</b></li>
<li>Pollo mostaza -- Precio tapa: <b>4.50€</b> Precio plato: <b>8.50€</b></li>
<li>Huevos rotos -- Precio plato: <b>5.00€</b></li>
<li>Pan bao -- Precio tapa: <b>5.00€</b> Precio plato: <b>10.00€</b></li>
<li>Carrillada ibérica -- Precio tapa: <b>3.50€ (Preguntar, en menú real 8.50€)</b> Precio plato: <b>7.00€</b></li>
</ul>
</details>
<details>
<summary><h3>Arroces</h3></summary>
<ul>
<li>Timbal de arroz con chipirones y alioli -- Tapa: <b>4.00€</b> Plato: <b>7.50€</b></li>
<li>Arroz negro -- Tapa: <b>4.50€</b> Plato: <b>8.50€</b></li>
<li>Rissotto cuatro quesos -- Tapa: <b>4.50€</b> Plato: <b>8.50€</b></li>
</ul>
</details>
<details>
<summary><h3>Pescados</h3></summary>
<ul>
<li>Gambas al ajillo -- Precio tapa: <b>4.50€</b> Precio plato: <b>8.00€</b></li>
<li>Gambas a la plancha -- Precio tapa: <b>5.00€</b> Precio plato: <b>10.00€</b></li>
<li>Flamenquín de melva -- Precio tapa: <b>3.50€</b> Precio plato: <b>7.00€</b></li>
<li>Chipirones -- Precio tapa: <b>4.00€</b> Precio plato: <b>7.50€</b></li>
<li>Choco -- Precio plato: <b>S/P</b></li>
<li>Lubina -- Precio plato: <b>S/P</b></li>
<li>Pez espada -- Precio plato: <b>12.00€</b></li>
<li>Merluza confitada -- Precio plato: <b>8.00€</b></li>
<li>Bacalao confitado -- Precio plato: <b>9.00€</b></li>
<li>Montadito de gambas con alioli -- Precio plato: <b>3.20€</b></li>
<li>Almejas con langostinos -- Precio tapa: <b>4.50€</b> Precio plato: <b>8.00€</b></li>
</ul>
</details>
<details>
<summary><h3>Carnes</h3></summary>
<ul>
<li>Solomillo de pavo -- Precio plato: <b>9.00€</b></li>
<li>Medallones de solomillo (Roque, whisky, mojo) -- Precio tapa: <b>3.50€</b> Precio plato: <b>6.50€</b></li>
<li>Solomillo rulo de queso y miel de caña -- Precio tapa: <b>4.00€</b> Precio plato: <b>7.50€</b></li>
<li>Solomillo ibérico -- Precio plato: <b>12.50€</b></li>
<li>Abanico ibérico -- Precio plato: <b>12.50€</b></li>
<li>Presa ibérica -- Precio plato: <b>14.00€</b></li>
<li>Presa con mostaza -- Precio tapa: <b>4.00€</b> Precio plato: <b>8.00€</b></li>
<li>Presa con setas y roquefort -- Precio tapa: <b>4.50€</b> Precio plato: <b>9.00€</b></li>
<li>Chuletón de ternera -- Precio plato: <b>S/P</b></li>
<li>Entrecot de ternera -- Precio plato: <b>S/P</b></li>
<li>Chuletón de vaca vieja madurada -- Precio plato: <b>S/P</b></li>
<li>Entrecot de vaca vieja -- Precio plato: <b>S/P</b></li>
<li>Hamburguesa de buey -- Precio plato: <b>5.50€</b></li>
<li>Mini hamburguesa -- Precio plato: <b>3.00€</b></li>
<li>Montadito de pollo o lomo -- Precio plato: <b>3.00€</b></li>
<li>Montadito de solomillo y queso viejo -- Precio plato: <b>3.80€</b></li>
<li>Serranito -- Precio plato: <b>5.50€</b></li>
<li>Mini serranito de pollo o lomo -- Precio plato: <b>4.00€</b></li>
</ul>
</details>
<details>
<summary><h3>Postres</h3></summary>
<ul>
<li>Gofres con nata y chocolate -- Precio: <b>3.80€</b></li>
<li>Tortitas americanas -- Precio: <b>4.00€</b></li>
<li>Tarta (Porción) -- Precio: <b>3.80€</b></li>
<li>Coulant con helado de vainilla -- Precio: <b>4.00€</b></li>
</ul>
</details>
<h3>Ensaladas</h3>
<table>
<tr>
<th>Nombre</th>
<th>Precio</th>
</tr>
<tr>
<td>Mixta</td>
<td>6.00€</td>
</tr>
<tr>
<td>César</td>
<td>6.50€</td>
</tr>
<tr>
<td>Trópical</td>
<td>6.50€</td>
</tr>
</table>
<h3>Revueltos</h3>
<table>
<tr>
<th>Nombre</th>
<th>Precio</th>
</tr>
<tr>
<td>Gula langostinos</td>
<td>7.50€</td>
</tr>
<tr>
<td>Bacalao dorado</td>
<td>7.50€</td>
</tr>
<tr>
<td>Morcilla de arroz</td>
<td>7.50€</td>
</tr>
</table>
<h3>Tapas frías.</h3>
<table>
<tr>
<th>Nombre</th>
<th>Tapa</th>
<th>Plato</th>
</tr>
<tr>
<td>Ensaladilla rusa</td>
<td>3.00€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Aliños del día (Aliño Melva/Salpicón de marisco)</td>
<td>3.00€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Cóctel de mariscos</td>
<td>---</td>
<td>4.00€</td>
</tr>
<tr>
<td>Ensaladilla de cangrejo</td>
<td>3.50€</td>
<td>7.00€</td>
</tr>
</table>
<h3>Para compartir</h3>
<table>
<tr>
<th>Nombre</th>
<th>Precio tapa</th>
<th>Precio plato</th>
</tr>
<tr>
<td>Papas bravas</td>
<td>3.50€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Papas de mi prima</td>
<td>3.50€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Papas arrieras</td>
<td>3.80€</td>
<td>7.00€</td>
</tr>
<tr>
<td>Bartolitos. (Langostinos con bacon)</td>
<td>3.80€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Queso rulo con bacon, nueces y miel de caña</td>
<td>3.80€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Morcilla crocanti</td>
<td>3.50€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Muss de pato</td>
<td>4.00€</td>
<td>8.00€</td>
</tr>
<tr>
<td>Duo de rulo y muss de pato</td>
<td>---</td>
<td>7.00€</td>
</tr>
<tr>
<td>Talegitas de queso</td>
<td>3.50€</td>
<td>6.00€</td>
</tr>
<tr>
<td>Champiñones con alioli y jamón</td>
<td>3.50€</td>
<td>6.50€</td>
</tr>
<tr>
<td>Fideos tostados</td>
<td>---</td>
<td>4.00€</td>
</tr>
<tr>
<td>Variado de croquetas</td>
<td>---</td>
<td>11.00€</td>
</tr>
<tr>
<td>Croquetas de secreto y miel</td>
<td>4.00€</td>
<td>8.00€</td>
</tr>
</table>
<h3>Rico rico</h3>
<table>
<tr>
<th>Nombre</th>
<th>Precio tapa</th>
<th>Precio plato</th>
</tr>
<tr>
<td>Pollo kentucky</td>
<td>---</td>
<td>3.50€</td>
</tr>
<tr>
<td>Pollo mostaza</td>
<td>4.50€</td>
<td>8.50€</td>
</tr>
<tr>
<td>Huevos rotos</td>
<td>---</td>
<td>5.00€</td>
</tr>
<tr>
<td>Pan bao</td>
<td>5.00€</td>
<td>10.00</td>
</tr>
<tr>
<td>Carrillada ibérica</td>
<td>3.50€</td>
<td>7.00€</td>
</tr>
</table>
<h3>Arroces</h3>
<table>
<tr>
<td>Nombre</td>
<td>Tapa</td>
<td>Plato</td>
</tr>
<tr>
<td>Timbal de arroz con chipirones y alioli</td>
<td>4.00€</td>
<td>7.50€</td>
</tr>
<tr>
<td>Arroz negro</td>
<td>4.50€</td>
<td>8.50€</td>
</tr>
<tr>
<td>Bissotio cuatro quesos</td>
<td>4.50€</td>
<td>8.50€</td>
</tr>
</table>
<h3>Pescados</h3>
<table>
<tr>
<th>Nombre</th>
<th>Precio tapa</th>
<th>Precio plato</th>
</tr>
<tr>
<td>Gambas al ajillo</td>
<td>4.50€</td>
<td>8.00€</td>
</tr>
<tr>
<td>Flamenquín de melva</td>
<td>3.50€</td>
<td>7.00€</td>
</tr>
<tr>
<td>Chipirones</td>
<td>4.00€</td>
<td>7.50€</td>
</tr>
<tr>
<td>Choco</td>
<td>---</td>
<td>S/P</td>
</tr>
<tr>
<td>Lubina</td>
<td>---</td>
<td>S/P</td>
</tr>
<tr>
<td>Pez espada</td>
<td>---</td>
<td>12.00€</td>
</tr>
<tr>
<td>Merluza confitada</td>
<td>---</td>
<td>8.00€</td>
</tr>
<tr>
<td>Bacalao confitado</td>
<td>---</td>
<td>9.00€</td>
</tr>
<tr>
<td>Montadito de gambas con alioli</td>
<td>---</td>
<td>3.20€</td>
</tr>
<tr>
<td>Almejas con langostinos</td>
<td>4.50€</td>
<td>8.00€</td>
</tr>
</table>
<h3>Carnes</h3>
<table>
<tr>
<th>Nombre</th>
<th>Precio tapa</th>
<th>Precio plato</th>
</tr>
<tr>
<td>Solomillo de pavo</td>
<td>---</td>
<td>9.00€</td>
</tr>
<tr>
<td>Medallones de solomillo (Roque, whisky, mojo)</td>
<td>3.50€</td>
<td>6.50€</td>
</tr>
<tr>
<td>Solomillo rulo de queso y miel de caña</td>
<td>4.00€</td>
<td>7.50€</td>
</tr>
<tr>
<td>Solomillo ibérico</td>
<td>---</td>
<td>12.50€</td>
</tr>
<tr>
<td>Abanico ibérico</td>
<td>---</td>
<td>12.50€</td>
</tr>
<tr>
<td>Presa ibérica</td>
<td>---</td>
<td>14.00€</td>
</tr>
<tr>
<td>Presa con mostaza</td>
<td>4.00€</td>
<td>8.00€</td>
</tr>
<tr>
<td>Presa con setas y roquefort</td>
<td>4.50€</td>
<td>9.00€</td>
</tr>
<tr>
<td>Chuletón de ternera</td>
<td>---</td>
<td>S/P</td>
</tr>
<tr>
<td>Entrecot de ternera</td>
<td>---</td>
<td>S/P</td>
</tr>
<tr>
<td>Chuletón de vaca vieja madurada</td>
<td>---</td>
<td>S/P</td>
</tr>
<tr>
<td>Entrecot de vaca vieja</td>
<td>---</td>
<td>S/P</td>
</tr>
<tr>
<td>Hamburguesa de buey</td>
<td>---</td>
<td>5.50€</td>
</tr>
<tr>
<td>Mini hamburguesa</td>
<td>---</td>
<td>3.00€</td>
</tr>
<tr>
<td>Montadito de pollo o lomo</td>
<td>---</td>
<td>3.00€</td>
</tr>
<tr>
<td>Montadito de solomillo y queso viejo</td>
<td>---</td>
<td>3.80</td>
</tr>
<tr>
<td>Serranito</td>
<td>---</td>
<td>5.50€</td>
</tr>
<tr>
<td>Mini de pollo o lomo</td>
<td>---</td>
<td>4.00€</td>
</tr>
</table>
<h3>Postres</h3>
<table>
<tr>
<th>Nombre</th>
<th>Precio</th>
</tr>
<tr>
<td>Gofres con nata y chocolate</td>
<td>3.80€</td>
</tr>
<tr>
<td>Tortitas americanas</td>
<td>4.00€</td>
</tr>
<tr>
<td>Tarta (Porción)</td>
<td>3.80€</td>
</tr>
<tr>
<td>Coulant con helado de vainilla</td>
<td>4.00€</td>
</tr>
</table>
<p>¿Quieres ver tu negocio localizado en Burguillos en este espacio? Contacta con <a href="mailto:contact@owlcode.tech">contact@owlcode.tech</a>.</p>
</content>

View File

@ -13,8 +13,6 @@
</attributes>
<slug>hamburgueseria-la-ermita</slug>
<content>
<h2 style="background: black; color: red;">Menú actualizado con nuevos precios y productos 2024-01-18</h2>
<img width="50%" alt="Front door of 'Hamburguesería la Ermita'" style="border: solid 1px black;" src="/img/hamburgueseria-la-ermita.webp"/>
<h2>Información de contacto para preparación de pedidos.</h2>
@ -25,194 +23,174 @@
<p>Procedemos a listar la carta.</p>
<details>
<summary><h3>Entrantes.</h3></summary>
<h3>Entrantes.</h3>
<ul>
<li>Alitas <b>3€ tapa</b>.</li>
<li>Chili Cheese Bite <b>3€ tapa</b>.</li>
<li>Fingers de mozarella <b>3€ tapa</b>.</li>
<li>Aros de cebolla <b>3€ tapa</b>.</li>
<li>Nuggets <b>3€ tapa</b>.</li>
<li>Crujientes de pollo <b>3€ tapa</b>.</li>
<li>Lagrimitas <b>3€ tapa</b>.</li>
<li>Croquetas de jamón / queso azul y cebolla caramelizada / pizza <b>3€ tapa</b>.</li>
<li>Patatas alioli <b>3€ tapa 5.50€ plato</b>.</li>
<li>Ensaladilla <b>3€ tapa 5.50€ plato</b>.</li>
</ul>
</details>
<ul>
<li>Alitas <b>3€ tapa</b>.</li>
<li>Chili Cheese Bite <b>3€ tapa</b>.</li>
<li>Fingers de mozarella <b>3€ tapa</b>.</li>
<li>Aros de cebolla <b>3€ tapa</b>.</li>
<li>Nuggets <b>3€ tapa</b>.</li>
<li>Crujientes de pollo <b>3€ tapa</b>.</li>
<li>Lagrimitas <b>3€ tapa</b>.</li>
<li>Croquetas de jamón / queso azul y cebolla caramelizada / pizza <b>3€ tapa</b>.</li>
<li>Patatas alioli <b>2.50€ tapa 4€ plato</b>.</li>
<li>Ensaladilla <b>2.50€ tapa 4€ plato</b>.</li>
</ul>
<details>
<summary><h3>Pizzas.</h3></summary>
<h3>Pizzas.</h3>
<p>Todas las pizzas vienen con 2 ingredientes incluidos en el precio, por 0.60€ puedes añadir un ingrediente extra por 0.70€ o una salsa extra por 0.50€.</p>
<p>Todas las pizzas vienen con 2 ingredientes incluidos en el precio, por 0.60€ puedes añadir un ingrediente extra por 0.60€ o una salsa extra por 0.30€.</p>
<p>Tienes la posibilidad de pedir una pizza mediana nutella por 5.50€ desde Burguillos.info suponemos que no se permiten
otros ingredientes para evitar aberraciones gastronómicas.</p>
<p>Tienes la posibilidad de pedir una pizza mediana nutella por 5.50€ desde Burguillos.info suponemos que no se permiten
otros ingredientes para evitar aberraciones gastronómicas.</p>
<h4>Tamaño de pizza.</h4>
<h4>Tamaño de pizza.</h4>
<ul>
<li>Mediana <b>5.90€</b>.</li>
<li>Grande <b>9.50€</b>.</li>
</ul>
<ul>
<li>Mediana <b>5.50€</b>.</li>
<li>Grande <b>9.00€</b>.</li>
</ul>
<h4>Ingredientes disponibles.</h4>
<h4>Ingredientes disponibles.</h4>
<ul>
<li>Jamón York</li>
<li>Bacon</li>
<li>Salchicha</li>
<li>Pepperoni</li>
<li>Roquefort</li>
<li>Jamón</li>
<li>Barbacoa</li>
<li>Atún</li>
<li>Cebolla</li>
<li>Pimientos</li>
<li>Gambas</li>
<li>Huevo</li>
<li>Aceitunas</li>
<li>Pepinillos</li>
<li>Champiñones</li>
<li>Maíz</li>
<li>Piña</li>
<li>Anchoa</li>
<li>Rulo de cabra</li>
<li>Rúcula</li>
<li>Tomate natural</li>
<li>Carbonara</li>
<li>Carne kebab</li>
<li>Pollo asado y salsa kebab</li>
<li>4 quesos</li>
</ul>
</details>
<ul>
<li>Jamón York</li>
<li>Bacon</li>
<li>Salchicha</li>
<li>Pepperoni</li>
<li>Roquefort</li>
<li>Jamón</li>
<li>Barbacoa</li>
<li>Atún</li>
<li>Cebolla</li>
<li>Pimientos</li>
<li>Gambas</li>
<li>Huevo</li>
<li>Aceitunas</li>
<li>Pepinillos</li>
<li>Champiñones</li>
<li>Maíz</li>
<li>Piña</li>
<li>Anchoa</li>
<li>Rulo de cabra</li>
<li>Rúcula</li>
<li>Tomate natural</li>
<li>Carbonara</li>
<li>Carne kebab</li>
<li>Pollo asado y salsa kebab</li>
<li>4 quesos</li>
</ul>
<details>
<summary><h3>Bebidas.</h3></summary>
<h3>Bebidas.</h3>
<ul>
<li>Refresco <b>1.50€</b></li>
<li>Cerveza de barril <b>1.40€</b></li>
<li>Tercio <b>1.50€</b></li>
<li>Tinto <b>1.50€</b></li>
<li>Radler <b>1.50€</b></li>
<li>Cerveza sin alcohol botellín <b>1.30€</b></li>
<li>Litro <b>3.00€</b></li>
<li>Agua pequeña <b>1.00€</b></li>
<li>Agua grande <b>1.50€</b></li>
<li>Zumo <b>1.20€</b></li>
<li>Café e infusión <b>1.20€</b></li>
<li>Combinado <b>5.00€</b></li>
</ul>
</details>
<ul>
<li>Refresco <b>1.50€</b></li>
<li>Cerveza de barril <b>1.30€</b></li>
<li>Tercio <b>1.50€</b></li>
<li>Tinto <b>1.50€</b></li>
<li>Radler <b>1.50€</b></li>
<li>Cerveza sin alcohol botellín <b>1.20€</b></li>
<li>Litro <b>3.00€</b></li>
<li>Agua pequeña <b>0.80€</b></li>
<li>Agua grande <b>1.20€</b></li>
<li>Zumo <b>1.00€</b></li>
<li>Café e infusión <b>1.20€</b></li>
<li>Combinado <b>4.50€</b></li>
</ul>
<details>
<summary><h3>Helados.</h3></summary>
<h3>Helados.</h3>
<ul>
<li>Sandy <b>2.30€</b></li>
<li>Mix Sandy <b>3.30€</b></li>
<li>Mini Mix Sandy <b>2.60€</b></li>
<li>Batido pequeño <b>2.60€</b></li>
<li>Batido grande <b>3.30€</b></li>
</ul>
</details>
<ul>
<li>Sandy <b>2.20€</b></li>
<li>Mini Sandy <b>1.50€</b></li>
<li>Mix Sandy <b>3.20€</b></li>
<li>Mini Mix Sandy <b>2.50€</b></li>
<li>Batido pequeño <b>2.50€</b></li>
<li>Batido grande <b>3.20€</b></li>
</ul>
<details>
<summary><h3>Bocadillos.</h3></summary>
<h3>Bocadillos.</h3>
<ul>
<li>Tortilla <b>3.50€</b></li>
<li>Cochinito <b>3.20€</b></li>
<li>Lomo adobado <b>3.20€</b></li>
<li>Bacon y queso <b>3.20€</b></li>
<li>Atún y pimiento <b>3.20€</b></li>
<li>Carne mechada y chimichurri <b>3.70€</b></li>
<li>Filete de lomo, mayonesa y lechuga <b>3.90€</b></li>
<li>Serranito de pollo o cerdo <b>4.90€</b></li>
</ul>
</details>
<ul>
<li>Tortilla <b>3.00€</b></li>
<li>Cochinito <b>3.00€</b></li>
<li>Lomo adobado <b>3.00€</b></li>
<li>Bacon y queso <b>3.00€</b></li>
<li>Atún y pimiento <b>3.00€</b></li>
<li>Carne mechada y chimichurri <b>3.50€</b></li>
<li>Filete de lomo, mayonesa y lechuga <b>3.50€</b></li>
<li>Serranito de pollo o cerdo <b>4.50€</b></li>
</ul>
<details>
<summary><h3>Montaditos.</h3></summary>
<h3>Montaditos.</h3>
<ul>
<li>Gambas alioli <b>3.00€</b></li>
<li>Melva con pimiento <b>3.00€</b></li>
<li>Carne mechada con chimichurri <b>3.00€</b></li>
<li>Solomillo (Whisky, roquefort o pimienta) <b>3.00€</b></li>
<li>Pollo o lomo <b>3.00€</b></li>
</ul>
</details>
<ul>
<li>Gambas alioli <b>2.50€</b></li>
<li>Melva con pimiento <b>2.50€</b></li>
<li>Carne mechada con chimichurri <b>2.50€</b></li>
<li>Solomillo (Whisky, roquefort o pimienta) <b>2.50€</b></li>
<li>Pollo o lomo <b>2.50€</b></li>
</ul>
<details>
<summary><h3>Perritos.</h3></summary>
<h3>Perritos.</h3>
<ul>
<li>Simple (Salchicha + salsa) <b>2.20€</b></li>
<li>Completo (Salchicha, cebolla frita, zanahoria, patatas paja y salsa) <b>2.70€</b></li>
</ul>
</details>
<ul>
<li>Simple (Salchicha + salsa) <b>2.00€</b></li>
<li>Completo (Salchicha, cebolla frita, zanahoria, patatas paja y salsa) <b>2.50€</b></li>
</ul>
<details>
<summary><h3>Carnes.</h3></summary>
<ul>
<li>Pechuga y patatas <b>5.90€ plato</b>.</li>
<li>Solomillo (Whisky, roquefort o pimienta) <b>3.00€ tapa 5.90€ plato</b>.</li>
</ul>
</details>
<h3>Carnes.</h3>
<ul>
<li>Pechuga y patatas <b>5.00€ plato</b>.</li>
<li>Solomillo (Whisky, roquefort o pimienta) <b>3.00€ tapa 5.00€ plato</b>.</li>
</ul>
<details>
<summary><h3>Patatas gratinadas.</h3></summary>
<h3>Patatas gratinadas.</h3>
<p>En formato pequeño cuestan 4.00€ y en formato grande 6.00€, a elegir entre las siguientes combinaciones.</p>
<p>En formato pequeño cuestan 4.00€ y en formato grande 6.00€, a elegir entre las siguientes combinaciones.</p>
<ul>
<li>Alioli + queso + bacon.</li>
<li>Salsa cheedar + bacon + cebolla frita.</li>
<li>Salsa kebab + carne kebab.</li>
</ul>
</details>
<ul>
<li>Alioli + queso + bacon.</li>
<li>Salsa cheedar + bacon + cebolla frita.</li>
<li>Salsa kebab + carne kebab.</li>
</ul>
<details>
<summary><h3>Patatas normales.</h3></summary>
<h3>Patatas normales.</h3>
<ul>
<li>Pequeñas <b>1.00€</b></li>
<li>Grandes <b>1.50€</b></li>
<li>Gajo <b>grande 1.50€</b></li>
</ul>
</details>
<ul>
<li>Pequeñas <b>1.00€</b></li>
<li>Grandes <b>1.50€</b></li>
<li>Gajo <b>grande 1.50€</b></li>
<li>Cris Criss - Cross <b>grande 2.00€</b></li>
</ul>
<details>
<summary><h3>Hamburguesas.</h3></summary>
<h3>Hamburguesas.</h3>
<p>Puedes solicitar un extra en salsa por 0.50€.</p>
<p>Puedes solicitar un extra en salsa por 0.50€.</p>
<ul>
<li>Solo carne <b>1.80€</b>.</li>
<li>Solo queso <b>2.30€</b>.</li>
<li>BBQ (Carne, salsa barbacoa, queso y pepinillo) <b>2.90€</b>.</li>
<li>Texas (Carne, salsa barbacoa, queso y aros de cebolla) <b>2.90€</b>.</li>
<li>Cheedar simple (Carne, salsa cheedar, tomate, cebolla frita) <b>2.90€</b>.</li>
<li>Salad (Carne, mayonesa, lechuga y tomate) <b>2.90€</b>.</li>
<li>Porky (Carne cerdo, bacon, salsa bacon, queso, tomate, cebolla frita) + patatas <b>6.90€</b>.</li>
<li>Roquefort (Carne mixta, salsa roquefort, lechuga, tomate, queso granapadano, bacon y cebolla frita) + patatas <b>6.90€</b>.</li>
<li>Campera (Pollo campero, mayonesa, lechuga, tomate, queso y bacon) + patatas <b>6.90€</b>.</li>
<li>Miel y mostaza (Carne mixta, rúcula, tomate, cebolla caramelizada, queso gouda, bacon, salsa miel y mostaza) + patatas <b>6.90€</b>.</li>
<li>Boletus (Carne mixta, queso gouda, bacon, champiñones y salsa boletus) + patatas <b>6.90€</b>.</li>
<li>La Ermita (Carne retinto, salsa barbacoa especial, tomate, cebolla frita, rulo de cabra y bacon) + patatas <b>7.90€</b>.</li>
<li>Cabrales (Carne mixta, salsa cabrales, gouda, bacon, lechuga, tomate y queso) + patatas <b>6.90€</b>.</li>
<li>Cheedar (Carne mixta o pollo empanado, salsa cheedar, cebolla frita, tomate, queso y bacon) + patatas <b>6.90€</b>.</li>
<li>Completa (Carne mixta, mayonesa, lechuga, cebolla frita, tomate, pepinillo y cebolla) + patatas <b>6.90€</b>.</li>
<li>Huevo (Carne mixta, mayonesa, lechuga, cebolla frita, tomate, queso, huevo y bacon) + patatas <b>6.90€</b>.</li>
<li>Steak (Carne mixta, mayonesa, lechuga, tomate, queso, bacon y salsa bbq) + patatas <b>6.90€</b>.</li>
<li>Cheese bacon (Carne mixta, queso, bacon, pepinillo, ketchup, mostaza y cebolla) + patatas <b>6.90€</b>.</li>
<li>Romana (Carne mixta, salsa bbq, lechuga, tomate, queso y aros de cebolla) + patatas <b>6.90€</b>.</li>
</ul>
</details>
<ul>
<li>Solo carne <b>1.50€</b>.</li>
<li>Solo queso <b>2.00€</b>.</li>
<li>BBQ (Carne, salsa barbacoa, queso y pepinillo) <b>2.50€</b>.</li>
<li>Texas (Carne, salsa barbacoa, queso y aros de cebolla) <b>2.50€</b>.</li>
<li>Cheedar simple (Carne, salsa cheedar, tomate, cebolla frita) <b>2.50€</b>.</li>
<li>Salad (Carne, mayonesa, lechuga y tomate) <b>2.50€</b>.</li>
<li>Porky (Carne cerdo, bacon, salsa bacon, queso, tomate, cebolla frita) + patatas <b>6.50€</b>.</li>
<li>Roquefort (Carne mixta, salsa roquefort, lechuga, tomate, queso granapadano, bacon y cebolla frita) + patatas <b>6.50€</b>.</li>
<li>Campera (Pollo campero, mayonesa, lechuga, tomate, queso y bacon) + patatas <b>6.50€</b>.</li>
<li>Miel y mostaza (Carne mixta, rúcula, tomate, cebolla caramelizada, queso gouda, bacon, salsa miel y mostaza) + patatas <b>6.50€</b>.</li>
<li>Boletus (Carne mixta, queso gouda, bacon, champiñones y salsa boletus) + patatas <b>6.50€</b>.</li>
<li>La Ermita (Carne retinto, salsa barbacoa especial, tomate, cebolla frita, rulo de cabra y bacon) + patatas <b>7.50€</b>.</li>
<li>Cabrales (Carne mixta, salsa cabrales, gouda, bacon, lechuga, tomate y queso) + patatas <b>6.50€</b>.</li>
<li>Cheedar (Carne mixta o pollo empanado, salsa cheedar, cebolla frita, tomate, queso y bacon) + patatas <b>6.50€</b>.</li>
<li>Completa (Carne mixta, mayonesa, lechuga, cebolla frita, tomate, pepinillo y cebolla) + patatas <b>6.50€</b>.</li>
<li>Huevo (Carne mixta, mayonesa, lechuga, cebolla frita, tomate, queso, huevo y bacon) + patatas <b>6.50€</b>.</li>
<li>Steak (Carne mixta, mayonesa, lechuga, tomate, queso, bacon y salsa bbq) + patatas <b>6.50€</b>.</li>
<li>Cheese bacon (Carne mixta, queso, bacon, pepinillo, ketchup, mostaza y cebolla) + patatas <b>6.50€</b>.</li>
<li>Romana (Carne mixta, salsa bbq, lechuga, tomate, queso y aros de cebolla) + patatas <b>6.50€</b>.</li>
</ul>
<p>¿Quieres ver tu negocio localizado en Burguillos en este espacio? Contacta con <a href="mailto:contact@owlcode.tech">contact@owlcode.tech</a>.</p>
</content>

View File

@ -1,31 +0,0 @@
<post>
<author>Burguillos.info</author>
<date>2022-12-02T12:56+00:00</date>
<title>Burguillos Dental - Dentista en Burguillos.</title>
<ogdesc>Burguillos Dental - Dentista en Burguillos.</ogdesc>
<category>dentistas</category>
<slug>burguillos-dental</slug>
<img src="/img/burguillos-dental.webp"/>
<content>
<img alt="" src="/img/burguillos-dental.webp"/>
<h2>Hazte tu aparato dental/ortodoncia invisible en Burguillos Dental.</h2>
<p>Burguillos Dental, ubicado en la <a href="/posts/centro-medico-juan-manuel-perez-sanchez">Clínica Juan Manuel Pérez Sánchez</a> poseé un equipo de odontologos y dentistas altamente cualificados.</p>
<p>Ofrecen los siguientes servicios para el cuidado de tu boca:</p>
<ul>
<li>Odontologia general.</li>
<li>Ortodoncia.</li>
<li>Ortodoncia Invisible.</li>
<li>Odontopediatria.</li>
<li>Estetica Dental y Labios.</li>
<li>Protesis y Aparatos Dentales, Implantes.</li>
<li>Plan de atención infantil de la Junta de Andalucía.</li>
<li>Radiografías Panorex-Teleradiografía.</li>
</ul>
<p>Cita previa en <a href="tel:+34635061176">635061176</a> o <a href="mailto:policlinicaburguillos@gmail.com">policlinicaburguillos@gmail.com</a>.</p>
<p>Localizado en Calle la Fuente número 24.</p>
</content>
</post>

@ -1 +0,0 @@
Subproject commit ae2bd63d8fbf93b8bb2a2fa9cc16405a16d0223d

11
generate_proto.sh Normal file
View File

@ -0,0 +1,11 @@
#!/bin/bash
rm -rf ./js-src/generated/
mkdir ./js-src/generated/
protoc --plugin="protoc-gen-ts=./node_modules/.bin/protoc-gen-ts" \
--plugin="protoc-gen-js=./node_modules/.bin/protoc-gen-js" \
--ts_opt=esModuleInterop=true \
--js_out="import_style=commonjs,binary:./js-src/generated" \
--ts_out="./js-src/generated" \
--proto_path="proto" \
$(find proto/ -name '*.proto')

View File

@ -0,0 +1,67 @@
import Conquer from '@burguillosinfo/conquer'
import MapState from '@burguillosinfo/conquer/map-state'
export default class CreateNode {
private conquer: Conquer
private createNodeSlide: HTMLElement
constructor(conquer: Conquer) {
this.conquer = conquer
this.getCreateNodeCancel().addEventListener('click', () => {
this.conquer.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
this.conquer.removeState(MapState.CREATE_NODE)
this.conquer.addState(MapState.NORMAL)
})
this.getCreateNodeNewNodeElement().addEventListener('click', () => {
const state = this.conquer.getState()
if (state & MapState.SELECT_WHERE_TO_CREATE_NODE) {
this.conquer.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
return
}
this.conquer.addState(MapState.SELECT_WHERE_TO_CREATE_NODE)
})
}
private getCreateNodeCancel(): HTMLElement {
const createNodeCancel = document.querySelector('#create-node-exit')
if (createNodeCancel === null || !(createNodeCancel instanceof HTMLElement)) {
Conquer.fail('Unable to find #create-node-exit.')
}
return createNodeCancel
}
private getCreateNodeNewNodeElement(): HTMLElement {
const createNodeNewElement = document.querySelector('#create-node-new-node')
if (createNodeNewElement === null || !(createNodeNewElement instanceof HTMLElement)) {
Conquer.fail('Unable to find #create-node-slide.')
}
return createNodeNewElement
}
private getCreateNodeSlide(): HTMLElement {
const createNodeSlide = document.querySelector('#create-node-slide')
if (createNodeSlide === null || !(createNodeSlide instanceof HTMLElement)) {
Conquer.fail('Unable to find #create-node-slide.')
}
return createNodeSlide
}
public refreshState() {
if (!(this.conquer.getState() & MapState.CREATE_NODE)) {
this.getCreateNodeSlide().classList.add('conquer-display-none')
return
}
this.refreshCreateNodeNewNodeState()
this.getCreateNodeSlide().classList.remove('conquer-display-none')
}
private refreshCreateNodeNewNodeState(): void {
const createNodeNewNode = this.getCreateNodeNewNodeElement()
if (this.conquer.getState() & MapState.SELECT_WHERE_TO_CREATE_NODE) {
createNodeNewNode.innerText = 'Cancelar.'
} else {
createNodeNewNode.innerText = 'Crear nodo.'
}
}
}

View File

@ -0,0 +1,53 @@
import Conquer from '@burguillosinfo/conquer'
export default class FightSelectorSlide {
private callbacks: Record<string, Array<() => void>> = {}
public on(eventName: string, callback: () => void): void {
if (this.callbacks[eventName] === undefined) {
this.callbacks[eventName] = []
}
this.callbacks[eventName].push(callback)
}
private runCallbacks(eventName: string) {
const callbacks = this.callbacks[eventName];
if (callbacks === undefined) {
return
}
for (const callback of callbacks) {
callback()
}
}
private getSelectorSlide(): HTMLElement {
const selectorSlide = document.querySelector('#fight-battle-selector-slide');
if (!(selectorSlide instanceof HTMLElement)) {
Conquer.fail('selectorSlide is not HTMLElement');
}
return selectorSlide;
}
public startHook(): void {
this.createEventListeners();
}
public getGlobalBattleButton(): HTMLElement {
const globalBattleButton = this.getSelectorSlide().querySelector('button.fight-global-button');
if (!(globalBattleButton instanceof HTMLElement)) {
Conquer.fail('globalBattleButton is not HTMLElement');
}
return globalBattleButton;
}
private createEventListeners(): void {
const globalBattleButton = this.getGlobalBattleButton();
globalBattleButton.addEventListener('click', () => {
this.runCallbacks('global-battle');
});
}
public show(): void {
this.getSelectorSlide().classList.remove('conquer-display-none');
}
public hide(): void {
this.getSelectorSlide().classList.add('conquer-display-none');
}
}

584
js-src/conquer/index.ts Normal file
View File

@ -0,0 +1,584 @@
import Map from "ol/Map"
import MapEvent from "ol/MapEvent"
import MapBrowserEvent from "ol/MapBrowserEvent"
import View from "ol/View"
import Projection from "ol/proj/Projection.js"
import TileLayer from 'ol/layer/Tile'
import OSM from 'ol/source/OSM'
import * as olProj from "ol/proj"
import Feature from 'ol/Feature'
import Point from 'ol/geom/Point'
import VectorLayer from 'ol/layer/Vector'
import VectorSource from 'ol/source/Vector'
import Stroke from 'ol/style/Stroke'
import Fill from 'ol/style/Fill'
import CircleStyle from 'ol/style/Circle'
import Icon from 'ol/style/Icon'
import Style from 'ol/style/Style'
import ConquerLogin from '@burguillosinfo/conquer/login'
import InterfaceManager from '@burguillosinfo/conquer/interface-manager'
import SelfPlayerUI from '@burguillosinfo/conquer/interface/self-player'
import CreateNode from '@burguillosinfo/conquer/create-node'
import MapState from '@burguillosinfo/conquer/map-state'
import MapNode from '@burguillosinfo/conquer/map-node'
import NewNodeUI from '@burguillosinfo/conquer/interface/new-node'
import NewTeamUI from '@burguillosinfo/conquer/interface/new-team'
import WebSocket from '@burguillosinfo/conquer/websocket'
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import ConquerUser from '@burguillosinfo/conquer/user'
import FightSelectorSlide from '@burguillosinfo/conquer/fight-selector-slide';
import SelectFightUI from '@burguillosinfo/conquer/interface/select-fight';
import ConquerUserCurrentEnemy from '@burguillosinfo/conquer/user-current-enemy'
type StylesInterface = Record<string, Style>
export default class Conquer {
private conquerContainer: HTMLDivElement
private map: Map
private enabledOnRotate = true
private rotation = 0;
private currentLongitude: number
private intervalSendCoordinates: number | null = null;
private currentLatitude: number
private rotationOffset = 0
private heading = 0
private disableSetRotationOffset = false
private currentPositionFeature: Feature | null
private vectorLayer: VectorLayer<VectorSource> | null = null
private alpha = 0
private beta = 0
private gamma = 0
private conquerLogin: ConquerLogin
private selfPlayerUI: SelfPlayerUI | null = null
private interfaceManager: InterfaceManager
private firstSetCenter = true
private firstSetRotation = true
private state: MapState = MapState.NOTHING
private createNodeObject: CreateNode
private serverNodes: Record<string, MapNode> = {}
private coordinate_1 = 0;
private coordinate_2 = 0;
private fightSelectorSlide: FightSelectorSlide;
private loggedIn = false;
public getServerNodes(): Record<string, MapNode> {
return this.serverNodes
}
public getState(): MapState {
return this.state
}
public setState(state: MapState) {
this.state = state
this.refreshState()
}
public removeState(state: MapState) {
this.state &= ~state
this.refreshState()
}
public addState(state: MapState) {
this.state |= state
this.refreshState()
}
private refreshFightSlide(): void {
if (this.loggedIn && (this.getState() & MapState.NORMAL) !== 0) {
this.fightSelectorSlide.show();
}
if (!this.loggedIn) {
this.fightSelectorSlide.hide();
}
}
private refreshState(): void {
this.refreshFightSlide();
this.createNodeObject.refreshState()
return
}
static start() {
const conquerContainer = document.querySelector(".conquer-container")
if (conquerContainer === null || !(conquerContainer instanceof HTMLDivElement)) {
Conquer.fail('.conquer-container is not a div.')
}
const conquer = new Conquer(conquerContainer)
conquer.run()
}
setCenterDisplaced(lat: number, lon: number) {
if (this.firstSetCenter || !(this.state & MapState.FREE_MOVE)) {
this.coordinate_1 = lon;
this.coordinate_2 = lat;
const olCoordinates = this.realCoordinatesToOl(lat, lon)
const size = this.map.getSize()
if (size === undefined) {
return
}
this.map.getView().centerOn(olCoordinates, size, [size[0]/2, size[1]-60])
this.firstSetCenter = false
}
}
static fail(error: string): never {
alert('Error de interfaz')
throw new Error(error)
}
public isStateCreatingNode(): boolean {
return !!(this.getState() & MapState.CREATE_NODE)
}
public isStateSelectWhereToCreateNode(): boolean {
return !!(this.getState() & MapState.SELECT_WHERE_TO_CREATE_NODE)
}
private createNodeCounter = 0
async onClickWhereToCreateNode(event: MapEvent) {
if (!(event instanceof MapBrowserEvent)) {
return
}
const pixel = event.pixel
const coordinates = this.map.getCoordinateFromPixel(pixel)
const newNodeUI = new NewNodeUI(coordinates)
const oldState = this.getState();
newNodeUI.on('close', () => {
this.interfaceManager.remove(newNodeUI)
this.setState(oldState);
})
this.interfaceManager.push(newNodeUI)
this.removeState(MapState.SELECT_WHERE_TO_CREATE_NODE)
}
private isStateFillingFormCreateNode(): boolean {
return !!(this.getState() & MapState.FILLING_FORM_CREATE_NODE)
}
async onClickMap(event: MapEvent): Promise<void> {
if (this.isStateCreatingNode() && this.isStateSelectWhereToCreateNode()) {
this.onClickWhereToCreateNode(event)
}
if (!(this.getState() & MapState.NORMAL)) {
return
}
if (this.vectorLayer === null) {
return
}
if (!(event instanceof MapBrowserEvent)) {
return
}
if (event.dragging) {
return
}
const pixel = event.pixel
const features = this.map.getFeaturesAtPixel(pixel)
const feature = features.length ? features[0] : undefined
if (feature === undefined) {
return
}
if (!(feature instanceof Feature)) {
return
}
this.onClickFeature(feature)
}
async onClickSelf(): Promise<void> {
if (!(this.state & MapState.NORMAL)) {
return
}
const selfPlayerUI = new SelfPlayerUI(!!(this.getState() & (MapState.FREE_MOVE)))
selfPlayerUI.on('close', () => {
this.interfaceManager.remove(selfPlayerUI)
})
selfPlayerUI.on('enable-explorer-mode', () => {
this.addState(MapState.FREE_MOVE);
});
selfPlayerUI.on('disable-explorer-mode', () => {
this.removeState(MapState.FREE_MOVE);
});
selfPlayerUI.on('createNodeStart', () => {
this.addState(MapState.CREATE_NODE)
this.removeState(MapState.NORMAL)
})
selfPlayerUI.on('open-create-team', () => {
this.onOpenCreateTeam();
});
this.interfaceManager.push(selfPlayerUI)
this.selfPlayerUI = selfPlayerUI
}
private onOpenCreateTeam(): void {
const newTeamUI = new NewTeamUI();
newTeamUI.on('close', () => {
this.interfaceManager.remove(newTeamUI);
});
this.interfaceManager.push(newTeamUI);
}
private isFeatureEnabledMap: Record<string, boolean> = {}
async onClickFeature(feature: Feature): Promise<void> {
if (this.isFeatureEnabledMap[feature.getProperties().type] === undefined) {
this.isFeatureEnabledMap[feature.getProperties().type] = true
}
if (!this.isFeatureEnabledMap[feature.getProperties().type]) {
return
}
this.isFeatureEnabledMap[feature.getProperties().type] = false
window.setTimeout(() => {
this.isFeatureEnabledMap[feature.getProperties().type] = true
}, 100);
const candidateNode = this.getServerNodes()[feature.getProperties().type];
if (candidateNode !== undefined) {
candidateNode.click(this.interfaceManager);
return;
}
if (feature === this.currentPositionFeature) {
this.onClickSelf()
return
}
}
async onLoginSuccess(): Promise<void> {
this.loggedIn = true;
this.refreshFightSlide();
this.clearIntervalSendCoordinates();
this.createIntervalSendCoordinates();
this.clearIntervalPollNearbyNodes();
this.createIntervalPollNearbyNodes();
}
private intervalPollNearbyNodes: number | null = null;
private clearIntervalPollNearbyNodes(): void {
if (this.intervalPollNearbyNodes !== null) {
window.clearInterval(this.intervalPollNearbyNodes)
this.intervalPollNearbyNodes = null;
}
}
private async getNearbyNodes(): Promise<void> {
const urlNodes = new URL('/conquer/node/near', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let response;
try {
response = await fetch(urlNodes);
} catch (error) {
console.error(error);
return;
}
let responseBody;
try {
responseBody = await response.json();
} catch (error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (response.status !== 200) {
console.error(responseBody.error);
return;
}
const serverNodes: Record<string, MapNode> = {};
const nodes = JsonSerializer.deserialize(responseBody, MapNode);
if (!(nodes instanceof Array)) {
console.error('Received null instead of node list.');
return;
}
for (const node of nodes) {
if (!(node instanceof MapNode)) {
console.error('Received node is not a MapNode.');
continue;
}
node.on('update-nodes', async () => {
await this.sendCoordinatesToServer();
this.getNearbyNodes();
});
serverNodes[node.getId()] = node;
}
this.serverNodes = serverNodes;
this.refreshLayers();
}
private createIntervalPollNearbyNodes(): void {
this.getNearbyNodes();
this.intervalPollNearbyNodes = window.setInterval(() => {
this.getNearbyNodes();
}, 40000)
}
private createIntervalSendCoordinates(): void {
this.intervalSendCoordinates = window.setInterval(() => {
this.sendCoordinatesToServer();
}, 40000);
}
private async sendCoordinatesToServer(): Promise<void> {
const urlLog = new URL('/conquer/user/coordinates', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let res;
try {
res = await fetch(urlLog, {
method: 'POST',
body: JSON.stringify([
this.coordinate_1,
this.coordinate_2,
])});
} catch (error) {
console.error(error)
return;
}
let responseBody;
try {
responseBody = await res.json();
} catch(error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (res.status !== 200) {
console.error(responseBody.error);
}
}
private runPreStartState(): void {
const createNodeObject = new CreateNode(this)
this.createNodeObject = createNodeObject
const interfaceManager = new InterfaceManager()
this.interfaceManager = interfaceManager
const conquerLogin = new ConquerLogin(interfaceManager)
conquerLogin.on('login', () => {
this.onLoginSuccess();
});
conquerLogin.on('logout', () => {
this.onLogout();
});
conquerLogin.start()
this.conquerLogin = conquerLogin
this.fightSelectorSlide = new FightSelectorSlide();
this.fightSelectorSlide.on('global-battle', () => {
this.startGlobalBattleSelector();
});
this.fightSelectorSlide.startHook();
}
private async startGlobalBattleSelector(): Promise<void> {
const enemies = await ConquerUserCurrentEnemy.getGlobalEnemies();
if (enemies !== null) {
const selectFightUI = new SelectFightUI(enemies);
selectFightUI.on('close', () => {
this.interfaceManager.remove(selectFightUI);
});
this.interfaceManager.push(selectFightUI);
}
}
private onLogout(): void {
this.loggedIn = false;
this.refreshFightSlide();
this.clearIntervalSendCoordinates();
this.clearIntervalPollNearbyNodes();
}
private clearIntervalSendCoordinates(): void {
if (this.intervalSendCoordinates !== null) {
window.clearInterval(this.intervalSendCoordinates);
this.intervalSendCoordinates = null;
}
}
async run() {
this.runPreStartState()
this.setState(MapState.NORMAL | MapState.FREE_ROTATION)
const conquerContainer = this.conquerContainer
//layer.on('prerender', (evt) => {
// // return
// if (evt.context) {
// const context = evt.context as CanvasRenderingContext2D
// context.filter = 'grayscale(80%) invert(100%) '
// context.globalCompositeOperation = 'source-over'
// }
//})
//layer.on('postrender', (evt) => {
// if (evt.context) {
// const context = evt.context as CanvasRenderingContext2D
// context.filter = 'none'
// }
//})
olProj.useGeographic()
const osm = new OSM()
osm.setUrls([`${window.location.protocol}//${window.location.hostname}:${window.location.port}/conquer/tile/{z}/{x}/{y}.png`])
this.map = new Map({
target: conquerContainer,
layers: [
new TileLayer({
source: osm
})
],
view: new View({
zoom: 19,
maxZoom: 22,
}),
})
this.setLocationChangeTriggers()
this.setRotationChangeTriggers()
}
setRotationChangeTriggers(): void {
if (window.DeviceOrientationEvent) {
window.addEventListener("deviceorientation", (event) => {
if (event.alpha !== null && event.beta !== null && event.gamma !== null) {
this.onRotate(event.alpha, event.beta, event.gamma)
}
}, true)
}
}
addCurrentLocationMarkerToMap(currentLatitude: number,
currentLongitude: number) {
const currentPositionFeature = new Feature({
type: 'currentPositionFeature',
geometry: new Point(this.realCoordinatesToOl(currentLatitude, currentLongitude))
})
this.currentPositionFeature = currentPositionFeature
}
processLocation(location: GeolocationPosition) {
this.currentLatitude = location.coords.latitude
this.currentLongitude = location. coords.longitude
if (location.coords.heading !== null && (this.alpha != 0 || this.beta != 0 || this.gamma != 0) && !this.disableSetRotationOffset) {
this.disableSetRotationOffset = true
this.heading = location.coords.heading
this.rotationOffset = this.compassHeading(this.alpha, this.beta, this.gamma) + (location.coords.heading*Math.PI*2)/360
}
this.setCenterDisplaced(this.currentLatitude, this.currentLongitude)
this.addCurrentLocationMarkerToMap(this.currentLatitude, this.currentLongitude)
this.refreshLayers()
}
private async refreshLayers(): Promise<void> {
if (this.currentPositionFeature === null) {
return
}
const user = await ConquerUser.getSelfUser()
let color = 'white';
if (user !== null) {
const team = await user.getTeam();
if (team !== null) {
color = team.getColor();
}
}
const styles: StylesInterface = {
currentPositionFeature: new Style({
image: new Icon({
crossOrigin: 'anonymous',
src: '/img/arrow-player.svg',
color: color,
scale: 0.2,
rotation: this.rotation,
rotateWithView: true,
}),
zIndex: 4,
})
};
const features = [];
features.push(this.currentPositionFeature);
for (const key in this.getServerNodes()) {
styles[key] = await this.getServerNodes()[key].getStyle()
features.push(this.getServerNodes()[key].getFeature())
}
const vectorLayer = new VectorLayer<VectorSource>({
source: new VectorSource({
features: features
}),
})
if (this.vectorLayer !== null) {
this.map.removeLayer(this.vectorLayer)
this.vectorLayer = null;
}
vectorLayer.setStyle((feature) => {
return styles[feature.getProperties().type]
})
this.map.addLayer(vectorLayer)
this.vectorLayer = vectorLayer
this.map.on('click', (event: MapEvent) => {
this.onClickMap(event)
})
}
setLocationChangeTriggers(): void {
window.setInterval(() => {
this.disableSetRotationOffset = false
}, 10000)
this.currentPositionFeature = null
window.setTimeout(() => {
window.setInterval(() => {
navigator.geolocation.getCurrentPosition((location) => {
this.processLocation(location)
}, () => {
return
}, {
enableHighAccuracy: true,
})
}, 3000)
}, 1000)
// const initialLatitude = 37.58237
//const initialLongitude = -5.96766
const initialLongitude = 2.500845037550267
const initialLatitude = 48.81050698635832
this.setCenterDisplaced(initialLatitude, initialLongitude)
this.addCurrentLocationMarkerToMap(initialLatitude, initialLongitude)
this.refreshLayers()
navigator.geolocation.watchPosition((location) => {
this.processLocation(location)
}, (err) => {
return
}, {
enableHighAccuracy: true,
})
}
realCoordinatesToOl(lat: number, lon: number): number[] {
return olProj.transform(
[lon, lat],
new Projection({ code: "WGS84" }),
new Projection({ code: "EPSG:900913" }),
)
}
compassHeading(alpha:number, beta:number, gamma:number): number {
const alphaRad = alpha * (Math.PI / 180)
return alphaRad
}
logToServer(logValue: string) {
const urlLog = new URL('/conquer/log', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
urlLog.searchParams.append('log', logValue)
fetch(urlLog).then(() => {
return
}).catch((error) => {
console.error(error)
})
}
onRotate(alpha: number, beta: number, gamma: number) {
if (this.enabledOnRotate) {
this.alpha = alpha
this.beta = beta
this.gamma = gamma
this.enabledOnRotate = false
this.rotation = -(this.compassHeading(alpha, beta, gamma) - this.rotationOffset);
if (this.currentPositionFeature !== null) {
this.currentPositionFeature.changed();
}
if (this.firstSetRotation || !(this.state & MapState.FREE_ROTATION)) {
this.map.getView().setRotation((this.compassHeading(alpha, beta, gamma) - this.rotationOffset))
this.firstSetRotation = false
}
window.setTimeout(() => {
this.enabledOnRotate = true
}, 10)
}
this.setCenterDisplaced(this.currentLatitude, this.currentLongitude)
}
constructor(conquerContainer: HTMLDivElement) {
this.conquerContainer = conquerContainer
}
}

View File

@ -0,0 +1,61 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerInterface from '@burguillosinfo/conquer/interface'
export default class ConquerInterfaceManager {
private interfaces: ConquerInterface[] = []
public push(conquerInterface: ConquerInterface) {
const nodesForInterface = conquerInterface.getNodes()
for (const nodeInInterface of nodesForInterface) {
nodeInInterface.id = ""
document.body.appendChild(nodeInInterface)
}
this.interfaces.push(conquerInterface)
conquerInterface.run()
let startInterface = this.interfaces.length - 2;
if (startInterface < 0) {
startInterface = 0
}
this.recalculateAllZIndexes(startInterface)
}
public remove(conquerInterface: ConquerInterface) {
for (let i = this.interfaces.length - 1; i >= 0; i--) {
if (conquerInterface !== this.interfaces[i]) {
continue
}
this.interfaces.splice(i, 1)
for (const nodeToDelete of conquerInterface.getNodes()) {
document.body.removeChild(nodeToDelete)
}
conquerInterface.prune()
this.recalculateAllZIndexes()
}
}
private recalculateAllZIndexes(start = 0) : void {
let currentZindex = 5;
if (start < 0) {
Conquer.fail('ConquerInterfaceManager.recalculateAllZIndexes must not be passed negative values.')
}
if (start > 0) {
const lastInterface = this.interfaces[start-1];
if (lastInterface === undefined) {
Conquer.fail('Last interface should not be null, dying...')
}
const lastInterfaceNodes = lastInterface.getNodes()
const lastInterfaceLastNode = lastInterfaceNodes[lastInterfaceNodes.length-1]
if (lastInterfaceLastNode === undefined) {
Conquer.fail('Last interface last node should not be null, dying...')
}
currentZindex = parseInt(lastInterfaceLastNode.style.zIndex)
}
for (let i = start; i < this.interfaces.length; i++) {
const conquerInterface = this.interfaces[i]
for (const node of conquerInterface.getNodes()) {
node.style.zIndex = currentZindex + ''
currentZindex++
}
}
}
}

View File

@ -0,0 +1,53 @@
import Conquer from '@burguillosinfo/conquer'
export default abstract class ConquerInterface {
private alreadyGenerated = false
private nodes: HTMLElement[]
private callbacks: Record<string, Array<() => void>> = {}
public getNodes(): HTMLElement[] {
if (!this.alreadyGenerated) {
this.nodes = this.generateNodes()
this.alreadyGenerated = true
}
return this.nodes
}
protected abstract generateNodes(): HTMLElement[]
public run(): void {
return
}
public prune(): void {
this.callbacks = {};
return
}
protected getNodeFromTemplateId(id: string): HTMLElement {
let template = document.getElementById(id)
if (template === null) {
Conquer.fail(`Unable to find template id ${id}.`)
}
const finalNode = template.cloneNode(true)
if (!(finalNode instanceof HTMLElement)) {
Conquer.fail('The node is not an Element.')
}
finalNode.classList.remove('conquer-display-none')
return finalNode
}
public on(eventName: string, callback: () => void): void {
if (this.callbacks[eventName] === undefined) {
this.callbacks[eventName] = []
}
this.callbacks[eventName].push(callback)
}
protected runCallbacks(eventName: string) {
const callbacks = this.callbacks[eventName];
if (callbacks === undefined) {
return
}
for (const callback of callbacks) {
callback()
}
}
}

View File

@ -0,0 +1,29 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerInterface from '@burguillosinfo/conquer/interface'
export default abstract class AbstractTopBarInterface extends ConquerInterface {
constructor() {
super()
const exitButton = this.getExitButton()
exitButton.addEventListener('click', () => {
this.runCallbacks('close')
})
}
protected generateNodes(): HTMLElement[] {
const newNode = this.getNodeFromTemplateId('conquer-interface-with-top-bar-template')
return [newNode]
}
protected getMainNode(): HTMLElement {
return this.getNodes()[0]
}
protected getExitButton(): HTMLElement {
const maybeExitButton = this.getMainNode().querySelector('.conquer-exit-button')
if (maybeExitButton === null || !(maybeExitButton instanceof HTMLElement)) {
Conquer.fail('No exit button.')
}
return maybeExitButton
}
public generateInterfaceElementCentered(): HTMLElement {
return this.getNodeFromTemplateId('conquer-interface-element-padded-template')
}
}

View File

@ -0,0 +1,209 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerLogin from '@burguillosinfo/conquer/login'
import ConquerInterface from '@burguillosinfo/conquer/interface'
export default class LoginUI extends ConquerInterface {
private conquerLogin: ConquerLogin
private conquerLoginGoToRegister: HTMLAnchorElement
private conquerLoginError: HTMLParagraphElement
private conquerLoginSuccess: HTMLParagraphElement
private conquerLoginUsername: HTMLInputElement
private conquerLoginPassword: HTMLInputElement
private conquerLoginSubmit: HTMLButtonElement
private conquerRegisterGoToLogin: HTMLAnchorElement
private conquerRegisterUsername: HTMLInputElement
private conquerRegisterPassword: HTMLInputElement
private conquerRegisterRepeatPassword: HTMLInputElement
private conquerRegisterSubmit: HTMLButtonElement
private conquerRegisterError: HTMLParagraphElement
constructor(conquerLogin: ConquerLogin) {
super()
this.conquerLogin = conquerLogin
}
public run() {
this.conquerLogin.on('login', () => {
this.runCallbacks('close');
});
this.storeRegisterElements()
this.storeLoginElements()
}
private getLoginDiv(): HTMLDivElement {
const element = this.getNodes()[1];
if (element === undefined || !(element instanceof HTMLDivElement)) {
Conquer.fail('Login is not a div.')
}
return element
}
private getOverlayDiv(): HTMLDivElement {
const element = this.getNodes()[0]
if (element === undefined || !(element instanceof HTMLDivElement)) {
Conquer.fail('Overlay transparent is not a div.')
}
return element
}
private getRegisterDiv(): HTMLDivElement {
const element = this.getNodes()[2]
if (element === undefined || !(element instanceof HTMLDivElement)) {
Conquer.fail('Register is not a div.')
}
return element
}
public generateNodes(): HTMLElement[] {
const resultArray = []
const overlay = this.getNodeFromTemplateId('conquer-overlay-transparent-template')
overlay.classList.remove('conquer-display-none')
resultArray.push(overlay)
const login = this.getNodeFromTemplateId('conquer-login-template')
login.classList.remove('conquer-display-none')
if (!(login instanceof HTMLDivElement)) {
Conquer.fail('Login is required to be a Div.')
}
resultArray.push(login)
const register = this.getNodeFromTemplateId('conquer-register-template')
resultArray.push(register)
return resultArray
}
private async storeRegisterElements() {
const registerElement = this.getRegisterDiv()
const conquerRegisterGoToLogin = registerElement.querySelector('.conquer-register-go-to-login')
if (conquerRegisterGoToLogin === null || !(conquerRegisterGoToLogin instanceof HTMLAnchorElement)) {
Conquer.fail('Link to go to login from register is invalid.')
}
this.conquerRegisterGoToLogin = conquerRegisterGoToLogin
this.conquerRegisterGoToLogin.addEventListener('click', () => {
this.goToLogin()
})
const conquerRegisterUsername = registerElement.querySelector('.conquer-register-username')
if (conquerRegisterUsername === null || !(conquerRegisterUsername instanceof HTMLInputElement)) {
Conquer.fail('No username field in conquer register.')
}
this.conquerRegisterUsername = conquerRegisterUsername
const conquerRegisterPassword = registerElement.querySelector('.conquer-register-password')
if (conquerRegisterPassword === null || !(conquerRegisterPassword instanceof HTMLInputElement)) {
Conquer.fail('No password field in conquer register.')
}
this.conquerRegisterPassword = conquerRegisterPassword
const conquerRegisterRepeatPassword = registerElement.querySelector('.conquer-register-repeat-password')
if (conquerRegisterRepeatPassword === null || !(conquerRegisterRepeatPassword instanceof HTMLInputElement)) {
Conquer.fail('No repeat password field in conquer register.')
}
this.conquerRegisterRepeatPassword = conquerRegisterRepeatPassword
const conquerRegisterSubmit = registerElement.querySelector('.conquer-register-submit')
if (conquerRegisterSubmit === null || !(conquerRegisterSubmit instanceof HTMLButtonElement)) {
Conquer.fail('No register submit button found.')
}
this.conquerRegisterSubmit = conquerRegisterSubmit
this.conquerRegisterSubmit.addEventListener('click', (event: Event) => {
event.preventDefault()
const username = this.conquerRegisterUsername.value
const password = this.conquerRegisterPassword.value
const repeatPassword = this.conquerRegisterRepeatPassword.value
this.conquerLogin.onRegisterRequest(this, username, password, repeatPassword)
})
const conquerRegisterError = registerElement.querySelector('.conquer-register-error')
if (conquerRegisterError === null || !(conquerRegisterError instanceof HTMLParagraphElement)) {
Conquer.fail('Unable to find the conquer error element.')
}
this.conquerRegisterError = conquerRegisterError
}
private storeLoginElements() {
const loginElement = this.getLoginDiv()
const conquerLoginGoToRegister = loginElement.querySelector('.conquer-login-go-to-register')
if (conquerLoginGoToRegister === null || !(conquerLoginGoToRegister instanceof HTMLAnchorElement)) {
Conquer.fail('Link to go to register from login is invalid.')
}
this.conquerLoginGoToRegister = conquerLoginGoToRegister
this.conquerLoginGoToRegister.addEventListener('click', () => {
this.goToRegister()
})
const conquerLoginError = loginElement.querySelector('.conquer-login-error')
if (conquerLoginError === null || !(conquerLoginError instanceof HTMLParagraphElement)) {
Conquer.fail('Unable to find conquer login error.')
}
this.conquerLoginError = conquerLoginError
const conquerLoginSuccess = loginElement.querySelector('.conquer-login-success')
if (conquerLoginSuccess === null || !(conquerLoginSuccess instanceof HTMLParagraphElement)) {
Conquer.fail('Unable to find conquer login success.')
}
this.conquerLoginSuccess = conquerLoginSuccess
const conquerLoginUsername = loginElement.querySelector('.conquer-login-username')
if (conquerLoginUsername === null || !(conquerLoginUsername instanceof HTMLInputElement)) {
Conquer.fail('Unable to find conquer login username field.')
}
this.conquerLoginUsername = conquerLoginUsername
const conquerLoginPassword = loginElement.querySelector('.conquer-login-password')
if (conquerLoginPassword === null || !(conquerLoginPassword instanceof HTMLInputElement)) {
Conquer.fail('Unable to find conquer login password field.')
}
this.conquerLoginPassword = conquerLoginPassword
const conquerLoginSubmit = loginElement.querySelector('.conquer-login-submit')
if (conquerLoginSubmit === null || !(conquerLoginSubmit instanceof HTMLButtonElement)) {
Conquer.fail('Unable to find the submit button for the login.')
}
this.conquerLoginSubmit = conquerLoginSubmit
this.conquerLoginSubmit.addEventListener('click', (event: Event) => {
event.preventDefault()
const username = this.conquerLoginUsername.value
const password = this.conquerLoginPassword.value
this.conquerLogin.onLoginRequested(this, username, password)
})
}
private async goToRegister(): Promise<void> {
await this.removeLoginRegisterCombo()
const registerElement = this.getRegisterDiv()
registerElement.classList.remove('conquer-display-none')
}
public async removeLoginRegisterCombo(): Promise<void> {
const registerElement = this.getRegisterDiv()
const overlayElement = this.getOverlayDiv()
overlayElement.classList.add('conquer-display-none')
const loginElement = this.getLoginDiv()
loginElement.classList.add('conquer-display-none')
registerElement.classList.add('conquer-display-none')
}
public async addNewLoginSuccessText(message: string): Promise<void> {
this.unsetLoginAndRegisterErrors()
this.conquerLoginSuccess.innerText = message
this.conquerLoginSuccess.classList.remove('conquer-display-none')
}
public async addNewLoginError(error: string): Promise<void> {
this.unsetLoginAndRegisterErrors()
this.conquerLoginSuccess.classList.add('conquer-display-none')
this.conquerLoginError.innerText = error
this.conquerLoginError.classList.remove('conquer-display-none')
}
public async addNewRegisterError(error: string): Promise<void> {
this.unsetLoginAndRegisterErrors()
this.conquerLoginSuccess.classList.add('conquer-display-none')
this.conquerRegisterError.innerText = error
this.conquerRegisterError.classList.remove('conquer-display-none')
}
public async unsetLoginAndRegisterErrors() {
this.conquerRegisterError.classList.add('conquer-display-none')
this.conquerLoginError.classList.add('conquer-display-none')
}
public async goToLogin(): Promise<void> {
await this.removeLoginRegisterCombo()
const loginElement = this.getLoginDiv()
loginElement.classList.remove('conquer-display-none')
}
public async addNewLoginRegisterError(message: string): Promise<void> {
this.addNewRegisterError(message)
this.addNewLoginError(message)
}
}

View File

@ -0,0 +1,104 @@
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import Conquer from '@burguillosinfo/conquer'
export default class NewNodeUI extends AbstractTopBarInterface {
private coordinates: number[];
public getSubmitButton(): HTMLElement {
const submitButton = this.getMainNode().querySelector('button.new-node-form-submit')
if (submitButton === null || !(submitButton instanceof HTMLElement)) {
Conquer.fail('SubmitButton is null');
}
return submitButton;
}
public getErrorElement(): HTMLElement {
const errorElement = this.getMainNode().querySelector('p.conquer-error');
if (errorElement === null || !(errorElement instanceof HTMLElement)) {
Conquer.fail('No error element set');
}
return errorElement;
}
public getSelectNodeType(): HTMLSelectElement {
const selectElement = this.getMainNode().querySelector('select.conquer-node-type');
if (selectElement === null || !(selectElement instanceof HTMLSelectElement)) {
Conquer.fail('SelectElementNodeType is null');
}
return selectElement
}
public getInputNodeName(): HTMLInputElement {
const nodeName = this.getMainNode().querySelector('input.conquer-node-name')
if (nodeName === null || !(nodeName instanceof HTMLInputElement)) {
Conquer.fail('NodeName is null');
}
return nodeName
}
public getTextAreaNodeDescription(): HTMLTextAreaElement {
const nodeDescription = this.getMainNode().querySelector('textarea.conquer-node-description')
if (nodeDescription === null || !(nodeDescription instanceof HTMLTextAreaElement)) {
Conquer.fail('NodeDescription is null');
}
return nodeDescription
}
constructor(coordinates: number[]) {
super()
this.coordinates = coordinates
}
public run() {
const mainNode = this.getMainNode()
const form = this.getNodeFromTemplateId('conquer-new-node-form-creation-template')
mainNode.append(form)
this.getSubmitButton().addEventListener('click', (event) => {
event.preventDefault();
this.onSubmit();
});
form.classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none')
}
private setError(error: string): void {
const errorElement = this.getErrorElement();
errorElement.classList.remove('conquer-display-none')
errorElement.innerText = error
}
private onSubmit(): void {
const selectNodeType = this.getSelectNodeType();
const inputNodeName = this.getInputNodeName();
const textAreaNodeDescription = this.getTextAreaNodeDescription();
const description = textAreaNodeDescription.value;
const nodeName = inputNodeName.value;
const selectedOptionsNodeType = selectNodeType.selectedOptions;
if (selectedOptionsNodeType.length < 1) {
this.setError('Debes selecionar un tipo de nodo.');
return;
}
const selectedOptionNodeType = selectedOptionsNodeType[0];
const nodeType = selectedOptionNodeType.value;
if (nodeName.length < 5) {
this.setError('Todos los nodos deben tener un nombre mayor a 4 caracteres.');
return;
}
const urlNode = new URL('/conquer/node', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlNode, {
method: 'PUT',
body: JSON.stringify({
description: description,
name: nodeName,
type: nodeType,
coordinates: this.coordinates,
}),
}).then(async (res) => {
let responseBody;
try {
responseBody = await res.json();
} catch (error) {
this.setError( 'Respuesta erronea del servidor.');
return;
}
if (res.status !== 200) {
this.setError(responseBody.error);
return;
}
this.runCallbacks('close')
});
}
}

View File

@ -0,0 +1,97 @@
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import Conquer from '@burguillosinfo/conquer'
export default class NewTeamUI extends AbstractTopBarInterface {
public getSubmitButton(): HTMLElement {
const submitButton = this.getMainNode().querySelector('button.new-team-form-submit')
if (submitButton === null || !(submitButton instanceof HTMLElement)) {
Conquer.fail('SubmitButton is null');
}
return submitButton;
}
public getErrorElement(): HTMLElement {
const errorElement = this.getMainNode().querySelector('p.conquer-error');
if (errorElement === null || !(errorElement instanceof HTMLElement)) {
Conquer.fail('No error element set');
}
return errorElement;
}
public getInputTeamName(): HTMLInputElement {
const teamName = this.getMainNode().querySelector('input.conquer-team-name');
if (teamName === null || !(teamName instanceof HTMLInputElement)) {
Conquer.fail('TeamName is null');
}
return teamName;
}
public getInputTeamColor(): HTMLInputElement {
const teamColor = this.getMainNode().querySelector('input.conquer-team-color');
if (teamColor === null || !(teamColor instanceof HTMLInputElement)) {
Conquer.fail('TeamColor is null');
}
return teamColor;
}
public getTextareaTeamDescription(): HTMLTextAreaElement {
const teamDescription = this.getMainNode().querySelector('textarea.conquer-team-description')
if (teamDescription === null || !(teamDescription instanceof HTMLTextAreaElement)) {
Conquer.fail('TeamDescription is null');
}
return teamDescription
}
constructor() {
super()
}
public run() {
const mainNode = this.getMainNode()
const form = this.getNodeFromTemplateId('conquer-new-team-form-creation-template')
mainNode.append(form)
this.getSubmitButton().addEventListener('click', (event) => {
event.preventDefault();
this.onSubmit();
});
form.classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none')
}
private setError(error: string): void {
const errorElement = this.getErrorElement();
errorElement.classList.remove('conquer-display-none')
errorElement.innerText = error
}
private onSubmit(): void {
const inputTeamName = this.getInputTeamName();
const textareaTeamDescription = this.getTextareaTeamDescription();
const inputTeamColor = this.getInputTeamColor();
const name = inputTeamName.value;
const description = textareaTeamDescription.value;
const color = inputTeamColor.value;
if (name.length < 5) {
this.setError('Todos los equipos deben tener un nombre mayor a 4 caracteres.');
return;
}
const urlTeam = new URL('/conquer/team', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlTeam, {
method: 'PUT',
body: JSON.stringify({
description : description,
name : name,
color : color,
}),
}).then(async (res) => {
let responseBody;
try {
responseBody = await res.json();
} catch (error) {
this.setError( 'Respuesta erronea del servidor.');
return;
}
if (res.status !== 200) {
this.setError(responseBody.error);
return;
}
this.runCallbacks('close')
});
}
}

View File

@ -0,0 +1,157 @@
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import Conquer from '@burguillosinfo/conquer'
import MapNode from '@burguillosinfo/conquer/map-node'
import ConquerUser from '@burguillosinfo/conquer/user'
export default class NodeView extends AbstractTopBarInterface {
private node: MapNode;
private user: ConquerUser;
private view: HTMLElement | null = null;
public getNode(): MapNode {
return this.node;
}
private getNodeNameH2(): HTMLElement {
const element = this.getMainNode().querySelector('h2.node-name');
if (!(element instanceof HTMLElement)) {
Conquer.fail('h2.node-name is not a H2 or does not exist.');
}
return element;
}
private getView(): HTMLElement {
if (this.view === null) {
const view = this.getNodeFromTemplateId('conquer-view-node-template')
this.view = view;
}
return this.view;
}
private getNodeDescriptionParagraph(): HTMLElement {
const element = this.getMainNode().querySelector('p.node-description');
if (!(element instanceof HTMLElement)) {
Conquer.fail('p.node-description is not a P or does not exist.');
}
return element;
}
constructor(node: MapNode) {
super()
this.node = node;
}
public async run() {
const user = await ConquerUser.getSelfUser();
if (user === null) {
this.runCallbacks('close');
return;
}
this.user = user;
const mainNode = this.getMainNode()
this.runCallbacks('update-nodes');
try {
this.node = await this.node.fetch();
} catch (error) {
this.runCallbacks('close');
}
mainNode.append(this.getView())
this.getNodeNameH2().innerText = this.node.getName();
this.getNodeDescriptionParagraph().innerText = this.node.getDescription()
+ "\n"
+ (this.node.isNear()
? 'Estas cerca y puedes interactuar con este sitio.'
: 'Estás demasiado lejos para hacer nada aquí.');
this.populateTeamData();
if (this.node.isNear()) {
await this.runIfNear();
}
this.getView().classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none')
}
private async populateTeamData() {
const element = document.createElement('p');
const team = await this.node.getTeam();
(() => {
if (team === null) {
element.innerText = 'El nodo no pertenece a ningún equipo todavía.';
return;
}
const spanText = document.createElement('span');
spanText.innerText = 'Equipo: ';
element.append(spanText);
const spanCircle = document.createElement('span');
spanCircle.classList.add('conquer-team-circle');
spanCircle.style.backgroundColor = team.getColor();
element.append(spanCircle);
const spanTeamName = document.createElement('span');
spanTeamName.style.color = team.getColor();
spanTeamName.innerText = ' ' + team.getName();
element.append(spanTeamName);
})();
this.getView().append(element);
}
private async runIfNear(): Promise<void> {
const userTeam = await this.user.getTeam();
const nodeTeam = await this.node.getTeam();
if (userTeam === null) {
const paragraphNoTeam = document.createElement('p');
paragraphNoTeam.innerText = 'Parece que no has seleccionado equipo aun,'
+ ' pulsa el botón de seleccionar equipo para comenzar tu aventura,'
+ ' si quieres cambiar de equipo en el futuro puedes hacerlo sin problemas.';
this.getView().append(paragraphNoTeam);
}
const selectTeamButton = document.createElement('button');
selectTeamButton.innerText = 'Seleccionar equipo';
selectTeamButton.addEventListener('click', () => {
this.runCallbacks('open-select-team');
this.runCallbacks('close');
});
this.getView().append(selectTeamButton);
if (await this.isOpposingNode()) {
const conquerForTeamButton = document.createElement('button');
conquerForTeamButton.innerText = 'Conquistar';
conquerForTeamButton.addEventListener('click', () => {
this.conquerThisNodeForTeam();
});
this.getView().append(conquerForTeamButton);
}
}
private async conquerThisNodeForTeam() {
const urlNode = new URL('/conquer/node/' + this.node.getUUID() + '/try-conquer',
window.location.protocol + '//'
+ window.location.hostname + ':'
+ window.location.port)
const response = await fetch(urlNode, {
method: 'POST',
});
this.runCallbacks('update-nodes');
this.runCallbacks('close');
}
private async isOpposingNode(): Promise<boolean> {
const userTeam = await this.user.getTeam();
const nodeTeam = await this.node.getTeam();
if (userTeam === null) {
return false;
}
if (nodeTeam === null) {
return true;
}
if (nodeTeam.getUUID() !== userTeam.getUUID()) {
return true;
}
return false;
}
private async isNodeFree(): Promise<boolean> {
return await this.node.getTeam() === null;
}
}

View File

@ -0,0 +1,72 @@
import Conquer from '@burguillosinfo/conquer';
import ConquerUser from '@burguillosinfo/conquer/user';
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface';
import ConquerUserCurrentEnemy from '@burguillosinfo/conquer/user-current-enemy'
export default class SelectFightUI extends AbstractTopBarInterface {
private enemies: ConquerUserCurrentEnemy[];
private form: HTMLElement | null = null;
constructor(enemies: ConquerUserCurrentEnemy[]) {
super();
this.enemies = enemies;
}
public async run(): Promise<void> {
const user = await ConquerUser.getSelfUser()
if (user === null) {
this.runCallbacks('close')
return
}
this.getMainNode().append(this.getForm());
this.populateEnemies();
this.getMainNode().classList.remove('conquer-display-none');
}
private populateEnemies(): void {
for (const enemy of this.enemies) {
this.appendEnemy(enemy);
}
}
private appendEnemy(enemy: ConquerUserCurrentEnemy) {
const form = this.getForm();
const enemyNode = this.getNodeFromTemplateId('conquer-select-fight-item-template');
this.getNameEnemyNodeElement(enemyNode).innerText = enemy.getSpecies().getName();
this.getLevelEnemyNodeElement(enemyNode).innerText = '' + enemy.getLevel();
this.getImageEnemyNodeElement(enemyNode).src = enemy.getSpecies().getImage();
form.append(enemyNode);
}
private getImageEnemyNodeElement(enemyNode: HTMLElement): HTMLImageElement {
const conquerImage = enemyNode.querySelector('.conquer-image');
if (!(conquerImage instanceof HTMLImageElement)) {
Conquer.fail('conquerImage is not HTMLImageElement.')
}
return conquerImage;
}
private getLevelEnemyNodeElement(enemyNode: HTMLElement): HTMLElement {
const conquerLevel = enemyNode.querySelector('.conquer-level');
if (!(conquerLevel instanceof HTMLElement)) {
Conquer.fail('conquerLevel is not HTMLElement.')
}
return conquerLevel;
}
private getNameEnemyNodeElement(enemyNode: HTMLElement): HTMLElement {
const conquerName = enemyNode.querySelector('.conquer-name');
if (!(conquerName instanceof HTMLElement)) {
Conquer.fail('conquerName is not HTMLElement.')
}
return conquerName;
}
private getForm(): HTMLElement {
if (this.form === null) {
const form = this.getNodeFromTemplateId('conquer-select-fight-list-template')
this.form = form;
}
return this.form;
}
}

View File

@ -0,0 +1,89 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerUser from '@burguillosinfo/conquer/user'
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
import MapNode from '@burguillosinfo/conquer/map-node'
import ConquerTeam from '@burguillosinfo/conquer/team';
export default class SelectTeamUI extends AbstractTopBarInterface {
private node: MapNode;
private user: ConquerUser;
private form: HTMLElement | null = null;
constructor(node: MapNode) {
super();
this.node = node;
}
public async run(): Promise<void> {
const user = await ConquerUser.getSelfUser()
if (user === null) {
this.runCallbacks('close')
return
}
this.user = user
await this.populateTeams();
this.getForm().classList.remove('conquer-display-none');
this.getMainNode().append(this.getForm());
this.getMainNode().classList.remove('conquer-display-none');
}
private async populateTeams() {
const teams = await ConquerTeam.getTeams();
for (const team of teams) {
this.populateTeam(team);
}
}
private populateTeam(team: ConquerTeam) {
const teamDiv = this.getNodeFromTemplateId('conquer-team-to-select-template')
const nameParagraph = teamDiv.querySelector('p.conquer-name');
const descriptionParagraph = teamDiv.querySelector('p.conquer-description');
const submit = teamDiv.querySelector('button.conquer-submit');
if (!(nameParagraph instanceof HTMLParagraphElement)
|| !(descriptionParagraph instanceof HTMLParagraphElement)
|| !(submit instanceof HTMLButtonElement)) {
Conquer.fail('Select team name inclusive or description container are not correctly defined in template.');
}
nameParagraph.innerText = team.getName();
descriptionParagraph.innerText = team.getDescription();
nameParagraph.style.color = team.getColor();
submit.addEventListener('click', async () => {
this.onSelectTeam(team);
});
this.getForm().append(teamDiv);
}
private async onSelectTeam(team: ConquerTeam) {
const urlTeam = new URL('/conquer/user/team',
window.location.protocol + '//'
+ window.location.hostname + ':'
+ window.location.port);
const response = await fetch(urlTeam, {
method: 'POST',
body: JSON.stringify({
team: team.getUUID(),
node: this.node.getUUID(),
}),
});
let responseBody;
try {
responseBody = await response.json();
if (response.status !== 200) {
console.error(responseBody.error);
return;
}
this.runCallbacks('update-nodes');
this.runCallbacks('close')
} catch (error) {
console.error('Error parsing json', error);
}
}
private getForm(): HTMLElement {
if (this.form === null) {
const form = this.getNodeFromTemplateId('conquer-select-team-list-template')
this.form = form;
}
return this.form;
}
}

View File

@ -0,0 +1,147 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerUser from '@burguillosinfo/conquer/user'
import AbstractTopBarInterface from '@burguillosinfo/conquer/interface/abstract-top-bar-interface'
export default class SelfPlayerUI extends AbstractTopBarInterface {
private selfPlayer: ConquerUser | null = null
private userWelcome: HTMLElement | null = null
private isExplorerModeEnabled: boolean;
private userTeamData: HTMLElement | null = null;
constructor(isExplorerModeEnabled: boolean) {
super();
this.isExplorerModeEnabled = isExplorerModeEnabled;
}
public async run(): Promise<void> {
const selfPlayerNode = this.getMainNode()
const user = await ConquerUser.getSelfUser()
if (user === null) {
this.runCallbacks('close')
return
}
this.selfPlayer = user
this.populateWelcome()
this.populateCreateNodeOption()
this.populateToggleExplorerModeOption();
this.populateCreateTeamButton();
await this.populateUserTeamData();
selfPlayerNode.classList.remove('conquer-display-none')
}
private populateToggleExplorerModeOption(): void {
const toggleExplorerModeButton = document.createElement('button');
this.setTextToggleExplorerModeButton(toggleExplorerModeButton);
toggleExplorerModeButton.addEventListener('click', () => {
(() => {
if (this.isExplorerModeEnabled) {
this.runCallbacks('disable-explorer-mode');
return;
}
this.runCallbacks('enable-explorer-mode');
})();
this.runCallbacks('close');
});
const toggleExplorerModeInterface = this.generateInterfaceElementCentered()
toggleExplorerModeInterface.appendChild(toggleExplorerModeButton)
this.getMainNode().appendChild(toggleExplorerModeInterface)
}
private populateCreateTeamButton(): void {
// Only admins can create teams.
if (!this.selfPlayer?.isAdmin()) {
return;
}
const createTeamButton = document.createElement('button');
createTeamButton.innerText = 'Crea un nuevo equipo';
createTeamButton.addEventListener('click', () => {
this.runCallbacks('open-create-team');
this.runCallbacks('close');
});
const createTeamButtonInterface = this.generateInterfaceElementCentered()
createTeamButtonInterface.append(createTeamButton);
this.getMainNode().appendChild(createTeamButtonInterface);
}
private setTextToggleExplorerModeButton(button: HTMLElement): void {
if (this.isExplorerModeEnabled) {
button.innerText = 'Desactivar movimiento libre en el mapa.';
return;
}
button.innerText = 'Activar movimiento libre en el mapa.';
}
private populateCreateNodeOption() {
// Only admins can create nodes.
if (!this.selfPlayer?.isAdmin()) {
return
}
const createNodeButton = document.createElement('button')
createNodeButton.innerText = 'Crear Nuevo Nodo'
createNodeButton.addEventListener('click', () => {
this.runCallbacks('createNodeStart')
// We close because it is a sensible thing to do.
this.runCallbacks('close')
})
const createNodeButtonInterface = this.generateInterfaceElementCentered()
createNodeButtonInterface.appendChild(createNodeButton)
this.getMainNode().appendChild(createNodeButtonInterface)
}
private async getUserTeamData(): Promise<HTMLElement> {
if (this.userTeamData !== null) {
return this.userTeamData;
}
const element = document.createElement('p');
this.userTeamData = element;
if (this.selfPlayer === null) {
throw new Error('User still not set')
}
const team = await this.selfPlayer.getTeam();
if (team === null) {
element.innerText = 'No tienes equipo aun,'
+ ' ve al nodo más cercano para unirte a un equipo.';
return this.userTeamData;
}
const spanText = document.createElement('span');
spanText.innerText = 'Equipo: ';
element.append(spanText);
const spanCircle = document.createElement('span');
spanCircle.classList.add('conquer-team-circle');
spanCircle.style.backgroundColor = team.getColor();
element.append(spanCircle);
const spanTeamName = document.createElement('span');
spanTeamName.style.color = team.getColor();
spanTeamName.innerText = ' ' + team.getName();
element.append(spanTeamName);
return this.userTeamData;
}
private async populateUserTeamData(): Promise<void> {
const userTeamData = await this.getUserTeamData();
const userTeamDataInterface = this.generateInterfaceElementCentered();
userTeamDataInterface.append(userTeamData);
this.getMainNode().append(userTeamDataInterface);
}
private populateWelcome(): void {
const userWelcome = this.getUserWelcome()
const userWelcomeInterface = this.generateInterfaceElementCentered();
userWelcomeInterface.appendChild(userWelcome)
this.getMainNode().appendChild(userWelcomeInterface)
}
private getUserWelcome(): HTMLElement {
if (this.userWelcome !== null) {
return this.userWelcome
}
const element = document.createElement('h2')
if (this.selfPlayer === null) {
throw new Error('User still not set')
}
element.innerText = `¡Hola, ${this.selfPlayer.getUsername()}!`
this.userWelcome = element
return this.userWelcome
}
}

136
js-src/conquer/login.ts Normal file
View File

@ -0,0 +1,136 @@
import Conquer from '@burguillosinfo/conquer'
import ConquerInterfaceManager from '@burguillosinfo/conquer/interface-manager'
import LoginUI from '@burguillosinfo/conquer/interface/login'
export type ConquerLoginEventCallback = () => void
export default class Login {
private conquerLogin: HTMLDivElement
private conquerInterfaceManager: ConquerInterfaceManager
private cachedIsLoggedIn: boolean | null = null
constructor(conquerInterfaceManager: ConquerInterfaceManager) {
this.conquerInterfaceManager = conquerInterfaceManager
}
public async start(): Promise<void> {
this.loopCheckLogin()
}
public async onRegisterRequest(loginUI: LoginUI, username: string, password: string, repeatPassword: string): Promise<void> {
const urlUser = new URL('/conquer/user', window.location.protocol +
'//' + window.location.hostname + ':' + window.location.port)
let responseJson
let status
try {
const response = await fetch(urlUser, {
method: 'PUT',
body: JSON.stringify({
username: username,
password: password,
repeat_password: repeatPassword
})
})
responseJson = await response.json()
status = response.status
} catch(e) {
console.error(e)
loginUI.addNewRegisterError('El servidor ha enviado datos inesperados.')
return
}
if (status !== 200) {
loginUI.addNewRegisterError(responseJson.error)
return
}
loginUI.addNewLoginSuccessText(`Usuario registrado ${username}.`)
loginUI.goToLogin()
}
private async loopCheckLogin(): Promise<void> {
window.setInterval(() => {
this.isLogged().then((isLogged) => {
if (isLogged) {
if (this.cachedIsLoggedIn !== true) {
this.cachedIsLoggedIn = true;
this.onLoginSuccess();
}
return;
}
if (this.cachedIsLoggedIn !== false) {
this.cachedIsLoggedIn = false;
this.onLogout()
}
})
}, 5000)
}
private async onLogout(): Promise<void> {
const interfaceManager = this.conquerInterfaceManager
const loginUI = new LoginUI(this)
for (const callback of this.callbacks.logout) {
callback();
}
loginUI.on('close', () => {
interfaceManager.remove(loginUI);
})
interfaceManager.push(loginUI)
}
private callbacks: Record<string, Array<ConquerLoginEventCallback>> = {}
public async on(name: string, callback: ConquerLoginEventCallback) {
if (this.callbacks[name] === undefined) {
this.callbacks[name] = []
}
this.callbacks[name].push(callback)
}
private async onLoginSuccess(): Promise<void> {
this.cachedIsLoggedIn = true
for (const callback of this.callbacks.login) {
callback()
}
}
public async onLoginRequested(loginUI: LoginUI, username: string, password: string): Promise<void> {
const urlUser = new URL('/conquer/user/login', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let responseJson
let status
try {
const response = await fetch(urlUser, {
method: 'POST',
body: JSON.stringify({
username: username,
password: password,
})
})
responseJson = await response.json()
status = response.status
} catch(e) {
console.error(e)
loginUI.addNewLoginError('El servidor ha enviado datos inesperados.')
return
}
if (status !== 200) {
loginUI.addNewLoginError(responseJson.error)
return
}
loginUI.unsetLoginAndRegisterErrors()
const isLogged = await this.isLogged()
if (isLogged) {
this.onLoginSuccess()
}
}
public async isLogged(): Promise<boolean> {
const urlUser = new URL('/conquer/user', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
let status
try {
const response = await fetch(urlUser)
status = response.status
} catch {
return false
}
return status === 200
}
}

163
js-src/conquer/map-node.ts Normal file
View File

@ -0,0 +1,163 @@
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import Style from 'ol/style/Style'
import Feature from 'ol/Feature'
import CircleStyle from 'ol/style/Circle'
import Point from 'ol/geom/Point'
import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke'
import InterfaceManager from '@burguillosinfo/conquer/interface-manager'
import NodeView from '@burguillosinfo/conquer/interface/node-view'
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import SelectTeamUI from '@burguillosinfo/conquer/interface/select-team';
import ConquerTeam from '@burguillosinfo/conquer/team';
@JsonObject()
export default class MapNode {
private feature: Feature | null = null;
private callbacks: Record<string, Array<() => void>> = {}
private cachedTeam: ConquerTeam | null = null;
constructor(
@JsonProperty() private uuid: string,
@JsonProperty() private coordinate_1: number,
@JsonProperty() private coordinate_2: number,
@JsonProperty() private type: string,
@JsonProperty() private name: string,
@JsonProperty() private description: string,
@JsonProperty() private kind: string,
@JsonProperty() private is_near: boolean,
@JsonProperty() private team: string,
) {
}
public async getTeam(): Promise<ConquerTeam | null> {
if (this.cachedTeam === null) {
if (this.team === null) {
return null;
}
this.cachedTeam = await ConquerTeam.getTeam(this.team);
}
return this.cachedTeam;
}
public async fetch(): Promise<MapNode> {
const urlNode = new URL('/conquer/node/' + this.uuid, window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
const response = await fetch(urlNode);
let responseBody;
const errorThrow = new Error('Unable to fetch node updated.');
try {
responseBody = await response.json();
} catch (error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
throw errorThrow;
}
if (response.status !== 200) {
console.error(responseBody.error);
throw errorThrow;
}
const node = JsonSerializer.deserialize(responseBody, MapNode);
if (!(node instanceof MapNode)) {
console.error('Unexpected JSON value for MapNode.');
throw errorThrow;
}
return node;
}
public click(interfaceManager: InterfaceManager): void {
const viewNodeInterface = new NodeView(this);
viewNodeInterface.on('close', () => {
interfaceManager.remove(viewNodeInterface);
});
viewNodeInterface.on('update-nodes', () => {
this.runCallbacks('update-nodes');
});
viewNodeInterface.on('open-select-team', () => {
this.openSelectTeam(interfaceManager);
});
interfaceManager.push(viewNodeInterface);
this.runCallbacks('click');
}
public openSelectTeam(interfaceManager: InterfaceManager): void {
const selectTeamUI = new SelectTeamUI(this);
selectTeamUI.on('update-nodes', () => {
this.runCallbacks('update-nodes');
});
selectTeamUI.on('close', () => {
interfaceManager.remove(selectTeamUI);
});
interfaceManager.push(selectTeamUI);
}
public on(eventName: string, callback: () => void): void {
if (this.callbacks[eventName] === undefined) {
this.callbacks[eventName] = []
}
this.callbacks[eventName].push(callback)
}
protected runCallbacks(eventName: string) {
const callbacks = this.callbacks[eventName];
if (callbacks === undefined) {
return
}
for (const callback of callbacks) {
callback()
}
}
public getType(): string {
return this.type;
}
public isNear(): boolean {
return this.is_near;
}
public getName(): string {
return this.name;
}
public getDescription(): string {
return this.description;
}
public getId(): string {
return 'node-' + this.uuid;
}
public getUUID(): string {
return this.uuid;
}
public getFeature(): Feature {
if (this.feature === null) {
this.feature = new Feature({
geometry: new Point([this.coordinate_1, this.coordinate_2]),
type: 'node-' + this.uuid,
})
}
return this.feature;
}
public async getStyle(): Promise<Style> {
const team = await this.getTeam();
let color = 'white';
if (team !== null) {
color = team.getColor();
}
return new Style({
image: new CircleStyle({
radius: 14,
fill: new Fill({color: color}),
stroke: new Stroke({
color: 'black',
width: 5,
})
})
});
}
}

View File

@ -0,0 +1,11 @@
enum MapState {
NOTHING = 0x0,
NORMAL = 0x1,
FREE_MOVE = 0x2,
FREE_ROTATION = 0x4,
CREATE_NODE = 0x8,
SELECT_WHERE_TO_CREATE_NODE = 0x10,
FILLING_FORM_CREATE_NODE = 0x20,
}
export default MapState

View File

@ -0,0 +1,6 @@
import { JsonSerializer, throwError } from 'typescript-json-serializer';
export default new JsonSerializer({
errorCallback: throwError,
additionalPropertiesPolicy: 'disallow',
});

25
js-src/conquer/specie.ts Normal file
View File

@ -0,0 +1,25 @@
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
@JsonObject()
export default class Specie {
@JsonProperty()
private id: string;
@JsonProperty()
private name: string;
@JsonProperty()
private image: string;
public getId(): string {
return this.id;
}
public getName(): string {
return this.name;
}
public getImage(): string {
return this.image;
}
}

117
js-src/conquer/team.ts Normal file
View File

@ -0,0 +1,117 @@
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import Conquer from '@burguillosinfo/conquer'
@JsonObject()
export default class ConquerTeam {
@JsonProperty()
private kind: string;
@JsonProperty()
private uuid: string;
@JsonProperty()
private name: string;
@JsonProperty()
private description: string;
@JsonProperty()
private points: number;
@JsonProperty()
private color: string;
public getUUID(): string {
return this.uuid;
}
public getName(): string {
return this.name;
}
public getDescription(): string {
return this.description;
}
public getColor(): string {
return this.color;
}
constructor(uuid: string, name: string, description: string, points: number, color: string) {
this.kind = 'ConquerTeam';
this.uuid = uuid;
this.name = name;
this.description = description;
this.points = points;
this.color = color;
}
public static async getTeams(): Promise<ConquerTeam[]> {
const urlTeam = new URL('/conquer/teams', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching teams.')
}
const teamData = await response.json()
const teams = JsonSerializer.deserialize(teamData, ConquerTeam);
if (teams === undefined || teams === null) {
Conquer.fail('Teams cannot be null, server error.');
}
if (!(teams instanceof Array)) {
throw new Error('Unable to parse team.');
}
const teamsSanitized: ConquerTeam[] = [];
for (const team of teams) {
if (!(team instanceof ConquerTeam)) {
console.error('Received null team from server, fix this error.');
continue;
}
teamsSanitized.push(team);
}
return teamsSanitized;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Teams.');
}
}
public static async getTeam(uuid: string): Promise<ConquerTeam> {
const urlTeam = new URL('/conquer/team/' + uuid, window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching team.')
}
const teamData = await response.json()
let team = JsonSerializer.deserialize(teamData, ConquerTeam);
if (team === undefined) {
team = null;
}
if (!(team instanceof ConquerTeam)) {
throw new Error('Unable to parse team.');
}
return team;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Team.');
}
}
public static async getSelfTeam(): Promise<ConquerTeam | null> {
const urlTeam = new URL('/conquer/user/team', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching team.')
}
const teamData = await response.json()
let team = JsonSerializer.deserialize(teamData, ConquerTeam);
if (team === undefined) {
team = null;
}
if (team !== null && !(team instanceof ConquerTeam)) {
throw new Error('Unable to parse team.');
}
return team;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Team.');
}
}
}

View File

@ -0,0 +1,66 @@
import Conquer from '@burguillosinfo/conquer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import Specie from '@burguillosinfo/conquer/specie';
import JsonSerializer from '@burguillosinfo/conquer/serializer';
@JsonObject()
export default class ConquerUserCurrentEnemy {
@JsonProperty()
private uuid: string;
@JsonProperty()
private species: Specie;
@JsonProperty()
private level: number;
@JsonProperty()
private max_health: number;
public getUUID(): string {
return this.uuid;
}
public getSpecies(): Specie {
return this.species;
}
public getLevel(): number {
return this.level;
}
public getMaxHealth(): number {
return this.max_health;
}
public static async getGlobalEnemies(): Promise<ConquerUserCurrentEnemy[] | null> {
const urlEnemies = new URL('/conquer/user/enemies/global', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port);
const response = await fetch(urlEnemies);
let responseBody;
try {
responseBody = await response.json();
if (response.status !== 200) {
console.error(responseBody.error);
return null;
}
const enemiesRaw = JsonSerializer.deserialize(responseBody, ConquerUserCurrentEnemy);
const enemiesReturnArray: ConquerUserCurrentEnemy[] = [];
if (!(enemiesRaw instanceof Array)) {
console.error('Incorrect type retrieved from ' + urlEnemies);
return null;
}
for (const enemy of enemiesRaw) {
if (!(enemy instanceof ConquerUserCurrentEnemy)) {
console.error('Incorrect type for enemy, maybe null or undef.', enemy);
return null;
}
enemiesReturnArray.push(enemy);
}
return enemiesReturnArray;
} catch(error) {
console.error(error, 'Invalid response from server seeking for possible battles.');
return null;
}
}
}

78
js-src/conquer/user.ts Normal file
View File

@ -0,0 +1,78 @@
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import ConquerTeam from '@burguillosinfo/conquer/team';
export interface UserData {
is_admin: number
kind: string
last_activity?: string
registration_date?: string
username: string
uuid: string
}
@JsonObject()
export default class ConquerUser {
@JsonProperty()
private is_admin: boolean;
@JsonProperty()
private kind: string;
@JsonProperty()
private last_activity: string | null;
@JsonProperty()
private registration_date: string | null;
@JsonProperty()
private username: string;
@JsonProperty()
private uuid: string;
@JsonProperty()
private team: string | null;
private cachedTeam: ConquerTeam | null = null;
constructor(kind: string, uuid: string, username: string, is_admin = false, registration_date: string | null = null, last_activity: string | null = null) {
this.kind = kind;
this.uuid = uuid;
this.username = username;
this.is_admin = is_admin;
this.registration_date = registration_date;
this.last_activity = last_activity;
}
public async getTeam(): Promise<ConquerTeam | null> {
if (this.cachedTeam === null) {
if (this.team === null) {
return null;
}
this.cachedTeam = await ConquerTeam.getTeam(this.team);
}
return this.cachedTeam;
}
public static async getSelfUser(): Promise<ConquerUser | null> {
const urlUser = new URL('/conquer/user', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlUser)
if (response.status !== 200) {
throw new Error('Invalid response fetching user.')
}
const userData = await response.json()
const user = JsonSerializer.deserialize(userData, ConquerUser);
if (!(user instanceof ConquerUser)) {
throw new Error('Unable to parse user.');
}
return user;
} catch (error) {
console.error(error)
return null
}
}
public getUsername(): string {
if (this.username === null) {
throw new Error('User username cannot be null.')
}
return this.username
}
public isAdmin(): boolean {
return this.is_admin
}
}

View File

@ -0,0 +1,30 @@
export default class ConquerWebSocket {
private webSocket: WebSocket | null = null
private socketReady = false
private getWebSocket(): WebSocket {
if (this.webSocket !== null && this.socketReady) {
return this.webSocket
}
this.webSocket = new WebSocket(`wss://${window.location.hostname}:${window.location.port}/conquer/websocket`)
this.webSocket.addEventListener('close', (event) => {
this.onSocketClose(event)
})
this.webSocket.addEventListener('error', (event) => {
this.onSocketClose(event)
})
this.webSocket.addEventListener('open', (event) => {
this.onSocketOpen(event)
})
return this.webSocket
}
private onSocketOpen(event: Event) {
this.socketReady = true
}
private onSocketClose(event: Event) {
this.socketReady = false
console.error(event)
}
}

View File

@ -1,12 +1,23 @@
"use strict";
import Tablesort from 'tablesort';
import Conquer from '@burguillosinfo/conquer/index';
import CarouselAd from '@burguillosinfo/carousel-ad'
window.Tablesort = require('tablesort');
require('tablesort/src/sorts/tablesort.number');
let fakeSearchInput
let searchMobile
document.addEventListener("DOMContentLoaded", function () {
onDomContentLoaded();
}, false);
function onDomContentLoaded() {
const path = window.location.pathname
if (path.match(/^(?:\/)?conquer(?:$|\/)/)) {
Conquer.start();
return
}
const menu_expand = document.querySelector('a.menu-expand');
const mobile_foldable = document.querySelector('nav.mobile-foldable');
const transparentFullscreenHide = document.querySelector('div.transparent-fullscreen-hide');
@ -64,8 +75,7 @@ document.addEventListener("DOMContentLoaded", function () {
fakeSearchInput = searchMobile.querySelector('input')
addListenersSearch()
}
}, false);
}
function fillFarmaciaGuardia() {
const farmaciaName = document.querySelector('#farmacia-name');
const farmaciaAddress = document.querySelector('#farmacia-address');
@ -121,14 +131,6 @@ function addListenersSearch() {
}
const nextResult = searchInPage.querySelector('a.down');
const prevResult = searchInPage.querySelector('a.up');
window.addEventListener("keydown", (e) => {
if (e.key.toLowerCase() === "f" && e.ctrlKey) {
openAllDetails()
}
});
window.addEventListener("blur", (e) => {
openAllDetails()
})
if (nextResult !== null && prevResult !== null) {
nextResult.addEventListener('click', () => {
searchInWebsite(fakeSearchInput.value, true);
@ -157,7 +159,6 @@ function addListenersSearch() {
function searchInWebsite(value, isToBottom) {
window.find(value, false, !isToBottom, true)
const selection = window.getSelection()
openAllDetails()
if (selection.anchorNode === null) {
const pageContents = document.querySelector('div.page-contents');
pageContents.focus()
@ -177,12 +178,6 @@ function searchInWebsite(value, isToBottom) {
}
}
function openAllDetails() {
for (const detail of document.querySelectorAll('details')) {
detail.open = true
}
}
function _getOffsetTopWithNParent(element, nParent, _carry = 0) {
if (element === null) {
return null;

View File

@ -7,6 +7,7 @@ use Mojo::Base 'Mojolicious', -signatures;
# This method will run once at server start
sub startup ($self) {
my $metrics = BurguillosInfo::Controller::Metrics->new;
$self->sessions->default_expiration(0);
$self->hook(
around_dispatch => sub {
my $next = shift;
@ -19,26 +20,66 @@ sub startup ($self) {
);
push @{ $self->commands->namespaces }, 'BurguillosInfo::Command';
$self->hook(
before_render => sub($c, $args) {
before_render => sub ( $c, $args ) {
my $current_route = $c->url_for;
$c->stash(current_route => $current_route);
my $is_android = $c->req->headers->user_agent =~ /android/i;
$c->stash(is_android => $is_android);
$c->stash( current_route => $current_route );
my $is_android = $c->req->headers->user_agent =~ /android/i;
$c->stash( is_android => $is_android );
my $onion_base_url = $self->config->{onion_base_url};
my $base_url = $self->config->{base_url};
if (!defined $onion_base_url) {
my $base_url = $self->config->{base_url};
if ( !defined $onion_base_url ) {
return;
}
$current_route =~ s/^$base_url//;
$c->res->headers->header('Onion-Location' => $onion_base_url.$current_route);
$c->res->headers->header(
'Onion-Location' => $onion_base_url . $current_route );
}
);
my $config = $self->plugin('JSONConfig');
$self->config(
hypnotoad => { proxy => 1, listen => [$self->config('listen') // 'http://localhost:3000'] } );
hypnotoad => {
proxy => 1,
listen => [ $self->config('listen') // 'http://localhost:3000' ]
}
);
$self->config( css_version => int( rand(10000) ) );
$self->secrets( $self->config->{secrets} );
$self->helper(
current_user => sub ($c) {
use BurguillosInfo::Schema;
$self->session(expiration => 0);
my $user_uuid = $c->session->{conquer}{user};
if ( !defined $user_uuid ) {
return;
}
my $user_resultset =
BurguillosInfo::Schema->Schema->resultset('ConquerUser');
my @user_candidates =
$user_resultset->search( { uuid => $user_uuid } );
my $user = $user_candidates[0];
# Just to make clear what happens if there is no user we return.
if ( !defined $user ) {
return;
}
return $user;
}
);
$self->helper(
set_current_user => sub ( $c, $user ) {
$self->session(expiration => 0);
if ( !defined $user
|| !$user->can('uuid')
|| !$user->can('get_from_storage') )
{
die "$user is not a valid user for it's usage in a session.";
}
$user = $user->get_from_storage;
$c->session->{conquer}{user} = $user->uuid;
}
);
# Router
my $r = $self->routes;
@ -48,12 +89,29 @@ sub startup ($self) {
$r->get('/sitemap.xml')->to('Sitemap#sitemap');
$r->get('/robots.txt')->to('Robots#robots');
# $r->get('/:post')->to('Page#post');
$r->get('/stats')->to('Metrics#stats');
$r->get('/conquer')->to('Conquer#index');
$r->put('/conquer/user')->to('UserConquer#create');
$r->get('/conquer/user/team')->to('UserConquer#getSelfTeam');
$r->post('/conquer/user/team')->to('UserConquer#setTeamForUser');
$r->post('/conquer/user/coordinates')->to('UserConquer#setCoordinates');
$r->get('/conquer/team/<uuid>')->to('ConquerTeam#get');
$r->put('/conquer/team')->to('ConquerTeam#put');
$r->get('/conquer/teams')->to('ConquerTeam#getAll');
$r->put('/conquer/node')->to('ConquerNode#create');
$r->get('/conquer/node/near')->to('ConquerNode#nearbyNodes');
$r->get('/conquer/user/enemies/global')->to('ConquerUserCurrentEnemy#listEnemiesGlobal');
$r->post('/conquer/user/enemies/fight')->to('ConquerUserCurrentEnemy#fightEnemy');
$r->get('/conquer/node/<uuid>')->to('ConquerNode#get');
$r->post('/conquer/node/<uuid>/try-conquer')->to('ConquerNode#tryConquer');
$r->get('/conquer/user')->to('UserConquer#get_self');
$r->post('/conquer/user/login')->to('UserConquer#login');
$r->get('/conquer/tile/<zoom>/<x>/<y>.png')->to('ConquerTile#tile');
$r->get('/search.json')->to('Search#search');
$r->get('/farmacia-guardia.json')->to('FarmaciaGuardia#current');
$r->get('/<:category>.rss')->to('Page#category_rss');
$r->get('/:category_slug/atributo/<:attribute_slug>-preview.png')->to('Attribute#get_attribute_preview');
$r->get('/:category_slug/atributo/<:attribute_slug>-preview.png')
->to('Attribute#get_attribute_preview');
$r->get('/:category_slug/atributo/:attribute_slug')->to('Attribute#get');
$r->get('/<:category>-preview.png')->to('Page#get_category_preview');
$r->get('/:category')->to('Page#category');

View File

@ -47,20 +47,17 @@ sub get_next ( $self, $current_ad_number = undef ) {
if ( !defined $current_ad_number ) {
$current_ad_number = 0;
}
my $ad;
while (!defined $ad || $ad->id eq $current_ad_number) {
$ad = $self->get_rand_ad($array)->clone;
}
my $ad = $self->get_rand_ad($array)->clone;
return {
ad => $ad->serialize,
continue => 1,
current_ad_number => $ad->id,
current_ad_number => $self->_get_next_number($current_ad_number),
};
}
sub get_rand_ad($self, $array) {
my $valid_ads = [ grep { $_->is_active } @$array ];
my $max_weight = $self->sum_weights($valid_ads);
my $max_weight = $self->sum_weights($array);
my $rand = int(rand() * $max_weight);
my $sum_weight = 0;
for my $ad (@$valid_ads) {

View File

@ -21,7 +21,7 @@ sub weight {
}
sub is_active ($self) {
return 0;
return 1;
}
sub img {

View File

@ -1,65 +0,0 @@
package BurguillosInfo::Ads::BurguillosDental;
use v5.36.0;
use strict;
use warnings;
use utf8;
use feature 'signatures';
use Moo;
use parent 'BurguillosInfo::Ad';
sub id ($self) {
return 'burguillos-dental';
}
sub weight {
return 50;
}
sub max_alternative {
return 3;
}
sub seconds($self) {
return 15;
}
sub default_alternative($self) {
return int($self->alternative * ($self->max_alternative + 1));
}
sub is_active ($self) {
return 0;
}
sub img ($self) {
if ( $self->default_alternative == 2 ) {
return '/img/burguillos-dental-ad-0-small.webp'
}
if ( $self->default_alternative == 1 ) {
return '/img/burguillos-dental-ad-1-small.webp'
}
return '/img/burguillos-dental-ad-1-small.webp'
}
sub text($self) {
if ( $self->default_alternative == 2 ) {
return 'Pide presupuesto para conseguir una sonrisa perfecta en Burguillos Dental, '.
'ubicado en Centro Médico Juan Manuel Pérez Sanchez.';
}
if ( $self->default_alternative == 1 ) {
return '¿Te has hecho ya tu limpieza completa de boca anual? Confia en profesionales, confia en Burguillos Dental, '.
'ubicado en Centro Médico Juan Manuel Pérez Sanchez.';
}
return '¿Te duele un diente? No lo dejes, ven a Burguillos Dental '.
'ubicado en Centro Médico Juan Manuel Pérez Sanchez.';
}
sub href {
return '/posts/burguillos-dental?come-from-ad=1';
}
1;

View File

@ -1,55 +0,0 @@
package BurguillosInfo::Ads::ChaletEnVentaCalleHinojo;
use v5.36.0;
use strict;
use warnings;
use utf8;
use DateTime;
use feature 'signatures';
use Moo;
use parent 'BurguillosInfo::Ad';
sub id ($self) {
return 'chalet-en-venta-calle-hinojo';
}
sub weight {
return 50;
}
sub max_alternative {
return 1;
}
sub seconds($self) {
return 15;
}
sub default_alternative($self) {
return int($self->alternative * ($self->max_alternative + 1));
}
sub is_active ($self) {
if (DateTime->new(year => 2024, month => 6, day => 11) < DateTime->now()) {
return 0;
}
return 1;
}
sub img ($self) {
return '/img/chalet-calle-hinojo.webp';
}
sub text($self) {
return 'Chalet pareado en venta en calle Hinojo por 160 000€';
}
sub href {
return 'https://www.idealista.com/inmueble/104802645/';
}
1;

View File

@ -29,7 +29,7 @@ sub default_alternative($self) {
}
sub is_active ($self) {
return 0;
return 1;
}
sub img ($self) {

View File

@ -33,7 +33,7 @@ sub default_alternative($self) {
}
sub is_active ($self) {
return 0;
return 1;
}
sub img ($self) {

View File

@ -29,7 +29,7 @@ sub default_alternative($self) {
}
sub is_active ($self) {
return 0;
return 1;
}
sub img ($self) {

View File

@ -13,7 +13,6 @@ sub next_ad {
my $self = shift;
my $ads_factory = BurguillosInfo::Ads->new;
my $current_ad_number = $self->param('n');
$self->res->headers->access_control_allow_origin('*');
$self->render( json => $ads_factory->get_next($current_ad_number) );
}
1;

View File

@ -0,0 +1,15 @@
package BurguillosInfo::Controller::Conquer;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
sub index($self) {
$self->render;
}
1;

View File

@ -0,0 +1,185 @@
package BurguillosInfo::Controller::ConquerNode;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use BurguillosInfo::Schema;
sub get($self) {
my $uuid = $self->param('uuid');
my $user = $self->current_user;
if (!defined $uuid || !$uuid) {
return $self->render(status => 400, json => {
error => 'UUID de nodo invalido.',
});
}
my $schema = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my @nodes = $schema->search({uuid => $uuid});
if (!scalar @nodes) {
return $self->render(status => 404, json => {
error => 'Nodo no encontrado',
});
}
my $node = $nodes[0];
if (defined $user) {
return $self->render(json => $node->serialize($user));
}
return $self->render(json => $node->serialize());
}
sub tryConquer($self) {
my $user = $self->current_user;
my $schema = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
if (!defined $user) {
return $self->render(status => 401, json => {
error => 'You must be logged to conquer a node.',
});
}
if (!defined $user->team) {
return $self->render(status => 400, json => {
error => 'You must belong to a team to conquer a node.',
});
}
my $uuid = $self->param('uuid');
my ($node) = $schema->search({uuid => $uuid});
if (!defined $node) {
return $self->render(status => 404, json => {
error => 'No existe ese nodo.',
});
}
$node->team($user->team);
$node->update;
$self->render(json => {
ok => $JSON::true,
});
}
sub create ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'No estás autenticado.',
}
);
}
if ( !$user->is_admin ) {
return $self->render(
status => 403,
json => {
error => 'No tienes permiso para hacer eso.',
}
);
}
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $name = $input->{name};
my $coordinates = $input->{coordinates};
my $type = $input->{type};
my $description = $input->{description};
if ( ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2 ) {
return $self->render(
status => 400,
json => {
error => 'Formato erroneo de coordenadas.',
}
);
}
my ($coordinate_1, $coordinate_2) = $coordinates->@*;
if ( !defined $name || length $name < 5 ) {
return $self->render(
status => 400,
json => {
error =>
'Número incorrecto de carácteres en el nombre del nodo.',
}
);
}
if ( !defined $description ) {
return $self->render(
status => 400,
json => {
error => 'La descripción puede estar vacía, '
. 'pero debe existir, si ves este error '
. 'desde la aplicación es un error de programación.',
}
);
}
if ( !defined $type ) {
return $self->render(
status => 400,
json => {
error => 'Los nodos deben tener un tipo.'
}
);
}
if ( $type ne 'normal' ) {
return $self->render(
status => 400,
json => {
error => 'Tipo de nodo no soportado.',
}
);
}
my $uuid_node = create_uuid_string();
my $node;
eval {
$node = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->new(
{
uuid => $uuid_node,
description => $description,
name => $name,
type => $type,
geometry => \['ST_MakePoint(?, ?)', $coordinate_1, $coordinate_2],
}
);
$node->insert;
$node = $node->get_from_storage;
};
if ($@) {
warn $@;
return $self->render(
status => 500,
json => {
error => 'El servidor no pudo almacenar el nodo, reporta este error.',
}
);
}
return $self->render(
status => 200,
json => $node->serialize,
);
}
sub nearbyNodes($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->render(status => 401, json => {
error => 'No estás loggeado.',
});
}
my @nodes = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->search({});
@nodes = map { $_->serialize($user) } @nodes;
return $self->render(json => \@nodes);
}
sub _expectJson ($self) {
my $input;
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
}
1;

View File

@ -0,0 +1,180 @@
package BurguillosInfo::Controller::ConquerTeam;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use JSON;
use BurguillosInfo::Schema;
sub getAll ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'You must be logged to fetch teams.',
}
);
}
my $uuid = $self->param('uuid');
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search({});
return $self->render( json => [ map { $_->serialize } @teams ] );
}
sub get ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'You must be logged to fetch a team.',
}
);
}
my $uuid = $self->param('uuid');
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search(
{
'uuid' => $uuid,
}
);
if ( scalar @teams <= 0 ) {
return $self->render(
status => 404,
json => {
error => 'This team does not exist.',
}
);
}
my $team = $teams[0];
return $self->render( json => $team->serialize );
}
sub getSelfTeam ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'You must be logged to fetch your Team.',
}
);
}
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search(
{
'players.uuid' => $user->uuid
},
{
join => 'players',
}
);
if ( scalar @teams <= 0 ) {
return $self->render( json => undef );
}
my $team = $teams[0];
return $self->render( json => $team );
}
sub _expectJson ($self) {
my $input;
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
}
sub put ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'No estás autenticado.',
}
);
}
if ( !$user->is_admin ) {
return $self->render(
status => 403,
json => {
error => 'No tienes permiso para hacer eso.',
}
);
}
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $name = $input->{name};
my $description = $input->{description};
my $color = $input->{color};
if ( !defined $name || length $name < 5 ) {
return $self->render(
status => 400,
json => {
error =>
'Número incorrecto de carácteres en el nombre del equipo..',
}
);
}
if ( !defined $description ) {
return $self->render(
status => 400,
json => {
error => 'La descripción puede estar vacía, '
. 'pero debe existir, si ves este error '
. 'desde la aplicación es un error de programación.',
}
);
}
my $color_regex_char = qr/[0-9a-fA-F]/;
if ( !defined $color || $color !~ /^#(?:${color_regex_char}{6}|${color_regex_char}{3})$/ ) {
return $self->render(
status => 400,
json => {
error => 'Formato de color invalido',
}
);
}
my $uuid_team = create_uuid_string();
my $team;
eval {
$team = BurguillosInfo::Schema->Schema->resultset('ConquerTeam')->new(
{
uuid => $uuid_team,
description => $description,
name => $name,
color => $color,
}
);
$team->insert;
};
if ($@) {
warn $@;
return $self->render(
status => 500,
json => {
error =>
'El servidor no pudo almacenar el equipo, reporta este error.',
}
);
}
return $self->render(
status => 200,
json => $team->serialize,
);
}
1;

View File

@ -0,0 +1,70 @@
package BurguillosInfo::Controller::ConquerTile;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use Path::Tiny;
use Mojo::UserAgent;
use DateTime::Format::HTTP;
use DateTime;
my $cache_files_dir =
path(__FILE__)->parent->parent->parent->parent->child('cache/tiles/');
sub _cache_response ($self) {
my $tomorrow_same_hour_datetime = DateTime->now->add( days => 1 );
$self->res->headers->cache_control("max_age=@{[3600*24]}");
$self->res->headers->expires(
DateTime::Format::HTTP->format_datetime($tomorrow_same_hour_datetime) );
}
sub tile ($self) {
my $zoom = $self->stash('zoom');
my $x = $self->stash('x');
my $y = $self->stash('y');
my $candidate_file = $cache_files_dir->child("$zoom-$x-$y.png");
if ( -f $candidate_file ) {
$self->_cache_response;
return $self->_render_png($candidate_file);
}
if ( !defined $self->current_user ) {
return $self->render(
status => 401,
text => '¡¡No estás loggeado, no puedes cargar mapa nuevo.!!'
);
}
$self->_cache_response;
my $file_to_write = $candidate_file;
my $ua = Mojo::UserAgent->new;
my $png_tile =
$ua->get("https://tile.openstreetmap.org/$zoom/$x/$y.png")->result->body;
open my $fh, '|-', 'convert', '/dev/stdin', '-channel', 'RGB', '-negate',
$file_to_write;
print $fh $png_tile;
close $fh;
$self->_render_png($file_to_write);
$self->_delete_extra_files();
}
sub _delete_extra_files ($self) {
my @files = $cache_files_dir->children;
if ( scalar @files < 20001 ) {
return;
}
@files = sort { -M $a <=> -M $b } @files;
for ( my $i = 0 ; $i < ( scalar @files ) - 20000 ; $i++ ) {
system 'rm', '-v', $files[$i];
}
}
sub _render_png ( $self, $file ) {
system 'touch', $file;
return $self->render( data => $file->slurp_raw, status => 200,
format => 'png' );
}
1;

View File

@ -0,0 +1,103 @@
package BurguillosInfo::Controller::ConquerUserCurrentEnemy;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use JSON;
use BurguillosInfo::Schema;
use BurguillosInfo::Species;
sub listEnemiesGlobal ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'Debes estar autenticado.',
}
);
}
my $current_enemies = $self->_get_enemies($user);
if ( scalar @$current_enemies ) {
return $self->_return_enemies($current_enemies);
}
$self->_generate_enemies_global($user);
$current_enemies = $self->_get_enemies($user);
return $self->_return_enemies($current_enemies);
}
sub _return_enemies ( $self, $current_enemies ) {
return $self->render( json => [ map { $_->serialize } @$current_enemies ] );
}
sub _get_enemies ( $self, $user ) {
my $resultset_current_enemies =
BurguillosInfo::Schema->Schema->resultset('ConquerUserCurrentEnemy');
my @current_enemies = $resultset_current_enemies->search(
{
'user_object.uuid' => $user->uuid,
},
{
join => 'user_object',
}
);
return \@current_enemies;
}
sub _generate_enemies_global ( $self, $user ) {
my $minimum_number_enemies = 2;
my $maximum_number_enemies = 6;
my $number_enemies =
$self->_calculate_number_of_enemies( $minimum_number_enemies,
$maximum_number_enemies );
for ( my $i = 0 ; $i < $number_enemies ; $i++ ) {
$self->_generate_enemy_global($user);
}
}
sub _generate_enemy_global ( $self, $user ) {
my $resultset_current_enemies =
BurguillosInfo::Schema->Schema->resultset('ConquerUserCurrentEnemy');
my $uuid = create_uuid_string();
my $species = BurguillosInfo::Species->new;
my @species = @{ $species->list_can_be_global };
my $selected_species = $species[int( rand( scalar @species ) )];
my $enemy = $resultset_current_enemies->new(
{
uuid => $uuid,
species => $selected_species->id,
level => $self->_get_level_enemy($user),
user => $user->uuid,
}
);
$enemy->insert;
}
sub _get_level_enemy ( $self, $user ) {
my $max_enemy_level = int( $user->level / 2 ) + 1;
if ( $max_enemy_level < $user->level - 10 ) {
$max_enemy_level = $user->level - 10;
}
if ( $max_enemy_level < 3 ) {
$max_enemy_level = 3;
}
my $min_enemy_level = $max_enemy_level - 5;
if ( $min_enemy_level < 2 ) {
$min_enemy_level = 2;
}
return $min_enemy_level +
int( rand( $max_enemy_level - $min_enemy_level + 1 ) );
}
sub _calculate_number_of_enemies ( $self, $min, $max ) {
return $min + int( rand( $max - $min + 1 ) );
}
1;

View File

@ -89,8 +89,6 @@ sub submit_login {
$self->render( text => 'Server error.', status => 500 );
return;
}
say $password;
say $bcrypted_pass;
if ( !bcrypt_check( $password, $bcrypted_pass ) ) {
$self->render( text => 'Wrong password', status => 401 );
return;

View File

@ -0,0 +1,235 @@
package BurguillosInfo::Controller::UserConquer;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use Crypt::Bcrypt qw/bcrypt bcrypt_check/;
use Crypt::URandom qw/urandom/;
use JSON;
use BurguillosInfo::Schema;
my $username_minimum_chars = 3;
my $username_maximum_chars = 15;
my $password_minimum_chars = 8;
my $password_maximum_chars = 4096;
sub setTeamForUser($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->_renderError(401, 'No estás loggeado.');
}
my $input = $self->_expectJson;
if (!defined $input) {
return;
}
my $node_uuid = $input->{node};
my $team_uuid = $input->{team};
my $resultset_team = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my $resultset_node = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my @teams = $resultset_team->search({uuid => $team_uuid});
my @nodes = $resultset_node->search({uuid => $node_uuid});
if (scalar @teams < 1) {
return $self->render(status => 404, json => {
error => 'No se encontró ese equipo.',
});
}
if (scalar @nodes < 1) {
return $self->render(status => 404, json => {
error => 'No se encontró este nodo.',
});
}
my $team = $teams[0];
my $node = $nodes[0];
if (!$node->is_near($user)) {
return $self->render(status => 400, json => {
error => 'Estás demasiado lejos del nodo.',
});
}
$user = $user->get_from_storage;
$user->team_object($team);
$user->update;
return $self->render(json => {
ok => $JSON::true,
});
}
sub get_self ($self) {
my $user = $self->current_user;
if ( !defined $user ) {
return $self->_renderError( 401, 'No estás loggeado.' );
}
return $self->render( json => $user->serialize_to_owner, status => 200 );
}
sub create ($self) {
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $username = $input->{username};
my $password = $input->{password};
my $repeat_password = $input->{repeat_password};
return
unless $self->_createCheckInput( $username, $password, $repeat_password );
return $self->_createUser( $username, $password );
}
sub _expectJson ($self) {
my $input;
eval { $input = $self->req->json; };
if ($@) {
say STDERR $@;
$self->_renderError( 400, 'Se esperaba JSON.' );
return;
}
return $input;
}
sub login ($self) {
my $input = $self->_expectJson;
if ( !defined $input ) {
return;
}
my $username = $input->{username};
my $password = $input->{password};
my $resultset_conquer_user =
BurguillosInfo::Schema->Schema->resultset('ConquerUser');
my @tentative_users =
$resultset_conquer_user->search( { username => $username } );
my $tentative_user = $tentative_users[0];
if ( !defined $tentative_user ) {
$self->_renderError( 401, 'El usuario especificado no existe.' );
return;
}
if ( !bcrypt_check( $password, $tentative_user->encrypted_password ) ) {
$self->_renderError( 401, 'Contraseña incorrecta.' );
return;
}
my $user = $tentative_user;
$self->set_current_user($user);
$self->render(
json => {
success => $JSON::true
},
status => 200
);
}
sub setCoordinates ($self) {
my $input = $self->_expectJson;
my $user = $self->current_user;
if ( !defined $user ) {
return $self->render(
status => 401,
json => {
error => 'Debes estar loggeado para cambiar tus'
. ' coordenadas.',
}
);
}
if ( !defined $input ) {
return;
}
if ( ref $input ne 'ARRAY' && scalar $input->@* == 2 ) {
return $self->render(
status => 400,
json => {
error => 'Mal formato de coordenadas, debe ser '
. 'un array de exactamente 2 números reales.',
}
);
}
$user->coordinates($input);
$user->update;
return $self->render(
status => 200,
json => {
ok => $JSON::true,
}
);
}
sub _createUser ( $self, $username, $password ) {
my $user;
my $uuid = create_uuid_string();
my $new_salt = urandom(16);
my $encrypted_password = bcrypt $password, '2b', 12, $new_salt;
eval {
$user = BurguillosInfo::Schema->Schema->resultset('ConquerUser')->new(
{
uuid => $uuid,
encrypted_password => $encrypted_password,
username => $username
}
);
$user->coordinates( [ 0, 0 ] );
$user->insert;
};
if ($@) {
if ( $@ =~ /Key \((.*?)\)=\((.*?)\) already exists\./ ) {
return $self->_renderError( 400,
"La clave $1 ($2) ya existe en la base de datos.",
);
}
say STDERR $@;
return $self->_renderError( 400,
'No se pudo crear el usuario por razones desconocidas.' );
}
$self->render( status => 200, json => $user->serialize_to_owner );
return 1;
}
sub _renderError ( $self, $status, $message ) {
$self->render( status => $status, json => { error => $message } );
return 0;
}
sub _createCheckInput ( $self, $username, $password, $repeat_password ) {
if ( !defined $username
|| $username !~
/^(?:\w|\d|[ÑÁÉÍÓÚñáéíóú ]){$username_minimum_chars,$username_maximum_chars}$/
)
{
return $self->_renderError( 400,
"Username invalido, las reglas son tamaño entre $username_minimum_chars y $username_maximum_chars"
. ' carácteres y solo se podrán usar letras, números y espacios.'
);
}
if ( !defined $password
|| $password eq $username
|| $password !~ /^.{$password_minimum_chars,$password_maximum_chars}$/
|| $password =~ /^\d+$/ )
{
return $self->_renderError(
400,
'Contraseña invalida, las reglas son la contraseña debe ser'
. ' distinta al nombre de usuario, la contraseña debe tener entre'
. " $password_minimum_chars y $password_maximum_chars carácteres"
. ' (Tu contraseña no se guardará en texto plano, el límite de'
. " $password_maximum_chars caracteres es para evitar denegaciones"
. ' de servicio), la contraseña no puede estar compuesta solo de números.',
);
}
if ( !defined $repeat_password || $password ne $repeat_password ) {
$self->_renderError(
400,
'El campo de repetir contraseña debe coincidir de forma'
. ' totalmente exacta con el campo de contraseña para asegurar'
. ' que podrás recordar la contraseña y/o que no has cometido'
. ' ningún error, si pierdes el acceso a tu cuenta no podrás'
. ' recuperarlo de ningún modo.',
);
return 0;
}
return 1;
}
1;

View File

@ -28,11 +28,11 @@ sub MIGRATIONS {
path TEXT,
FOREIGN KEY (path) REFERENCES paths(path)
)',
'ALTER TABLE paths ADD column last_seen TIMESTAMP;',
'ALTER TABLE paths ADD COLUMN last_seen TIMESTAMP;',
'ALTER TABLE paths ALTER COLUMN last_seen SET DEFAULT NOW();',
'ALTER TABLE requests ADD PRIMARY KEY (uuid)',
'CREATE INDEX request_extra_index on requests (date, path);',
'ALTER TABLE requests ADD column referer text;',
'ALTER TABLE requests ADD COLUMN referer text;',
'CREATE INDEX request_referer_index on requests (referer);',
'ALTER TABLE requests ADD COLUMN country TEXT;',
'CREATE INDEX request_country_index on requests (country);',
@ -49,18 +49,69 @@ sub MIGRATIONS {
id_farmacia TEXT NOT NULL
);',
'CREATE INDEX farmacia_guardia_index on farmacia_guardia (date, id_farmacia, uuid);',
'CREATE TABLE conquer_user (
uuid UUID NOT NULL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
encrypted_password TEXT NOT NULL,
last_activity TIMESTAMP NOT NULL DEFAULT NOW(),
is_admin BOOLEAN NOT NULL DEFAULT false,
registration_date TIMESTAMP NOT NULL DEFAULT NOW()
);',
'CREATE TABLE conquer_node (
uuid UUID NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
coordinate_1 REAL NOT NULL,
coordinate_2 REAL NOT NULL,
description TEXT NOT NULL
);',
'CREATE INDEX index_conquer_node_coordinate_1 on conquer_node (coordinate_1);',
'CREATE INDEX index_conquer_node_coordinate_2 on conquer_node (coordinate_2);',
'ALTER TABLE conquer_user ADD COLUMN last_coordinate_1 REAL NOT NULL DEFAULT 0;',
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_1 DROP DEFAULT;',
'ALTER TABLE conquer_user ADD COLUMN last_coordinate_2 REAL NOT NULL DEFAULT 0;',
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_2 DROP DEFAULT;',
'CREATE TABLE conquer_teams (
uuid UUID NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT \'\',
points INTEGER NOT NULL DEFAULT 0
);',
'ALTER TABLE conquer_user ADD COLUMN team UUID REFERENCES conquer_teams (uuid);',
'ALTER TABLE conquer_node ADD COLUMN team UUID REFERENCES conquer_teams (uuid);',
'ALTER TABLE conquer_teams ADD COLUMN color TEXT NOT NULL DEFAULT \'#000\';',
'ALTER TABLE conquer_teams ALTER COLUMN color SET DEFAULT \'#555\';',
'ALTER TABLE conquer_teams ALTER COLUMN color SET DEFAULT \'#aaa\';',
'CREATE EXTENSION IF NOT EXISTS postgis;',
'ALTER TABLE conquer_node ADD COLUMN geometry GEOMETRY NULL;',
'UPDATE conquer_node SET geometry=ST_MakePoint(coordinate_1, coordinate_2);',
'ALTER TABLE conquer_node ALTER COLUMN geometry SET NOT NULL;',
'ALTER TABLE conquer_node DROP COLUMN coordinate_1;',
'ALTER TABLE conquer_node DROP COLUMN coordinate_2;',
'ALTER TABLE conquer_user ADD COLUMN experience INTEGER NOT NULL DEFAULT 125;',
'ALTER TABLE conquer_user ADD COLUMN current_hp INTEGER NOT NULL DEFAULT 999;',
'CREATE TABLE conquer_user_current_enemy (
uuid UUID NOT NULL PRIMARY KEY,
"user" UUID NOT NULL REFERENCES conquer_user(uuid),
species INTEGER NOT NULL,
is_battled BOOLEAN DEFAULT false,
is_selected_to_battle BOOLEAN DEFAULT false,
level INTEGER NOT NULL
);',
'ALTER TABLE conquer_user_current_enemy ALTER COLUMN species TYPE TEXT;',
);
}
sub _populate_locations ($dbh) {
require BurguillosInfo;
require BurguillosInfo::Tracking;
my $tracking = BurguillosInfo::Tracking->new( BurguillosInfo->new );
my $page = 0;
while (1) {
last if !_update_request_page( $dbh, $tracking, $page );
$page += 100;
}
# This subroutine crashes the migrations.
# require BurguillosInfo;
# require BurguillosInfo::Tracking;
# my $tracking = BurguillosInfo::Tracking->new( BurguillosInfo->new );
# my $page = 0;
# while (1) {
# last if !_update_request_page( $dbh, $tracking, $page );
# $page += 100;
# }
}
sub _update_request_page ( $dbh, $tracking, $page ) {

View File

@ -0,0 +1,64 @@
package BurguillosInfo::Schema;
use v5.36.0;
use strict;
use warnings;
use utf8;
our $VERSION = 1;
use feature 'signatures';
use BurguillosInfo;
use parent 'DBIx::Class::Schema';
__PACKAGE__->load_namespaces();
my $schema;
sub Schema ($class) {
if ( !defined $schema ) {
use BurguillosInfo::DB;
BurguillosInfo::DB->connect;
my $app = BurguillosInfo->new;
my $config = $app->{config};
my $database_config = $config->{db};
my $dbname = $database_config->{database};
my $host = $database_config->{host};
my $port = $database_config->{port};
my $user = $database_config->{user};
my $password = $database_config->{password};
my $dsn = 'dbi:Pg:';
if ( !defined $dbname ) {
die "The key database/dbname must be configured.";
}
$dsn .= "dbname=$dbname";
if ( defined $host ) {
$dsn .= ";host=$host";
}
if ( defined $port ) {
$dsn .= ";port=$port";
}
# Undef is perfectly fine for username and password.
$schema = $class->connect(
$dsn, $user,
$password,
{
auto_savepoint => 1,
Callbacks => {
connected => sub {
shift->do('set timezone = UTC');
return;
}
},
quote_char => '"',
}
);
}
return $schema;
}
1;

View File

@ -0,0 +1,121 @@
package BurguillosInfo::Schema::Result::ConquerNode;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
use GIS::Distance;
__PACKAGE__->table('conquer_node');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
name => {
data_type => 'text',
is_nullable => 0,
default_value => \'0',
},
type => {
data_type => 'text',
is_nullable => 0,
},
description => {
data_type => 'text',
is_nullable => 0,
},
geometry => {
data_type => 'geometry',
is_nullable => 0,
},
team => {
data_type => 'uuid',
is_nullable => 1,
},
);
sub coordinate_2 ($self) {
require BurguillosInfo::Schema;
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my ($new_self) = $resultset->search(
{ uuid => $self->uuid },
{
'+select' => {
ST_Y => { ST_Centroid => 'geometry' },
-as => 'coordinate_2',
}
}
);
return $new_self->get_column('coordinate_2');
}
sub coordinate_1 ($self) {
require BurguillosInfo::Schema;
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my ($new_self) = $resultset->search(
{ uuid => $self->uuid },
{
'+select' => {
ST_X => { ST_Centroid => 'geometry' },
-as => 'coordinate_1',
}
}
);
return $new_self->get_column('coordinate_1');
}
sub serialize ( $self, $player = undef ) {
$self = $self->get_from_storage();
my $return = {
kind => 'ConquerNode',
uuid => $self->uuid,
name => $self->name,
description => $self->description,
type => $self->type,
coordinate_1 => $self->coordinate_1,
coordinate_2 => $self->coordinate_2,
is_near => $self->is_near($player),
team => $self->team,
};
return $return;
}
sub is_near ( $self, $player ) {
if ( !defined $player ) {
return $JSON::false;
}
# Meters
if ( $self->get_distance_to_player($player) < 100 ) {
return $JSON::true;
}
return $JSON::false;
}
sub get_distance_to_player ( $self, $player ) {
my $longitude_player = $player->last_coordinate_1;
my $latitude_player = $player->last_coordinate_2;
my $longitude_node = $self->coordinate_1;
my $latitude_node = $self->coordinate_2;
my $gis = GIS::Distance->new;
# Setting distance to meters.
my $distance = $gis->distance_metal(
$latitude_node, $longitude_node,
$latitude_player, $longitude_player
) * 1000;
}
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->belongs_to( 'team_object',
'BurguillosInfo::Schema::Result::ConquerTeam', 'team' );
1;

View File

@ -0,0 +1,55 @@
package BurguillosInfo::Schema::Result::ConquerTeam;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
__PACKAGE__->table('conquer_teams');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
name => {
data_type => 'text',
is_nullable => 0,
},
description => {
data_type => 'text',
is_nullable => 0,
},
points => {
data_type => 'integer',
is_nullable => 0,
},
color => {
data_type => 'text',
is_nullable => 0,
},
);
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->has_many( players => 'BurguillosInfo::Schema::Result::ConquerUser', 'team');
__PACKAGE__->has_many( nodes => 'BurguillosInfo::Schema::Result::ConquerNode', 'team');
sub serialize ($self) {
$self = $self->get_from_storage();
return {
kind => 'ConquerTeam',
uuid => $self->uuid,
name => $self->name,
description => $self->description,
points => $self->points,
color => $self->color,
};
}
1;

View File

@ -0,0 +1,153 @@
package BurguillosInfo::Schema::Result::ConquerUser;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
__PACKAGE__->table('conquer_user');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
team => {
data_type => 'uuid',
is_nullable => 1,
},
username => {
data_type => 'text',
is_nullable => 0,
},
encrypted_password => {
data_type => 'text',
is_nullable => 0,
},
last_activity => {
data_type => 'timestamp',
is_nullable => 0,
default_value => \'NOW()',
},
registration_date => {
data_type => 'timestamp',
is_nullable => 0,
default_value => \'NOW()',
},
is_admin => {
data_type => 'boolean',
is_nullable => 0,
default_value => \'0',
},
last_coordinate_1 => {
data_type => 'real',
is_nullable => 0,
default_value => \'0',
},
last_coordinate_2 => {
data_type => 'real',
is_nullable => 0,
default_value => \'0',
},
experience => {
data_type => 'integer',
is_nullable => 0,
default_value => \'125',
},
current_hp => {
data_type => 'integer',
is_nullable => 0,
default_value => \'999',
}
);
sub max_health($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int(
(($base * 2 + $born_value) * $self->level)
/ 100 + $self->level + 10
);
}
sub level($self) {
$self = $self->get_from_storage();
return int($self->experience ** (1/3) + 0.0000000000001);
}
sub attack($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int(
(($base * 2 + $self->level)*$self->level)
/100
);
}
sub defense($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int(
(($base * 2 + $self->level)*$self->level)
/100
);
}
sub health($self, $health = undef) {
$self = $self->get_from_storage();
my $hp = $self->current_hp;
if ($hp > $self->max_health) {
$self->current_hp($self->max_health);
$self->update;
$self = $self->get_from_storage();
}
if (defined $health) {
if ($health > $self->max_health) {
$health = $self->max_health;
}
$self->current_hp($health);
$self->update;
$self = $self->get_from_storage();
}
return $self->current_hp;
}
sub coordinates ( $self, $coordinates = undef ) {
if ( defined $coordinates ) {
if ( ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2 ) {
die 'The second parameter of this subroutine '
. 'must be an ARRAYREF of exactly two elements.';
}
$self->last_coordinate_1( $coordinates->[0] );
$self->last_coordinate_2( $coordinates->[1] );
}
return [ $self->last_coordinate_1, $self->last_coordinate_2 ];
}
sub serialize_to_owner ($self) {
$self = $self->get_from_storage();
return {
kind => 'ConquerUser',
uuid => $self->uuid,
team => $self->team,
username => $self->username,
is_admin => $self->is_admin ? $JSON::true : $JSON::false,
last_activity => $self->last_activity,
registration_date => $self->registration_date,
};
}
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->belongs_to('team_object', 'BurguillosInfo::Schema::Result::ConquerTeam', 'team');
__PACKAGE__->add_unique_constraint( "unique_constraint_username",
['username'] );
1;

View File

@ -0,0 +1,101 @@
package BurguillosInfo::Schema::Result::ConquerUserCurrentEnemy;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
use BurguillosInfo::Species;
__PACKAGE__->table('conquer_user_current_enemy');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
user => {
data_type => 'uuid',
is_nullable => 0,
},
species => {
data_type => 'text',
is_nullable => 0,
},
is_selected_to_battle => {
data_type => 'boolean',
is_nullable => 0,
default_value => \'false',
},
is_battled => {
data_type => 'boolean',
is_nullable => 0,
default_value => \'false',
},
level => {
data_type => 'integer',
is_nullable => 0,
},
);
sub serialize ($self) {
my $species = BurguillosInfo::Species->new;
my $specie = $species->get( $self->species );
return {
uuid => $self->uuid,
species => $specie->serialize,
level => $self->level,
max_health => $self->max_health,
};
}
sub max_health ($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return
int( ( ( $base * 2 + $born_value ) * $self->level ) / 100 +
$self->level +
10 );
}
sub experience_drop ($self) {
$self = $self->get_from_storage();
return int( $self->level / 7 * 179 );
}
sub experience ($self) {
$self = $self->get_from_storage();
return int( $self->level**(3) );
}
sub attack ($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int( ( ( $base * 2 + $self->level ) * $self->level ) / 100 );
}
sub defense ($self) {
$self = $self->get_from_storage();
my $base = 50;
my $born_value = 31;
return int( ( ( $base * 2 + $self->level ) * $self->level ) / 100 );
}
sub health ($self) {
# Combat result is decided from the start of battle.
return $self->max_health;
}
__PACKAGE__->set_primary_key('uuid');
__PACKAGE__->belongs_to( 'user_object',
'BurguillosInfo::Schema::Result::ConquerUser', 'user' );
1;

View File

@ -0,0 +1,24 @@
package BurguillosInfo::Specie;
use v5.36.0;
use strict;
use warnings;
use feature 'signatures';
use Moo::Role;
sub can_be_global {
return 0;
}
sub serialize ($self) {
return {
id => $self->id,
name => $self->name,
image => $self->image,
};
}
requires 'id name image';
1;

View File

@ -0,0 +1,58 @@
package BurguillosInfo::Species;
use v5.36.0;
use strict;
use warnings;
use Moo;
use Module::Pluggable
search_path => ['BurguillosInfo::Species'],
instantiate => 'new',
on_require_error => sub ( $plugin, $error ) {
die $error;
};
{
my %hash_species;
sub _hash ($self) {
if ( !scalar keys %hash_species ) {
$self->_populate_hash;
}
return {%hash_species};
}
sub _populate_hash ($self) {
my @species = $self->plugins();
print Data::Dumper::Dumper \@species;
for my $specie (@species) {
$self->_check_specie_valid($specie);
if (exists $hash_species{$specie->id}) {
die "Duplicated species id @{[$specie->id]}.";
}
$hash_species{$specie->id} = $specie;
}
}
}
sub _check_specie_valid ( $self, $specie ) {
if ( !$specie->does('BurguillosInfo::Specie') ) {
die "$specie does not implement BurguillosInfo::Specie.";
}
}
sub get($self, $id) {
return $self->_hash->{$id};
}
sub list($self) {
my @species_keys = keys %{$self->_hash};
my $species = [ sort { $a->id cmp $b->id } map { $self->_hash->{$_} } @species_keys ];
return $species;
}
sub list_can_be_global($self) {
return [ grep { $_->can_be_global } $self->list->@* ];
}
1;

View File

@ -0,0 +1,28 @@
package BurguillosInfo::Species::Murcielago;
use v5.34.1;
use strict;
use warnings;
use Moo;
use parent 'BurguillosInfo::Specie';
sub id {
return 'murcielago';
}
sub name {
return 'Murcielago';
}
sub image {
return '/img/conquer/species/murcielago.png';
}
sub can_be_global {
return 1;
}
1;

View File

@ -25,7 +25,10 @@
"dependencies": {
"babel-loader": "^9.1.3",
"ol": "^8.1.0",
"protoc-gen-js": "^3.21.2",
"tablesort": "^5.3.0",
"ts-loader": "^9.5.0"
"ts-loader": "^9.5.0",
"ts-protoc-gen": "^0.15.0",
"typescript-json-serializer": "^6.0.1"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,18 +9,162 @@ body {
min-height: 100%;
width: 100%;
height: 100%; }
body summary h2, body summary h3, body summary h4, body summary h5 {
display: inline; }
body converse-muc-sidebar {
display: none !important; }
body div.converse-container {
body span.conquer-team-circle {
display: inline-block;
aspect-ratio: 1 / 1;
height: 1rem;
border-radius: 50%; }
body div.conquer-team-to-select {
padding: 5px;
border-radius: 5px;
background: beige;
border: solid black; }
body div.conquer-team-to-select button {
height: 60px; }
body p.conquer-register-error, body p.conquer-login-error, body p.conquer-login-success, body p.conquer-error {
color: red;
margin: 3px;
font-size: 1.3rem;
background: blanchedalmond;
padding: 3px;
border-radius: 10px;
border: solid 1px black;
overflow-y: scroll; }
body form {
display: flex;
flex-direction: column;
width: 100%; }
body form label {
width: 100%; }
body form label input, body form label textarea, body form label select {
width: 100%;
border: none;
background-image: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
background: white;
box-shadow: none;
min-height: 2rem;
border-radius: 0.5rem; }
body form label textarea {
height: 100px; }
body div.conquer-interface-element-padded {
width: calc(100% - 60px);
padding-left: 30px;
padding-right: 30px;
display: flex;
justify-content: center; }
body div.conquer-interface-element-padded.conquer-display-block {
display: block; }
body div.conquer-interface-element-padded.conquer-display-none {
display: none; }
body div.fight-battle-selector-slide {
display: flex;
position: fixed;
z-index: 1;
top: 0;
height: 100px; }
body div.fight-battle-selector-slide.conquer-display-none {
display: none; }
body div.fight-battle-selector-slide img {
height: 50px;
aspect-ratio: 1 / 1; }
body div.create-node-slide {
display: flex;
position: fixed;
z-index: 1;
bottom: 0;
height: 100px; }
body div.create-node-slide.conquer-display-none {
display: none; }
body p.conquer-login-success {
color: green; }
body a.conquer-exit-button {
color: white;
font-weight: bold;
text-decoration: none;
background: darkmagenta;
padding: 10px;
border-radius: 50%;
aspect-ratio: 1 / 1;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: xx-large;
border: 2px black solid; }
body div.conquer-overlay-transparent {
background: black;
opacity: 50%;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1; }
body div.conquer-self-player {
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: 0;
left: 0;
width: calc(100% - 12px);
height: calc(100% - 22px);
margin: 5px;
margin-top: 10px;
margin-bottom: 10px;
overflow-y: scroll; }
body div.conquer-top-bar {
display: flex;
width: calc(100% - 20px);
border-radius: 30px 30px 0 0;
padding-top: 20px;
padding-bottom: 20px;
padding-left: 10px;
padding-right: 10px;
background: darkcyan;
margin-left: 0;
border-bottom: 1px black solid;
display: flex;
justify-content: end; }
body div.conquer-login, body div.conquer-register {
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: calc( 50% - 200px - 10px);
left: calc( 50% - 150px - 10px);
padding: 10px;
height: 400px;
margin-left: 0px; }
body div.page-contents div.footer p.attribution {
font-size: 0.8em; }
body div.page-contents div.footer p.attribution a {
font-size: 0.8em; }
width: 300px;
z-index: 1; }
body div.conquer-login form, body div.conquer-register form {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center; }
body .conquer-display-none {
display: none; }
body div.conquer-container {
background: black;
height: 100dvh;
width: 100%; }
body div.conquer-select-fight div.conquer-image-container {
display: flex;
justify-content: center;
align-items: center; }
body div.conquer-select-fight div.conquer-button-container {
display: flex;
justify-content: center;
align-items: center; }
body div.ol-control {
display: none; }
body span.round-center {
background: blueviolet;
color: #FEFEFA;
@ -85,11 +229,9 @@ body {
margin-right: 10px;
display: flex; }
body div.search a.search-icon {
height: calc(100% - 40px);
height: calc(100% - 28px);
align-self: center;
margin: 20px;
margin-left: 7px;
margin-right: 7px;
margin: 7px;
display: flex;
background: aliceblue;
align-items: center;
@ -133,11 +275,11 @@ body {
height: 20%;
width: 100%; }
body div.carousel a {
position: absolute;
top: 0;
position: fixed;
top: 80%;
border: solid 3px black;
width: calc(100% - 6px);
height: calc(100% - 6px);
height: calc(20% - 6px);
left: 100%;
transition: left 1s ease-in;
font-size: 13px;
@ -155,10 +297,9 @@ body {
body div.carousel a:hover, body div.carousel a:focus {
background: blueviolet;
color: #f2eb8c; }
body div.carousel a p {
margin-bottom: 1px; }
body div.carousel a h4 {
margin: 0; }
body div.carousel div.promoted-tag, body div.carousel h3 {
margin: 0;
margin-right: 5px; }
body div.carousel img {
margin: 10px;
height: calc(100% - 20px);
@ -218,7 +359,7 @@ body {
left: 0;
width: 100%;
opacity: 40%;
top: 80px;
top: 60px;
height: calc(100% - 60px);
z-index: 250;
display: none; }
@ -228,9 +369,9 @@ body {
visibility: hidden;
position: fixed;
left: 100%;
width: Min(70%, 600px);
top: 80px;
height: calc(100% - 80px);
width: 70%;
top: 60px;
height: calc(100% - 60px);
z-index: 500;
transition: left 0.5s ease-in, visibility 0.5s ease-in;
background: #FEFEFA; }
@ -276,12 +417,12 @@ body {
body a.menu-expand:hover .open-menu-icon, body a.menu-expand:focus .open-menu-icon, body a.menu-expand.active .open-menu-icon {
display: none; }
body nav > a.menu-expand > img {
width: 40px;
height: 40px; }
width: 30px;
height: 30px; }
body nav > a > img.index-image-menu {
vertical-align: middle;
width: 60px;
height: 60px; }
width: 40px;
height: 40px; }
body nav > a > img.index-image-menu, body div.burguillos-logo-container > img {
transition-property: transform;
transition-duration: 2s;
@ -292,7 +433,7 @@ body {
body div.search-in-page {
display: none;
position: fixed;
top: 80px;
top: 60px;
height: 60px;
width: 100%;
align-items: center;
@ -325,38 +466,24 @@ body {
align-items: center;
width: 100%;
background: blueviolet;
height: 80px;
justify-content: start;
flex-direction: row;
height: 60px;
top: 0%; }
body nav.mobile-shortcuts a {
height: 100%;
width: 80px;
width: 16.6666666667%;
padding-left: 0;
padding-top: 0;
padding-right: 0;
padding-bottom: 0; }
body nav.mobile-shortcuts a.go-to-index {
position: absolute;
left: 0;
top: 0; }
body nav.mobile-shortcuts div.search {
position: absolute;
left: 80px;
top: 10%;
width: Min(calc(100% - 90px * 2), 500px);
width: calc(100% * 4 / 6 - 20px);
height: 80%;
border-radius: 10px; }
body nav.mobile-shortcuts a.menu-expand {
position: absolute;
left: Min(calc(100% - 80px), 600px);
top: 0;
align-self: end; }
body div.page-contents {
background: #FEFEFA;
position: fixed;
top: 80px;
height: calc(80% - 80px);
top: 60px;
height: calc(80% - 60px);
width: 100%;
overflow-y: scroll; }
body div.page-contents div.child-categories-mobile a {
@ -516,14 +643,6 @@ body {
body div.page-contents table th, body div.page-contents table td {
font-size: 20px; } }
@media (min-width: 768px) {
body converse-muc-sidebar {
display: flex !important; }
body converse-muc-sidebar.hidden {
display: none !important; }
body div.converse-container {
margin-left: 15px; } }
@media (min-width: 694px) {
body div.carousel a {
font-size: 20px; }
@ -537,11 +656,37 @@ body {
body div.page-contents img {
max-width: 694px; } }
@media (min-width: 700px) {
body nav.mobile-foldable {
left: -100%; }
body nav.mobile-foldable.show {
left: 0; } }
@media (min-width: 1100px) {
body nav.mobile-foldable, body nav.mobile-foldable.show {
display: none; }
body nav.mobile-shortcuts {
display: none; }
body div.search-in-page.active {
display: none; }
body div.page-contents {
top: 0%;
left: 5%;
height: 80%;
width: 90%;
border: solid 1px black; }
body div.page-contents div.description.open-browser-container {
margin-left: 0;
margin-right: 0; }
body div.page-contents div.description {
margin-left: 10%;
margin-right: 10%; }
body div.page-contents nav.desktop {
display: block;
height: auto;
height: 60px; }
body div.page-contents nav.desktop a {
display: table-cell;
height: 60px; }
body div.page-contents nav.desktop a img.index-image-menu {
height: 40px;
width: 40px; }
body div.page-contents.no-carousel {
height: 100%; } }
@media (min-width: 1333px) {
body div.page-contents div.description div.articles a {
@ -552,24 +697,6 @@ body {
body div.page-contents div.description div.articles a:nth-child(3n+1) {
margin-left: 0%; } }
@media (min-width: 848px) {
body div.page-contents div.description div.articles a {
width: 30%;
margin-left: 3%; }
body div.page-contents div.description div.articles a:nth-child(2n+1) {
margin-left: 3%; }
body div.page-contents div.description div.articles a:nth-child(3n+1) {
margin-left: 0%; } }
@media (min-width: 1333px) {
body div.page-contents div.description div.articles a {
width: 22%;
margin-left: 3%; }
body div.page-contents div.description div.articles a:nth-child(3n+1) {
margin-left: 3%; }
body div.page-contents div.description div.articles a:nth-child(4n+1) {
margin-left: 0%; } }
@media (max-width: 200px) {
body {
font-size: 20px; } }

View File

@ -10,7 +10,6 @@ $accent-secondary: #fde68f;
$primary-secondary: #590e11;
$background_sidebar: $background-page; //#F5F5DC;
$color_sidebar: #dcdcf5;
$attribution_font_size: 0.8em;
html {
height: 100%;
@ -18,25 +17,194 @@ html {
}
body {
summary {
h2, h3, h4, h5 {
display: inline;
span.conquer-team-circle {
display: inline-block;
aspect-ratio: 1 / 1;
height: 1rem;
border-radius: 50%;
}
div.conquer-team-to-select {
padding: 5px;
border-radius: 5px;
background: beige;
border: solid black;
button {
height: 60px;
}
}
converse-muc-sidebar {
display: none !important;
p.conquer-register-error, p.conquer-login-error, p.conquer-login-success,p.conquer-error {
color: red;
margin: 3px;
font-size: 1.3rem;
background: blanchedalmond;
padding: 3px;
border-radius: 10px;
border: solid 1px black;
overflow-y: scroll;
}
div.converse-container {
width: 100%;
height: 400px;
margin-left: 0px;
}
div.page-contents div.footer p.attribution {
font-size: $attribution_font_size;
a {
font-size: $attribution_font_size;
form {
display: flex;
flex-direction: column;
width: 100%;
label {
width: 100%;
input, textarea, select {
width: 100%;
border:none;
background-image:none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
background: white;
box-shadow: none;
min-height: 2rem;
border-radius: 0.5rem;
}
textarea {
height: 100px;
}
}
}
div.conquer-interface-element-padded {
width: calc(100% - 60px);
padding-left: 30px;
padding-right: 30px;
display: flex;
justify-content: center;
&.conquer-display-block {
display: block;
}
&.conquer-display-none {
display: none;
}
}
div.fight-battle-selector-slide {
display: flex;
position: fixed;
z-index: 1;
top: 0;
height: 100px;
&.conquer-display-none {
display: none;
}
img {
height: 50px;
aspect-ratio: 1 / 1;
}
}
div.create-node-slide {
display: flex;
position: fixed;
z-index: 1;
bottom: 0;
height: 100px;
&.conquer-display-none {
display: none;
}
}
p.conquer-login-success {
color: green;
}
a.conquer-exit-button {
color: white;
font-weight: bold;
text-decoration: none;
background: darkmagenta;
padding: 10px;
border-radius: 50%;
aspect-ratio: 1 / 1;
width: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: xx-large;
border: 2px black solid;
}
div.conquer-overlay-transparent {
background: black;
opacity: 50%;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1;
}
div.conquer-self-player {
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: 0;
left: 0;
width: calc(100% - 12px);
height: calc(100% - 22px);
margin: 5px;
margin-top: 10px;
margin-bottom: 10px;
overflow-y: scroll;
}
div.conquer-top-bar {
display: flex;
width: calc(100% - 20px);
border-radius: 30px 30px 0 0;
padding-top: 20px;
padding-bottom: 20px;
padding-left: 10px;
padding-right: 10px;
background: darkcyan;
margin-left: 0;
border-bottom: 1px black solid;
display: flex;
justify-content: end;
}
div.conquer-login,div.conquer-register {
form {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
}
border: 1px solid black;
position: fixed;
color: black;
font-size: 1.5rem;
border-radius: 30px;
background: darkseagreen;
top: calc( 50% - 200px - 10px );
left: calc( 50% - 150px - 10px );
padding: 10px;
height: 400px;
width: 300px;
z-index: 1;
}
.conquer-display-none {
display: none;
}
div.conquer-container {
background: black;
height: 100dvh;
width: 100%;
}
div.conquer-select-fight {
div.conquer-image-container {
display: flex;
justify-content: center;
align-items: center;
}
div.conquer-button-container {
display: flex;
justify-content: center;
align-items: center;
}
}
div.ol-control {
display: none;
}
span.round-center {
background: $background_div;
color: $background_sidebar;
@ -128,11 +296,9 @@ body {
display: flex;
a.search-icon {
height: calc(100% - 40px);
height: calc(100% - 28px);
align-self: center;
margin: 20px;
margin-left: 7px;
margin-right: 7px;
margin: 7px;
display: flex;
background: aliceblue;
align-items: center;
@ -200,11 +366,11 @@ body {
width: 100%;
a {
position: absolute;
top: 0;
position: fixed;
top: 80%;
border: solid 3px black;
width: calc(100% - 6px);
height: calc(100% - 6px);
height: calc(20% - 6px);
left: 100%;
transition: left 1s ease-in;
@ -227,12 +393,11 @@ body {
background: $background_div;
color: $color_div;
}
p {
margin-bottom: 1px;
}
h4 {
margin: 0;
}
}
div.promoted-tag, h3 {
margin: 0;
margin-right: 5px;
}
img {
@ -321,7 +486,7 @@ body {
left: 0;
width: 100%;
opacity: 40%;
top: 80px;
top: 60px;
height: calc(100% - 60px);
z-index: 250;
display: none;
@ -335,9 +500,9 @@ body {
visibility: hidden;
position: fixed;
left: 100%;
width: Min(70%, 600px);
top: 80px;
height: calc(100% - 80px);
width: 70%;
top: 60px;
height: calc(100% - 60px);
z-index: 500;
transition: left 0.5s ease-in, visibility 0.5s ease-in;
background: $background_sidebar;
@ -411,14 +576,14 @@ body {
}
nav > a.menu-expand > img {
width: 40px;
height: 40px;
width: 30px;
height: 30px;
}
nav > a > img.index-image-menu {
vertical-align: middle;
width: 60px;
height: 60px;
width: 40px;
height: 40px;
}
nav > a > img.index-image-menu, div.burguillos-logo-container > img {
@ -435,7 +600,7 @@ body {
div.search-in-page {
display: none;
position: fixed;
top: 80px;
top: 60px;
height: 60px;
width: 100%;
align-items: center;
@ -478,48 +643,30 @@ body {
align-items: center;
width: 100%;
background: $background_div;
height: 80px;
justify-content: start;
flex-direction: row;
height: 60px;
top: 0%;
a {
height: 100%;
width: 80px;
width: (100% / 6);
padding-left: 0;
padding-top: 0;
padding-right: 0;
padding-bottom: 0;
}
a.go-to-index {
position: absolute;
left: 0;
top: 0;
}
div.search {
position: absolute;
left: 80px;
top: 10%;
width: Min(calc(100% - 90px * 2), 500px);
width: calc(100% * 4 / 6 - 20px);
height: 80%;
border-radius: 10px;
}
a.menu-expand {
position: absolute;
left: Min(calc(100% - 80px), 600px);
top: 0;
align-self: end;
}
}
div.page-contents {
background: $background-page;
position: fixed;
top: 80px;
height: calc(80% - 80px);
top: 60px;
height: calc(80% - 60px);
width: 100%;
overflow-y: scroll;
@ -775,19 +922,6 @@ body {
}
}
}
@media (min-width: 768px) {
body {
converse-muc-sidebar {
display: flex !important;
&.hidden {
display: none !important;
}
}
div.converse-container {
margin-left: 15px;
}
}
}
@media (min-width: 694px) {
body {
@ -822,14 +956,57 @@ body {
}
}
@media (min-width: 700px) {
@media (min-width: 1100px) {
body {
nav.mobile-foldable {
left: -100%;
nav.mobile-foldable, nav.mobile-foldable.show {
display: none;
}
nav.mobile-shortcuts {
display: none;
}
div.search-in-page.active {
display: none;
}
div.page-contents {
div.description.open-browser-container {
margin-left: 0;
margin-right: 0;
}
nav.mobile-foldable.show {
left: 0;
div.description {
margin-left: 10%;
margin-right: 10%;
}
nav.desktop {
display: block;
height: auto;
height: 60px;
a {
display: table-cell;
height: 60px;
img.index-image-menu {
height: 40px;
width: 40px;
}
}
}
top: 0%;
left: 5%;
height: 80%;
width: 90%;
border: solid 1px black;
}
div.page-contents.no-carousel {
height: 100%;
}
}
}
@ -856,51 +1033,6 @@ body {
}
}
@media (min-width: 848px) {
body {
div.page-contents {
div.description {
div.articles {
a {
&:nth-child(2n+1) {
margin-left: 3%;
}
&:nth-child(3n+1) {
margin-left: 0%;
}
width: 30%;
margin-left: 3%;
}
}
}
}
}
}
@media (min-width: 1333px) {
body {
div.page-contents {
div.description {
div.articles {
a {
&:nth-child(3n+1) {
margin-left: 3%;
}
&:nth-child(4n+1) {
margin-left: 0%;
}
width: 22%;
margin-left: 3%;
}
}
}
}
}
}
@media (max-width: 200px) {
body {
font-size: 20px;

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

79316
public/dist/converse.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,371 +0,0 @@
/*!
MIT License
Copyright (c) 2018 Arturas Molcanovas <a.molcanovas@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*!
localForage -- Offline Storage, Improved
Version 1.10.0
https://localforage.github.io/localForage
(c) 2013-2017 Mozilla, Apache License 2.0
*/
/*!
* Sizzle CSS Selector Engine v2.3.6
* https://sizzlejs.com/
*
* Copyright JS Foundation and other contributors
* Released under the MIT license
* https://js.foundation/
*
* Date: 2021-02-16
*/
/*!
* URI.js - Mutating URLs
*
* Version: 1.19.11
*
* Author: Rodney Rehm
* Web: http://medialize.github.io/URI.js/
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
*
*/
/*!
* URI.js - Mutating URLs
* IPv6 Support
*
* Version: 1.19.11
*
* Author: Rodney Rehm
* Web: http://medialize.github.io/URI.js/
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
*
*/
/*!
* URI.js - Mutating URLs
* Second Level Domain (SLD) Support
*
* Version: 1.19.11
*
* Author: Rodney Rehm
* Web: http://medialize.github.io/URI.js/
*
* Licensed under
* MIT License http://www.opensource.org/licenses/mit-license
*
*/
/*!
2020 Jason Mulligan <jason.mulligan@avoidwork.com>
@version 7.0.0
*/
/*!
MIT License
Copyright (c) 2018 Arturas Molcanovas <a.molcanovas@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
this file except in compliance with the License. You may obtain a copy of the
License at http://www.apache.org/licenses/LICENSE-2.0
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
MERCHANTABLITY OR NON-INFRINGEMENT.
See the Apache Version 2.0 License for specific language governing permissions
and limitations under the License.
***************************************************************************** */
/*! @license DOMPurify 2.3.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.6/LICENSE */
/*! https://mths.be/punycode v1.4.0 by @mathias */
/**
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description This is the form utilities module.
*/
/**
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @copyright Alfredo Medrano Sánchez and the Converse.js contributors
* @description
* Component inspired by the one from fa-icons
* https://github.com/obsidiansoft-io/fa-icons/blob/master/LICENSE
* @license Mozilla Public License (MPLv2)
*/
/**
* @copyright JC Brand
* @license Mozilla Public License (MPLv2)
* @description A plugin which restricts Converse to only one chat.
*/
/**
* @copyright Shachaf Ben-Kiki and the Converse.js contributors
* @description
* Started as a fork of Shachaf Ben-Kiki's jsgif library
* https://github.com/shachaf/jsgif
* @license MIT License
*/
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description Converse.js plugin which add support for XEP-0206: XMPP Over BOSH
*/
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description Converse.js plugin which adds support for XEP-0198: Stream Management
*/
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description This is the core utilities module.
*/
/**
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @description
* Converse.js plugin which shows a list of currently open
* rooms in the "Rooms Panel" of the ControlBox.
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @description
* Converse.js plugin which add support for registering
* an "App Server" as defined in XEP-0357
* @copyright 2021, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @description
* Converse.js plugin which adds views for bookmarks specified in XEP-0048.
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @description Converse.js (A browser based XMPP chat client)
* @copyright 2021, The Converse developers
* @license Mozilla Public License (MPLv2)
*/
/**
* @description Converse.js plugin which adds views for XEP-0048 bookmarks
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @description UI code XEP-0313 Message Archive Management
* @copyright 2021, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @description XEP-0313 Message Archive Management
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @license
* Copyright 2019 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/
/**
* @license MIT or GPL-2.0
* @fileOverview Favico animations
* @author Miroslav Magda, http://blog.ejci.net
* @source: https://github.com/ejci/favico.js
* @version 0.3.10
*/
/**
* @module converse-carbons
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description Implements support for XEP-0280 Message Carbons
*/
/**
* @module converse-chatboxviews
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-dragresize
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-emoji
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-fullscreen
* @license Mozilla Public License (MPLv2)
* @copyright 2022, the Converse.js contributors
*/
/**
* @module converse-headlines-view
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-minimize
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-notification
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-pubsub
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-register
* @description
* This is a Converse.js plugin which add support for in-band registration
* as specified in XEP-0077.
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
*/
/**
* @module converse-rsm
* @copyright The Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description XEP-0059 Result Set Management
* Some code taken from the Strophe RSM plugin, licensed under the MIT License
* Copyright 2006-2017 Strophe (https://github.com/strophe/strophejs)
*/
/**
* @module i18n
* @copyright 2022, the Converse.js contributors
* @license Mozilla Public License (MPLv2)
* @description This is the internationalization module
*/
/**
* @preserve jed.js https://github.com/SlexAxton/Jed
*/
/**
* Clears the specified timeout and interval.
* @method u#clearTimers
* @param {number} timeout - Id if the timeout to clear.
* @param {number} interval - Id of the interval to clear.
* @private
* @copyright Simen Bekkhus 2016
* @license MIT
*/
/**
* Creates a {@link Promise} that resolves if the passed in function returns a truthy value.
* Rejects if it throws or does not return truthy within the given max_wait.
* @method u#waitUntil
* @param {Function} func - The function called every check_delay,
* and the result of which is the resolved value of the promise.
* @param {number} [max_wait=300] - The time to wait before rejecting the promise.
* @param {number} [check_delay=3] - The time to wait before each invocation of {func}.
* @returns {Promise} A promise resolved with the value of func,
* or rejected with the exception thrown by it or it times out.
* @copyright Simen Bekkhus 2016
* @license MIT
*/
/*@cc_on!@*/

File diff suppressed because one or more lines are too long

11
public/dist/emojis.js vendored

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -1,90 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="192px" height="192px" viewBox="0 0 192 192" enable-background="new 0 0 192 192" xml:space="preserve"> <image id="image0" width="192" height="192" x="0" y="0"
href="data:image/png;base64,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" />
</svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,310 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512" enable-background="new 0 0 512 512" xml:space="preserve"> <image id="image0" width="512" height="512" x="0" y="0"
href="data:image/png;base64,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" />
</svg>

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,19 +0,0 @@
<svg class="converse-svg-logo"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 364 364">
<title>Converse</title>
<g class="cls-1" id="g904">
<g data-name="Layer 2">
<g data-name="Layer 7">
<path
class="cls-3"
d="M221.46,103.71c0,18.83-29.36,18.83-29.12,0C192.1,84.88,221.46,84.88,221.46,103.71Z" />
<path
class="cls-4"
d="M179.9,4.15A175.48,175.48,0,1,0,355.38,179.63,175.48,175.48,0,0,0,179.9,4.15Zm-40.79,264.5c-.23-17.82,27.58-17.82,27.58,0S138.88,286.48,139.11,268.65ZM218.6,168.24A79.65,79.65,0,0,1,205.15,174a12.76,12.76,0,0,0-6.29,4.65L167.54,222a1.36,1.36,0,0,1-2.46-.8v-35.8a2.58,2.58,0,0,0-3.06-2.53c-15.43,3-30.23,7.7-42.73,19.94-38.8,38-29.42,105.69,16.09,133.16a162.25,162.25,0,0,1-91.47-67.27C-3.86,182.26,34.5,47.25,138.37,25.66c46.89-9.75,118.25,5.16,123.73,62.83C265.15,120.64,246.56,152.89,218.6,168.24Z" />
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -1,63 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 364 364"
version="1.1">
<title>Logo Converse</title>
<defs>
<linearGradient
id="gradient"
x1="92.14"
y1="27.64"
x2="267.65"
y2="331.62"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
stop-color="#fff1d1"/>
<stop
offset="0.05"
stop-color="#fae8c1"/>
<stop
offset="0.15"
stop-color="#f0d5a1"/>
<stop
offset="0.27"
stop-color="#e7c687"/>
<stop
offset="0.4"
stop-color="#e1bb72"/>
<stop
offset="0.54"
stop-color="#dcb264"/>
<stop
offset="0.71"
stop-color="#daad5c"/>
<stop
offset="1"
stop-color="#d9ac59"/>
</linearGradient>
<filter id="shadow">
<feGaussianBlur in="SourceAlpha" stdDeviation="2.3" result="blur1"/>
<feOffset in="blur1" dx="3" dy="3" result="blur2"/>
<feColorMatrix in="blur2" type="matrix" result="blur3"
values="1 0 0 0 0.6
0 1 0 0 0.6
0 0 1 0 0.6
0 0 0 1 0"/>
<feMerge>
<feMergeNode in="blur3"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<g filter="url(#shadow)">
<path
d="M221.46,103.71c0,18.83-29.36,18.83-29.12,0C192.1,84.88,221.46,84.88,221.46,103.71Z"
fill="#d9ac59"/>
<path
d="M179.9,4.15A175.48,175.48,0,1,0,355.38,179.63,175.48,175.48,0,0,0,179.9,4.15Zm-40.79,264.5c-.23-17.82,27.58-17.82,27.58,0S138.88,286.48,139.11,268.65ZM218.6,168.24A79.65,79.65,0,0,1,205.15,174a12.76,12.76,0,0,0-6.29,4.65L167.54,222a1.36,1.36,0,0,1-2.46-.8v-35.8a2.58,2.58,0,0,0-3.06-2.53c-15.43,3-30.23,7.7-42.73,19.94-38.8,38-29.42,105.69,16.09,133.16a162.25,162.25,0,0,1-91.47-67.27C-3.86,182.26,34.5,47.25,138.37,25.66c46.89-9.75,118.25,5.16,123.73,62.83C265.15,120.64,246.56,152.89,218.6,168.24Z"
fill="url(#gradient)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,11 +0,0 @@
(self["webpackChunkconverse_js"] = self["webpackChunkconverse_js"] || []).push([[9210],{
/***/ 5903:
/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
!function(e,a){ true?module.exports=a(__webpack_require__(7484)):0}(this,(function(e){"use strict";function a(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var n=a(e),t={name:"af",weekdays:"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag".split("_"),months:"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember".split("_"),weekStart:1,weekdaysShort:"Son_Maa_Din_Woe_Don_Vry_Sat".split("_"),monthsShort:"Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des".split("_"),weekdaysMin:"So_Ma_Di_Wo_Do_Vr_Sa".split("_"),ordinal:function(e){return e},formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd, D MMMM YYYY HH:mm"},relativeTime:{future:"oor %s",past:"%s gelede",s:"'n paar sekondes",m:"'n minuut",mm:"%d minute",h:"'n uur",hh:"%d ure",d:"'n dag",dd:"%d dae",M:"'n maand",MM:"%d maande",y:"'n jaar",yy:"%d jaar"}};return n.default.locale(t,null,!0),t}));
/***/ })
}]);
//# sourceMappingURL=af-js.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"locales/dayjs/af-js.js","mappings":";;;;;AAAA,eAAe,KAAoD,kBAAkB,mBAAO,CAAC,IAAO,GAAG,CAA0I,CAAC,mBAAmB,aAAa,cAAc,+CAA+C,WAAW,cAAc,wZAAwZ,SAAS,UAAU,iHAAiH,eAAe,wLAAwL,qCAAqC","sources":["webpack://converse.js/./node_modules/dayjs/locale/af.js"],"sourcesContent":["!function(e,a){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=a(require(\"dayjs\")):\"function\"==typeof define&&define.amd?define([\"dayjs\"],a):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).dayjs_locale_af=a(e.dayjs)}(this,(function(e){\"use strict\";function a(e){return e&&\"object\"==typeof e&&\"default\"in e?e:{default:e}}var n=a(e),t={name:\"af\",weekdays:\"Sondag_Maandag_Dinsdag_Woensdag_Donderdag_Vrydag_Saterdag\".split(\"_\"),months:\"Januarie_Februarie_Maart_April_Mei_Junie_Julie_Augustus_September_Oktober_November_Desember\".split(\"_\"),weekStart:1,weekdaysShort:\"Son_Maa_Din_Woe_Don_Vry_Sat\".split(\"_\"),monthsShort:\"Jan_Feb_Mrt_Apr_Mei_Jun_Jul_Aug_Sep_Okt_Nov_Des\".split(\"_\"),weekdaysMin:\"So_Ma_Di_Wo_Do_Vr_Sa\".split(\"_\"),ordinal:function(e){return e},formats:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd, D MMMM YYYY HH:mm\"},relativeTime:{future:\"oor %s\",past:\"%s gelede\",s:\"'n paar sekondes\",m:\"'n minuut\",mm:\"%d minute\",h:\"'n uur\",hh:\"%d ure\",d:\"'n dag\",dd:\"%d dae\",M:\"'n maand\",MM:\"%d maande\",y:\"'n jaar\",yy:\"%d jaar\"}};return n.default.locale(t,null,!0),t}));"],"names":[],"sourceRoot":""}

View File

@ -1,11 +0,0 @@
(self["webpackChunkconverse_js"] = self["webpackChunkconverse_js"] || []).push([[5073],{
/***/ 9911:
/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
!function(e,_){ true?module.exports=_(__webpack_require__(7484)):0}(this,(function(e){"use strict";function _(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var t=_(e),d={name:"am",weekdays:"እሑድ_ሰኞ_ማክሰኞ_ረቡዕ_ሐሙስ_አርብ_ቅዳሜ".split("_"),weekdaysShort:"እሑድ_ሰኞ_ማክሰ_ረቡዕ_ሐሙስ_አርብ_ቅዳሜ".split("_"),weekdaysMin:"እሑ_ሰኞ_ማክ_ረቡ_ሐሙ_አር_ቅዳ".split("_"),months:"ጃንዋሪ_ፌብሯሪ_ማርች_ኤፕሪል_ሜይ_ጁን_ጁላይ_ኦገስት_ሴፕቴምበር_ኦክቶበር_ኖቬምበር_ዲሴምበር".split("_"),monthsShort:"ጃንዋ_ፌብሯ_ማርች_ኤፕሪ_ሜይ_ጁን_ጁላይ_ኦገስ_ሴፕቴ_ኦክቶ_ኖቬም_ዲሴም".split("_"),weekStart:1,yearStart:4,relativeTime:{future:"በ%s",past:"%s በፊት",s:"ጥቂት ሰከንዶች",m:"አንድ ደቂቃ",mm:"%d ደቂቃዎች",h:"አንድ ሰዓት",hh:"%d ሰዓታት",d:"አንድ ቀን",dd:"%d ቀናት",M:"አንድ ወር",MM:"%d ወራት",y:"አንድ ዓመት",yy:"%d ዓመታት"},formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"MMMM D ፣ YYYY",LLL:"MMMM D ፣ YYYY HH:mm",LLLL:"dddd ፣ MMMM D ፣ YYYY HH:mm"},ordinal:function(e){return e+"ኛ"}};return t.default.locale(d,null,!0),d}));
/***/ })
}]);
//# sourceMappingURL=am-js.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"locales/dayjs/am-js.js","mappings":";;;;;AAAA,eAAe,KAAoD,kBAAkB,mBAAO,CAAC,IAAO,GAAG,CAA0I,CAAC,mBAAmB,aAAa,cAAc,+CAA+C,WAAW,cAAc,4VAA4V,mKAAmK,UAAU,wHAAwH,qBAAqB,eAAe,qCAAqC","sources":["webpack://converse.js/./node_modules/dayjs/locale/am.js"],"sourcesContent":["!function(e,_){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=_(require(\"dayjs\")):\"function\"==typeof define&&define.amd?define([\"dayjs\"],_):(e=\"undefined\"!=typeof globalThis?globalThis:e||self).dayjs_locale_am=_(e.dayjs)}(this,(function(e){\"use strict\";function _(e){return e&&\"object\"==typeof e&&\"default\"in e?e:{default:e}}var t=_(e),d={name:\"am\",weekdays:\"እሑድ_ሰኞ_ማክሰኞ_ረቡዕ_ሐሙስ_አርብ_ቅዳሜ\".split(\"_\"),weekdaysShort:\"እሑድ_ሰኞ_ማክሰ_ረቡዕ_ሐሙስ_አርብ_ቅዳሜ\".split(\"_\"),weekdaysMin:\"እሑ_ሰኞ_ማክ_ረቡ_ሐሙ_አር_ቅዳ\".split(\"_\"),months:\"ጃንዋሪ_ፌብሯሪ_ማርች_ኤፕሪል_ሜይ_ጁን_ጁላይ_ኦገስት_ሴፕቴምበር_ኦክቶበር_ኖቬምበር_ዲሴምበር\".split(\"_\"),monthsShort:\"ጃንዋ_ፌብሯ_ማርች_ኤፕሪ_ሜይ_ጁን_ጁላይ_ኦገስ_ሴፕቴ_ኦክቶ_ኖቬም_ዲሴም\".split(\"_\"),weekStart:1,yearStart:4,relativeTime:{future:\"በ%s\",past:\"%s በፊት\",s:\"ጥቂት ሰከንዶች\",m:\"አንድ ደቂቃ\",mm:\"%d ደቂቃዎች\",h:\"አንድ ሰዓት\",hh:\"%d ሰዓታት\",d:\"አንድ ቀን\",dd:\"%d ቀናት\",M:\"አንድ ወር\",MM:\"%d ወራት\",y:\"አንድ ዓመት\",yy:\"%d ዓመታት\"},formats:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"MMMM D ፣ YYYY\",LLL:\"MMMM D ፣ YYYY HH:mm\",LLLL:\"dddd ፣ MMMM D ፣ YYYY HH:mm\"},ordinal:function(e){return e+\"ኛ\"}};return t.default.locale(d,null,!0),d}));"],"names":[],"sourceRoot":""}

View File

@ -1,11 +0,0 @@
(self["webpackChunkconverse_js"] = self["webpackChunkconverse_js"] || []).push([[9406],{
/***/ 7200:
/***/ (function(module, __unused_webpack_exports, __webpack_require__) {
!function(_,e){ true?module.exports=e(__webpack_require__(7484)):0}(this,(function(_){"use strict";function e(_){return _&&"object"==typeof _&&"default"in _?_:{default:_}}var t=e(_),d={name:"ar-dz",weekdays:"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت".split("_"),months:انفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdaysShort:"احد_اثنين_ثلاثاء_اربعاء_خميس_جمعة_سبت".split("_"),monthsShort:انفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر".split("_"),weekdaysMin:"أح_إث_ثلا_أر_خم_جم_سب".split("_"),ordinal:function(_){return _},formats:{LT:"HH:mm",LTS:"HH:mm:ss",L:"DD/MM/YYYY",LL:"D MMMM YYYY",LLL:"D MMMM YYYY HH:mm",LLLL:"dddd D MMMM YYYY HH:mm"},meridiem:function(_){return _>12?"ص":"م"},relativeTime:{future:"في %s",past:"منذ %s",s:"ثوان",m:"دقيقة",mm:"%d دقائق",h:"ساعة",hh:"%d ساعات",d:"يوم",dd:"%d أيام",M:"شهر",MM:"%d أشهر",y:"سنة",yy:"%d سنوات"}};return t.default.locale(d,null,!0),d}));
/***/ })
}]);
//# sourceMappingURL=ar-dz-js.js.map

View File

@ -1 +0,0 @@
{"version":3,"file":"locales/dayjs/ar-dz-js.js","mappings":";;;;;AAAA,eAAe,KAAoD,kBAAkB,mBAAO,CAAC,IAAO,GAAG,CAA6I,CAAC,mBAAmB,aAAa,cAAc,+CAA+C,WAAW,cAAc,sZAAsZ,SAAS,UAAU,gHAAgH,sBAAsB,oBAAoB,eAAe,uJAAuJ,qCAAqC","sources":["webpack://converse.js/./node_modules/dayjs/locale/ar-dz.js"],"sourcesContent":["!function(_,e){\"object\"==typeof exports&&\"undefined\"!=typeof module?module.exports=e(require(\"dayjs\")):\"function\"==typeof define&&define.amd?define([\"dayjs\"],e):(_=\"undefined\"!=typeof globalThis?globalThis:_||self).dayjs_locale_ar_dz=e(_.dayjs)}(this,(function(_){\"use strict\";function e(_){return _&&\"object\"==typeof _&&\"default\"in _?_:{default:_}}var t=e(_),d={name:\"ar-dz\",weekdays:\"الأحد_الإثنين_الثلاثاء_الأربعاء_الخميس_الجمعة_السبت\".split(\"_\"),months:\"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر\".split(\"_\"),weekdaysShort:\"احد_اثنين_ثلاثاء_اربعاء_خميس_جمعة_سبت\".split(\"_\"),monthsShort:\"جانفي_فيفري_مارس_أفريل_ماي_جوان_جويلية_أوت_سبتمبر_أكتوبر_نوفمبر_ديسمبر\".split(\"_\"),weekdaysMin:\"أح_إث_ثلا_أر_خم_جم_سب\".split(\"_\"),ordinal:function(_){return _},formats:{LT:\"HH:mm\",LTS:\"HH:mm:ss\",L:\"DD/MM/YYYY\",LL:\"D MMMM YYYY\",LLL:\"D MMMM YYYY HH:mm\",LLLL:\"dddd D MMMM YYYY HH:mm\"},meridiem:function(_){return _>12?\"ص\":\"م\"},relativeTime:{future:\"في %s\",past:\"منذ %s\",s:\"ثوان\",m:\"دقيقة\",mm:\"%d دقائق\",h:\"ساعة\",hh:\"%d ساعات\",d:\"يوم\",dd:\"%d أيام\",M:\"شهر\",MM:\"%d أشهر\",y:\"سنة\",yy:\"%d سنوات\"}};return t.default.locale(d,null,!0),d}));"],"names":[],"sourceRoot":""}

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