]> git.frustrated-labs.net Git - larousseapi/commitdiff
feat: add web ui
authorAlexander Goussas <[email protected]>
Sun, 31 May 2026 16:51:59 +0000 (11:51 -0500)
committerAlexander Goussas <[email protected]>
Sun, 31 May 2026 16:51:59 +0000 (11:51 -0500)
12 files changed:
build.gradle
src/main/java/net/frustratedfunctor/ExceptionMappers.java
src/main/java/net/frustratedfunctor/definitions/JsoupWordDefinitionRepository.java
src/main/java/net/frustratedfunctor/definitions/WordDefinitionRepository.java
src/main/java/net/frustratedfunctor/translations/WordTranslation.java
src/main/java/net/frustratedfunctor/translations/WordTranslationService.java
src/main/java/net/frustratedfunctor/ui/UiResource.java [new file with mode: 0644]
src/main/java/net/frustratedfunctor/ui/WordTranslationsDefinitions.java [new file with mode: 0644]
src/main/resources/META-INF/resources/logo.png [new file with mode: 0644]
src/main/resources/META-INF/resources/styles.css [new file with mode: 0644]
src/main/resources/application.properties
src/main/resources/templates/pub/index.html [new file with mode: 0644]

index b5a4601271c053905e1f92ff8723d4585f60c1db..e4461d8fb3ad397a130ef6d0a458d64d3973dd9f 100644 (file)
@@ -9,6 +9,8 @@ repositories {
 }
 
 dependencies {
+    implementation 'io.quarkiverse.qute.web:quarkus-qute-web'
+    implementation 'io.quarkus:quarkus-rest-qute'
     implementation 'io.quarkus:quarkus-micrometer-opentelemetry'
     implementation 'io.quarkus:quarkus-opentelemetry'
     implementation 'io.quarkus:quarkus-kubernetes'
index 1e2290e3976d1182b5a0dfd7b18cc319d1499a00..f4733557ec228f38fead840247074a88bb2170a7 100644 (file)
@@ -7,7 +7,7 @@ import org.jboss.resteasy.reactive.server.ServerExceptionMapper;
 final public class ExceptionMappers {
     @ServerExceptionMapper
     public RestResponse<Void> mapException(InvalidWordException ex) {
-        Log.info("Got bad word");
+        Log.infof("Got bad word: %s", ex.getMessage());
         return RestResponse.status(400);
     }
 }
index b1bb296a7c60fa6f818b0098d99103f4de060dcd..96e7d788037dc3c753e11c8d68ab8fe3700b1f0c 100644 (file)
@@ -22,7 +22,7 @@ public class JsoupWordDefinitionRepository implements WordDefinitionRepository {
                     .map(Element::ownText)
                     .toList();
         } catch (Exception e) {
-            Log.error("Error while fetching word definitions", e);
+            Log.error("Error while fetching word translations", e);
             return new ArrayList<>();
         }
     }
index de8dd353cfecc406ef97df3461f2789bf3727973..f5e2d90db01ed8d7ce9dc67e421097a4d72e9e6a 100644 (file)
@@ -5,10 +5,10 @@ import java.util.List;
 public interface WordDefinitionRepository {
 
     /**
-     * Get a list of definitions for the given word.
+     * Get a list of translations for the given word.
      *
-     * @param word The word to get definitions for.
-     * @return A list of found definitions for the word.
+     * @param word The word to get translations for.
+     * @return A list of found translations for the word.
      */
     List<String> definitionsForWord(String word);
 
index 85778fceffe94996d15177f8c4a052e8aaa83793..2ea5957b2620fd05ea174e9e0ad09d14beb4851b 100644 (file)
@@ -4,6 +4,6 @@ import java.util.List;
 
 public record WordTranslation(
         String word,
-        List<String> definitions
+        List<String> translations
 ) {
 }
index b98c68df49f43927c5f1f9bdc9360cb83977c80c..6f547f99e9001a462a626c10f061c0328745184c 100644 (file)
@@ -14,7 +14,7 @@ public class WordTranslationService {
 
     public WordTranslation translate(String word) {
         if (word == null || word.isEmpty()) {
-            throw new InvalidWordException("The word cannot be empty");
+            throw new InvalidWordException("The word cannot be empty:" + word);
         }
 
         final var wordDefs = repository.translationsForWord(word);
diff --git a/src/main/java/net/frustratedfunctor/ui/UiResource.java b/src/main/java/net/frustratedfunctor/ui/UiResource.java
new file mode 100644 (file)
index 0000000..0fe5dc2
--- /dev/null
@@ -0,0 +1,39 @@
+package net.frustratedfunctor.ui;
+
+import io.quarkus.qute.Location;
+import io.quarkus.qute.Template;
+import io.quarkus.qute.TemplateInstance;
+import jakarta.ws.rs.FormParam;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+import net.frustratedfunctor.definitions.WordDefinitionService;
+import net.frustratedfunctor.translations.WordTranslationService;
+
+@Path("/ui")
+public class UiResource {
+
+    private final WordDefinitionService definitions;
+    private final WordTranslationService translations;
+
+    @Location("pub/index.html")
+    Template index;
+
+    public UiResource(WordDefinitionService definitions, WordTranslationService translations) {
+        this.definitions = definitions;
+        this.translations = translations;
+    }
+
+    @Path("getTranslationsDefinitions")
+    @POST
+    @Produces(MediaType.TEXT_HTML)
+    public TemplateInstance getTranslationsDefinitions(@FormParam("word") String word) {
+        final var data = new WordTranslationsDefinitions(
+                translations.translate(word).translations(),
+                definitions.define(word).definitions()
+        );
+        return index.data("translationsDefinitions", data);
+    }
+
+}
diff --git a/src/main/java/net/frustratedfunctor/ui/WordTranslationsDefinitions.java b/src/main/java/net/frustratedfunctor/ui/WordTranslationsDefinitions.java
new file mode 100644 (file)
index 0000000..281a626
--- /dev/null
@@ -0,0 +1,9 @@
+package net.frustratedfunctor.ui;
+
+import java.util.List;
+
+public record WordTranslationsDefinitions(
+        List<String> translations,
+        List<String> definitions
+) {
+}
diff --git a/src/main/resources/META-INF/resources/logo.png b/src/main/resources/META-INF/resources/logo.png
new file mode 100644 (file)
index 0000000..764882d
Binary files /dev/null and b/src/main/resources/META-INF/resources/logo.png differ
diff --git a/src/main/resources/META-INF/resources/styles.css b/src/main/resources/META-INF/resources/styles.css
new file mode 100644 (file)
index 0000000..756ece5
--- /dev/null
@@ -0,0 +1,41 @@
+main {
+    max-width: 60%;
+    margin: auto;
+    margin-top: 48px;
+    min-height: 90vh;
+
+    h1 {
+        text-align: center;
+    }
+}
+
+fieldset {
+    width: 100%;
+
+    input, button {
+        width: 100%;
+        margin-bottom: 2px;
+    }
+}
+
+form {
+    margin-bottom: 10px;
+}
+
+.card {
+    padding: 0.4rem !important;
+}
+
+footer {
+    padding: 1rem;
+    background-color: black;
+    min-height: 10vh;
+    color: white;
+    text-align: center;
+}
+
+@media only screen and (max-width: 600px) {
+    main {
+        max-width: 90%;
+    }
+}
\ No newline at end of file
index a2c0b74070c766500f5dd4c8e1eaeda408ffa47f..11c18a68963bdbec794553aad894e9721a153edc 100644 (file)
@@ -9,3 +9,5 @@ quarkus.otel.exporter.otlp.endpoint=http://localhost:4317
 quarkus.application.name=larousse-api
 quarkus.otel.simple=true
 %prod.quarkus.otel.simple=false
+quarkus.qute.property-not-found-strategy=default
+quarkus.qute.strict-rendering=false
\ No newline at end of file
diff --git a/src/main/resources/templates/pub/index.html b/src/main/resources/templates/pub/index.html
new file mode 100644 (file)
index 0000000..e3a5cba
--- /dev/null
@@ -0,0 +1,87 @@
+{@net.frustratedfunctor.ui.WordTranslationsDefinitions  translationsDefinitions}
+<!DOCTYPE html>
+<html lang="en">
+<meta charset="UTF-8">
+<meta name="description" content="Translations and definitions service for French and English">
+<title>Frustrated Langs</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script defer src="https://analytics.frustrated-labs.net/script.js"
+        data-website-id="ac647826-77cd-48d0-b184-2da490b4adb4"></script>
+<script defer src="https://analytics.frustrated-labs.net/recorder.js"
+        data-website-id="ac647826-77cd-48d0-b184-2da490b4adb4" data-sample-rate="0.15" data-mask-level="moderate"
+        data-max-duration="300000"></script>
+<link rel="stylesheet" href="/styles.css">
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/picnic">
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css"
+      integrity="sha512-2SwdPD6INVrV/lHTZbO2nodKhrnDdJK9/kg2XD1r9uGqPo1cUbujc+IYdlYdEErWNu69gVcYgdxlmVmzTWnetw=="
+      crossorigin="anonymous" referrerpolicy="no-referrer"/>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/solid.min.css"
+      integrity="sha512-EHa6vH03/Ty92WahM0/tet1Qicl76zihDCkBnFhN3kFGQkC+mc86d7V+6y2ypiLbk3h0beZAGdUpzfMcb06cMg=="
+      crossorigin="anonymous" referrerpolicy="no-referrer"/>
+<style>
+</style>
+<body>
+
+<header>
+    <nav class="demo">
+        <a href="#" class="brand">
+            <img class="logo" src="/logo.png" alt="logo"/>
+            <span>Frustrated Langs</span>
+        </a>
+
+        <!-- responsive-->
+        <input id="bmenub" type="checkbox" class="show">
+        <label for="bmenub" class="burger pseudo button">&#8801;</label>
+
+        <div class="menu">
+            <a href="#" class="pseudo button">About</a>
+            <a href="#" class="pseudo button">API</a>
+            <a href="#" class="button">Contribute</a>
+        </div>
+    </nav>
+</header>
+
+<main class="">
+    <h1>Frustrated Langs</h1>
+
+    <div class="card">
+        <small
+                style="margin-bottom: 3px;"
+                data-tooltip="We currently only support French to English"
+                class="tooltip-bottom">
+            <i class="fa-solid fa-info"></i>
+            Search for a word in the box below.
+        </small>
+        <form method="POST" action="/ui/getTranslationsDefinitions">
+            <fieldset>
+                <label class="stack"><input name="word" type="text" placeholder="Jambon..."></label>
+                <button class="stack">Let's Go!</button>
+            </fieldset>
+        </form>
+        {#if translationsDefinitions && !translationsDefinitions.translations.empty}
+        <div id="translations">
+            <h2>Translations</h2>
+            {#each translationsDefinitions.translations}
+            <span class="stack pseudo button">{it}</span>
+            {/each}
+        </div>
+        {/if}
+        {#if translationsDefinitions && !translationsDefinitions.definitions.empty}
+        <div id="definitions">
+            <h2>Definitions</h2>
+            {#each translationsDefinitions.definitions}
+            <span class="stack pseudo button">{it}</span>
+            {/each}
+        </div>
+        {/if}
+    </div>
+</main>
+
+<footer>
+    <small>Copyright 2026 Alexander Goussas</small>
+    <br>
+    <small>Made with Quarkus and k8s and <3</small>
+</footer>
+
+</body>
+</html>
\ No newline at end of file