Created
November 26, 2025 20:15
-
-
Save sunmeat/f1b5bbe24ccf0c80350fd234349b1f86 to your computer and use it in GitHub Desktop.
spring boot + firestore 3
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
| 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("&","&").replace("<","<").replace(">",">"); | |
| } | |
| } | |
| ============================================================================================================ | |
| 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