Skip to content

Instantly share code, notes, and snippets.

@sunmeat
Created November 26, 2025 20:15
Show Gist options
  • Select an option

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

Select an option

Save sunmeat/f1b5bbe24ccf0c80350fd234349b1f86 to your computer and use it in GitHub Desktop.
spring boot + firestore 3
WebController.java:
package site.sunmeat.hibernate;
import com.google.api.core.ApiFuture;
import com.google.cloud.firestore.*;
import com.google.firebase.cloud.FirestoreClient;
import jakarta.annotation.PostConstruct;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Controller
@DependsOn("initializeFirebase")
public class WebController {
private Firestore db;
private static final String COLLECTION = "students";
@PostConstruct
public void init() {
db = FirestoreClient.getFirestore();
}
@GetMapping("/")
public String index() {
return "index";
}
@GetMapping("/students-list")
@ResponseBody
public String getStudentsList() {
try {
ApiFuture<QuerySnapshot> future = db.collection(COLLECTION)
.orderBy("name")
.get();
var docs = future.get().getDocuments();
if (docs.isEmpty()) {
return "<div style='text-align:center;padding:80px;color:#70b3ff;font-size:18px'>Немає студентів<br><br>Додай першого ↓</div>";
}
var sb = new StringBuilder();
for (QueryDocumentSnapshot d : docs) {
String name = d.getString("name");
Long age = d.getLong("age");
String group = d.getString("group");
Double grade = d.getDouble("averageGrade");
if (name == null) continue;
sb.append("<div class='message other'>")
.append("<div class='login'>").append(esc(name)).append("</div>")
.append("<div>").append(age != null ? age : "?").append(" років</div>")
.append("<div><b>Група:</b> ").append(esc(group != null ? group : "—")).append("</div>")
.append("<div><b>Середній бал:</b> ")
.append(grade != null ? String.format("%.2f", grade) : "—")
.append("</div></div>");
}
return sb.toString();
} catch (Exception e) {
e.printStackTrace();
return "<div style='color:#ff5555;text-align:center;padding:20px'>Помилка сервера</div>";
}
}
@PostMapping("/add-student")
@ResponseBody
public Map<String,String> addStudent(
@RequestParam(name = "name") String name,
@RequestParam(name = "age") int age,
@RequestParam(name = "group") String group,
@RequestParam(name = "grade") double grade) {
Map<String, Object> data = Map.of(
"name", name.trim(),
"age", age,
"group", group.trim(),
"averageGrade", grade,
"timestamp", FieldValue.serverTimestamp()
);
db.collection(COLLECTION).add(data);
return Map.of("status", "ok");
}
private String esc(String s) {
if (s == null) return "";
return s.replace("&","&amp;").replace("<","&lt;").replace(">","&gt;");
}
}
============================================================================================================
style.css:
:root {
--primary: #1e3a8a;
--primary-light: #3b82f6;
--accent: #10b981;
--bg: #f8fafc;
--card: #ffffff;
--text: #1e293b;
--text-light: #64748b;
--border: #e2e8f0;
--shadow: 0 4px 12px rgba(0,0,0,0.08);
--radius: 12px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: var(--primary);
color: white;
padding: 20px;
text-align: center;
font-size: 24px;
font-weight: 600;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.messages {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.message.other {
background: var(--card);
border-radius: var(--radius);
padding: 16px 20px;
box-shadow: var(--shadow);
border-left: 5px solid var(--primary);
transition: all 0.2s;
}
.message.other:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.12);
}
.login {
font-size: 18px;
font-weight: 600;
color: var(--primary);
margin-bottom: 8px;
}
.message.other > div:not(.login) {
margin: 6px 0;
color: var(--text-light);
font-size: 15px;
}
.message.other > div:not(.login) b {
color: var(--text);
font-weight: 600;
}
.input-area {
padding: 16px;
background: white;
border-top: 1px solid var(--border);
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr auto;
gap: 12px;
align-items: center;
flex-shrink: 0;
}
.input-area input {
padding: 14px 16px;
border: 2px solid var(--border);
border-radius: 10px;
font-size: 15px;
transition: border 0.2s;
}
.input-area input:focus {
outline: none;
border-color: var(--primary-light);
box-shadow: 0 0 0 3px rgba(59,130,246,0.2);
}
.input-area button {
background: var(--accent);
color: white;
border: none;
padding: 14px 24px;
border-radius: 10px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: background 0.3s;
white-space: nowrap;
}
.input-area button:hover {
background: #059669;
}
.messages:empty::after {
content: "Немає студентів. Додай першого →";
text-align: center;
padding: 60px 20px;
color: var(--text-light);
font-size: 18px;
}
@media (max-width: 768px) {
.input-area { grid-template-columns: 1fr; }
.input-area button { margin-top: 8px; }
}
.messages::-webkit-scrollbar { width: 8px; }
.messages::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; }
==========================================================================================================
index.html:
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Збереження об'єктів</title>
<script src="https://unpkg.com/[email protected]"></script>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div class="header">
<h1>Список студентів</h1>
</div>
<div id="studentsList" class="messages"
hx-get="/students-list"
hx-trigger="every 2s, studentAdded from:body"
hx-swap="innerHTML">
</div>
<div class="input-area">
<input type="text" id="name" placeholder="Ім'я та прізвище">
<input type="number" id="age" placeholder="Вік" min="16" max="40">
<input type="text" id="group" placeholder="Група">
<input type="number" id="grade" placeholder="Середній бал" step="0.01" min="0" max="12">
<button onclick="addStudent()">Додати студента</button>
</div>
<script>
function addStudent() {
const name = document.getElementById('name').value.trim();
const age = document.getElementById('age').value;
const group = document.getElementById('group').value.trim();
const grade = document.getElementById('grade').value;
if (!name || !age || !group || !grade) {
alert('Заповніть усі поля!');
return;
}
fetch('/add-student', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ name, age, group, grade })
})
.then(() => {
document.getElementById('name').value = '';
document.getElementById('age').value = '';
document.getElementById('group').value = '';
document.getElementById('grade').value = '';
htmx.trigger('#studentsList', 'studentAdded');
});
}
let atBottom = true;
document.querySelector('.messages')?.addEventListener('scroll', e => {
const el = e.target;
atBottom = Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < 100;
});
document.body.addEventListener('htmx:afterSwap', e => {
if (e.detail.target.id === 'studentsList' && atBottom) {
e.detail.target.scrollTop = e.detail.target.scrollHeight;
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment