Created
November 26, 2025 19:11
-
-
Save sunmeat/0fb39cc831386017d7d4c6a2f9885c36 to your computer and use it in GitHub Desktop.
firestore + spring boot
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: | |
| plugins { | |
| id 'java' | |
| id 'org.springframework.boot' version '4.0.0-SNAPSHOT' | |
| id 'io.spring.dependency-management' version '1.1.7' | |
| } | |
| group = 'site.sunmeat' | |
| version = '0.0.1-SNAPSHOT' | |
| description = 'Demo project for Spring Boot' | |
| java { | |
| toolchain { | |
| languageVersion = JavaLanguageVersion.of(25) | |
| } | |
| } | |
| repositories { | |
| mavenCentral() | |
| maven { url = 'https://repo.spring.io/snapshot' } | |
| google() // !!! | |
| } | |
| dependencies { | |
| implementation 'org.springframework.boot:spring-boot-starter-web' | |
| implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' | |
| implementation 'com.google.firebase:firebase-admin:+' // !!! | |
| } | |
| tasks.named('test') { | |
| useJUnitPlatform() | |
| } | |
| =================================================================================================== | |
| HibernateApplication.java: | |
| package site.sunmeat.hibernate; | |
| import java.awt.Desktop; | |
| import java.io.*; | |
| 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.annotation.Bean; | |
| import org.springframework.context.event.EventListener; | |
| import org.springframework.stereotype.Component; | |
| import com.google.auth.oauth2.GoogleCredentials; | |
| import com.google.firebase.*; | |
| @SpringBootApplication | |
| public class HibernateApplication { | |
| public static void main(String[] args) { | |
| SpringApplication.run(HibernateApplication.class, args); | |
| } | |
| @Bean | |
| public FirebaseApp initializeFirebase() throws IOException { | |
| InputStream serviceAccount = getClass() | |
| .getClassLoader() | |
| .getResourceAsStream("firebase-service-account.json"); | |
| FirebaseOptions options = FirebaseOptions.builder() | |
| .setCredentials(GoogleCredentials.fromStream(serviceAccount)) | |
| .build(); | |
| if (FirebaseApp.getApps().isEmpty()) { | |
| FirebaseApp.initializeApp(options); | |
| } | |
| return FirebaseApp.getInstance(); | |
| } | |
| } | |
| @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 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.Service; | |
| import org.springframework.web.bind.annotation.*; | |
| import java.util.*; | |
| @Service | |
| @DependsOn("initializeFirebase") | |
| class RandomNumberService { | |
| private Firestore db; | |
| @PostConstruct | |
| public void init() { | |
| db = FirestoreClient.getFirestore(); | |
| System.out.println("Firestore ініціалізовано успішно!"); | |
| } | |
| private static final String COLLECTION = "random_numbers"; | |
| public void sendRandomNumber() { | |
| Random random = new Random(); | |
| int number = random.nextInt(1000) + 1; | |
| Map<String, Object> data = new HashMap<>(); | |
| data.put("number", number); | |
| data.put("timestamp", System.currentTimeMillis()); | |
| db.collection(COLLECTION).add(data); | |
| } | |
| public List<Integer> getAllNumbers() { | |
| try { | |
| Query query = db.collection(COLLECTION).orderBy("timestamp", Query.Direction.ASCENDING); | |
| ApiFuture<QuerySnapshot> future = query.get(); | |
| QuerySnapshot snapshot = future.get(); | |
| List<Integer> numbers = new ArrayList<>(); | |
| for (DocumentSnapshot doc : snapshot.getDocuments()) { | |
| Long num = doc.getLong("number"); | |
| if (num != null) { | |
| numbers.add(num.intValue()); | |
| } | |
| } | |
| return numbers; | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| return Collections.emptyList(); | |
| } | |
| } | |
| } | |
| @RestController | |
| @RequestMapping("/api/numbers") | |
| public class WebController { | |
| private final RandomNumberService numberService; | |
| public WebController(RandomNumberService numberService) { | |
| this.numberService = numberService; | |
| } | |
| @PostMapping("/send") | |
| public Map<String, Object> sendNumber() { | |
| numberService.sendRandomNumber(); | |
| Map<String, Object> response = new HashMap<>(); | |
| response.put("success", true); | |
| response.put("message", "Число відправлено в Firestore!"); | |
| return response; | |
| } | |
| @GetMapping("/list") | |
| public List<Integer> getNumbers() { | |
| return numberService.getAllNumbers(); | |
| } | |
| } | |
| =================================================================================================== | |
| styles.css: | |
| @import url('https://fonts.googleapis.com/css2?family=Google+Sans:wght@300;400;500;600;700&family=Google+Sans+Mono:wght@400;500&display=swap'); | |
| :root { | |
| --md-sys-color-primary: #8ab4f8; | |
| --md-sys-color-on-primary: #000c1a; | |
| --md-sys-color-primary-container: #001d36; | |
| --md-sys-color-on-primary-container: #c5ddff; | |
| --md-sys-color-secondary: #b8c1cc; | |
| --md-sys-color-on-secondary: #1d1f26; | |
| --md-sys-color-secondary-container: #363b43; | |
| --md-sys-color-on-secondary-container: #d9dce4; | |
| --md-sys-color-error: #ffb4ab; | |
| --md-sys-color-background: #000c1a; | |
| --md-sys-color-on-background: #e1e2e5; | |
| --md-sys-color-surface: #0b0e14; | |
| --md-sys-color-on-surface: #e1e2e5; | |
| --md-sys-color-surface-variant: #41464e; | |
| --md-sys-color-on-surface-variant: #c1c5cf; | |
| --md-sys-color-outline: #8b919e; | |
| } | |
| body { | |
| margin: 0; | |
| min-height: 100vh; | |
| font-family: 'Google Sans', system-ui, sans-serif; | |
| background: var(--md-sys-color-background); | |
| color: var(--md-sys-color-on-background); | |
| line-height: 1.5; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| padding: 40px 24px; | |
| } | |
| h1 { | |
| font-size: 36px; | |
| font-weight: 400; | |
| color: var(--md-sys-color-primary); | |
| margin: 0 0 16px; | |
| letter-spacing: -0.5px; | |
| } | |
| h2 { | |
| font-size: 22px; | |
| font-weight: 500; | |
| color: var(--md-sys-color-on-surface); | |
| margin: 60px 0 32px; | |
| text-align: center; | |
| } | |
| button { | |
| padding: 10px 24px; | |
| font-size: 14px; | |
| font-weight: 500; | |
| font-family: 'Google Sans', sans-serif; | |
| background: var(--md-sys-color-primary); | |
| color: var(--md-sys-color-on-primary); | |
| border: none; | |
| border-radius: 50px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.3); | |
| min-height: 40px; | |
| } | |
| button:hover { | |
| background: #669df6; | |
| box-shadow: 0 4px 8px rgba(0,0,0,0.4); | |
| transform: translateY(-1px); | |
| } | |
| button:nth-of-type(2) { | |
| background: transparent; | |
| color: var(--md-sys-color-primary); | |
| border: 1px solid var(--md-sys-color-outline); | |
| margin-left: 12px; | |
| } | |
| button:nth-of-type(2):hover { | |
| background: rgba(138, 180, 248, 0.08); | |
| } | |
| #numberStatus { | |
| margin: 20px 0; | |
| font-size: 15px; | |
| color: #ffb4ab; | |
| text-align: center; | |
| min-height: 24px; | |
| font-weight: 500; | |
| } | |
| #numbersList { | |
| position: relative; | |
| margin: 32px auto; | |
| max-width: 900px; | |
| background: var(--md-sys-color-surface); | |
| border-radius: 12px; | |
| border: 1px solid var(--md-sys-color-outline); | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.2); | |
| overflow: hidden; | |
| font-family: 'Google Sans Mono', monospace; | |
| } | |
| #numbersList::before { | |
| content: "random_numbers"; | |
| position: absolute; | |
| top: 12px; | |
| left: 20px; | |
| font-size: 12px; | |
| font-weight: 500; | |
| color: var(--md-sys-color-on-surface-variant); | |
| background: var(--md-sys-color-surface); | |
| padding: 0 8px; | |
| z-index: 2; | |
| } | |
| #numbersList textarea { | |
| width: 100%; | |
| min-height: 500px; | |
| padding: 52px 24px 24px; | |
| margin: 0; | |
| border: none; | |
| background: transparent; | |
| color: var(--md-sys-color-primary); | |
| font-family: 'Google Sans Mono', monospace; | |
| font-size: 1.6rem; | |
| font-weight: 500; | |
| line-height: 2.4; | |
| resize: none; | |
| outline: none; | |
| text-align: center; | |
| letter-spacing: 1px; | |
| } | |
| #numbersList textarea::placeholder { | |
| color: #5f6368; | |
| opacity: 0.6; | |
| } | |
| #numbersList textarea::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| #numbersList textarea::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| #numbersList textarea::-webkit-scrollbar-thumb { | |
| background: #41464e; | |
| border-radius: 4px; | |
| } | |
| #numbersList textarea::-webkit-scrollbar-thumb:hover { | |
| background: #5f6368; | |
| } | |
| .movie-card { | |
| background: var(--md-sys-color-surface) !important; | |
| border: 1px solid var(--md-sys-color-outline) !important; | |
| border-left: 4px solid var(--md-sys-color-primary) !important; | |
| border-radius: 12px !important; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.3) !important; | |
| } | |
| .header { | |
| color: var(--md-sys-color-primary) !important; | |
| background: none !important; | |
| -webkit-text-fill-color: unset !important; | |
| font-family: 'Google Sans', sans-serif !important; | |
| font-size: 28px !important; | |
| font-weight: 500 !important; | |
| } | |
| @media (max-width: 768px) { | |
| h1 { font-size: 28px; } | |
| button { width: 100%; margin: 8px 0 !important; } | |
| #numbersList textarea { font-size: 1.4rem; padding: 56px 16px 16px; } | |
| } | |
| =================================================================================================== | |
| index.html: | |
| <!DOCTYPE html> | |
| <html xmlns:th="http://www.thymeleaf.org"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title th:text="${title}">Firestore</title> | |
| <script src="https://unpkg.com/[email protected]"></script> | |
| <link rel="stylesheet" href="/styles.css"> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h2>Колекція: random_numbers</h2> | |
| <div style="text-align: center; margin-bottom: 20px;"> | |
| <button hx-post="/api/numbers/send" | |
| hx-target="#numberStatus" | |
| hx-swap="innerHTML"> | |
| Надіслати випадкове число | |
| </button> | |
| <button hx-get="/api/numbers/list" | |
| hx-target="#numbersTextarea" | |
| hx-swap="innerHTML" | |
| style="margin-left: 12px;"> | |
| Оновити список | |
| </button> | |
| </div> | |
| <p id="numberStatus"></p> | |
| <div id="numbersList"> | |
| <textarea id="numbersTextarea" readonly placeholder="Тут з'являться числа з Firestore..."></textarea> | |
| </div> | |
| </div> | |
| <script> | |
| document.body.addEventListener('htmx:afterSwap', function(evt) { | |
| if (evt.detail.target.id === 'numbersTextarea') { | |
| const text = evt.detail.xhr.responseText.trim(); | |
| evt.detail.target.value = text || 'Поки що немає даних...'; | |
| } | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment