Skip to content

Commit f28e404

Browse files
committed
Finished the project, edited readme
1 parent 6241e35 commit f28e404

7 files changed

Lines changed: 320 additions & 141 deletions

File tree

Frontend/solarWatch/src/pages/Content.jsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { useEffect, useState } from "react";
2+
import { useNavigate } from "react-router-dom";
23
import { fetchData } from "../utils";
34
import Globe from "../components/Globe";
45
import CityList from "../components/CityList";
56
import Searchbar from "../components/Searchbar/Searchbar";
67
import Notification from "../components/Notification";
78

89
export default function Content() {
10+
const navigate = useNavigate();
911
const [recommendations, setRecommendations] = useState([]);
1012
const [query, setQuery] = useState("");
1113
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
@@ -14,6 +16,17 @@ export default function Content() {
1416
const jwt = localStorage.getItem("jwt");
1517
const [notification, setNotification] = useState(null);
1618

19+
useEffect(() => {
20+
if (!jwt) {
21+
navigate("/");
22+
}
23+
}, [jwt, navigate]);
24+
25+
const handleLogout = () => {
26+
localStorage.removeItem("jwt");
27+
navigate("/");
28+
};
29+
1730
useEffect(() => {
1831
const loadRecommendations = async () => {
1932
const data = await fetchData("report/cityNames", "GET", null, jwt);
@@ -44,6 +57,12 @@ export default function Content() {
4457

4558
return (
4659
<div className="p-6 w-full flex flex-col gap-4 z-10 h-screen box-border">
60+
<button
61+
onClick={handleLogout}
62+
className="absolute top-4 right-4 px-4 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors"
63+
>
64+
Logout
65+
</button>
4766
<Searchbar
4867
recommendations={recommendations}
4968
setQuery={setQuery}

README.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,88 @@
1-
No starter code is provided. Start from scratch!
1+
# Solar Watch
2+
3+
A modern web application that provides solar information for cities around the world, featuring an interactive 3D globe visualization.
4+
5+
## 🌟 Features
6+
7+
- Interactive 3D globe visualization using Three.js
8+
- City search with autocomplete
9+
- Solar information display for selected cities
10+
- User authentication system
11+
- Responsive design
12+
- Real-time data updates
13+
14+
## 🛠️ Technology Stack
15+
16+
### Frontend
17+
- ![React](https://img.shields.io/badge/React-19.0.0-61DAFB?style=for-the-badge&logo=react&logoColor=white)
18+
- ![Vite](https://img.shields.io/badge/Vite-6.2.0-646CFF?style=for-the-badge&logo=vite&logoColor=white)
19+
- ![React Router](https://img.shields.io/badge/React_Router-7.5.0-CA4245?style=for-the-badge&logo=reactrouter&logoColor=white)
20+
- ![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4.1.3-06B6D4?style=for-the-badge&logo=tailwindcss&logoColor=white)
21+
- ![DaisyUI](https://img.shields.io/badge/DaisyUI-5.0.13-5A0EF8?style=for-the-badge&logo=daisyui&logoColor=white)
22+
- ![Framer Motion](https://img.shields.io/badge/Framer_Motion-12.6.3-0055FF?style=for-the-badge&logo=framer&logoColor=white)
23+
- ![Three.js](https://img.shields.io/badge/Three.js-0.175.0-000000?style=for-the-badge&logo=threedotjs&logoColor=white)
24+
25+
### Backend
26+
- ![Spring Boot](https://img.shields.io/badge/Spring_Boot-3.2.0-6DB33F?style=for-the-badge&logo=springboot&logoColor=white)
27+
- ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-Latest-336791?style=for-the-badge&logo=postgresql&logoColor=white)
28+
- ![JWT](https://img.shields.io/badge/JWT-black?style=for-the-badge&logo=jsonwebtokens&logoColor=white)
29+
- ![Hibernate](https://img.shields.io/badge/Hibernate-6.4.0-BC2E1F?style=for-the-badge&logo=hibernate&logoColor=white)
30+
31+
### DevOps & Infrastructure
32+
- ![Docker](https://img.shields.io/badge/Docker-Latest-2496ED?style=for-the-badge&logo=docker&logoColor=white)
33+
- ![Docker Compose](https://img.shields.io/badge/Docker_Compose-Latest-2496ED?style=for-the-badge&logo=docker&logoColor=white)
34+
- ![Nginx](https://img.shields.io/badge/Nginx-Latest-009639?style=for-the-badge&logo=nginx&logoColor=white)
35+
- ![PostgreSQL](https://img.shields.io/badge/PostgreSQL-Latest-336791?style=for-the-badge&logo=postgresql&logoColor=white)
36+
37+
## 🚀 Getting Started
38+
39+
### Prerequisites
40+
- Docker and Docker Compose
41+
- Node.js (for local frontend development)
42+
- Java JDK (for local backend development)
43+
44+
### Installation
45+
46+
1. Clone the repository:
47+
```bash
48+
git clone [repository-url]
49+
```
50+
51+
2. Start the application using Docker Compose:
52+
```bash
53+
docker-compose up --build
54+
```
55+
56+
3. Access the application:
57+
- Frontend: http://localhost:5173
58+
- Backend API: http://localhost:8081
59+
60+
### Development
61+
62+
#### Frontend Development
63+
```bash
64+
cd Frontend/solarWatch
65+
npm install
66+
npm run dev
67+
```
68+
69+
#### Backend Development
70+
```bash
71+
cd solar-watch
72+
./mvnw spring-boot:run
73+
```
74+
75+
## 🔧 Environment Variables
76+
77+
### Backend
78+
- `DB_USERNAME`: PostgreSQL username
79+
- `DB_PASSWORD`: PostgreSQL password
80+
- `DB_URL`: Database connection URL
81+
- `SECRET_KEY`: JWT secret key
82+
- `EXPIRATION`: JWT token expiration time
83+
- `PIC_API`: API key for external services
84+
- `SPRING_JPA_HIBERNATE_DDL_AUTO`: Database schema update mode
85+
- `SPRING_JPA_DATABASE_PLATFORM`: Hibernate dialect
86+
87+
### Frontend
88+
- Environment variables are configured in the Vite configuration

solar-watch/src/main/java/com/codecool/solarwatch/controller/SolarWatchController.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import org.springframework.web.bind.annotation.RequestMapping;
88
import org.springframework.web.bind.annotation.RequestParam;
99
import org.springframework.web.bind.annotation.RestController;
10-
import reactor.core.publisher.Mono;
1110

1211
import java.time.LocalDate;
1312
import java.time.format.DateTimeFormatter;
@@ -25,9 +24,9 @@ public SolarWatchController(SolarWatchService solarWatchService) {
2524
}
2625

2726
@GetMapping("/report")
28-
public Mono<FormattedSolarReport> getSunReport(@RequestParam(defaultValue = "Budapest") String city, @RequestParam(required = false) String date) {
27+
public ResponseEntity<FormattedSolarReport> getSunReport(@RequestParam(defaultValue = "Budapest") String city, @RequestParam(required = false) String date) {
2928
LocalDate parsedDate = parseDate(date);
30-
return solarWatchService.getCitySolarData(city, parsedDate);
29+
return ResponseEntity.ok(solarWatchService.getCitySolarData(city, parsedDate).block());
3130
}
3231

3332
@GetMapping("/reports")

solar-watch/src/main/java/com/codecool/solarwatch/service/GeocodeService.java

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,18 @@ public GeocodeService(WebClient webClient) {
2020
this.webClient = webClient;
2121
}
2222

23-
public Mono<GeocodeReport> getCityGeocode(String city) {
23+
public GeocodeReport getCityGeocode(String city) {
2424
String url = String.format("http://api.openweathermap.org/geo/1.0/direct?q=%s&limit=%s&appid=%s", city, limit, API_KEY);
2525

26-
return getGeocodeReport(url)
27-
.filter(response -> response != null && response.length > 0)
28-
.map(response -> {
29-
if (response != null && response.length > 0) {
30-
GeocodeReport firstResult = response[0];
31-
logger.info("First geocode result: {}", firstResult);
32-
return firstResult;
33-
} else {
34-
throw new CityError();
35-
}
36-
})
37-
.switchIfEmpty(Mono.error(new CityError()));
38-
}
39-
26+
GeocodeReport[] response = getGeocodeReport(url).block();
27+
if (response == null || response.length == 0) {
28+
throw new CityError();
29+
}
4030

31+
GeocodeReport firstResult = response[0];
32+
logger.info("First geocode result: {}", firstResult);
33+
return firstResult;
34+
}
4135

4236
private Mono<GeocodeReport[]> getGeocodeReport(String url) {
4337
return webClient
@@ -47,5 +41,4 @@ private Mono<GeocodeReport[]> getGeocodeReport(String url) {
4741
.bodyToMono(GeocodeReport[].class)
4842
.log();
4943
}
50-
5144
}

solar-watch/src/main/java/com/codecool/solarwatch/service/SolarWatchService.java

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.codecool.solarwatch.model.*;
44
import com.codecool.solarwatch.model.dto.FormattedSolarReport;
5+
import com.codecool.solarwatch.model.dto.GeocodeReport;
56
import com.codecool.solarwatch.model.dto.SolarResults;
67
import com.codecool.solarwatch.model.dto.SolarWatchReport;
78
import com.codecool.solarwatch.repository.CityRepository;
@@ -41,47 +42,43 @@ public SolarWatchService(WebClient webClient, GeocodeService geocodeService, Sol
4142
}
4243

4344
public Mono<FormattedSolarReport> getCitySolarData(String city, LocalDate date) {
44-
return Mono.defer(() -> {
45-
return cityRepository.findByName(city)
46-
.map(Mono::just)
47-
.orElseGet(() -> geocodeService.getCityGeocode(city)
48-
.map(geoRep -> {
49-
String imageUrl = null;
50-
try {
51-
imageUrl = pictureProviderService.getPictureUrl(geoRep.name());
52-
} catch (IOException | InterruptedException e) {
53-
e.printStackTrace();
54-
}
55-
return new City(geoRep.name(), geoRep.country(), imageUrl, geoRep.lon(), geoRep.lat());
56-
})
57-
.flatMap(newCity -> Mono.fromCallable(() -> cityRepository.save(newCity)))
58-
);
59-
}).flatMap(searchedCity -> {
60-
double lon = searchedCity.getLon();
61-
double lat = searchedCity.getLat();
62-
return Mono.justOrEmpty(solarDataRepository.findByCityAndDate(searchedCity, date))
63-
.switchIfEmpty(Mono.defer(() -> {
64-
String url = (date != null)
65-
? String.format("https://api.sunrise-sunset.org/json?lat=%s&lng=%s&date=%s", lat, lon, date)
66-
: String.format("https://api.sunrise-sunset.org/json?lat=%s&lng=%s", lat, lon);
67-
68-
return getSolarWatchReport(url)
69-
.map(response -> new SolarData(searchedCity, response.results().sunrise(), response.results().sunset(), date))
70-
.flatMap(solarData -> Mono.fromCallable(() -> solarDataRepository.save(solarData)));
71-
}))
72-
.then(Mono.fromCallable(() -> solarDataRepository.findAllByCity(searchedCity)))
73-
.map(solarDataList -> new FormattedSolarReport(
74-
searchedCity.getName(),
75-
searchedCity.getCountry(),
76-
searchedCity.getPictureUrl(),
77-
searchedCity.getLon(),
78-
searchedCity.getLat(),
79-
getAllSolarDataByCity(searchedCity)
80-
));
81-
});
45+
City searchedCity = cityRepository.findByName(city)
46+
.orElseGet(() -> {
47+
try {
48+
GeocodeReport geoRep = geocodeService.getCityGeocode(city);
49+
String imageUrl = pictureProviderService.getPictureUrl(geoRep.name());
50+
return cityRepository.save(new City(geoRep.name(), geoRep.country(), imageUrl, geoRep.lon(), geoRep.lat()));
51+
} catch (Exception e) {
52+
logger.error("Error getting city geocode", e);
53+
throw new RuntimeException("Error getting city geocode", e);
54+
}
55+
});
56+
57+
double lon = searchedCity.getLon();
58+
double lat = searchedCity.getLat();
59+
60+
SolarData solarData = solarDataRepository.findByCityAndDate(searchedCity, date)
61+
.orElseGet(() -> {
62+
String url = (date != null)
63+
? String.format("https://api.sunrise-sunset.org/json?lat=%s&lng=%s&date=%s", lat, lon, date)
64+
: String.format("https://api.sunrise-sunset.org/json?lat=%s&lng=%s", lat, lon);
65+
66+
SolarWatchReport response = getSolarWatchReport(url).block();
67+
SolarData newData = new SolarData(searchedCity, response.results().sunrise(), response.results().sunset(), date);
68+
return solarDataRepository.save(newData);
69+
});
70+
71+
List<SolarData> allSolarData = solarDataRepository.findAllByCity(searchedCity);
72+
return Mono.just(new FormattedSolarReport(
73+
searchedCity.getName(),
74+
searchedCity.getCountry(),
75+
searchedCity.getPictureUrl(),
76+
searchedCity.getLon(),
77+
searchedCity.getLat(),
78+
getAllSolarDataByCity(searchedCity)
79+
));
8280
}
8381

84-
8582
private Mono<SolarWatchReport> getSolarWatchReport(String url) {
8683
return webClient
8784
.get()
@@ -91,7 +88,6 @@ private Mono<SolarWatchReport> getSolarWatchReport(String url) {
9188
.log();
9289
}
9390

94-
9591
public List<FormattedSolarReport> getAllFormattedReports() {
9692
List<City> cities = cityRepository.findAll();
9793

solar-watch/src/test/java/com/codecool/solarwatch/service/GeocodeServiceTs.java

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
import org.mockito.junit.jupiter.MockitoExtension;
1111
import org.springframework.web.reactive.function.client.WebClient;
1212
import reactor.core.publisher.Mono;
13-
import reactor.test.StepVerifier;
1413

14+
import static org.junit.jupiter.api.Assertions.*;
1515
import static org.mockito.ArgumentMatchers.anyString;
1616
import static org.mockito.Mockito.when;
1717

@@ -49,10 +49,15 @@ void getCityGeocode_whenValidCity_returnsGeocodeReport() {
4949
when(responseSpec.bodyToMono(GeocodeReport[].class))
5050
.thenReturn(Mono.just(new GeocodeReport[]{expectedReport}));
5151

52-
// Act & Assert
53-
StepVerifier.create(geocodeService.getCityGeocode(cityName))
54-
.expectNext(expectedReport)
55-
.verifyComplete();
52+
// Act
53+
GeocodeReport result = geocodeService.getCityGeocode(cityName);
54+
55+
// Assert
56+
assertNotNull(result);
57+
assertEquals(expectedReport.name(), result.name());
58+
assertEquals(expectedReport.country(), result.country());
59+
assertEquals(expectedReport.lon(), result.lon());
60+
assertEquals(expectedReport.lat(), result.lat());
5661
}
5762

5863
@Test
@@ -64,9 +69,7 @@ void getCityGeocode_whenInvalidCity_throwsCityError() {
6469
.thenReturn(Mono.just(new GeocodeReport[0]));
6570

6671
// Act & Assert
67-
StepVerifier.create(geocodeService.getCityGeocode(nonExistentCity))
68-
.expectError(CityError.class)
69-
.verify();
72+
assertThrows(CityError.class, () -> geocodeService.getCityGeocode(nonExistentCity));
7073
}
7174

7275
@Test
@@ -78,8 +81,6 @@ void getCityGeocode_whenApiReturnsEmpty_throwsCityError() {
7881
.thenReturn(Mono.empty());
7982

8083
// Act & Assert
81-
StepVerifier.create(geocodeService.getCityGeocode(cityName))
82-
.expectError(CityError.class)
83-
.verify();
84+
assertThrows(CityError.class, () -> geocodeService.getCityGeocode(cityName));
8485
}
8586
}

0 commit comments

Comments
 (0)