Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created November 26, 2025 12:41
Show Gist options
  • Select an option

  • Save sunmeat/9215463aa7d668b326031514c6ce94c3 to your computer and use it in GitHub Desktop.

Select an option

Save sunmeat/9215463aa7d668b326031514c6ce94c3 to your computer and use it in GitHub Desktop.
spring boot + API
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