Created
November 26, 2025 12:41
-
-
Save sunmeat/9215463aa7d668b326031514c6ce94c3 to your computer and use it in GitHub Desktop.
spring boot + API
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| build.gradle: | |
| dependencies { | |
| implementation 'org.springframework.boot:spring-boot-starter-web' | |
| implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' | |
| implementation("org.springframework.boot:spring-boot-starter-webflux") | |
| implementation("com.fasterxml.jackson.core:jackson-databind") | |
| developmentOnly("org.springframework.boot:spring-boot-devtools") | |
| } | |
| ====================================================================================================== | |
| HibernateApplication.java: | |
| package site.sunmeat.hibernate; | |
| import java.awt.Desktop; | |
| import java.net.URI; | |
| import org.springframework.boot.SpringApplication; | |
| import org.springframework.boot.autoconfigure.SpringBootApplication; | |
| import org.springframework.boot.context.event.ApplicationReadyEvent; | |
| import org.springframework.context.event.EventListener; | |
| import org.springframework.stereotype.Component; | |
| @SpringBootApplication | |
| public class HibernateApplication { | |
| public static void main(String[] args) { | |
| SpringApplication.run(HibernateApplication.class, args); | |
| } | |
| } | |
| @Component | |
| class BrowserLauncher { | |
| @EventListener(ApplicationReadyEvent.class) | |
| public void launchBrowser() { | |
| System.setProperty("java.awt.headless", "false"); | |
| Desktop desktop = Desktop.getDesktop(); | |
| try { | |
| desktop.browse(new URI("http://localhost:8080")); | |
| } catch (Exception e) { } | |
| } | |
| } | |
| ====================================================================================================== | |
| WebController.java: | |
| package site.sunmeat.hibernate; | |
| import org.springframework.http.MediaType; | |
| import org.springframework.stereotype.Controller; | |
| import org.springframework.ui.Model; | |
| import org.springframework.web.bind.annotation.*; | |
| import org.springframework.web.reactive.function.client.WebClient; | |
| import reactor.core.publisher.Mono; // клас з бібліотеки Project Reactor (реактивне програмування в Spring). Mono = 0 або 1 результат | |
| @Controller | |
| public class WebController { | |
| private final WebClient webClient = WebClient.create("https://v2.jokeapi.dev"); | |
| record Joke(String type, String joke, String setup, String delivery) { | |
| String content() { | |
| return "single".equals(type) ? joke : setup + "<br><br>" + delivery; | |
| } | |
| } | |
| private final String[] frame = {"#8B0000","#006400","#00008B","#8B8000","#008B8B","#4B0082"}; | |
| private final String[] text = {"#FF3333","#00FF7F","#6495ED","#FFD700","#00FFFF","#FF00FF"}; | |
| @GetMapping("/") | |
| public String index(Model model) { | |
| model.addAttribute("title", "API"); | |
| return "index"; | |
| } | |
| @GetMapping(value = "/joke", produces = MediaType.TEXT_HTML_VALUE) | |
| // обробляємо GET-запит на /joke і повертаємо чистий HTML | |
| @ResponseBody | |
| public Mono<String> getJoke( | |
| @RequestParam(name = "n", defaultValue = "1") int n) { | |
| if (n < 1 || n > 50) n = 1; | |
| int number = n; | |
| return webClient.get() | |
| .uri("/joke/Programming?lang=en") | |
| .retrieve() // отримуємо відповідь | |
| .bodyToMono(Joke.class) // десеріалізуємо JSON у об’єкт класу Joke | |
| .timeout(java.time.Duration.ofSeconds(10)) // максимум 10 секунд на запит, інакше таймаут | |
| .onErrorReturn(new Joke("single", | |
| "Помилка мережі… жарт №" + number + " не прийшов", null, null)) | |
| .map(joke -> { | |
| String f = frame[(number - 1) % frame.length]; | |
| String t = text[(number - 1) % text.length]; | |
| return """ | |
| <div class="joke-card" style="--frame:%s;--text:%s"> | |
| <div class="header">Жарт №%d</div> | |
| <div class="body">%s</div> | |
| </div> | |
| """.formatted(f, t, number, joke.content()); // формується готова HTML-картка жарту | |
| }); | |
| } | |
| } | |
| ====================================================================================================== | |
| styles.css: | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: 'Fira Code', 'JetBrains Mono', Consolas, monospace; | |
| background: #0d1117; | |
| color: #c9d1d9; | |
| overflow-x: hidden; | |
| position: relative; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background: | |
| repeating-linear-gradient(90deg, rgba(22, 27, 34, 0.8) 0px, rgba(22, 27, 34,1) 1px, transparent 1px, transparent 80px), | |
| repeating-linear-gradient(0deg, rgba(22, 27, 34, 0.8) 0px, rgba(22, 27,34,1) 1px, transparent 1px, transparent 40px); | |
| pointer-events: none; | |
| z-index: 1; | |
| } | |
| @keyframes coffeeMove { | |
| from { transform: translate(0, 0); } | |
| to { transform: translate(200px, 200px); } | |
| } | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 60px 20px; | |
| text-align: center; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| h1 { | |
| font-size: 4.2rem; | |
| font-weight: 700; | |
| color: #58a6ff; | |
| text-shadow: 0 0 20px #1f6feb; | |
| margin-bottom: 20px; | |
| letter-spacing: 3px; | |
| } | |
| p { | |
| font-size: 1.6rem; | |
| color: #8b949e; | |
| margin-bottom: 50px; | |
| } | |
| button { | |
| padding: 18px 50px; | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| background: #21262d; | |
| color: #58a6ff; | |
| border: 2px solid #30363d; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.5); | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| button:hover { | |
| background: #161b22; | |
| border-color: #58a6ff; | |
| transform: translateY(-4px); | |
| box-shadow: 0 10px 25px rgba(88,166,255,0.3); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .jokes-list { | |
| margin-top: 60px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 32px; | |
| align-items: center; | |
| } | |
| .joke-card { | |
| width: 92%; | |
| max-width: 760px; | |
| background: #161b22; | |
| border-left: 6px solid var(--frame); | |
| border-radius: 0 12px 12px 0; | |
| padding: 28px 32px; | |
| box-shadow: | |
| 0 8px 25px rgba(0,0,0,0.6), | |
| inset 0 1px 0 rgba(255,255,255,0.05); | |
| opacity: 0; | |
| transform: translateX(-100px); | |
| animation: slideIn 0.6s ease-out forwards; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .joke-card::before { | |
| content: '>'; | |
| position: absolute; | |
| left: 12px; | |
| top: 28px; | |
| color: #3fb950; | |
| font-weight: bold; | |
| font-size: 1.4rem; | |
| opacity: 0.4; | |
| } | |
| @keyframes slideIn { | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| .joke-card .header { | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| color: var(--text); | |
| margin: 0 0 20px 20px; | |
| text-shadow: 0 0 15px currentColor; | |
| } | |
| .joke-card .body { | |
| font-size: 1.55rem; | |
| line-height: 1.8; | |
| color: #c9d1d9; | |
| margin-left: 20px; | |
| white-space: pre-wrap; | |
| text-align: left; | |
| } | |
| .htmx-indicator { | |
| margin: 40px auto; | |
| font-size: 1.8rem; | |
| color: #8b949e; | |
| } | |
| @media (max-width: 640px) { | |
| h1 { font-size: 2.8rem; } | |
| button { padding: 16px 32px; font-size: 1.3rem; } | |
| .joke-card { padding: 24px; } | |
| .joke-card .header { font-size: 1.6rem; } | |
| .joke-card .body { font-size: 1.4rem; } | |
| } | |
| ====================================================================================================== | |
| index.html: | |
| <!DOCTYPE html> | |
| <html xmlns:th="http://www.thymeleaf.org"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title th:text="${title}">API</title> | |
| <script src="https://unpkg.com/[email protected]"></script> | |
| <link rel="stylesheet" href="style.css"> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>50 жартів про програмування</h1> | |
| <button id="start" | |
| hx-get="/joke?n=1" | |
| hx-target="#jokes" | |
| hx-swap="beforeend"> | |
| Завантажити дані | |
| </button> | |
| <div id="jokes" class="jokes-list"></div> | |
| </div> | |
| <script> | |
| document.body.addEventListener('htmx:afterSwap', function(evt) { | |
| if (evt.detail.xhr.responseURL.includes('/joke')) { | |
| const match = evt.detail.xhr.responseURL.match(/n=(\d+)/); | |
| if (match) { | |
| const current = parseInt(match[1]); | |
| if (current < 50) { | |
| const next = current + 1; | |
| htmx.ajax('GET', `/joke?n=${next}`, { | |
| target: '#jokes', | |
| swap: 'beforeend' | |
| }); | |
| } else { | |
| document.getElementById('start').textContent = "Готово!"; | |
| document.getElementById('start').disabled = true; | |
| } | |
| } | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment