}
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'
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);
}
}
.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<>();
}
}
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);
public record WordTranslation(
String word,
- List<String> definitions
+ List<String> translations
) {
}
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);
--- /dev/null
+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);
+ }
+
+}
--- /dev/null
+package net.frustratedfunctor.ui;
+
+import java.util.List;
+
+public record WordTranslationsDefinitions(
+ List<String> translations,
+ List<String> definitions
+) {
+}
--- /dev/null
+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
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
--- /dev/null
+{@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">≡</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