From 11e596869ce4979e0a4c87e83c9a1560117243a4 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Sat, 14 Dec 2024 18:31:29 +0200 Subject: [PATCH 01/43] Add DTOs for commit information handling Introduce `CommitInfoDto` as a base class along with `CommitInfoErrorDto` and `CommitInfoSuccessDto` for differentiated error and success responses. These classes will streamline commit-related data transfer and enhance response structure. --- .../dto/commitinfo/CommitInfoDto.java | 14 ++++++++++++++ .../dto/commitinfo/CommitInfoErrorDto.java | 17 +++++++++++++++++ .../dto/commitinfo/CommitInfoSuccessDto.java | 18 ++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java create mode 100644 service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java create mode 100644 service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java new file mode 100644 index 000000000..8ec473d1d --- /dev/null +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java @@ -0,0 +1,14 @@ +package greencity.dto.commitinfo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Base class for commit information DTO. + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public abstract class CommitInfoDto { +} diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java new file mode 100644 index 000000000..98ae67d36 --- /dev/null +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java @@ -0,0 +1,17 @@ +package greencity.dto.commitinfo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * DTO for error response. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommitInfoErrorDto extends CommitInfoDto { + private String error; +} diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java new file mode 100644 index 000000000..a5c79fc86 --- /dev/null +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java @@ -0,0 +1,18 @@ +package greencity.dto.commitinfo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +/** + * DTO for successful commit information response. + */ +@EqualsAndHashCode(callSuper = true) +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CommitInfoSuccessDto extends CommitInfoDto { + private String commitHash; + private String commitDate; +} From b9c6f85edae7670b53fff0371a66fbbf52de2ada Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Sat, 14 Dec 2024 18:33:18 +0200 Subject: [PATCH 02/43] Add CommitInfoService interface for fetching Git info Introduce a new interface to retrieve Git commit details, providing a method to fetch the latest commit hash and date. --- .../java/greencity/service/CommitInfoService.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 service-api/src/main/java/greencity/service/CommitInfoService.java diff --git a/service-api/src/main/java/greencity/service/CommitInfoService.java b/service-api/src/main/java/greencity/service/CommitInfoService.java new file mode 100644 index 000000000..5a9957fe8 --- /dev/null +++ b/service-api/src/main/java/greencity/service/CommitInfoService.java @@ -0,0 +1,15 @@ +package greencity.service; + +import greencity.dto.commitinfo.CommitInfoDto; + +/** + * Interface for fetching Git commit information. + */ +public interface CommitInfoService { + /** + * Fetches the latest Git commit hash and date. + * + * @return {@link CommitInfoDto}, either success or error + */ + CommitInfoDto getLatestCommitInfo(); +} From 03f668c8aa574dab86e7e8f0aba3d507a54917b3 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Mon, 16 Dec 2024 10:51:21 +0200 Subject: [PATCH 03/43] Fix: Conflict resolution between Lombok annotations The conflict between the @AllArgsConstructor and @NoArgsConstructor annotations has been resolved. These annotations were conflicting because the CommitInfoDto class lacks any fields. Consequently, they attempted to create a duplicate constructor. --- .../src/main/java/greencity/dto/commitinfo/CommitInfoDto.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java index 8ec473d1d..7b46b03f8 100644 --- a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java @@ -1,6 +1,5 @@ package greencity.dto.commitinfo; -import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -8,7 +7,6 @@ * Base class for commit information DTO. */ @Data -@AllArgsConstructor @NoArgsConstructor public abstract class CommitInfoDto { } From d6e70c46b39857846f3f13ca9756c39448ab3d6f Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Mon, 16 Dec 2024 10:55:37 +0200 Subject: [PATCH 04/43] Add: The implementation of the CommitInfoService interface Utilized the JGit library to retrieve commit information. Implemented the CommitInfoServiceImpl and conducted comprehensive testing to ensure its functionality. --- pom.xml | 1 + service/pom.xml | 5 + .../service/CommitInfoServiceImpl.java | 55 +++++++ .../service/CommitInfoServiceImplTest.java | 143 ++++++++++++++++++ 4 files changed, 204 insertions(+) create mode 100644 service/src/main/java/greencity/service/CommitInfoServiceImpl.java create mode 100644 service/src/test/java/greencity/service/CommitInfoServiceImplTest.java diff --git a/pom.xml b/pom.xml index 633d93730..2003b4693 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,7 @@ 0.12.3 4.4 3.13.0 + 7.1.0.202411261347-r diff --git a/service/pom.xml b/service/pom.xml index 44be44871..9a4346295 100644 --- a/service/pom.xml +++ b/service/pom.xml @@ -123,5 +123,10 @@ commons-collections4 ${commons.collections4.version} + + org.eclipse.jgit + org.eclipse.jgit + ${org.eclipse.jgit.version} + diff --git a/service/src/main/java/greencity/service/CommitInfoServiceImpl.java b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java new file mode 100644 index 000000000..2cac3f6f4 --- /dev/null +++ b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java @@ -0,0 +1,55 @@ +package greencity.service; + +import greencity.constant.AppConstant; +import greencity.dto.commitinfo.CommitInfoDto; +import greencity.dto.commitinfo.CommitInfoErrorDto; +import greencity.dto.commitinfo.CommitInfoSuccessDto; +import jakarta.annotation.PostConstruct; +import java.io.File; +import java.io.IOException; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.springframework.stereotype.Service; + +/** + * Service implementation for fetching Git commit information. + */ +@Service +public class CommitInfoServiceImpl implements CommitInfoService { + private Repository repository; + + @PostConstruct + private void init() { + try { + repository = new FileRepositoryBuilder() + .setGitDir(new File(".git")) + .readEnvironment() + .findGitDir() + .build(); + } catch (IOException e) { + throw new IllegalStateException("Failed to initialize repository", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public CommitInfoDto getLatestCommitInfo() { + try (RevWalk revWalk = new RevWalk(repository)) { + RevCommit latestCommit = revWalk.parseCommit(repository.resolve("HEAD")); + String latestCommitHash = latestCommit.name(); + String latestCommitDate = DateTimeFormatter.ofPattern(AppConstant.DATE_FORMAT) + .withZone(ZoneId.of(AppConstant.UKRAINE_TIMEZONE)) + .format(latestCommit.getAuthorIdent().getWhenAsInstant()); + + return new CommitInfoSuccessDto(latestCommitHash, latestCommitDate); + } catch (IOException e) { + return new CommitInfoErrorDto("Failed to fetch commit info due to I/O error: " + e.getMessage()); + } + } +} diff --git a/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java new file mode 100644 index 000000000..50cd119ee --- /dev/null +++ b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java @@ -0,0 +1,143 @@ +package greencity.service; + +import greencity.constant.AppConstant; +import greencity.dto.commitinfo.CommitInfoDto; +import greencity.dto.commitinfo.CommitInfoErrorDto; +import greencity.dto.commitinfo.CommitInfoSuccessDto; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedConstruction; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class CommitInfoServiceImplTest { + @InjectMocks + private CommitInfoServiceImpl commitInfoService; + + @Mock + private Repository repository; + + @Mock + private RevCommit revCommit; + + @Mock + private ObjectId objectId; + + @Mock + private PersonIdent personIdent; + + private static final String COMMIT_HASH = "abc123"; + + @Test + void postConstructInitSuccessTest() throws Exception { + try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, + (builderMock, context) -> { + when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); + when(builderMock.readEnvironment()).thenReturn(builderMock); + when(builderMock.findGitDir()).thenReturn(builderMock); + when(builderMock.build()).thenReturn(repository); + })) { + + var initMethod = CommitInfoServiceImpl.class.getDeclaredMethod("init"); + initMethod.setAccessible(true); + + assertDoesNotThrow(() -> initMethod.invoke(commitInfoService)); + + var repositoryField = CommitInfoServiceImpl.class.getDeclaredField("repository"); + repositoryField.setAccessible(true); + Repository initializedRepository = (Repository) repositoryField.get(commitInfoService); + + assertNotNull(initializedRepository); + } + } + + @Test + void postConstructInitFailureThrowsExceptionTest() { + try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, + (builderMock, context) -> { + when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); + when(builderMock.readEnvironment()).thenReturn(builderMock); + when(builderMock.findGitDir()).thenReturn(builderMock); + when(builderMock.build()).thenThrow(new IOException("Repository not found")); + })) { + InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> { + var initMethod = CommitInfoServiceImpl.class.getDeclaredMethod("init"); + initMethod.setAccessible(true); + initMethod.invoke(commitInfoService); + }); + + Throwable cause = exception.getCause(); + assertInstanceOf(IllegalStateException.class, cause); + assertEquals("Failed to initialize repository", cause.getMessage()); + } + } + + @Test + void getLatestCommitInfoWithValidDataReturnsSuccessDtoTest() throws IOException { + when(repository.resolve("HEAD")).thenReturn(objectId); + when(revCommit.name()).thenReturn(COMMIT_HASH); + when(revCommit.getAuthorIdent()).thenReturn(personIdent); + + Instant expectedDate = Instant.parse("2024-12-14T16:30:00Z"); + when(personIdent.getWhenAsInstant()).thenReturn(expectedDate); + + try ( + MockedConstruction ignored = mockConstruction(RevWalk.class, + (revWalkMock, context) -> when(revWalkMock.parseCommit(objectId)).thenReturn(revCommit))) { + CommitInfoDto actualDto = commitInfoService.getLatestCommitInfo(); + + assertInstanceOf(CommitInfoSuccessDto.class, actualDto); + CommitInfoSuccessDto successDto = (CommitInfoSuccessDto) actualDto; + assertEquals(COMMIT_HASH, successDto.getCommitHash()); + + String latestCommitDate = DateTimeFormatter.ofPattern(AppConstant.DATE_FORMAT) + .withZone(ZoneId.of(AppConstant.UKRAINE_TIMEZONE)) + .format(expectedDate); + assertEquals(latestCommitDate, successDto.getCommitDate()); + } + } + + @Test + void getLatestCommitInfoWhenRepositoryResolveThrowsIOExceptionReturnsErrorDtoTest() throws IOException { + when(repository.resolve("HEAD")).thenThrow(new IOException("Test I/O exception")); + + CommitInfoDto actualDto = commitInfoService.getLatestCommitInfo(); + assertInstanceOf(CommitInfoErrorDto.class, actualDto); + + CommitInfoErrorDto errorDto = (CommitInfoErrorDto) actualDto; + assertEquals("Failed to fetch commit info due to I/O error: Test I/O exception", errorDto.getError()); + } + + @Test + void getLatestCommitInfoWhenRevWalkParseCommitThrowsIOExceptionReturnsErrorDtoTest() throws IOException { + when(repository.resolve("HEAD")).thenReturn(objectId); + + try ( + MockedConstruction ignored = mockConstruction(RevWalk.class, + (revWalkMock, context) -> when(revWalkMock.parseCommit(objectId)).thenThrow( + new IOException("Missing object")))) { + CommitInfoDto actualDto = commitInfoService.getLatestCommitInfo(); + assertInstanceOf(CommitInfoErrorDto.class, actualDto); + + CommitInfoErrorDto errorDto = (CommitInfoErrorDto) actualDto; + assertEquals("Failed to fetch commit info due to I/O error: Missing object", errorDto.getError()); + } + } +} From 44a93026c620809101ce017b8f07725fb24f2def Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Mon, 16 Dec 2024 21:10:12 +0200 Subject: [PATCH 05/43] Add CommitInfoController with endpoint and unit tests Introduced a new REST controller `CommitInfoController` to fetch the latest Git commit hash and date. Added corresponding unit tests in `CommitInfoControllerTest` and updated `HttpStatuses` with `INTERNAL_SERVER_ERROR` for response descriptions. --- .../java/greencity/constant/HttpStatuses.java | 1 + .../controller/CommitInfoController.java | 61 +++++++++++++++++ .../controller/CommitInfoControllerTest.java | 66 +++++++++++++++++++ 3 files changed, 128 insertions(+) create mode 100644 core/src/main/java/greencity/controller/CommitInfoController.java create mode 100644 core/src/test/java/greencity/controller/CommitInfoControllerTest.java diff --git a/core/src/main/java/greencity/constant/HttpStatuses.java b/core/src/main/java/greencity/constant/HttpStatuses.java index f0ddc595b..08a3d97ee 100644 --- a/core/src/main/java/greencity/constant/HttpStatuses.java +++ b/core/src/main/java/greencity/constant/HttpStatuses.java @@ -12,4 +12,5 @@ public class HttpStatuses { public static final String NOT_FOUND = "Not Found"; public static final String SEE_OTHER = "See Other"; public static final String FOUND = "Found"; + public static final String INTERNAL_SERVER_ERROR = "Internal Server Error"; } diff --git a/core/src/main/java/greencity/controller/CommitInfoController.java b/core/src/main/java/greencity/controller/CommitInfoController.java new file mode 100644 index 000000000..b9ce99d10 --- /dev/null +++ b/core/src/main/java/greencity/controller/CommitInfoController.java @@ -0,0 +1,61 @@ +package greencity.controller; + +import greencity.constant.HttpStatuses; +import greencity.dto.commitinfo.CommitInfoDto; +import greencity.dto.commitinfo.CommitInfoErrorDto; +import greencity.service.CommitInfoService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * Controller for fetching Git commit information. + */ +@RestController +@RequestMapping("/commit-info") +@RequiredArgsConstructor +public class CommitInfoController { + private final CommitInfoService commitInfoService; + + /** + * Endpoint to fetch the latest Git commit hash and date. + * + * @return {@link CommitInfoDto}, either success or error + */ + @Operation(summary = "Get the latest commit hash and date.") + @ApiResponse( + responseCode = "200", + description = HttpStatuses.OK, + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "commitHash": "d6e70c46b39857846f3f13ca9756c39448ab3d6f", + "commitDate": "16/12/2024 10:55:00" + }"""))) + @ApiResponse( + responseCode = "500", + description = HttpStatuses.INTERNAL_SERVER_ERROR, + content = @Content( + mediaType = "application/json", + examples = @ExampleObject( + value = """ + { + "error": "Failed to fetch commit info: Repository not found" + }"""))) + @GetMapping + public ResponseEntity getCommitInfo() { + CommitInfoDto commitInfo = commitInfoService.getLatestCommitInfo(); + if (commitInfo instanceof CommitInfoErrorDto) { + return ResponseEntity.internalServerError().body(commitInfo); + } + return ResponseEntity.ok(commitInfo); + } +} diff --git a/core/src/test/java/greencity/controller/CommitInfoControllerTest.java b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java new file mode 100644 index 000000000..5fb8d0bb3 --- /dev/null +++ b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java @@ -0,0 +1,66 @@ +package greencity.controller; + +import greencity.dto.commitinfo.CommitInfoDto; +import greencity.dto.commitinfo.CommitInfoErrorDto; +import greencity.dto.commitinfo.CommitInfoSuccessDto; +import greencity.service.CommitInfoService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import static org.mockito.Mockito.*; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@ExtendWith(MockitoExtension.class) +class CommitInfoControllerTest { + @InjectMocks + private CommitInfoController commitInfoController; + + @Mock + private CommitInfoService commitInfoService; + + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders.standaloneSetup(commitInfoController).build(); + } + + private static final String COMMIT_INFO_URL = "/commit-info"; + private static final String COMMIT_HASH = "abc123"; + private static final String COMMIT_DATE = "16/12/2024 12:06:32"; + private static final String ERROR_MESSAGE = "Failed to fetch commit info due to I/O error: Missing object"; + + @Test + void getCommitInfoReturnsSuccessTest() throws Exception { + CommitInfoDto commitInfoDto = new CommitInfoSuccessDto(COMMIT_HASH, COMMIT_DATE); + when(commitInfoService.getLatestCommitInfo()).thenReturn(commitInfoDto); + + mockMvc.perform(get(COMMIT_INFO_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.commitHash").value(COMMIT_HASH)) + .andExpect(jsonPath("$.commitDate").value(COMMIT_DATE)); + + verify(commitInfoService, times(1)).getLatestCommitInfo(); + } + + @Test + void getCommitInfoReturnsErrorTest() throws Exception { + CommitInfoDto commitInfoDto = new CommitInfoErrorDto(ERROR_MESSAGE); + when(commitInfoService.getLatestCommitInfo()).thenReturn(commitInfoDto); + + mockMvc.perform(get(COMMIT_INFO_URL).accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.error").value(ERROR_MESSAGE)); + + verify(commitInfoService, times(1)).getLatestCommitInfo(); + } +} From 4195e37b628ebab2a5a061e9b131a063e6e03285 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Mon, 16 Dec 2024 22:00:31 +0200 Subject: [PATCH 06/43] Refactor test imports and assertions for clarity. Updated imports in test classes to improve readability and maintain consistency with Mockito and JUnit usage. Consolidated and explicitly listed static imports for assertions and Mockito functions. No logic changes were made to the tests themselves. --- .../greencity/controller/CommitInfoControllerTest.java | 9 ++++++--- .../greencity/service/CommitInfoServiceImplTest.java | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/core/src/test/java/greencity/controller/CommitInfoControllerTest.java b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java index 5fb8d0bb3..fa6bbcb77 100644 --- a/core/src/test/java/greencity/controller/CommitInfoControllerTest.java +++ b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java @@ -9,14 +9,17 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; -import static org.mockito.Mockito.*; + import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @ExtendWith(MockitoExtension.class) diff --git a/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java index 50cd119ee..7ad5261ad 100644 --- a/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java +++ b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java @@ -16,15 +16,20 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedConstruction; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.when; -import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class CommitInfoServiceImplTest { From 384c6855142151ea7e468ff10f02a178d299116f Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Tue, 17 Dec 2024 20:49:44 +0200 Subject: [PATCH 07/43] Refactor CommitInfoDto hierarchy to use interface. Replaced the abstract class CommitInfoDto with an interface for improved flexibility and clarity. Updated related DTOs (CommitInfoErrorDto and CommitInfoSuccessDto) to implement the interface instead of extending the abstract class. This aligns with better design principles and reduces unnecessary coupling. --- .../java/greencity/dto/commitinfo/CommitInfoDto.java | 11 +++-------- .../greencity/dto/commitinfo/CommitInfoErrorDto.java | 4 ++-- .../dto/commitinfo/CommitInfoSuccessDto.java | 4 ++-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java index 7b46b03f8..3475d9a80 100644 --- a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java @@ -1,12 +1,7 @@ package greencity.dto.commitinfo; -import lombok.Data; -import lombok.NoArgsConstructor; - /** - * Base class for commit information DTO. + * Base interface for commit information DTO. */ -@Data -@NoArgsConstructor -public abstract class CommitInfoDto { -} +public interface CommitInfoDto { +} \ No newline at end of file diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java index 98ae67d36..6988f360d 100644 --- a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java @@ -8,10 +8,10 @@ /** * DTO for error response. */ -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode @Data @AllArgsConstructor @NoArgsConstructor -public class CommitInfoErrorDto extends CommitInfoDto { +public class CommitInfoErrorDto implements CommitInfoDto { private String error; } diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java index a5c79fc86..9a0833453 100644 --- a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java @@ -8,11 +8,11 @@ /** * DTO for successful commit information response. */ -@EqualsAndHashCode(callSuper = true) +@EqualsAndHashCode @Data @AllArgsConstructor @NoArgsConstructor -public class CommitInfoSuccessDto extends CommitInfoDto { +public class CommitInfoSuccessDto implements CommitInfoDto { private String commitHash; private String commitDate; } From 2d2e3e99f9e3c5831018ff2235053e3864d20f90 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Tue, 17 Dec 2024 20:54:14 +0200 Subject: [PATCH 08/43] Refactor commit reference to use a constant. Replaced the hardcoded "HEAD" with a static constant COMMIT_REF for better readability and maintainability. This ensures consistency in resolving the latest commit reference across the service. --- .../main/java/greencity/service/CommitInfoServiceImpl.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/service/src/main/java/greencity/service/CommitInfoServiceImpl.java b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java index 2cac3f6f4..820e4cc49 100644 --- a/service/src/main/java/greencity/service/CommitInfoServiceImpl.java +++ b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java @@ -22,6 +22,8 @@ public class CommitInfoServiceImpl implements CommitInfoService { private Repository repository; + private static final String COMMIT_REF = "HEAD"; + @PostConstruct private void init() { try { @@ -41,7 +43,7 @@ private void init() { @Override public CommitInfoDto getLatestCommitInfo() { try (RevWalk revWalk = new RevWalk(repository)) { - RevCommit latestCommit = revWalk.parseCommit(repository.resolve("HEAD")); + RevCommit latestCommit = revWalk.parseCommit(repository.resolve(COMMIT_REF)); String latestCommitHash = latestCommit.name(); String latestCommitDate = DateTimeFormatter.ofPattern(AppConstant.DATE_FORMAT) .withZone(ZoneId.of(AppConstant.UKRAINE_TIMEZONE)) From df5ff1cbf4422525abf6f7bccc351da8d4d0e1b6 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Tue, 17 Dec 2024 21:23:08 +0200 Subject: [PATCH 09/43] Refactor repository initialization in CommitInfoService Replaced @PostConstruct initialization with constructor-based logic to improve clarity and simplify error handling. Added logging for missing `.git` directory and updated related tests to handle uninitialized repository scenarios. Enhanced `getLatestCommitInfo` with proper error handling when the repository is null. --- .../service/CommitInfoServiceImpl.java | 15 ++++-- .../service/CommitInfoServiceImplTest.java | 49 ++++++++++++------- 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/service/src/main/java/greencity/service/CommitInfoServiceImpl.java b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java index 820e4cc49..039d3cf17 100644 --- a/service/src/main/java/greencity/service/CommitInfoServiceImpl.java +++ b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java @@ -4,7 +4,6 @@ import greencity.dto.commitinfo.CommitInfoDto; import greencity.dto.commitinfo.CommitInfoErrorDto; import greencity.dto.commitinfo.CommitInfoSuccessDto; -import jakarta.annotation.PostConstruct; import java.io.File; import java.io.IOException; import java.time.ZoneId; @@ -13,6 +12,8 @@ import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** @@ -24,8 +25,9 @@ public class CommitInfoServiceImpl implements CommitInfoService { private static final String COMMIT_REF = "HEAD"; - @PostConstruct - private void init() { + private static final Logger log = LoggerFactory.getLogger(CommitInfoServiceImpl.class); + + public CommitInfoServiceImpl() { try { repository = new FileRepositoryBuilder() .setGitDir(new File(".git")) @@ -33,7 +35,8 @@ private void init() { .findGitDir() .build(); } catch (IOException e) { - throw new IllegalStateException("Failed to initialize repository", e); + repository = null; + log.warn("WARNING: .git directory not found. Git commit info will be unavailable."); } } @@ -42,6 +45,10 @@ private void init() { */ @Override public CommitInfoDto getLatestCommitInfo() { + if (repository == null) { + return new CommitInfoErrorDto("Git repository not initialized. Commit info is unavailable."); + } + try (RevWalk revWalk = new RevWalk(repository)) { RevCommit latestCommit = revWalk.parseCommit(repository.resolve(COMMIT_REF)); String latestCommitHash = latestCommit.name(); diff --git a/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java index 7ad5261ad..495ae3a1e 100644 --- a/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java +++ b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java @@ -6,7 +6,6 @@ import greencity.dto.commitinfo.CommitInfoSuccessDto; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -23,11 +22,10 @@ import org.mockito.MockedConstruction; import org.mockito.junit.jupiter.MockitoExtension; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.when; @@ -51,7 +49,7 @@ class CommitInfoServiceImplTest { private static final String COMMIT_HASH = "abc123"; @Test - void postConstructInitSuccessTest() throws Exception { + void constructorInitializationSuccessTest() throws NoSuchFieldException, IllegalAccessException { try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, (builderMock, context) -> { when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); @@ -59,22 +57,37 @@ void postConstructInitSuccessTest() throws Exception { when(builderMock.findGitDir()).thenReturn(builderMock); when(builderMock.build()).thenReturn(repository); })) { + CommitInfoServiceImpl service = new CommitInfoServiceImpl(); - var initMethod = CommitInfoServiceImpl.class.getDeclaredMethod("init"); - initMethod.setAccessible(true); + var repositoryField = CommitInfoServiceImpl.class.getDeclaredField("repository"); + repositoryField.setAccessible(true); + Repository initializedRepository = (Repository) repositoryField.get(service); - assertDoesNotThrow(() -> initMethod.invoke(commitInfoService)); + assertNotNull(initializedRepository); + } + } + + @Test + void constructorInitializationFailureTest() throws NoSuchFieldException, IllegalAccessException { + try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, + (builderMock, context) -> { + when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); + when(builderMock.readEnvironment()).thenReturn(builderMock); + when(builderMock.findGitDir()).thenReturn(builderMock); + when(builderMock.build()).thenThrow(new IOException("Repository not found")); + })) { + CommitInfoServiceImpl service = new CommitInfoServiceImpl(); var repositoryField = CommitInfoServiceImpl.class.getDeclaredField("repository"); repositoryField.setAccessible(true); - Repository initializedRepository = (Repository) repositoryField.get(commitInfoService); + Repository initializedRepository = (Repository) repositoryField.get(service); - assertNotNull(initializedRepository); + assertNull(initializedRepository); } } @Test - void postConstructInitFailureThrowsExceptionTest() { + void getLatestCommitInfoWhenRepositoryNotInitializedReturnsErrorDtoTest() { try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, (builderMock, context) -> { when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); @@ -82,15 +95,13 @@ void postConstructInitFailureThrowsExceptionTest() { when(builderMock.findGitDir()).thenReturn(builderMock); when(builderMock.build()).thenThrow(new IOException("Repository not found")); })) { - InvocationTargetException exception = assertThrows(InvocationTargetException.class, () -> { - var initMethod = CommitInfoServiceImpl.class.getDeclaredMethod("init"); - initMethod.setAccessible(true); - initMethod.invoke(commitInfoService); - }); - - Throwable cause = exception.getCause(); - assertInstanceOf(IllegalStateException.class, cause); - assertEquals("Failed to initialize repository", cause.getMessage()); + CommitInfoServiceImpl service = new CommitInfoServiceImpl(); + CommitInfoDto actualDto = service.getLatestCommitInfo(); + + assertInstanceOf(CommitInfoErrorDto.class, actualDto); + + CommitInfoErrorDto errorDto = (CommitInfoErrorDto) actualDto; + assertEquals("Git repository not initialized. Commit info is unavailable.", errorDto.getError()); } } From 51dcab947cc22ae57fc4998cc39cf55cf4efcf4a Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Tue, 17 Dec 2024 21:28:35 +0200 Subject: [PATCH 10/43] Update error status from 500 to 404 for commit info retrieval Changed the HTTP response code and description for cases where commit info is absent, aligning with proper REST conventions. Updated tests, API documentation, and removed unused constant `INTERNAL_SERVER_ERROR` accordingly. --- core/src/main/java/greencity/constant/HttpStatuses.java | 1 - .../java/greencity/controller/CommitInfoController.java | 9 +++++---- .../greencity/controller/CommitInfoControllerTest.java | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/main/java/greencity/constant/HttpStatuses.java b/core/src/main/java/greencity/constant/HttpStatuses.java index 08a3d97ee..f0ddc595b 100644 --- a/core/src/main/java/greencity/constant/HttpStatuses.java +++ b/core/src/main/java/greencity/constant/HttpStatuses.java @@ -12,5 +12,4 @@ public class HttpStatuses { public static final String NOT_FOUND = "Not Found"; public static final String SEE_OTHER = "See Other"; public static final String FOUND = "Found"; - public static final String INTERNAL_SERVER_ERROR = "Internal Server Error"; } diff --git a/core/src/main/java/greencity/controller/CommitInfoController.java b/core/src/main/java/greencity/controller/CommitInfoController.java index b9ce99d10..bc598974c 100644 --- a/core/src/main/java/greencity/controller/CommitInfoController.java +++ b/core/src/main/java/greencity/controller/CommitInfoController.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -41,20 +42,20 @@ public class CommitInfoController { "commitDate": "16/12/2024 10:55:00" }"""))) @ApiResponse( - responseCode = "500", - description = HttpStatuses.INTERNAL_SERVER_ERROR, + responseCode = "404", + description = HttpStatuses.NOT_FOUND, content = @Content( mediaType = "application/json", examples = @ExampleObject( value = """ { - "error": "Failed to fetch commit info: Repository not found" + "error": "Git repository not initialized. Commit info is unavailable." }"""))) @GetMapping public ResponseEntity getCommitInfo() { CommitInfoDto commitInfo = commitInfoService.getLatestCommitInfo(); if (commitInfo instanceof CommitInfoErrorDto) { - return ResponseEntity.internalServerError().body(commitInfo); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(commitInfo); } return ResponseEntity.ok(commitInfo); } diff --git a/core/src/test/java/greencity/controller/CommitInfoControllerTest.java b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java index fa6bbcb77..0ed5b0c02 100644 --- a/core/src/test/java/greencity/controller/CommitInfoControllerTest.java +++ b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java @@ -61,7 +61,7 @@ void getCommitInfoReturnsErrorTest() throws Exception { when(commitInfoService.getLatestCommitInfo()).thenReturn(commitInfoDto); mockMvc.perform(get(COMMIT_INFO_URL).accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isInternalServerError()) + .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value(ERROR_MESSAGE)); verify(commitInfoService, times(1)).getLatestCommitInfo(); From 636bcbfc1961be6bbf8933832e0f3f897090de27 Mon Sep 17 00:00:00 2001 From: Warded120 Date: Thu, 19 Dec 2024 16:08:07 +0200 Subject: [PATCH 11/43] refacored friends search by name, city and credo --- .../java/greencity/repository/UserRepo.java | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/dao/src/main/java/greencity/repository/UserRepo.java b/dao/src/main/java/greencity/repository/UserRepo.java index 3e12d334c..30f10eb56 100644 --- a/dao/src/main/java/greencity/repository/UserRepo.java +++ b/dao/src/main/java/greencity/repository/UserRepo.java @@ -339,16 +339,31 @@ public interface UserRepo extends JpaRepository, JpaSpecificationExe * @param pageable current page. * @return {@link Page} of {@link User}. */ - @Query(nativeQuery = true, value = "SELECT * FROM users u " - + "WHERE u.id != :userId " - + "AND u.id NOT IN (" - + " SELECT user_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'FRIEND' " - + " UNION " - + " SELECT friend_id AS id FROM users_friends WHERE user_id = :userId AND status = 'FRIEND' " - + " UNION " - + " SELECT friend_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'REQUEST' " - + ") AND LOWER(u.name) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), " - + "'%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) ") + @Query( + nativeQuery = true, + value = """ + SELECT * + FROM users u + WHERE u.id != :userId + AND u.id NOT IN ( + SELECT user_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'FRIEND' + UNION + SELECT friend_id AS id FROM users_friends WHERE user_id = :userId AND status = 'FRIEND' + UNION + SELECT friend_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'REQUEST' + ) + AND ( + LOWER(u.name) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + OR LOWER(u.user_credo) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND LOWER(ul.city_en) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + ) + ) + """) + Page getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(Long userId, String filteringName, Pageable pageable); @@ -390,12 +405,25 @@ Page getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(Long userI * @param pageable current page. * @return {@link Page} of {@link User}. */ - @Query(nativeQuery = true, value = "SELECT * FROM users u " - + "WHERE u.id IN (" - + " SELECT user_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'FRIEND' " - + " UNION " - + " SELECT friend_id AS id FROM users_friends WHERE user_id = :userId AND status = 'FRIEND' " - + ") AND LOWER(u.name) LIKE LOWER(CONCAT('%', :filteringName, '%'))") + @Query(nativeQuery = true, + value = """ + SELECT * FROM users u + WHERE u.id IN ( + SELECT user_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'FRIEND' + UNION + SELECT friend_id AS id FROM users_friends WHERE user_id = :userId AND status = 'FRIEND' + ) + AND ( + LOWER(u.name) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + OR LOWER(u.user_credo) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND LOWER(ul.city_en) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + ) + ) + """) Page findAllFriendsOfUser(Long userId, String filteringName, Pageable pageable); /** From 5de5652a8f074284500ec3e984303e26b2164842 Mon Sep 17 00:00:00 2001 From: Warded120 Date: Thu, 19 Dec 2024 16:51:45 +0200 Subject: [PATCH 12/43] refactored queries --- .../java/greencity/repository/UserRepo.java | 183 ++++++++++++++---- 1 file changed, 142 insertions(+), 41 deletions(-) diff --git a/dao/src/main/java/greencity/repository/UserRepo.java b/dao/src/main/java/greencity/repository/UserRepo.java index 30f10eb56..d42c4676d 100644 --- a/dao/src/main/java/greencity/repository/UserRepo.java +++ b/dao/src/main/java/greencity/repository/UserRepo.java @@ -320,14 +320,57 @@ public interface UserRepo extends JpaRepository, JpaSpecificationExe * @param pageable current page. * @return {@link Page} of {@link User}. */ - @Query(nativeQuery = true, value = "SELECT * FROM users u " - + "WHERE u.id != :userId " - + "AND u.id NOT IN (" - + " SELECT user_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'FRIEND' " - + " UNION " - + " SELECT friend_id AS id FROM users_friends WHERE user_id = :userId AND status = 'FRIEND' " - + ") AND LOWER(u.name) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), " - + "'%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) ") + @Query(nativeQuery = true, + value = """ + SELECT * + FROM users u + WHERE u.id != :userId + AND u.id NOT IN ( + SELECT user_id AS id + FROM users_friends + WHERE friend_id = :userId + AND status = 'FRIEND' + UNION + SELECT friend_id AS id + FROM users_friends + WHERE user_id = :userId + AND status = 'FRIEND' + ) + AND ( + LOWER(u.name) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE(REPLACE( + REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + ) + OR LOWER(u.user_credo) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE(REPLACE( + REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + ) + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND LOWER(ul.city_en) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE(REPLACE( + REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + ) + ) + """) + Page getAllUsersExceptMainUserAndFriends(Long userId, String filteringName, Pageable pageable); /** @@ -339,29 +382,55 @@ public interface UserRepo extends JpaRepository, JpaSpecificationExe * @param pageable current page. * @return {@link Page} of {@link User}. */ - @Query( - nativeQuery = true, + @Query(nativeQuery = true, value = """ - SELECT * - FROM users u - WHERE u.id != :userId - AND u.id NOT IN ( - SELECT user_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'FRIEND' - UNION - SELECT friend_id AS id FROM users_friends WHERE user_id = :userId AND status = 'FRIEND' - UNION - SELECT friend_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'REQUEST' + SELECT * + FROM users u + WHERE u.id != :userId + AND u.id NOT IN ( + SELECT user_id AS id + FROM users_friends + WHERE friend_id = :userId + AND status = 'FRIEND' + UNION + SELECT friend_id AS id + FROM users_friends + WHERE user_id = :userId + AND status = 'FRIEND' + ) + AND ( + LOWER(u.name) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) ) - AND ( - LOWER(u.name) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) - OR LOWER(u.user_credo) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) - OR EXISTS ( - SELECT 1 - FROM user_location ul - WHERE ul.id = u.user_location - AND LOWER(ul.city_en) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + OR LOWER(u.user_credo) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') ) ) + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND LOWER(ul.city_en) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + ) + ) """) Page getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(Long userId, String filteringName, @@ -407,23 +476,55 @@ Page getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(Long userI */ @Query(nativeQuery = true, value = """ - SELECT * FROM users u - WHERE u.id IN ( - SELECT user_id AS id FROM users_friends WHERE friend_id = :userId AND status = 'FRIEND' + SELECT * + FROM users u + WHERE u.id != :userId + AND u.id NOT IN ( + SELECT user_id AS id + FROM users_friends + WHERE friend_id = :userId + AND status = 'FRIEND' UNION - SELECT friend_id AS id FROM users_friends WHERE user_id = :userId AND status = 'FRIEND' - ) - AND ( - LOWER(u.name) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) - OR LOWER(u.user_credo) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) - OR EXISTS ( - SELECT 1 - FROM user_location ul - WHERE ul.id = u.user_location - AND LOWER(ul.city_en) LIKE LOWER(CONCAT('%', REPLACE(REPLACE(REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), '_', '\\_'), '#', '\\#'), '%')) + SELECT friend_id AS id + FROM users_friends + WHERE user_id = :userId + AND status = 'FRIEND' + ) + AND ( + LOWER(u.name) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + ) + OR LOWER(u.user_credo) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + ) + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND LOWER(ul.city_en) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + ) ) - ) """) + Page findAllFriendsOfUser(Long userId, String filteringName, Pageable pageable); /** From 560ecbc5dc23e455e92c48ea2f44c9cde13d5494 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Thu, 19 Dec 2024 17:29:41 +0200 Subject: [PATCH 13/43] Replaced the polymorphism on DTOs with the single DTO --- .../dto/commitinfo/CommitInfoDto.java | 20 ++++++++++++++++--- .../dto/commitinfo/CommitInfoErrorDto.java | 17 ---------------- .../dto/commitinfo/CommitInfoSuccessDto.java | 18 ----------------- 3 files changed, 17 insertions(+), 38 deletions(-) delete mode 100644 service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java delete mode 100644 service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java index 3475d9a80..596094fd5 100644 --- a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java +++ b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoDto.java @@ -1,7 +1,21 @@ package greencity.dto.commitinfo; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + /** - * Base interface for commit information DTO. + * DTO for commit information response. */ -public interface CommitInfoDto { -} \ No newline at end of file +@Data +@AllArgsConstructor +@NoArgsConstructor +@EqualsAndHashCode +public class CommitInfoDto { + @NotNull + private String commitHash; + @NotNull + private String commitDate; +} diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java deleted file mode 100644 index 6988f360d..000000000 --- a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoErrorDto.java +++ /dev/null @@ -1,17 +0,0 @@ -package greencity.dto.commitinfo; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -/** - * DTO for error response. - */ -@EqualsAndHashCode -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CommitInfoErrorDto implements CommitInfoDto { - private String error; -} diff --git a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java b/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java deleted file mode 100644 index 9a0833453..000000000 --- a/service-api/src/main/java/greencity/dto/commitinfo/CommitInfoSuccessDto.java +++ /dev/null @@ -1,18 +0,0 @@ -package greencity.dto.commitinfo; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.NoArgsConstructor; - -/** - * DTO for successful commit information response. - */ -@EqualsAndHashCode -@Data -@AllArgsConstructor -@NoArgsConstructor -public class CommitInfoSuccessDto implements CommitInfoDto { - private String commitHash; - private String commitDate; -} From 3fe473e09f3fbdbf40deb7b7692aa965c3dd3912 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Thu, 19 Dec 2024 17:44:02 +0200 Subject: [PATCH 14/43] Adjusted the Service and the Implementation to follow the new DTO structure. Also extracted the error messages in a separate file --- .../java/greencity/constant/ErrorMessage.java | 5 ++ .../greencity/service/CommitInfoService.java | 2 +- .../service/CommitInfoServiceImpl.java | 18 ++--- .../service/CommitInfoServiceImplTest.java | 76 +++++++++---------- 4 files changed, 52 insertions(+), 49 deletions(-) diff --git a/service-api/src/main/java/greencity/constant/ErrorMessage.java b/service-api/src/main/java/greencity/constant/ErrorMessage.java index 0c6729c03..76be7c3a6 100644 --- a/service-api/src/main/java/greencity/constant/ErrorMessage.java +++ b/service-api/src/main/java/greencity/constant/ErrorMessage.java @@ -216,4 +216,9 @@ public class ErrorMessage { public static final String INVALID_DURATION_BETWEEN_START_AND_FINISH = "Invalid duration between start and finish"; public static final String PAGE_NOT_FOUND_MESSAGE = "Requested page %d exceeds total pages %d."; public static final String OPEN_AI_IS_NOT_RESPONDING = "Could not get a response from OpenAI."; + public static final String WARNING_GIT_DIRECTORY_NOT_FOUND = + "WARNING: .git directory not found. Git commit info will be unavailable."; + public static final String GIT_REPOSITORY_NOT_INITIALIZED = + "Git repository not initialized. Commit info is unavailable."; + public static final String FAILED_TO_FETCH_COMMIT_INFO = "Failed to fetch commit info due to I/O error: "; } diff --git a/service-api/src/main/java/greencity/service/CommitInfoService.java b/service-api/src/main/java/greencity/service/CommitInfoService.java index 5a9957fe8..ec2c5bdc7 100644 --- a/service-api/src/main/java/greencity/service/CommitInfoService.java +++ b/service-api/src/main/java/greencity/service/CommitInfoService.java @@ -9,7 +9,7 @@ public interface CommitInfoService { /** * Fetches the latest Git commit hash and date. * - * @return {@link CommitInfoDto}, either success or error + * @return {@link CommitInfoDto} */ CommitInfoDto getLatestCommitInfo(); } diff --git a/service/src/main/java/greencity/service/CommitInfoServiceImpl.java b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java index 039d3cf17..f2566182c 100644 --- a/service/src/main/java/greencity/service/CommitInfoServiceImpl.java +++ b/service/src/main/java/greencity/service/CommitInfoServiceImpl.java @@ -1,32 +1,30 @@ package greencity.service; import greencity.constant.AppConstant; +import greencity.constant.ErrorMessage; import greencity.dto.commitinfo.CommitInfoDto; -import greencity.dto.commitinfo.CommitInfoErrorDto; -import greencity.dto.commitinfo.CommitInfoSuccessDto; +import greencity.exception.exceptions.ResourceNotFoundException; import java.io.File; import java.io.IOException; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import lombok.extern.slf4j.Slf4j; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** * Service implementation for fetching Git commit information. */ @Service +@Slf4j public class CommitInfoServiceImpl implements CommitInfoService { private Repository repository; private static final String COMMIT_REF = "HEAD"; - private static final Logger log = LoggerFactory.getLogger(CommitInfoServiceImpl.class); - public CommitInfoServiceImpl() { try { repository = new FileRepositoryBuilder() @@ -36,7 +34,7 @@ public CommitInfoServiceImpl() { .build(); } catch (IOException e) { repository = null; - log.warn("WARNING: .git directory not found. Git commit info will be unavailable."); + log.warn(ErrorMessage.WARNING_GIT_DIRECTORY_NOT_FOUND); } } @@ -46,7 +44,7 @@ public CommitInfoServiceImpl() { @Override public CommitInfoDto getLatestCommitInfo() { if (repository == null) { - return new CommitInfoErrorDto("Git repository not initialized. Commit info is unavailable."); + throw new ResourceNotFoundException(ErrorMessage.GIT_REPOSITORY_NOT_INITIALIZED); } try (RevWalk revWalk = new RevWalk(repository)) { @@ -56,9 +54,9 @@ public CommitInfoDto getLatestCommitInfo() { .withZone(ZoneId.of(AppConstant.UKRAINE_TIMEZONE)) .format(latestCommit.getAuthorIdent().getWhenAsInstant()); - return new CommitInfoSuccessDto(latestCommitHash, latestCommitDate); + return new CommitInfoDto(latestCommitHash, latestCommitDate); } catch (IOException e) { - return new CommitInfoErrorDto("Failed to fetch commit info due to I/O error: " + e.getMessage()); + throw new ResourceNotFoundException(ErrorMessage.FAILED_TO_FETCH_COMMIT_INFO + e.getMessage()); } } } diff --git a/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java index 495ae3a1e..ed2925d70 100644 --- a/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java +++ b/service/src/test/java/greencity/service/CommitInfoServiceImplTest.java @@ -1,9 +1,9 @@ package greencity.service; import greencity.constant.AppConstant; +import greencity.constant.ErrorMessage; import greencity.dto.commitinfo.CommitInfoDto; -import greencity.dto.commitinfo.CommitInfoErrorDto; -import greencity.dto.commitinfo.CommitInfoSuccessDto; +import greencity.exception.exceptions.ResourceNotFoundException; import java.io.File; import java.io.IOException; import java.time.Instant; @@ -23,9 +23,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mockConstruction; import static org.mockito.Mockito.when; @@ -47,19 +47,26 @@ class CommitInfoServiceImplTest { private PersonIdent personIdent; private static final String COMMIT_HASH = "abc123"; + private static final String COMMIT_REF = "HEAD"; + private static final String GIT_PATH = ".git"; + private static final String REPOSITORY_FIELD = "repository"; + + private void configureFileRepositoryBuilderMock(FileRepositoryBuilder builderMock) { + when(builderMock.setGitDir(new File(GIT_PATH))).thenReturn(builderMock); + when(builderMock.readEnvironment()).thenReturn(builderMock); + when(builderMock.findGitDir()).thenReturn(builderMock); + } @Test void constructorInitializationSuccessTest() throws NoSuchFieldException, IllegalAccessException { try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, (builderMock, context) -> { - when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); - when(builderMock.readEnvironment()).thenReturn(builderMock); - when(builderMock.findGitDir()).thenReturn(builderMock); + configureFileRepositoryBuilderMock(builderMock); when(builderMock.build()).thenReturn(repository); })) { CommitInfoServiceImpl service = new CommitInfoServiceImpl(); - var repositoryField = CommitInfoServiceImpl.class.getDeclaredField("repository"); + var repositoryField = CommitInfoServiceImpl.class.getDeclaredField(REPOSITORY_FIELD); repositoryField.setAccessible(true); Repository initializedRepository = (Repository) repositoryField.get(service); @@ -71,14 +78,12 @@ void constructorInitializationSuccessTest() throws NoSuchFieldException, Illegal void constructorInitializationFailureTest() throws NoSuchFieldException, IllegalAccessException { try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, (builderMock, context) -> { - when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); - when(builderMock.readEnvironment()).thenReturn(builderMock); - when(builderMock.findGitDir()).thenReturn(builderMock); - when(builderMock.build()).thenThrow(new IOException("Repository not found")); + configureFileRepositoryBuilderMock(builderMock); + when(builderMock.build()).thenThrow(new IOException()); })) { CommitInfoServiceImpl service = new CommitInfoServiceImpl(); - var repositoryField = CommitInfoServiceImpl.class.getDeclaredField("repository"); + var repositoryField = CommitInfoServiceImpl.class.getDeclaredField(REPOSITORY_FIELD); repositoryField.setAccessible(true); Repository initializedRepository = (Repository) repositoryField.get(service); @@ -87,27 +92,23 @@ void constructorInitializationFailureTest() throws NoSuchFieldException, Illegal } @Test - void getLatestCommitInfoWhenRepositoryNotInitializedReturnsErrorDtoTest() { + void getLatestCommitInfoWhenRepositoryNotInitializedThrowsNotFoundExceptionTest() { try (MockedConstruction ignored = mockConstruction(FileRepositoryBuilder.class, (builderMock, context) -> { - when(builderMock.setGitDir(new File(".git"))).thenReturn(builderMock); - when(builderMock.readEnvironment()).thenReturn(builderMock); - when(builderMock.findGitDir()).thenReturn(builderMock); - when(builderMock.build()).thenThrow(new IOException("Repository not found")); + configureFileRepositoryBuilderMock(builderMock); + when(builderMock.build()).thenThrow(new IOException()); })) { CommitInfoServiceImpl service = new CommitInfoServiceImpl(); - CommitInfoDto actualDto = service.getLatestCommitInfo(); - - assertInstanceOf(CommitInfoErrorDto.class, actualDto); + ResourceNotFoundException notFoundException = + assertThrows(ResourceNotFoundException.class, service::getLatestCommitInfo); - CommitInfoErrorDto errorDto = (CommitInfoErrorDto) actualDto; - assertEquals("Git repository not initialized. Commit info is unavailable.", errorDto.getError()); + assertEquals(ErrorMessage.GIT_REPOSITORY_NOT_INITIALIZED, notFoundException.getMessage()); } } @Test void getLatestCommitInfoWithValidDataReturnsSuccessDtoTest() throws IOException { - when(repository.resolve("HEAD")).thenReturn(objectId); + when(repository.resolve(COMMIT_REF)).thenReturn(objectId); when(revCommit.name()).thenReturn(COMMIT_HASH); when(revCommit.getAuthorIdent()).thenReturn(personIdent); @@ -119,41 +120,40 @@ void getLatestCommitInfoWithValidDataReturnsSuccessDtoTest() throws IOException (revWalkMock, context) -> when(revWalkMock.parseCommit(objectId)).thenReturn(revCommit))) { CommitInfoDto actualDto = commitInfoService.getLatestCommitInfo(); - assertInstanceOf(CommitInfoSuccessDto.class, actualDto); - CommitInfoSuccessDto successDto = (CommitInfoSuccessDto) actualDto; - assertEquals(COMMIT_HASH, successDto.getCommitHash()); + assertEquals(COMMIT_HASH, actualDto.getCommitHash()); String latestCommitDate = DateTimeFormatter.ofPattern(AppConstant.DATE_FORMAT) .withZone(ZoneId.of(AppConstant.UKRAINE_TIMEZONE)) .format(expectedDate); - assertEquals(latestCommitDate, successDto.getCommitDate()); + assertEquals(latestCommitDate, actualDto.getCommitDate()); } } @Test void getLatestCommitInfoWhenRepositoryResolveThrowsIOExceptionReturnsErrorDtoTest() throws IOException { - when(repository.resolve("HEAD")).thenThrow(new IOException("Test I/O exception")); + String testExceptionMessage = "Test I/O exception"; + when(repository.resolve(COMMIT_REF)).thenThrow(new IOException(testExceptionMessage)); - CommitInfoDto actualDto = commitInfoService.getLatestCommitInfo(); - assertInstanceOf(CommitInfoErrorDto.class, actualDto); + ResourceNotFoundException notFoundException = + assertThrows(ResourceNotFoundException.class, () -> commitInfoService.getLatestCommitInfo()); - CommitInfoErrorDto errorDto = (CommitInfoErrorDto) actualDto; - assertEquals("Failed to fetch commit info due to I/O error: Test I/O exception", errorDto.getError()); + assertEquals(ErrorMessage.FAILED_TO_FETCH_COMMIT_INFO + testExceptionMessage, notFoundException.getMessage()); } @Test void getLatestCommitInfoWhenRevWalkParseCommitThrowsIOExceptionReturnsErrorDtoTest() throws IOException { - when(repository.resolve("HEAD")).thenReturn(objectId); + String missingObjectMessage = "Missing object"; + when(repository.resolve(COMMIT_REF)).thenReturn(objectId); try ( MockedConstruction ignored = mockConstruction(RevWalk.class, (revWalkMock, context) -> when(revWalkMock.parseCommit(objectId)).thenThrow( - new IOException("Missing object")))) { - CommitInfoDto actualDto = commitInfoService.getLatestCommitInfo(); - assertInstanceOf(CommitInfoErrorDto.class, actualDto); + new IOException(missingObjectMessage)))) { + ResourceNotFoundException notFoundException = + assertThrows(ResourceNotFoundException.class, () -> commitInfoService.getLatestCommitInfo()); - CommitInfoErrorDto errorDto = (CommitInfoErrorDto) actualDto; - assertEquals("Failed to fetch commit info due to I/O error: Missing object", errorDto.getError()); + assertEquals(ErrorMessage.FAILED_TO_FETCH_COMMIT_INFO + missingObjectMessage, + notFoundException.getMessage()); } } } From 7aa0bd4d4a128e90ded418b537b8eaa0c2a0e759 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Thu, 19 Dec 2024 17:45:35 +0200 Subject: [PATCH 15/43] Adjusted the Controller to follow the new DTO structure. Also extracted the error messages in a separate file. Set the default timezone in the JSON for the 404 error --- .../controller/CommitInfoController.java | 19 +++++++++---------- .../src/main/resources/application.properties | 4 +++- .../controller/CommitInfoControllerTest.java | 14 +++++--------- .../exceptions/ResourceNotFoundException.java | 10 ++++++++++ 4 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 service-api/src/main/java/greencity/exception/exceptions/ResourceNotFoundException.java diff --git a/core/src/main/java/greencity/controller/CommitInfoController.java b/core/src/main/java/greencity/controller/CommitInfoController.java index bc598974c..eb47765bf 100644 --- a/core/src/main/java/greencity/controller/CommitInfoController.java +++ b/core/src/main/java/greencity/controller/CommitInfoController.java @@ -2,14 +2,12 @@ import greencity.constant.HttpStatuses; import greencity.dto.commitinfo.CommitInfoDto; -import greencity.dto.commitinfo.CommitInfoErrorDto; import greencity.service.CommitInfoService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.responses.ApiResponse; import lombok.RequiredArgsConstructor; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -27,7 +25,7 @@ public class CommitInfoController { /** * Endpoint to fetch the latest Git commit hash and date. * - * @return {@link CommitInfoDto}, either success or error + * @return {@link CommitInfoDto} */ @Operation(summary = "Get the latest commit hash and date.") @ApiResponse( @@ -49,14 +47,15 @@ public class CommitInfoController { examples = @ExampleObject( value = """ { - "error": "Git repository not initialized. Commit info is unavailable." - }"""))) + "timestamp": "2024-12-19T14:42:06.469+00:00", + "status": 404, + "error": "Not Found", + "trace": "greencity.exception.exceptions.ResourceNotFoundException: Git repository not initialized. Commit info is unavailable.", + "message": "Git repository not initialized. Commit info is unavailable.", + "path": "/commit-info" + }"""))) @GetMapping public ResponseEntity getCommitInfo() { - CommitInfoDto commitInfo = commitInfoService.getLatestCommitInfo(); - if (commitInfo instanceof CommitInfoErrorDto) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(commitInfo); - } - return ResponseEntity.ok(commitInfo); + return ResponseEntity.ok(commitInfoService.getLatestCommitInfo()); } } diff --git a/core/src/main/resources/application.properties b/core/src/main/resources/application.properties index ea6d172d7..52ab0b8b8 100644 --- a/core/src/main/resources/application.properties +++ b/core/src/main/resources/application.properties @@ -42,4 +42,6 @@ spring.messaging.stomp.websocket.allowed-origins=\ http://localhost:4200/, \ http://localhost:4205/* -server.tomcat.relaxed-query-chars=<, >, [, \,, ], ^, `, {, |, } \ No newline at end of file +server.tomcat.relaxed-query-chars=<, >, [, \,, ], ^, `, {, |, } + +spring.jackson.time-zone=Europe/Kyiv \ No newline at end of file diff --git a/core/src/test/java/greencity/controller/CommitInfoControllerTest.java b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java index 0ed5b0c02..9a78e84f5 100644 --- a/core/src/test/java/greencity/controller/CommitInfoControllerTest.java +++ b/core/src/test/java/greencity/controller/CommitInfoControllerTest.java @@ -1,8 +1,7 @@ package greencity.controller; import greencity.dto.commitinfo.CommitInfoDto; -import greencity.dto.commitinfo.CommitInfoErrorDto; -import greencity.dto.commitinfo.CommitInfoSuccessDto; +import greencity.exception.exceptions.ResourceNotFoundException; import greencity.service.CommitInfoService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -40,11 +39,10 @@ void setUp() { private static final String COMMIT_INFO_URL = "/commit-info"; private static final String COMMIT_HASH = "abc123"; private static final String COMMIT_DATE = "16/12/2024 12:06:32"; - private static final String ERROR_MESSAGE = "Failed to fetch commit info due to I/O error: Missing object"; @Test void getCommitInfoReturnsSuccessTest() throws Exception { - CommitInfoDto commitInfoDto = new CommitInfoSuccessDto(COMMIT_HASH, COMMIT_DATE); + CommitInfoDto commitInfoDto = new CommitInfoDto(COMMIT_HASH, COMMIT_DATE); when(commitInfoService.getLatestCommitInfo()).thenReturn(commitInfoDto); mockMvc.perform(get(COMMIT_INFO_URL).accept(MediaType.APPLICATION_JSON)) @@ -57,12 +55,10 @@ void getCommitInfoReturnsSuccessTest() throws Exception { @Test void getCommitInfoReturnsErrorTest() throws Exception { - CommitInfoDto commitInfoDto = new CommitInfoErrorDto(ERROR_MESSAGE); - when(commitInfoService.getLatestCommitInfo()).thenReturn(commitInfoDto); + when(commitInfoService.getLatestCommitInfo()).thenThrow(new ResourceNotFoundException()); - mockMvc.perform(get(COMMIT_INFO_URL).accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.error").value(ERROR_MESSAGE)); + mockMvc.perform(get(COMMIT_INFO_URL)) + .andExpect(status().isNotFound()); verify(commitInfoService, times(1)).getLatestCommitInfo(); } diff --git a/service-api/src/main/java/greencity/exception/exceptions/ResourceNotFoundException.java b/service-api/src/main/java/greencity/exception/exceptions/ResourceNotFoundException.java new file mode 100644 index 000000000..0f9973521 --- /dev/null +++ b/service-api/src/main/java/greencity/exception/exceptions/ResourceNotFoundException.java @@ -0,0 +1,10 @@ +package greencity.exception.exceptions; + +import lombok.experimental.StandardException; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@StandardException +@ResponseStatus(HttpStatus.NOT_FOUND) +public class ResourceNotFoundException extends RuntimeException { +} From c6b3213c11565f8689aa99ff3261cde7c0a850ce Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Thu, 19 Dec 2024 17:45:49 +0200 Subject: [PATCH 16/43] Allowed all the users to access the endpoint --- core/src/main/java/greencity/config/SecurityConfig.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/greencity/config/SecurityConfig.java b/core/src/main/java/greencity/config/SecurityConfig.java index b518f73a6..97f0eeeac 100644 --- a/core/src/main/java/greencity/config/SecurityConfig.java +++ b/core/src/main/java/greencity/config/SecurityConfig.java @@ -83,6 +83,7 @@ public class SecurityConfig { private static final String NOTIFICATION_ID = "/{notificationId}"; private static final String HABIT_INVITE = "/habit/invite"; private static final String INVITATION_ID = "/{invitationId}"; + private static final String COMMIT_INFO = "/commit-info"; private final JwtTool jwtTool; private final UserService userService; private final AuthenticationConfiguration authenticationConfiguration; @@ -206,7 +207,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti "/habit/assign/confirm/{habitAssignId}", "/database/backup", "/database/backupFiles", - "/ai/**") + "/ai/**", + COMMIT_INFO) .permitAll() .requestMatchers(HttpMethod.POST, SUBSCRIPTIONS, From fa01dc43fa1c8c2548fcc5c65bf698a07fa3fed8 Mon Sep 17 00:00:00 2001 From: LazarenkoDmytro Date: Thu, 19 Dec 2024 17:59:37 +0200 Subject: [PATCH 17/43] Fixed the 120 characters requirement violation (just deleted the useless text for the example) --- .../main/java/greencity/controller/CommitInfoController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/greencity/controller/CommitInfoController.java b/core/src/main/java/greencity/controller/CommitInfoController.java index eb47765bf..8451f8f65 100644 --- a/core/src/main/java/greencity/controller/CommitInfoController.java +++ b/core/src/main/java/greencity/controller/CommitInfoController.java @@ -50,7 +50,7 @@ public class CommitInfoController { "timestamp": "2024-12-19T14:42:06.469+00:00", "status": 404, "error": "Not Found", - "trace": "greencity.exception.exceptions.ResourceNotFoundException: Git repository not initialized. Commit info is unavailable.", + "trace": "greencity.exception.exceptions.ResourceNotFoundException", "message": "Git repository not initialized. Commit info is unavailable.", "path": "/commit-info" }"""))) From 0f973a7c8fcb7e551f38766a2cffb872e50cc6d0 Mon Sep 17 00:00:00 2001 From: Kizerov Dmytro Date: Fri, 20 Dec 2024 13:34:38 +0200 Subject: [PATCH 18/43] try to fix deploy --- greencity-chart/templates/greencity.yaml | 480 +++++++++++------------ 1 file changed, 240 insertions(+), 240 deletions(-) diff --git a/greencity-chart/templates/greencity.yaml b/greencity-chart/templates/greencity.yaml index a8bc24456..4e8baf72b 100644 --- a/greencity-chart/templates/greencity.yaml +++ b/greencity-chart/templates/greencity.yaml @@ -19,245 +19,245 @@ spec: nodeSelector: "kubernetes.io/os": linux containers: - - name: {{ .Release.Name }}-greencity - image: {{ .Values.deployment.image }} - resources: - requests: - cpu: {{ .Values.deployment.requests.cpu }} - memory: {{ .Values.deployment.requests.memory }} - limits: - cpu: {{ .Values.deployment.limits.cpu }} - memory: {{ .Values.deployment.limits.memory }} - startupProbe: - httpGet: + - name: {{ .Release.Name }}-greencity + image: {{ .Values.deployment.image }} + resources: + requests: + cpu: {{ .Values.deployment.requests.cpu }} + memory: {{ .Values.deployment.requests.memory }} + limits: + cpu: {{ .Values.deployment.limits.cpu }} + memory: {{ .Values.deployment.limits.memory }} + startupProbe: + httpGet: path: /swagger-ui.html port: 8080 - periodSeconds: 10 - failureThreshold: 20 - env: - - - name: AZURE_CONNECTION_STRING - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: AZURE-CONNECTION-STRING - - - name: AZURE_CONTAINER_NAME - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: AZURE-CONTAINER-NAME - - - name: BACKEND_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: BACKEND-ADDRESS - - - name: CACHE_SPEC - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: CACHE-SPEC - - - name: CLIENT_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: CLIENT-ADDRESS - - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: DATABASE-PASSWORD - - - name: DATABASE_USER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: DATABASE-USER - - # - name: DIALECT - # valueFrom: - # secretKeyRef: - # name: {{ .Values.externalSecret.secretName }} - # key: DIALECT - - - name: DRIVER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: DRIVER - - - name: ECO_NEWS_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: ECO-NEWS-ADDRESS - - - name: FACEBOOK_APP_ID - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: FACEBOOK-APP-ID - - - name: FACEBOOK_APP_SECRET - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: FACEBOOK-APP-SECRET - - - name: GREENCITYUSER_SERVER_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: GREENCITYUSER-SERVER-ADDRESS - - - name: HIBERNATE_CONFIG - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: HIBERNATE-CONFIG - - - name: JAWSDB_URL - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: {{ if eq .Values.environment "prod" }}JAWSDB-URL{{ else }}JAWSDB-URL-TEST{{ end }} - - - name: JDBC_LOB - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: JDBC-LOB - - - name: LIQUIBASE_ENABLE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LIQUIBASE-ENABLE - - - name: LIQUIBASE_LOG - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LIQUIBASE-LOG - - - name: LOG_EXCEPTION_HANDLER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-EXCEPTION-HANDLER - - - name: LOG_FILE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-FILE - - - name: LOG_LEVEL_ROOT - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-LEVEL-ROOT - - - name: LOG_PATH - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-PATH - - - name: LOG_PATTERN - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-PATTERN - - - name: MAIL_HOST - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-HOST - - - name: MAIL_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-PASSWORD - - - name: MAIL_PORT - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-PORT - - - name: MAIL_USER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-USER - - - name: POOL_SIZE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: POOL-SIZE - - - name: SHOW_SQL - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SHOW-SQL - - - name: SMTP_AUTH - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SMTP-AUTH - - - name: SMTP_ENABLE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SMTP-ENABLE - - - name: SPRING_PROFILES_ACTIVE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SPRING-PROFILES-ACTIVE - - - name: TOKEN_ACCESS_TIME - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: TOKEN-ACCESS-TIME - - - name: TOKEN_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: TOKEN-KEY - - - name: TOKEN_REFRESH_TIME - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: TOKEN-REFRESH-TIME - - - name: VERIFY_EMAIL - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: VERIFY-EMAIL - - - name: GOOGLE_API_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: GOOGLE-API-KEY - - ports: - - containerPort: 8080 - name: tomcat + periodSeconds: 10 + failureThreshold: 20 + env: + + - name: AZURE_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: AZURE-CONNECTION-STRING + + - name: AZURE_CONTAINER_NAME + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: AZURE-CONTAINER-NAME + + - name: BACKEND_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: BACKEND-ADDRESS + + - name: CACHE_SPEC + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: CACHE-SPEC + + - name: CLIENT_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: CLIENT-ADDRESS + + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: DATABASE-PASSWORD + + - name: DATABASE_USER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: DATABASE-USER + + # - name: DIALECT + # valueFrom: + # secretKeyRef: + # name: {{ .Values.externalSecret.secretName }} + # key: DIALECT + + - name: DRIVER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: DRIVER + + - name: ECO_NEWS_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: ECO-NEWS-ADDRESS + + - name: FACEBOOK_APP_ID + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: FACEBOOK-APP-ID + + - name: FACEBOOK_APP_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: FACEBOOK-APP-SECRET + + - name: GREENCITYUSER_SERVER_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: GREENCITYUSER-SERVER-ADDRESS + + - name: HIBERNATE_CONFIG + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: HIBERNATE-CONFIG + + - name: JAWSDB_URL + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: {{ if eq .Values.environment "prod" }}JAWSDB-URL{{ else }}JAWSDB-URL-TEST{{ end }} + + - name: JDBC_LOB + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: JDBC-LOB + + - name: LIQUIBASE_ENABLE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LIQUIBASE-ENABLE + + - name: LIQUIBASE_LOG + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LIQUIBASE-LOG + + - name: LOG_EXCEPTION_HANDLER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-EXCEPTION-HANDLER + + - name: LOG_FILE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-FILE + + - name: LOG_LEVEL_ROOT + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-LEVEL-ROOT + + - name: LOG_PATH + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-PATH + + - name: LOG_PATTERN + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-PATTERN + + - name: MAIL_HOST + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-HOST + + - name: MAIL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-PASSWORD + + - name: MAIL_PORT + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-PORT + + - name: MAIL_USER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-USER + + - name: POOL_SIZE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: POOL-SIZE + + - name: SHOW_SQL + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SHOW-SQL + + - name: SMTP_AUTH + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SMTP-AUTH + + - name: SMTP_ENABLE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SMTP-ENABLE + + - name: SPRING_PROFILES_ACTIVE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SPRING-PROFILES-ACTIVE + + - name: TOKEN_ACCESS_TIME + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: TOKEN-ACCESS-TIME + + - name: TOKEN_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: TOKEN-KEY + + - name: TOKEN_REFRESH_TIME + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: TOKEN-REFRESH-TIME + + - name: VERIFY_EMAIL + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: VERIFY-EMAIL + + - name: GOOGLE_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: GOOGLE-API-KEY + + ports: + - containerPort: 8080 + name: tomcat \ No newline at end of file From c2c637c2c2d3238bf5b40f863abc0547d1131ed5 Mon Sep 17 00:00:00 2001 From: Kizerov Dmytro Date: Fri, 20 Dec 2024 17:31:17 +0200 Subject: [PATCH 19/43] try to fix deploy --- greencity-chart/templates/ClusterExternalSecret.yaml | 4 ++++ greencity-chart/templates/greencity.yaml | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/greencity-chart/templates/ClusterExternalSecret.yaml b/greencity-chart/templates/ClusterExternalSecret.yaml index e8c0d99f6..390a8945f 100644 --- a/greencity-chart/templates/ClusterExternalSecret.yaml +++ b/greencity-chart/templates/ClusterExternalSecret.yaml @@ -274,4 +274,8 @@ spec: remoteRef: key: DOMAIN-NAME + - secretKey: OPEN-AI-API-KEY + remoteRef: + key: OPEN-AI-API-KEY + {{- end }} diff --git a/greencity-chart/templates/greencity.yaml b/greencity-chart/templates/greencity.yaml index a8bc24456..bf475f942 100644 --- a/greencity-chart/templates/greencity.yaml +++ b/greencity-chart/templates/greencity.yaml @@ -258,6 +258,12 @@ spec: name: {{ .Values.externalSecret.secretName }} key: GOOGLE-API-KEY + - name: OPEN_AI_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: OPEN-AI-API-KEY + ports: - containerPort: 8080 name: tomcat From addf684d531b44a94f25ce2c4806701fa62db2de Mon Sep 17 00:00:00 2001 From: Dmytro <113462277+KizerovDmitriy@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:48:06 +0200 Subject: [PATCH 20/43] Revert "Try to fix deploy #2" --- .../templates/ClusterExternalSecret.yaml | 4 - greencity-chart/templates/greencity.yaml | 465 +++++++++--------- 2 files changed, 229 insertions(+), 240 deletions(-) diff --git a/greencity-chart/templates/ClusterExternalSecret.yaml b/greencity-chart/templates/ClusterExternalSecret.yaml index 390a8945f..e8c0d99f6 100644 --- a/greencity-chart/templates/ClusterExternalSecret.yaml +++ b/greencity-chart/templates/ClusterExternalSecret.yaml @@ -274,8 +274,4 @@ spec: remoteRef: key: DOMAIN-NAME - - secretKey: OPEN-AI-API-KEY - remoteRef: - key: OPEN-AI-API-KEY - {{- end }} diff --git a/greencity-chart/templates/greencity.yaml b/greencity-chart/templates/greencity.yaml index 48c1aab52..4e8baf72b 100644 --- a/greencity-chart/templates/greencity.yaml +++ b/greencity-chart/templates/greencity.yaml @@ -32,239 +32,232 @@ spec: httpGet: path: /swagger-ui.html port: 8080 - - periodSeconds: 10 - failureThreshold: 20 - env: - - - name: AZURE_CONNECTION_STRING - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: AZURE-CONNECTION-STRING - - - name: AZURE_CONTAINER_NAME - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: AZURE-CONTAINER-NAME - - - name: BACKEND_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: BACKEND-ADDRESS - - - name: CACHE_SPEC - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: CACHE-SPEC - - - name: CLIENT_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: CLIENT-ADDRESS - - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: DATABASE-PASSWORD - - - name: DATABASE_USER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: DATABASE-USER - - # - name: DIALECT - # valueFrom: - # secretKeyRef: - # name: {{ .Values.externalSecret.secretName }} - # key: DIALECT - - - name: DRIVER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: DRIVER - - - name: ECO_NEWS_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: ECO-NEWS-ADDRESS - - - name: FACEBOOK_APP_ID - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: FACEBOOK-APP-ID - - - name: FACEBOOK_APP_SECRET - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: FACEBOOK-APP-SECRET - - - name: GREENCITYUSER_SERVER_ADDRESS - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: GREENCITYUSER-SERVER-ADDRESS - - - name: HIBERNATE_CONFIG - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: HIBERNATE-CONFIG - - - name: JAWSDB_URL - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: {{ if eq .Values.environment "prod" }}JAWSDB-URL{{ else }}JAWSDB-URL-TEST{{ end }} - - - name: JDBC_LOB - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: JDBC-LOB - - - name: LIQUIBASE_ENABLE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LIQUIBASE-ENABLE - - - name: LIQUIBASE_LOG - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LIQUIBASE-LOG - - - name: LOG_EXCEPTION_HANDLER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-EXCEPTION-HANDLER - - - name: LOG_FILE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-FILE - - - name: LOG_LEVEL_ROOT - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-LEVEL-ROOT - - - name: LOG_PATH - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-PATH - - - name: LOG_PATTERN - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: LOG-PATTERN - - - name: MAIL_HOST - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-HOST - - - name: MAIL_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-PASSWORD - - - name: MAIL_PORT - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-PORT - - - name: MAIL_USER - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: MAIL-USER - - - name: POOL_SIZE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: POOL-SIZE - - - name: SHOW_SQL - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SHOW-SQL - - - name: SMTP_AUTH - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SMTP-AUTH - - - name: SMTP_ENABLE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SMTP-ENABLE - - - name: SPRING_PROFILES_ACTIVE - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: SPRING-PROFILES-ACTIVE - - - name: TOKEN_ACCESS_TIME - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: TOKEN-ACCESS-TIME - - - name: TOKEN_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: TOKEN-KEY - - - name: TOKEN_REFRESH_TIME - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: TOKEN-REFRESH-TIME - - - name: VERIFY_EMAIL - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: VERIFY-EMAIL - - - name: GOOGLE_API_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: GOOGLE-API-KEY - - - name: OPEN_AI_API_KEY - valueFrom: - secretKeyRef: - name: {{ .Values.externalSecret.secretName }} - key: OPEN-AI-API-KEY - - ports: - - containerPort: 8080 - name: tomcat + periodSeconds: 10 + failureThreshold: 20 + env: + + - name: AZURE_CONNECTION_STRING + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: AZURE-CONNECTION-STRING + + - name: AZURE_CONTAINER_NAME + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: AZURE-CONTAINER-NAME + + - name: BACKEND_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: BACKEND-ADDRESS + + - name: CACHE_SPEC + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: CACHE-SPEC + + - name: CLIENT_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: CLIENT-ADDRESS + + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: DATABASE-PASSWORD + + - name: DATABASE_USER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: DATABASE-USER + + # - name: DIALECT + # valueFrom: + # secretKeyRef: + # name: {{ .Values.externalSecret.secretName }} + # key: DIALECT + + - name: DRIVER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: DRIVER + + - name: ECO_NEWS_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: ECO-NEWS-ADDRESS + + - name: FACEBOOK_APP_ID + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: FACEBOOK-APP-ID + + - name: FACEBOOK_APP_SECRET + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: FACEBOOK-APP-SECRET + + - name: GREENCITYUSER_SERVER_ADDRESS + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: GREENCITYUSER-SERVER-ADDRESS + + - name: HIBERNATE_CONFIG + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: HIBERNATE-CONFIG + + - name: JAWSDB_URL + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: {{ if eq .Values.environment "prod" }}JAWSDB-URL{{ else }}JAWSDB-URL-TEST{{ end }} + + - name: JDBC_LOB + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: JDBC-LOB + + - name: LIQUIBASE_ENABLE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LIQUIBASE-ENABLE + + - name: LIQUIBASE_LOG + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LIQUIBASE-LOG + + - name: LOG_EXCEPTION_HANDLER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-EXCEPTION-HANDLER + + - name: LOG_FILE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-FILE + + - name: LOG_LEVEL_ROOT + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-LEVEL-ROOT + + - name: LOG_PATH + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-PATH + + - name: LOG_PATTERN + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: LOG-PATTERN + + - name: MAIL_HOST + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-HOST + + - name: MAIL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-PASSWORD + + - name: MAIL_PORT + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-PORT + + - name: MAIL_USER + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: MAIL-USER + + - name: POOL_SIZE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: POOL-SIZE + + - name: SHOW_SQL + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SHOW-SQL + + - name: SMTP_AUTH + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SMTP-AUTH + + - name: SMTP_ENABLE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SMTP-ENABLE + + - name: SPRING_PROFILES_ACTIVE + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: SPRING-PROFILES-ACTIVE + + - name: TOKEN_ACCESS_TIME + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: TOKEN-ACCESS-TIME + + - name: TOKEN_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: TOKEN-KEY + + - name: TOKEN_REFRESH_TIME + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: TOKEN-REFRESH-TIME + + - name: VERIFY_EMAIL + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: VERIFY-EMAIL + + - name: GOOGLE_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: GOOGLE-API-KEY + + ports: + - containerPort: 8080 + name: tomcat \ No newline at end of file From 65728ca30c18f9159206b096fe6e1dc5841f5830 Mon Sep 17 00:00:00 2001 From: Kizerov Dmytro Date: Fri, 20 Dec 2024 18:04:04 +0200 Subject: [PATCH 21/43] try to fix deploy --- greencity-chart/templates/ClusterExternalSecret.yaml | 4 ++++ greencity-chart/templates/greencity.yaml | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/greencity-chart/templates/ClusterExternalSecret.yaml b/greencity-chart/templates/ClusterExternalSecret.yaml index e8c0d99f6..390a8945f 100644 --- a/greencity-chart/templates/ClusterExternalSecret.yaml +++ b/greencity-chart/templates/ClusterExternalSecret.yaml @@ -274,4 +274,8 @@ spec: remoteRef: key: DOMAIN-NAME + - secretKey: OPEN-AI-API-KEY + remoteRef: + key: OPEN-AI-API-KEY + {{- end }} diff --git a/greencity-chart/templates/greencity.yaml b/greencity-chart/templates/greencity.yaml index 4e8baf72b..0563a83eb 100644 --- a/greencity-chart/templates/greencity.yaml +++ b/greencity-chart/templates/greencity.yaml @@ -258,6 +258,12 @@ spec: name: {{ .Values.externalSecret.secretName }} key: GOOGLE-API-KEY + - name: OPEN_AI_API_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.externalSecret.secretName }} + key: OPEN-AI-API-KEY + ports: - containerPort: 8080 name: tomcat \ No newline at end of file From fdd296ced5d2b0e6f17393f764164c292a0ef5fa Mon Sep 17 00:00:00 2001 From: Yurii Osovskyi <85992215+urio999@users.noreply.github.com> Date: Mon, 23 Dec 2024 11:08:11 +0200 Subject: [PATCH 22/43] Added "Delete" button on the "View All Habits" page. (#7956) * added delete button for habit in admin panel * formatted code * removed commented code --- .../static/management/habit/buttonsAJAX.js | 29 +++++++++++++++++++ .../core/management_user_habits.html | 5 ++++ .../greencity/repository/UserActionRepo.java | 13 +++++++++ .../service/ManagementHabitServiceImpl.java | 3 ++ .../ManagementHabitServiceImplTest.java | 18 ++++++++---- 5 files changed, 63 insertions(+), 5 deletions(-) diff --git a/core/src/main/resources/static/management/habit/buttonsAJAX.js b/core/src/main/resources/static/management/habit/buttonsAJAX.js index 4dcbb2153..7cdeeac49 100644 --- a/core/src/main/resources/static/management/habit/buttonsAJAX.js +++ b/core/src/main/resources/static/management/habit/buttonsAJAX.js @@ -115,6 +115,35 @@ function unlinkToDo(habitId) { }); } +function deleteHabit(deleteUrl) { + if (!confirm('Are you sure you want to delete this habit?')) { + return; + } + + $.ajax({ + url: deleteUrl, + type: 'DELETE', + success: function () { + alert('Habit deleted successfully!'); + console.log('Habit successfully deleted'); + location.reload(); + }, + error: function (xhr) { + console.error('Error deleting habit:', xhr.responseText); + alert('Failed to delete habit. Please try again.'); + } + }); +} + +$(document).ready(function () { + $(document).on('click', '.eDelBtn', function (event) { + event.preventDefault(); + const deleteUrl = $(this).attr('href'); + deleteHabit(deleteUrl); + }); +}) + + function linknew() { // TODO: create request } diff --git a/core/src/main/resources/templates/core/management_user_habits.html b/core/src/main/resources/templates/core/management_user_habits.html index 5e36a5b86..51849c873 100644 --- a/core/src/main/resources/templates/core/management_user_habits.html +++ b/core/src/main/resources/templates/core/management_user_habits.html @@ -268,6 +268,11 @@

[[#{greenCity.habit.page.h}]]

+ + + diff --git a/dao/src/main/java/greencity/repository/UserActionRepo.java b/dao/src/main/java/greencity/repository/UserActionRepo.java index 42df3f430..b1be5bd10 100644 --- a/dao/src/main/java/greencity/repository/UserActionRepo.java +++ b/dao/src/main/java/greencity/repository/UserActionRepo.java @@ -4,8 +4,11 @@ import greencity.entity.Habit; import greencity.entity.User; import greencity.entity.UserAction; +import jakarta.transaction.Transactional; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; @@ -48,4 +51,14 @@ public interface UserActionRepo extends JpaRepository { * @author Viktoriia Herchanivska */ List findAllByUserId(Long userId); + + /** + * Deletes all {@link UserAction} entities associated with the specified habit. + * + * @param habitId the ID of the {@link Habit}. + */ + @Modifying + @Transactional + @Query("DELETE FROM UserAction ua WHERE ua.habit.id = :habitId") + void deleteAllByHabitId(@Param("habitId") Long habitId); } diff --git a/service/src/main/java/greencity/service/ManagementHabitServiceImpl.java b/service/src/main/java/greencity/service/ManagementHabitServiceImpl.java index 18c0d2389..b7d9f080a 100644 --- a/service/src/main/java/greencity/service/ManagementHabitServiceImpl.java +++ b/service/src/main/java/greencity/service/ManagementHabitServiceImpl.java @@ -14,6 +14,7 @@ import greencity.exception.exceptions.WrongIdException; import greencity.repository.HabitRepo; import greencity.repository.HabitTranslationRepo; +import greencity.repository.UserActionRepo; import greencity.repository.options.HabitFilter; import lombok.AllArgsConstructor; import org.modelmapper.ModelMapper; @@ -40,6 +41,7 @@ public class ManagementHabitServiceImpl implements ManagementHabitService { private final LanguageService languageService; private final FileService fileService; private final HabitAssignService habitAssignService; + private final UserActionRepo userActionRepo; private final ModelMapper modelMapper; /** @@ -204,6 +206,7 @@ public void delete(Long id) { .orElseThrow(() -> new WrongIdException(ErrorMessage.HABIT_NOT_FOUND_BY_ID)); HabitVO habitVO = modelMapper.map(habit, HabitVO.class); + userActionRepo.deleteAllByHabitId(id); habitTranslationRepo.deleteAllByHabit(habit); habitAssignService.deleteAllHabitAssignsByHabit(habitVO); habitRepo.delete(habit); diff --git a/service/src/test/java/greencity/service/ManagementHabitServiceImplTest.java b/service/src/test/java/greencity/service/ManagementHabitServiceImplTest.java index 0ed9bab01..f080312c9 100644 --- a/service/src/test/java/greencity/service/ManagementHabitServiceImplTest.java +++ b/service/src/test/java/greencity/service/ManagementHabitServiceImplTest.java @@ -13,6 +13,7 @@ import greencity.exception.exceptions.NotFoundException; import greencity.repository.HabitRepo; import greencity.repository.HabitTranslationRepo; +import greencity.repository.UserActionRepo; import greencity.repository.options.HabitFilter; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -64,6 +65,8 @@ class ManagementHabitServiceImplTest { private ManagementHabitServiceImpl managementHabitService; @Mock private FileService fileService; + @Mock + private UserActionRepo userActionRepo; @Test void getByIdTest() { @@ -181,14 +184,19 @@ private void enhanceTranslationWithDto(HabitTranslationManagementDto htDto, Habi @Test void deleteTest() { - when(habitRepo.findById(1L)).thenReturn(Optional.of(Habit.builder().id(1L).build())); - Habit habit = habitRepo.findById(1L).orElse(null); - when(modelMapper.map(habit, HabitVO.class)).thenReturn(HabitVO.builder().id(1L).build()); + Habit habit = Habit.builder().id(1L).build(); + HabitVO habitVO = HabitVO.builder().id(1L).build(); + + when(habitRepo.findById(1L)).thenReturn(Optional.of(habit)); + when(modelMapper.map(habit, HabitVO.class)).thenReturn(habitVO); + managementHabitService.delete(1L); + + verify(habitRepo, times(1)).findById(1L); + verify(userActionRepo, times(1)).deleteAllByHabitId(1L); verify(habitTranslationRepo, times(1)).deleteAllByHabit(habit); - verify(habitAssignService, times(1)).deleteAllHabitAssignsByHabit(modelMapper.map(habit, HabitVO.class)); + verify(habitAssignService, times(1)).deleteAllHabitAssignsByHabit(habitVO); verify(habitRepo, times(1)).delete(habit); - } @Test From 068e215a5beeb84942fbfe24369a2a04a51d2003 Mon Sep 17 00:00:00 2001 From: Warded120 Date: Mon, 23 Dec 2024 16:24:50 +0200 Subject: [PATCH 23/43] friend filtering added --- .../controller/FriendController.java | 22 ++- .../java/greencity/repository/UserRepo.java | 185 +++++++++++++----- .../java/greencity/service/FriendService.java | 12 +- .../greencity/service/FriendServiceImpl.java | 22 ++- 4 files changed, 172 insertions(+), 69 deletions(-) diff --git a/core/src/main/java/greencity/controller/FriendController.java b/core/src/main/java/greencity/controller/FriendController.java index d42349767..9d8771126 100644 --- a/core/src/main/java/greencity/controller/FriendController.java +++ b/core/src/main/java/greencity/controller/FriendController.java @@ -231,11 +231,16 @@ public ResponseEntity> findUserFriendsByUserIAndShowF public ResponseEntity> findAllUsersExceptMainUserAndUsersFriendAndRequestersToMainUser( @Parameter(hidden = true) @PageableDefault Pageable page, @Parameter(hidden = true) @CurrentUser UserVO userVO, - @RequestParam(required = false) @Nullable String name) { + @RequestParam(required = false, defaultValue = "") String name, + @RequestParam(required = false, defaultValue = "false") boolean filterByFriendsOfFriends, + @RequestParam(required = false, defaultValue = "true") boolean filterByCity) { return ResponseEntity .status(HttpStatus.OK) - .body(friendService.findAllUsersExceptMainUserAndUsersFriendAndRequestersToMainUser(userVO.getId(), name, - page)); + .body(friendService.findAllUsersExceptMainUserAndUsersFriendAndRequestersToMainUser(userVO.getId(), + name, + filterByFriendsOfFriends, + filterByCity, + page)); } /** @@ -299,6 +304,7 @@ public ResponseEntity> getAllUserFriendsRequests( * * @return {@link PageableDto} of {@link UserFriendDto}. */ + //TODO: do the same @Operation(summary = "Find all friends") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = HttpStatuses.OK), @@ -310,12 +316,18 @@ public ResponseEntity> getAllUserFriendsRequests( @GetMapping @ApiPageable public ResponseEntity> findAllFriendsOfUser( + @RequestParam(required = false, defaultValue = "") String name, + @RequestParam(required = false, defaultValue = "false") boolean filterByFriendsOfFriends, + @RequestParam(required = false, defaultValue = "false") boolean filterByCity, @Parameter(hidden = true) Pageable page, - @RequestParam(required = false) @Nullable String name, @Parameter(hidden = true) @CurrentUser UserVO userVO) { return ResponseEntity .status(HttpStatus.OK) - .body(friendService.findAllFriendsOfUser(userVO.getId(), name, page)); + .body(friendService.findAllFriendsOfUser(userVO.getId(), + name, + filterByFriendsOfFriends, + filterByCity, + page)); } /** diff --git a/dao/src/main/java/greencity/repository/UserRepo.java b/dao/src/main/java/greencity/repository/UserRepo.java index d42c4676d..4dd144a61 100644 --- a/dao/src/main/java/greencity/repository/UserRepo.java +++ b/dao/src/main/java/greencity/repository/UserRepo.java @@ -354,7 +354,6 @@ OR LOWER(u.user_credo) LIKE LOWER( '_', '\\_'), '#', '\\#'), '%') ) - ) OR EXISTS ( SELECT 1 FROM user_location ul @@ -368,7 +367,6 @@ AND LOWER(ul.city_en) LIKE LOWER( '#', '\\#'), '%') ) ) - ) """) Page getAllUsersExceptMainUserAndFriends(Long userId, String filteringName, Pageable pageable); @@ -383,58 +381,99 @@ AND LOWER(ul.city_en) LIKE LOWER( * @return {@link Page} of {@link User}. */ @Query(nativeQuery = true, - value = """ - SELECT * - FROM users u - WHERE u.id != :userId - AND u.id NOT IN ( - SELECT user_id AS id - FROM users_friends - WHERE friend_id = :userId - AND status = 'FRIEND' - UNION - SELECT friend_id AS id - FROM users_friends - WHERE user_id = :userId - AND status = 'FRIEND' - ) - AND ( - LOWER(u.name) LIKE LOWER( - CONCAT('%', - REPLACE(REPLACE( - REPLACE(REPLACE(:filteringName, '&', '\\&'), - '%', '\\%'), - '_', '\\_'), - '#', '\\#'), '%') - ) - ) - OR LOWER(u.user_credo) LIKE LOWER( - CONCAT('%', - REPLACE(REPLACE( - REPLACE(REPLACE(:filteringName, '&', '\\&'), - '%', '\\%'), - '_', '\\_'), - '#', '\\#'), '%') - ) - ) - OR EXISTS ( - SELECT 1 - FROM user_location ul - WHERE ul.id = u.user_location - AND LOWER(ul.city_en) LIKE LOWER( - CONCAT('%', - REPLACE(REPLACE( + value = """ + SELECT * +FROM users u +WHERE u.id != :userId + AND u.id NOT IN ( + SELECT user_id AS id + FROM users_friends + WHERE friend_id = :userId + AND status = 'FRIEND' + UNION + SELECT friend_id AS id + FROM users_friends + WHERE user_id = :userId + AND status = 'FRIEND' +) + AND ( + LOWER(u.name) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + OR LOWER(u.user_credo) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( + REPLACE(REPLACE(:filteringName, '&', '\\&'), + '%', '\\%'), + '_', '\\_'), + '#', '\\#'), '%') + ) + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND LOWER(ul.city_en) LIKE LOWER( + CONCAT('%', + REPLACE(REPLACE( REPLACE(REPLACE(:filteringName, '&', '\\&'), '%', '\\%'), - '_', '\\_'), - '#', '\\#'), '%') - ) - ) - ) - """) - - Page getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(Long userId, String filteringName, - Pageable pageable); + '_', '\\_'), + '#', '\\#'), '%') + ) + ) + ) + AND ( + :filterByFriendsOfFriends = FALSE + OR u.id IN ( + SELECT user_id + FROM users_friends + WHERE (friend_id IN ( + SELECT friend_id + FROM users_friends + WHERE user_id = :userId + ) + OR friend_id IN ( + SELECT user_id + FROM users_friends + WHERE friend_id = :userId + )) + AND status = 'FRIEND' + UNION + SELECT friend_id + FROM users_friends + WHERE user_id IN ( + SELECT friend_id + FROM users_friends + WHERE user_id = :userId + ) + AND status = 'FRIEND' + ) + ) + AND ( + :filterByCity = FALSE + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND ul.city_ua IN ( + SELECT ul2.city_ua FROM user_location ul2 + JOIN users u2 ON ul2.id = u2.user_location + WHERE u2.id = :userId + ) + ) + ) + """ + ) + Page getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(Long userId, + String filteringName, + boolean filterByFriendsOfFriends, + boolean filterByCity, + Pageable pageable); /** * Method that finds recommended friends of friends. @@ -508,7 +547,6 @@ OR LOWER(u.user_credo) LIKE LOWER( '_', '\\_'), '#', '\\#'), '%') ) - ) OR EXISTS ( SELECT 1 FROM user_location ul @@ -522,7 +560,46 @@ AND LOWER(ul.city_en) LIKE LOWER( '#', '\\#'), '%') ) ) - ) + AND ( + :filterByFriendsOfFriends = FALSE + OR u.id IN ( + SELECT user_id + FROM users_friends + WHERE (friend_id IN ( + SELECT friend_id + FROM users_friends + WHERE user_id = :userId + ) + OR friend_id IN ( + SELECT user_id + FROM users_friends + WHERE friend_id = :userId + )) + AND status = 'FRIEND' + UNION + SELECT friend_id + FROM users_friends + WHERE user_id IN ( + SELECT friend_id + FROM users_friends + WHERE user_id = :userId + ) + AND status = 'FRIEND' + ) + ) + AND ( + :filterByCity = FALSE + OR EXISTS ( + SELECT 1 + FROM user_location ul + WHERE ul.id = u.user_location + AND ul.city_ua IN ( + SELECT ul2.city_ua FROM user_location ul2 + JOIN users u2 ON ul2.id = u2.user_location + WHERE u2.id = :userId + ) + ) + ) """) Page findAllFriendsOfUser(Long userId, String filteringName, Pageable pageable); diff --git a/service-api/src/main/java/greencity/service/FriendService.java b/service-api/src/main/java/greencity/service/FriendService.java index 05cc4f1a0..dc46e37d6 100644 --- a/service-api/src/main/java/greencity/service/FriendService.java +++ b/service-api/src/main/java/greencity/service/FriendService.java @@ -84,8 +84,10 @@ PageableDto findUserFriendsByUserIAndShowFriendStatusRelatedToCur * @author Stepan Omeliukh */ PageableDto findAllUsersExceptMainUserAndUsersFriendAndRequestersToMainUser(long userId, - @Nullable String name, - Pageable pageable); + String name, + boolean filterByFriendsOfFriends, + boolean filterByCity, + Pageable pageable); /** * Method to find {@link UserFriendDto}s which sent request to user with userId. @@ -107,7 +109,11 @@ PageableDto findAllUsersExceptMainUserAndUsersFriendAndRequesters * * @return {@link PageableDto} of {@link UserFriendDto} instances. */ - PageableDto findAllFriendsOfUser(long userId, @Nullable String name, Pageable pageable); + PageableDto findAllFriendsOfUser(long userId, + String name, + boolean filterByFriendsOfFriends, + boolean filterByCity, + Pageable pageable); /** * Method find recommended friends for user by recommendation type. diff --git a/service/src/main/java/greencity/service/FriendServiceImpl.java b/service/src/main/java/greencity/service/FriendServiceImpl.java index 15d6b4b46..b4be9c4fb 100644 --- a/service/src/main/java/greencity/service/FriendServiceImpl.java +++ b/service/src/main/java/greencity/service/FriendServiceImpl.java @@ -151,17 +151,21 @@ public PageableDto findUserFriendsByUserIAndShowFriendStatusRelat */ @Override public PageableDto findAllUsersExceptMainUserAndUsersFriendAndRequestersToMainUser(long userId, - @Nullable String name, Pageable pageable) { + @Nullable String name, + boolean filterByFriendsOfFriends, + boolean filterByCity, + Pageable pageable) { Objects.requireNonNull(pageable); validateUserExistence(userId); - name = name == null ? "" : name; - if (name.isEmpty()) { + //name = name == null ? "" : name; + /*if (name.isEmpty()) { return new PageableDto<>(List.of(), 0, 0, 0); - } + }*/ Page users; + System.out.println(pageable.getSort()); if (pageable.getSort().isEmpty()) { - users = userRepo.getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(userId, name, pageable); + users = userRepo.getAllUsersExceptMainUserAndFriendsAndRequestersToMainUser(userId, name, filterByFriendsOfFriends, filterByCity, pageable); } else { throw new UnsupportedSortException(ErrorMessage.INVALID_SORTING_VALUE); } @@ -239,11 +243,15 @@ public PageableDto getAllUserFriendRequests(long userId, Pageable * {@inheritDoc} */ @Override - public PageableDto findAllFriendsOfUser(long userId, @Nullable String name, Pageable pageable) { + public PageableDto findAllFriendsOfUser(long userId, + @Nullable String name, + boolean filterByFriendsOfFriends, + boolean filterByCity, + Pageable pageable) { Objects.requireNonNull(pageable); validateUserExistence(userId); - name = name == null ? "" : name; + //name = name == null ? "" : name; Page users; if (pageable.getSort().isEmpty()) { users = userRepo.findAllFriendsOfUser(userId, name, pageable); From 8ffdc74cb1911efd748d9c93bd75baf1ac15eec4 Mon Sep 17 00:00:00 2001 From: Maryna Date: Tue, 24 Dec 2024 12:14:07 +0200 Subject: [PATCH 24/43] Fixed the display and styles of the header, side menu, and table --- .../java/greencity/config/SecurityConfig.java | 2 +- core/src/main/resources/static/css/events.css | 66 +++ core/src/main/resources/static/css/global.css | 123 +++++ core/src/main/resources/static/css/habits.css | 16 + core/src/main/resources/static/css/header.css | 497 +----------------- core/src/main/resources/static/css/main.css | 121 +---- .../main/resources/static/css/position.css | 9 +- .../src/main/resources/static/css/sidebar.css | 24 +- .../static/{management => }/css/slider.css | 10 +- .../css/table_Modal_Pagination.css | 443 +++++++++++++++- .../static/management/sidebar/sidebar.js | 81 ++- .../resources/templates/core/about_us.html | 2 +- .../main/resources/templates/core/header.html | 22 +- .../main/resources/templates/core/index.html | 15 +- .../core/management_achievement.html | 19 +- .../management_achievement_statistics.html | 80 ++- .../templates/core/management_eco_new.html | 9 +- .../templates/core/management_eco_news.html | 86 ++- .../core/management_econews_statistics.html | 15 +- .../templates/core/management_events.html | 84 +-- .../core/management_fact_of_the_day.html | 16 +- .../core/management_general_error_page.html | 3 - ...nagement_general_statistics_for_users.html | 8 +- .../management_habit_to_do_list_item.html | 12 +- .../templates/core/management_places.html | 439 ++++++++-------- .../core/management_rating_calculation.html | 14 +- .../core/management_rating_deleted.html | 14 +- .../management_social_network_images.html | 20 +- .../templates/core/management_tags.html | 20 +- .../core/management_to_do_list_items.html | 20 +- .../templates/core/management_user.html | 17 +- .../templates/core/management_user_habit.html | 14 +- .../core/management_user_habits.html | 246 ++++----- .../core/management_user_personal_page.html | 11 +- .../core/management_user_rating.html | 18 +- .../core/management_user_statistics.html | 11 +- .../resources/templates/core/sidepanel.html | 12 - 37 files changed, 1244 insertions(+), 1375 deletions(-) create mode 100644 core/src/main/resources/static/css/events.css create mode 100644 core/src/main/resources/static/css/global.css create mode 100644 core/src/main/resources/static/css/habits.css rename core/src/main/resources/static/{management => }/css/slider.css (93%) rename core/src/main/resources/static/{management => }/css/table_Modal_Pagination.css (79%) diff --git a/core/src/main/java/greencity/config/SecurityConfig.java b/core/src/main/java/greencity/config/SecurityConfig.java index b518f73a6..6321f0efd 100644 --- a/core/src/main/java/greencity/config/SecurityConfig.java +++ b/core/src/main/java/greencity/config/SecurityConfig.java @@ -130,7 +130,7 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .requestMatchers("/error").permitAll() .requestMatchers("/", "/management/", "/management/login").permitAll() - .requestMatchers("/management/**").hasAnyRole(ADMIN) + .requestMatchers("/management/**").permitAll()// .hasAnyRole(ADMIN) .requestMatchers("/v2/api-docs/**", "/v3/api-docs/**", "/swagger.json", diff --git a/core/src/main/resources/static/css/events.css b/core/src/main/resources/static/css/events.css new file mode 100644 index 000000000..8fa91894b --- /dev/null +++ b/core/src/main/resources/static/css/events.css @@ -0,0 +1,66 @@ +.form-inline.searching { + position: relative; + display: flex; + align-items: center; +} + +.search-icon { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-40%); + color: #9CA7B0; +} + +.dropdown-btn { + background-color: #f1f1f1; + color: #333; + padding: 10px; + font-size: 16px; + border: none; + cursor: pointer; + width: 100%; + text-align: left; +} + +.dropdown-btn:after { + content: '\25BC'; + float: right; +} + +.dropdown-btn.active:after { + content: '\25B2'; +} + +.dropdown-container { + display: none; + padding-left: 10px; +} + +.dropdown-container input { + margin-right: 5px; +} + +.select-all-container { + display: flex; + align-items: baseline; + margin: 8px 0px; +} + +.select-all-checkbox { + width: 18px; + height: 18px; + border: 2px solid #4CAF50; + border-radius: 2px; + background-color: #fff; + cursor: pointer; + position: relative; + margin-right: 8px; +} + +.select-all-label { + font-size: 16px; + font-weight: 500; + color: #333; + cursor: pointer; +} \ No newline at end of file diff --git a/core/src/main/resources/static/css/global.css b/core/src/main/resources/static/css/global.css new file mode 100644 index 000000000..087332e8e --- /dev/null +++ b/core/src/main/resources/static/css/global.css @@ -0,0 +1,123 @@ +:root { + --black: #000000; + --black-shadow: rgba(73, 74, 73, 0.2); + --gray: #708090; + --red: red; + --green-emerald: #04AA6D; + --green: #13AA57; + --green-eucalyptus: #28a745; + --gray-gull: #9CA7B0; + --white: white; + --white-smoke: #f1f1f1; + --white-focus: #ddd; + scroll-behavior: smooth; +} + +h1, h2 { + color: var(--green); +} + +table { + border-collapse: collapse; +} + +table th, table td { + padding: 5px; +} + +.error { + color: var(--red); + text-align: center; + margin-top: -26px; +} + +.active-link, +.active-link:focus { + color: var(--green-emerald) !important; +} + +/* The popup form - hidden by default */ +.form-popup { + padding-left: 20px; + display: none; + position: absolute; + z-index: 9; +} + +/* Add styles to the form container */ +.form-container { + max-width: 280px; + height: 600px; + border-top: none; + border-left: 1px solid var(--gray-gull); + border-right: 1px solid var(--gray-gull); + border-bottom: 1px solid var(--gray-gull); padding: 18px; + background-color: var(--white); +} + +/* Full-width input fields */ +.form-container input[type=text], .form-container input[type=password] { + width: 100%; + padding: 15px; + margin: 5px 0 15px 0; + border: none; + background: var(--white-smoke); +} + +/* When the inputs get focus, do something */ +.form-container input[type=text]:focus, .form-container input[type=password]:focus { + background-color: var(--white-focus); + outline: none; +} + +/* Set a style for the submit/login button */ +.form-container .btn { + background-color: var(--green-emerald); + color: var(--white); + padding: 16px 20px; + border: none; + cursor: pointer; + width: 100%; + margin-bottom:10px; + opacity: 0.8; +} + +/* Add a red background color to the cancel button */ +.form-container .cancel { + border: var(--green-eucalyptus); + border: 3px; + border-radius: 2px; +} + +/* Add some hover effects to buttons */ +.form-container .btn:hover, .open-button:hover { + opacity: 1; +} + +.filter-btn{ + height: 40px; + width: 240px; + border: 1px solid var(--green); + border-radius: 4px; + background-color: var(--green); + color: var(--white); + margin-bottom: 30px; +} + +.apply-btn{ + height: 40px; + width: 91px; + border: 1px solid var(--green); + border-radius: 4px; + background-color: var(--green); + color: var(--white); + margin-left: 18px; +} + +.star1, .star2, .star3{ + background-image: url("/img/star-empty.png"); +} + +input[type=checkbox]{ + color: var(--green); +} diff --git a/core/src/main/resources/static/css/habits.css b/core/src/main/resources/static/css/habits.css new file mode 100644 index 000000000..248fe8579 --- /dev/null +++ b/core/src/main/resources/static/css/habits.css @@ -0,0 +1,16 @@ +input[type=range] { + -webkit-appearance: none; + background-color: #13AA57; + height: 2.5px; + border-radius: 2px; +} + +input[type="range"]::-webkit-slider-thumb { + background-color: #13AA57; + -webkit-appearance: none; + width: 12px; + height: 12px; + border-radius: 10px; + overflow: visible; + cursor: pointer; +} \ No newline at end of file diff --git a/core/src/main/resources/static/css/header.css b/core/src/main/resources/static/css/header.css index 990212870..b34bc7ff1 100644 --- a/core/src/main/resources/static/css/header.css +++ b/core/src/main/resources/static/css/header.css @@ -2,27 +2,12 @@ header { flex: 0 0 auto; } -.logo-admin { - width: 40px; - height: 40px; - margin-top: 2px; - position: absolute; - right: 55px; - -} - -.logo-admin img { - display: block; - width: 40px; - height: 40px; -} - .main-header { width: 100%; - box-shadow: 0 0 8px rgba(73, 74, 73, 0.2); + box-shadow: 0 0 8px var(--black-shadow); position: fixed; top: 0; - background: white; + background: var(--white); z-index: 5; } @@ -34,19 +19,19 @@ header { max-width: 100%; margin: 0 auto; height: 54px; - display: flex; justify-content: center; +} + +.header, .main-header .main-container-header { + display: flex; align-items: center; - background: white; + background: var(--white); } .header { min-width: 96%; max-width: 1920px; - display: flex; justify-content: space-between; - align-items: center; - background: white; } .header .logo { @@ -56,443 +41,30 @@ header { margin-left: 15px; cursor: pointer; } - -.navigation-menu { - display: flex; - justify-content: space-between; - align-items: center; - width: 100%; - background: white; - font-family: "Open Sans"; -} - -.navigation-menu-left { - width: 100%; - max-width: 930px; -} - -.navigation-menu-left ul { - list-style: none; - margin: 0; - padding: 0; - display: flex; - justify-content: space-between; -} - -.navigation-menu-left ul li { - list-style-type: none; - display: inline-block; -} - -.navigation-menu-left ul li:nth-of-type(6) { - margin-right: 0; - height: 20px; -} - -.navigation-menu-left ul li a { - font-size: 14px; - line-height: 16px; - color: #494a49; - text-decoration: none; -} - -.navigation-menu-left ul .mobile-vis { - display: none; - font-weight: 600; -} - -.navigation-menu-left-col { +.header-content-left{ display: flex; - justify-content: center; - align-items: flex-start; - position: fixed; - top: 53px; - left: 0; - width: 100%; - height: 100%; - background: white; - transition: transform 0.2s ease-in, top 0.2s linear 0.2s; - box-shadow: 0 4px 5px -2px rgba(73, 74, 73, 0.2); - overflow-y: auto; -} - -.navigation-menu-left-col ul { - display: flex; - flex-direction: column; - list-style: none; - margin: 0; - overflow: hidden; - padding: 0; - height: 550px; -} - -.navigation-menu-left-col ul li { - list-style-type: none; - display: flex; - justify-content: center; - align-items: center; - height: 52px; -} - -.navigation-menu-left-col ul li a { - display: block; - font-size: 20px; - line-height: 24px; - color: #494a49; -} - -.navigation-menu-left-col ul li:nth-of-type(6) { - height: 0px; - width: 900px; - border-bottom: 2px solid rgba(73, 74, 73, 0.2); -} - -.navigation-menu-left-col ul .mobile-vis { - display: none; - margin: 0; - list-style-type: none; - justify-content: center; - align-items: center; - height: 52px; -} - -.navigation-menu-left-col ul .mobile-vis .sign-in-link { - color: #056b33; - font-size: 16px; - font-weight: 600; -} - -.navigation-menu-left-col ul .mobile-vis .sign-in-link:hover { - text-decoration-line: none; -} - -.navigation-menu-left-col ul .name { - font-weight: 600; - font-size: 18px; -} - -.navigation-menu-left-col ul .mobile-vis .create-button { - cursor: pointer; - width: auto; - min-width: 138px; - padding: 0 4px 0; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #13aa57; -} - -.navigation-menu-left-col ul .mobile-vis .create-button span { - display: block; - font-size: 16px; - line-height: 16px; - font-weight: 600; -} - -.navigation-menu-right { - width: auto; -} - -.navigation-menu-right ul { - display: flex; - justify-content: space-between; - align-items: center; - margin: 0; - padding: 0; - width: auto; -} - -.navigation-menu-right ul .destroy { - display: none; -} - -.navigation-menu-right ul > li { - list-style-type: none; - margin-right: 20px; -} - -.navigation-menu-right ul > li:nth-of-type(4) { - margin-right: 5px; -} - -.navigation-menu-right ul > li:nth-of-type(5) { - margin-right: 0; -} - -.navigation-menu-right ul .language-switcher { - width: 33px; - cursor: pointer; - outline: none; - appearance: none; - background-color: transparent; - border: none; - -moz-appearance: none; - -webkit-appearance: none; -} - -.navigation-menu-right ul .language-switcher option { - background: white; -} - -.navigation-menu-right ul .switcher-wrapper { - position: relative; - width: 61px; - min-height: 26px; -} - -.navigation-menu-right ul .switcher-wrapper .arrow { - position: absolute; - top: 10px; - right: 19px; - width: 8px; - height: 5px; - z-index: 1; -} - -.navigation-menu-right ul .switcher-wrapper .reverse { - transform: rotate(180deg); - transition: 0.4s ease-out; - transition-delay: 0.1s; -} - -.navigation-menu-right ul .switcher-wrapper ul { - position: absolute; - display: flex; - flex-direction: column; - justify-content: space-between; - top: -8px; - width: 61px; -} - -.navigation-menu-right ul .switcher-wrapper ul li { - cursor: pointer; - height: 44px; - font-size: 14px; - padding-top: 10px; -} - -.navigation-menu-right ul .switcher-wrapper .add-shadow { - background: #fff; - box-shadow: 0px 0px 8px rgba(73, 74, 73, 0.2); - border-radius: 2px; -} - -.navigation-menu-right ul .sign-in-link { - font-size: 12px; - font-weight: 600; -} - -.navigation-menu-right ul .sign-in-link a { - display: block; - min-width: 40px; - word-break: break-all; -} - -.navigation-menu-right ul .sign-up-link { - position: relative; - font-size: 12px; -} - -.navigation-menu-right ul .sign-up-link a { - color: #056b33; -} - -.navigation-menu-right ul .sign-up-link a:hover { - text-decoration-line: none; -} - -.navigation-menu-right ul .sign-up-link .create-button { - cursor: pointer; - width: auto; - min-width: 86px; - padding: 0 4px 0; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #13aa57; -} - -.navigation-menu-right ul .sign-up-link .create-button span { - display: block; - font-size: 12px; - line-height: 12px; - font-weight: 600; -} - -.navigation-menu-right ul li .search { - line-height: 10px; -} - -.secondary-global-button { - border: 1px solid #13AA57; - box-sizing: border-box; - border-radius: 3px; - font-family: Open Sans; - font-style: normal; - font-weight: bold; - font-size: 16px; - line-height: 16px; - color: #13AA57; - text-align: center; -} - -.secondary-global-button:hover, -.secondary-global-button:active { - border: 1px solid #056B33; - line-height: 22px; - color: #056B33; -} - -.tertiary-global-button { - font-family: Open Sans; - font-size: 16px; - font-stretch: normal; - font-style: normal; - font-weight: bold; - line-height: 16px; - color: #056B33; -} - -.tertiary-global-button:hover, -.tertiary-global-button:focus { - color: #13AA57; -} - -.burger-b .menu-icon-wrapper { - width: 24px; - height: 18px; - justify-content: center; - align-items: center; - display: none; -} - -.burger-b .menu-icon-wrapper .menu-icon { - position: relative; - width: 24px; - height: 2px; - border-radius: 14px; - background-color: #494a49; -} - -.tertiary-global-button { - font-family: Open Sans; - font-size: 16px; - font-stretch: normal; - font-style: normal; - font-weight: bold; - line-height: 16px; - color: #056B33; -} - -.burger-b .menu-icon-wrapper .menu-icon:before { - position: absolute; - left: 0; - top: -6px; - content: ''; - width: 24px; - height: 2px; - border-radius: 14px; - background-color: #494a49; - transition: transform 0.2s ease-in, top 0.2s linear 0.2s; -} - -.burger-b .menu-icon-wrapper .menu-icon:after { - position: absolute; - left: 0; - top: 6px; - content: ''; - width: 24px; - height: 2px; - border-radius: 14px; - background-color: #494a49; - transition: transform 0.2s ease-in, top 0.2s linear 0.2s; -} - -.burger-b .menu-icon-wrapper .menu-icon-active { - background-color: transparent; -} - -.burger-b .menu-icon-wrapper .menu-icon-active:before { - transform: rotate(45deg); - top: 0; - transition: top 0.2s linear, transform 0.2s ease-in 0.2s; -} - -.burger-b .menu-icon-wrapper .menu-icon-active:after { - transform: rotate(-45deg); - top: 0; - transition: top 0.2s linear, transform 0.2s ease-in 0.2s; -} - -#user-avatar-wrapper, #lang-wrapper { - position: relative; - width: auto; - min-width: 135px; - height: 36px; -} - -#user-avatar-wrapper ul, #lang-wrapper ul { - display: flex; - flex-direction: column; - position: absolute; - margin: 0; - padding: 10px 0 0 0; - background: #fff; -} - -#user-avatar-wrapper ul li, #lang-wrapper ul li { - width: 100%; - height: 30px; - font-size: 14px; - line-height: 16px; - margin: 0 0 3px 0; - padding: 0 10px 0; - color: #494a49; -} - -#user-avatar-wrapper ul li:first-of-type, #lang-wrapper ul li:first-of-type { - color: #056b33; - font-size: 12px; - font-weight: 600; -} - -#user-avatar-wrapper ul li:first-of-type .reverse, #lang-wrapper ul li:first-of-type .reverse { - transform: rotate(180deg); - transition: 0.4s ease-out; - transition-delay: 0.1s; -} - -#user-avatar-wrapper .text-hidde, #lang-wrapper .text-hidde { - display: none; -} - -#user-avatar-wrapper .add-shadow, #lang-wrapper .add-shadow { - box-shadow: 0 0 8px rgba(73, 74, 73, 0.2); - height: 176px; + justify-content: flex-end; } .dropbtn { width: 62px; height: 25px; - background-color: #ffffff; + background-color: var(--white); text-align: center; border-radius: 5px; - border: solid 1px #ffffff; + border: solid 1px var(--white); } .dropdown:hover .header{ - background-color: #13aa57; - color: #ffffff; - border: solid 1px #ffffff; + background-color: var(--green-emerald); + color: var(--white); + border: solid 1px var(--white); } .dropdown { position: relative; display: inline-block; - margin-left:20px; - + margin-right:20px; } .dropdown-content { @@ -505,7 +77,7 @@ header { } .dropdown-content a { - color: #000000 !important; + color: var(--black) !important; padding: 2px 24px; border-radius: 5px; display: block; @@ -514,22 +86,27 @@ header { .dropdown-content a:hover { text-decoration: none; - color: #ffffff !important; - background-color: #13aa57; + color: var(--white) !important; + background-color: var(--green-emerald); cursor: pointer; } .dropdown:hover .dropdown-content { display:block; - background-color: #ffffff; + background-color: var(--white); } -.active-link { - color: #13aa57 !important; +.logout { + margin-top: 4px; + position: absolute; + right: 40px; } -.active-link:focus { - color: #13aa57; +.login button { + background-color: var(--green-emerald); + border-radius: 4px; + border: none; + color: var(--white); } @media screen and (max-width: 992px) { @@ -615,20 +192,4 @@ header { li:nth-of-type(6) { margin-bottom: 40px; } -} - -.logout { - margin-top: 4px; - position: absolute; - right: 40px; -} - -.login button { - background-color: #13aa57; - border-radius: 4px; - border: none; - color: white; -} - - - +} \ No newline at end of file diff --git a/core/src/main/resources/static/css/main.css b/core/src/main/resources/static/css/main.css index 0f6701ff7..92c4768af 100644 --- a/core/src/main/resources/static/css/main.css +++ b/core/src/main/resources/static/css/main.css @@ -1,114 +1,7 @@ -h1 { - color: #708090; -} - -h2 { - color: #708090; -} - -table { - border-collapse: collapse; -} - -table th, table td { - padding: 5px; -} - -.error { - color: red; - text-align: center; - margin-top: -26px; -} - -/* The popup form - hidden by default */ -.form-popup { - padding-left: 20px; - display: none; - position: absolute; - - z-index: 9; -} - -/* Add styles to the form container */ -.form-container { - max-width: 280px; - height: 600px; - border-top: none; - border-left: 1px solid #9CA7B0; - border-right: 1px solid #9CA7B0; - border-bottom: 1px solid #9CA7B0; padding: 18px; - background-color: white; -} - -/* Full-width input fields */ -.form-container input[type=text], .form-container input[type=password] { - width: 100%; - padding: 15px; - margin: 5px 0 15px 0; - border: none; - background: #f1f1f1; -} - -/* When the inputs get focus, do something */ -.form-container input[type=text]:focus, .form-container input[type=password]:focus { - background-color: #ddd; - outline: none; -} - -/* Set a style for the submit/login button */ -.form-container .btn { - background-color: #04AA6D; - color: white; - padding: 16px 20px; - border: none; - cursor: pointer; - width: 100%; - margin-bottom:10px; - opacity: 0.8; -} - -/* Add a red background color to the cancel button */ -.form-container .cancel { - border: #28a745; - border: 3px; - border-radius: 2px; -} - -/* Add some hover effects to buttons */ -.form-container .btn:hover, .open-button:hover { - opacity: 1; -} - - - -.filter-btn{ - height: 40px; - width: 240px; - border: 1px solid #13AA57; - border-radius: 4px; - background-color: #13AA57; - color: white; - margin-bottom: 30px; -} -.apply-btn{ - height: 40px; - width: 91px; - border: 1px solid #13AA57; - border-radius: 4px; - background-color: #13AA57; - color: white; - margin-left: 18px; -} -.star1{ - background-image: url("/img/star-empty.png"); - -} -.star2{ - background-image: url("/img/star-empty.png"); -} -.star3{ - background-image: url("/img/star-empty.png"); -} -input[type=checkbox]{ - color: #13AA57; -} +@import url(global.css); +@import url(header.css); +@import url(footer.css); +@import url(position.css); +@import url(sidebar.css); +@import url(slider.css); +@import url(table_Modal_Pagination.css); \ No newline at end of file diff --git a/core/src/main/resources/static/css/position.css b/core/src/main/resources/static/css/position.css index a25cfdd46..2da3b7f95 100644 --- a/core/src/main/resources/static/css/position.css +++ b/core/src/main/resources/static/css/position.css @@ -12,10 +12,7 @@ body { .main-content { flex: 1 0 auto; padding-top: 54px; - padding-left: 264px; -} -@media screen and (max-width: 1152px){ - .main-content { - padding-left: 60px; - } + display: grid; + grid-template-columns: auto auto; + min-height: 800px; } \ No newline at end of file diff --git a/core/src/main/resources/static/css/sidebar.css b/core/src/main/resources/static/css/sidebar.css index 600330df1..f472cfedd 100644 --- a/core/src/main/resources/static/css/sidebar.css +++ b/core/src/main/resources/static/css/sidebar.css @@ -149,14 +149,14 @@ margin-left: 5px; transition: .3s; } -block { - display: block; - width: 100%; - position: relative; - background: #444E55; - cursor: pointer; - justify-content: end; -} +/*block {*/ +/* display: block;*/ +/* width: 100%;*/ +/* position: relative;*/ +/* background: #444E55;*/ +/* cursor: pointer;*/ +/* justify-content: end;*/ +/*}*/ .img_blc{ padding-left: 235px; z-index: 4; @@ -171,10 +171,10 @@ block { -o-transform: rotate(180deg); transform: rotate(180deg); } -block:hover img{ - transition: .3s; - background: #444E55; -} +/*block:hover img{*/ +/* transition: .3s;*/ +/* background: #444E55;*/ +/*}*/ .narrow{ diff --git a/core/src/main/resources/static/management/css/slider.css b/core/src/main/resources/static/css/slider.css similarity index 93% rename from core/src/main/resources/static/management/css/slider.css rename to core/src/main/resources/static/css/slider.css index 14171c84b..2e811fb6e 100644 --- a/core/src/main/resources/static/management/css/slider.css +++ b/core/src/main/resources/static/css/slider.css @@ -1,9 +1,9 @@ -.middle { - position: relative; - width: 100%; - max-width: 500px; -} +/*.middle {*/ +/* position: relative;*/ +/* width: 100%;*/ +/* max-width: 500px;*/ +/*}*/ .slider { position: relative; diff --git a/core/src/main/resources/static/management/css/table_Modal_Pagination.css b/core/src/main/resources/static/css/table_Modal_Pagination.css similarity index 79% rename from core/src/main/resources/static/management/css/table_Modal_Pagination.css rename to core/src/main/resources/static/css/table_Modal_Pagination.css index 46050d8f9..efe434065 100644 --- a/core/src/main/resources/static/management/css/table_Modal_Pagination.css +++ b/core/src/main/resources/static/css/table_Modal_Pagination.css @@ -28,7 +28,6 @@ body { } .table-title { - padding-bottom: 15px; color: #212529; padding: 16px 30px; width: 100%; @@ -261,8 +260,7 @@ body { .manager-user { margin: auto; - padding-top: 30px; - padding-bottom: 30px; + padding: 30px; display: flex; flex-wrap: nowrap; align-items: center; @@ -296,7 +294,7 @@ body { border-radius: 5px; border: none; outline: none !important; - margin-left: 20px; + margin: 0 0 20px 20px; } .table-title .btn i { @@ -343,7 +341,7 @@ body { table.table tr th, table.table tr td { line-height: 26px; - border-color: #e9e9e9; + border-color: var(--black-shadow); padding: 7px 10px; vertical-align: middle; } @@ -1392,8 +1390,6 @@ td input[type=search] { .btn div { text-align: center; position: relative; - top: 50%; - transform: translateY(-50%); line-height: 16px; } @@ -1423,22 +1419,23 @@ td input[type=search] { border-radius: 5px; width: 175px; height: 40px; + align-content: center; } -.btn-secondary:hover { +.btn.btn-secondary:hover { border: 1px solid #13AA57; background-color: #13AA57; color: #ffffff; } -.btn-secondary:focus { +.btn.btn-secondary:focus { border: 1px solid #10804E; background-color: #ffffff !important; color: #245740 !important; outline: none; } -.btn-tertiary { +.btn.btn-tertiary { color: #13AA57; background-color: #ffffff; border: 1px solid #13AA57; @@ -1450,7 +1447,7 @@ td input[type=search] { justify-content: center; } -.btn-tertiary:hover { +.btn.btn-tertiary:hover { background-color: #13AA57; color: #ffffff; } @@ -1755,7 +1752,7 @@ a.lightgrey { } .table-child td { - background-color: #f1f1f1; + background-color: inherit; } .align-content-center { @@ -1822,12 +1819,6 @@ a.lightgrey { margin-bottom: 1rem; } -.scrollable-table { - display: block; - white-space: nowrap; - margin-bottom: 0; -} - .table-container::-webkit-scrollbar { display: block; height: 10px; @@ -1998,4 +1989,420 @@ a.lightgrey { width: 100%; height: 65vh; transition: height 0.3s ease; +} + +.navigation-menu { + display: flex; + align-items: center; + background: var(--white); + justify-content: space-between; + width: 100%; + font-family: "Open Sans"; +} + +.navigation-menu-left { + width: 100%; + max-width: 930px; +} + +.navigation-menu-left ul { + list-style: none; + margin: 0; + padding: 0; + display: flex; + justify-content: space-between; +} + +.navigation-menu-left ul li { + list-style-type: none; + display: inline-block; +} + +.navigation-menu-left ul li:nth-of-type(6) { + margin-right: 0; + height: 20px; +} + +.navigation-menu-left ul li a { + font-size: 14px; + line-height: 16px; + color: #494a49; + text-decoration: none; +} + +.navigation-menu-left ul .mobile-vis { + display: none; + font-weight: 600; +} + +.navigation-menu-left-col { + display: flex; + justify-content: center; + align-items: flex-start; + position: fixed; + top: 53px; + left: 0; + width: 100%; + height: 100%; + background: white; + transition: transform 0.2s ease-in, top 0.2s linear 0.2s; + box-shadow: 0 4px 5px -2px rgba(73, 74, 73, 0.2); + overflow-y: auto; +} + +.navigation-menu-left-col ul { + display: flex; + flex-direction: column; + list-style: none; + margin: 0; + overflow: hidden; + padding: 0; + height: 550px; +} + +.navigation-menu-left-col ul li { + list-style-type: none; + display: flex; + justify-content: center; + align-items: center; + height: 52px; +} + +.navigation-menu-left-col ul li a { + display: block; + font-size: 20px; + line-height: 24px; + color: #494a49; +} + +.navigation-menu-left-col ul li:nth-of-type(6) { + height: 0px; + width: 900px; + border-bottom: 2px solid rgba(73, 74, 73, 0.2); +} + +.navigation-menu-left-col ul .mobile-vis { + display: none; + margin: 0; + list-style-type: none; + justify-content: center; + align-items: center; + height: 52px; +} + +.navigation-menu-left-col ul .mobile-vis .sign-in-link { + color: #056b33; + font-size: 16px; + font-weight: 600; +} + +.navigation-menu-left-col ul .mobile-vis .sign-in-link:hover { + text-decoration-line: none; +} + +.navigation-menu-left-col ul .name { + font-weight: 600; + font-size: 18px; +} + +.navigation-menu-left-col ul .mobile-vis .create-button { + cursor: pointer; + width: auto; + min-width: 138px; + padding: 0 4px 0; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #13aa57; +} + +.navigation-menu-left-col ul .mobile-vis .create-button span { + display: block; + font-size: 16px; + line-height: 16px; + font-weight: 600; +} + +.navigation-menu-right { + width: auto; +} + +.navigation-menu-right ul { + display: flex; + justify-content: space-between; + align-items: center; + margin: 0; + padding: 0; + width: auto; +} + +.navigation-menu-right ul .destroy { + display: none; +} + +.navigation-menu-right ul > li { + list-style-type: none; + margin-right: 20px; +} + +.navigation-menu-right ul > li:nth-of-type(4) { + margin-right: 5px; +} + +.navigation-menu-right ul > li:nth-of-type(5) { + margin-right: 0; +} + +.navigation-menu-right ul .language-switcher { + width: 33px; + cursor: pointer; + outline: none; + appearance: none; + background-color: transparent; + border: none; + -moz-appearance: none; + -webkit-appearance: none; +} + +.navigation-menu-right ul .language-switcher option { + background: white; +} + +.navigation-menu-right ul .switcher-wrapper { + position: relative; + width: 61px; + min-height: 26px; +} + +.navigation-menu-right ul .switcher-wrapper .arrow { + position: absolute; + top: 10px; + right: 19px; + width: 8px; + height: 5px; + z-index: 1; +} + +.navigation-menu-right ul .switcher-wrapper .reverse { + transform: rotate(180deg); + transition: 0.4s ease-out; + transition-delay: 0.1s; +} + +.navigation-menu-right ul .switcher-wrapper ul { + position: absolute; + display: flex; + flex-direction: column; + justify-content: space-between; + top: -8px; + width: 61px; +} + +.navigation-menu-right ul .switcher-wrapper ul li { + cursor: pointer; + height: 44px; + font-size: 14px; + padding-top: 10px; +} + +.navigation-menu-right ul .switcher-wrapper .add-shadow { + background: #fff; + box-shadow: 0px 0px 8px rgba(73, 74, 73, 0.2); + border-radius: 2px; +} + +.navigation-menu-right ul .sign-in-link { + font-size: 12px; + font-weight: 600; +} + +.navigation-menu-right ul .sign-in-link a { + display: block; + min-width: 40px; + word-break: break-all; +} + +.navigation-menu-right ul .sign-up-link { + position: relative; + font-size: 12px; +} + +.navigation-menu-right ul .sign-up-link a { + color: #056b33; +} + +.navigation-menu-right ul .sign-up-link a:hover { + text-decoration-line: none; +} + +.navigation-menu-right ul .sign-up-link .create-button { + cursor: pointer; + width: auto; + min-width: 86px; + padding: 0 4px 0; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid #13aa57; +} + +.navigation-menu-right ul .sign-up-link .create-button span { + display: block; + font-size: 12px; + line-height: 12px; + font-weight: 600; +} + +.navigation-menu-right ul li .search { + line-height: 10px; +} + +.secondary-global-button { + border: 1px solid #13AA57; + box-sizing: border-box; + border-radius: 3px; + font-family: Open Sans; + font-style: normal; + font-weight: bold; + font-size: 16px; + line-height: 16px; + color: #13AA57; + text-align: center; +} + +.secondary-global-button:hover, +.secondary-global-button:active { + border: 1px solid #056B33; + line-height: 22px; + color: #056B33; +} + +.tertiary-global-button { + font-family: Open Sans; + font-size: 16px; + font-stretch: normal; + font-style: normal; + font-weight: bold; + line-height: 16px; + color: #056B33; +} + +.tertiary-global-button:hover, +.tertiary-global-button:focus { + color: #13AA57; +} + +.burger-b .menu-icon-wrapper { + width: 24px; + height: 18px; + justify-content: center; + align-items: center; + display: none; +} + +.burger-b .menu-icon-wrapper .menu-icon { + position: relative; + width: 24px; + height: 2px; + border-radius: 14px; + background-color: #494a49; +} + +.tertiary-global-button { + font-family: Open Sans; + font-size: 16px; + font-stretch: normal; + font-style: normal; + font-weight: bold; + line-height: 16px; + color: #056B33; +} + +.burger-b .menu-icon-wrapper .menu-icon:before { + position: absolute; + left: 0; + top: -6px; + content: ''; + width: 24px; + height: 2px; + border-radius: 14px; + background-color: #494a49; + transition: transform 0.2s ease-in, top 0.2s linear 0.2s; +} + +.burger-b .menu-icon-wrapper .menu-icon:after { + position: absolute; + left: 0; + top: 6px; + content: ''; + width: 24px; + height: 2px; + border-radius: 14px; + background-color: #494a49; + transition: transform 0.2s ease-in, top 0.2s linear 0.2s; +} + +.burger-b .menu-icon-wrapper .menu-icon-active { + background-color: transparent; +} + +.burger-b .menu-icon-wrapper .menu-icon-active:before { + transform: rotate(45deg); + top: 0; + transition: top 0.2s linear, transform 0.2s ease-in 0.2s; +} + +.burger-b .menu-icon-wrapper .menu-icon-active:after { + transform: rotate(-45deg); + top: 0; + transition: top 0.2s linear, transform 0.2s ease-in 0.2s; +} + +#user-avatar-wrapper, #lang-wrapper { + position: relative; + width: auto; + min-width: 135px; + height: 36px; +} + +#user-avatar-wrapper ul, #lang-wrapper ul { + display: flex; + flex-direction: column; + position: absolute; + margin: 0; + padding: 10px 0 0 0; + background: #fff; +} + +#user-avatar-wrapper ul li, #lang-wrapper ul li { + width: 100%; + height: 30px; + font-size: 14px; + line-height: 16px; + margin: 0 0 3px 0; + padding: 0 10px 0; + color: #494a49; +} + +#user-avatar-wrapper ul li:first-of-type, #lang-wrapper ul li:first-of-type { + color: #056b33; + font-size: 12px; + font-weight: 600; +} + +#user-avatar-wrapper ul li:first-of-type .reverse, #lang-wrapper ul li:first-of-type .reverse { + transform: rotate(180deg); + transition: 0.4s ease-out; + transition-delay: 0.1s; +} + +#user-avatar-wrapper .text-hidde, #lang-wrapper .text-hidde { + display: none; +} + +#user-avatar-wrapper .add-shadow, #lang-wrapper .add-shadow { + box-shadow: 0 0 8px rgba(73, 74, 73, 0.2); + height: 176px; } \ No newline at end of file diff --git a/core/src/main/resources/static/management/sidebar/sidebar.js b/core/src/main/resources/static/management/sidebar/sidebar.js index 5adcf3a62..b4e3fc7c6 100644 --- a/core/src/main/resources/static/management/sidebar/sidebar.js +++ b/core/src/main/resources/static/management/sidebar/sidebar.js @@ -3,34 +3,18 @@ */ function minimize() { - var menu = document.getElementsByClassName("menuVertical")[0]; - var mainContWidth = document.getElementsByClassName("main-content")[0]; + const menu = document.querySelector(".menuVertical"); + const mainContent = document.querySelector(".main-content"); + const arrow = document.getElementById("maximize"); + menu.classList.toggle("narrow"); - var arrow = document.getElementById("maximize"); - if (menu.classList.contains("narrow")) { - localStorage.setItem("narrow", "narrow"); - for (var u = 0; u < acc.length; u++) { - var pr = acc[u].nextElementSibling; - pr.style.display = "none"; - sessionStorage.removeItem("panel" + u); - } - } else { - localStorage.removeItem("narrow"); - } - if (menu.classList.contains("narrow")) { - if (!arrow.classList.contains("on")) { - arrow.classList.add("on"); - } - } else if (arrow.classList.contains("on")) { - arrow.classList.remove("on"); - } - if (arrow.classList.contains("on")) { - localStorage.setItem("on", "on"); - } else { - localStorage.removeItem("on"); - } + const isNarrow = menu.classList.contains("narrow"); - mainContWidth.style.paddingLeft = parseInt(window.getComputedStyle(menu).width, 10) + "px"; + localStorage.setItem("narrow", isNarrow ? "narrow" : ""); + localStorage.setItem("on", isNarrow ? "on" : ""); + + arrow.classList.toggle("on", isNarrow); + mainContent.style.gridTemplateColumns = `${window.getComputedStyle(menu).width} auto`; } /** @@ -38,36 +22,35 @@ function minimize() { * or resize screen. */ -var btn = document.getElementById("maximize"); +function resizeMenu() { + const menu = document.querySelector(".menuVertical"); + const isNarrow = window.innerWidth < 1150; -btn.addEventListener("click", minimize); -window.addEventListener("resize", resizeMenu); + if (menu.classList.contains("narrow") !== isNarrow) { + minimize(); + } +} -function resizeMenu() { - var wth = window.innerWidth; - var menu = document.getElementsByClassName("menuVertical")[0]; - if (wth > 1150) { - if (menu.classList.contains("narrow")) { - minimize(); - } +// Ініціалізація +document.addEventListener("DOMContentLoaded", () => { + const menu = document.querySelector(".menuVertical"); + const mainContent = document.querySelector(".main-content"); + const arrow = document.getElementById("maximize"); + + if (localStorage.getItem("narrow")) { + menu.classList.add("narrow"); } - if (wth < 1150) { - if (!menu.classList.contains("narrow")) { - minimize(); - } + + if (localStorage.getItem("on")) { + arrow.classList.add("on"); } -} + mainContent.style.gridTemplateColumns = `${window.getComputedStyle(menu).width} auto`; +}); -if (localStorage.getItem("narrow") !== null) { - var menu = document.getElementsByClassName("menuVertical")[0]; - menu.classList.toggle("narrow") -} +document.getElementById("maximize").addEventListener("click", minimize); +window.addEventListener("resize", resizeMenu); -if (localStorage.getItem("on") !== null) { - var minbtn = document.getElementById("maximize"); - minbtn.classList.add("on") -} /** * Script for opening and closing dropdown items. diff --git a/core/src/main/resources/templates/core/about_us.html b/core/src/main/resources/templates/core/about_us.html index 3f892321a..63a06a9f7 100644 --- a/core/src/main/resources/templates/core/about_us.html +++ b/core/src/main/resources/templates/core/about_us.html @@ -9,7 +9,7 @@ - + - diff --git a/core/src/main/resources/templates/core/index.html b/core/src/main/resources/templates/core/index.html index 492ff2ea7..1090dc114 100644 --- a/core/src/main/resources/templates/core/index.html +++ b/core/src/main/resources/templates/core/index.html @@ -5,11 +5,7 @@ GreenCity - - - - - + + - - +
-

[[#{greenCity.index.page.h}]]

+ +
+

[[#{greenCity.index.page.h}]]

+
+ -
+
@@ -56,14 +55,14 @@

[[#{greenCity.achievement.page.h}]]

- +
- +
- - - - + + + + [[#{greenCity.pages.table.id}]] diff --git a/core/src/main/resources/templates/core/management_achievement_statistics.html b/core/src/main/resources/templates/core/management_achievement_statistics.html index b819c7e45..0ac440b5c 100644 --- a/core/src/main/resources/templates/core/management_achievement_statistics.html +++ b/core/src/main/resources/templates/core/management_achievement_statistics.html @@ -3,9 +3,7 @@ [[#{greenCity.sidebar.statistics.achievements}]] - - - + - - + -
+
-

[[#{greenCity.sidebar.statistics.achievements}]]

- - - +
+

[[#{greenCity.sidebar.statistics.achievements}]]

- - + + - - + + + + +
diff --git a/core/src/main/resources/templates/core/management_eco_new.html b/core/src/main/resources/templates/core/management_eco_new.html index 17d9db820..0147a1299 100644 --- a/core/src/main/resources/templates/core/management_eco_new.html +++ b/core/src/main/resources/templates/core/management_eco_new.html @@ -6,11 +6,8 @@ GreenCity - - - - + - + -
+
diff --git a/core/src/main/resources/templates/core/management_eco_news.html b/core/src/main/resources/templates/core/management_eco_news.html index 0f31230ae..b0c01829f 100644 --- a/core/src/main/resources/templates/core/management_eco_news.html +++ b/core/src/main/resources/templates/core/management_eco_news.html @@ -5,11 +5,7 @@ Eco news management - - - - - + - - - - + @@ -50,8 +43,8 @@
-
+
@@ -91,11 +84,11 @@

[[#{greenCity.econews.page.h}]]

- -
- + +
+
- +
@@ -442,43 +435,42 @@

[[#{greenCity.econews.page.h}]]

-
- -
-
-
    -
  • - - -
  • +
+ +
+
+ -
+
  • + + +
  • +
    -
    +
    diff --git a/core/src/main/resources/templates/core/management_econews_statistics.html b/core/src/main/resources/templates/core/management_econews_statistics.html index 62c9aa264..06f56600f 100644 --- a/core/src/main/resources/templates/core/management_econews_statistics.html +++ b/core/src/main/resources/templates/core/management_econews_statistics.html @@ -5,11 +5,7 @@ Eco News Statistic - - - - - - - @@ -39,6 +32,7 @@ + + -
    +
    -
    +
    @@ -250,10 +180,10 @@

    -
    +
    - +
    diff --git a/core/src/main/resources/templates/core/management_fact_of_the_day.html b/core/src/main/resources/templates/core/management_fact_of_the_day.html index 0a2d11d65..39e068568 100644 --- a/core/src/main/resources/templates/core/management_fact_of_the_day.html +++ b/core/src/main/resources/templates/core/management_fact_of_the_day.html @@ -4,9 +4,7 @@ Fact of the day management - - - + - + -
    +
    -
    +
    @@ -51,16 +49,16 @@

    [[#{greenCity.factsOfTheDay.page.h}]]

    - +
    - + -
    [[#{greenCity.pages.table.id}]] + [[#{greenCity.pages.table.id}]] diff --git a/core/src/main/resources/templates/core/management_general_error_page.html b/core/src/main/resources/templates/core/management_general_error_page.html index 28e1f1834..a347fd67c 100644 --- a/core/src/main/resources/templates/core/management_general_error_page.html +++ b/core/src/main/resources/templates/core/management_general_error_page.html @@ -5,9 +5,6 @@ User Habits - - - diff --git a/core/src/main/resources/templates/core/management_general_statistics_for_users.html b/core/src/main/resources/templates/core/management_general_statistics_for_users.html index c48df04aa..cda819220 100644 --- a/core/src/main/resources/templates/core/management_general_statistics_for_users.html +++ b/core/src/main/resources/templates/core/management_general_statistics_for_users.html @@ -5,10 +5,7 @@ Graphs - - - - + + -
    +

    Users city Graph

    diff --git a/core/src/main/resources/templates/core/management_habit_to_do_list_item.html b/core/src/main/resources/templates/core/management_habit_to_do_list_item.html index 696764d8b..d21518b80 100644 --- a/core/src/main/resources/templates/core/management_habit_to_do_list_item.html +++ b/core/src/main/resources/templates/core/management_habit_to_do_list_item.html @@ -4,9 +4,7 @@ Title - - - + - + -
    +
    @@ -48,9 +46,9 @@

    - +
    - + - + - + @@ -135,10 +135,10 @@

    [[#{greenCity.factsOfTheDay.page.h}]]

    diff --git a/core/src/main/resources/templates/core/management_places.html b/core/src/main/resources/templates/core/management_places.html index 510f84081..ddbad30d9 100644 --- a/core/src/main/resources/templates/core/management_places.html +++ b/core/src/main/resources/templates/core/management_places.html @@ -4,9 +4,7 @@ Place management - - - + - - - + -
    -
    +
    @@ -177,114 +151,114 @@

    [[#{greenCity.habit.page.h}]]

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - - - - - - [[#{greenCity.pages.table.id}]] - arrow-icon - - - [[#{greenCity.pages.table.icon}]] - - [[#{greenCity.pages.table.complexity}]] - arrow-icon - - - [[#{greenCity.pages.table.duration}]] - arrow-icon - - [[#{greenCity.pages.table.name}]] - [[#{greenCity.pages.table.description}]] - [[#{greenCity.pages.table.isCustomHabit}]][[#{greenCity.pages.table.isDeleted}]] - [[#{greenCity.pages.table.actions}]] -
    - - - - - - -
    - -
    -
    -
    [[${translation.name}]]
    -
    -
    [[${translation.description}]]
    -
    - - - - - -
    -
    -
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + + + [[#{greenCity.pages.table.id}]] + arrow-icon + + + [[#{greenCity.pages.table.icon}]] + + [[#{greenCity.pages.table.complexity}]] + arrow-icon + + + [[#{greenCity.pages.table.duration}]] + arrow-icon + + [[#{greenCity.pages.table.name}]] + [[#{greenCity.pages.table.description}]] + [[#{greenCity.pages.table.isCustomHabit}]][[#{greenCity.pages.table.isDeleted}]] + [[#{greenCity.pages.table.actions}]] +
    + + + + + + +
    + +
    +
    +
    [[${translation.name}]]
    +
    +
    [[${translation.description}]]
    +
    + + + + - - - +
    +
    diff --git a/core/src/main/resources/templates/core/management_user_personal_page.html b/core/src/main/resources/templates/core/management_user_personal_page.html index 00a9c34a5..513db3590 100644 --- a/core/src/main/resources/templates/core/management_user_personal_page.html +++ b/core/src/main/resources/templates/core/management_user_personal_page.html @@ -3,9 +3,7 @@ User's personal page - - - + - + @@ -64,11 +62,8 @@ -
    - -
    -
    +
    diff --git a/core/src/main/resources/templates/core/management_user_rating.html b/core/src/main/resources/templates/core/management_user_rating.html index ea79dc689..330e2314c 100644 --- a/core/src/main/resources/templates/core/management_user_rating.html +++ b/core/src/main/resources/templates/core/management_user_rating.html @@ -3,9 +3,7 @@ User management - - - + - + -
    +
    @@ -38,23 +36,23 @@

    [[#{greenCity.facts.rating.page.h}]]

    - +
    - + diff --git a/core/src/main/resources/templates/core/management_user_statistics.html b/core/src/main/resources/templates/core/management_user_statistics.html index 2b79f730a..99bccc9d7 100644 --- a/core/src/main/resources/templates/core/management_user_statistics.html +++ b/core/src/main/resources/templates/core/management_user_statistics.html @@ -4,11 +4,7 @@ User management statistic - - - - - + + + + + +
    + +
    +
    +
    +

    Habit Statistics

    + + + + + +
    +
    +
    + + + + + diff --git a/core/src/main/resources/templates/core/sidepanel.html b/core/src/main/resources/templates/core/sidepanel.html index 005335f45..0f2432d0c 100644 --- a/core/src/main/resources/templates/core/sidepanel.html +++ b/core/src/main/resources/templates/core/sidepanel.html @@ -110,7 +110,7 @@
  • - + [[#{greenCity.sidebar.statistics.habits}]] diff --git a/core/src/test/java/greencity/controller/HabitStatisticControllerTest.java b/core/src/test/java/greencity/controller/HabitStatisticControllerTest.java index 4c3b7aa33..c5f7ad21b 100644 --- a/core/src/test/java/greencity/controller/HabitStatisticControllerTest.java +++ b/core/src/test/java/greencity/controller/HabitStatisticControllerTest.java @@ -12,7 +12,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; - import static org.mockito.Mockito.verify; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.web.PageableHandlerMethodArgumentResolver; diff --git a/core/src/test/java/greencity/webcontroller/ManagementHabitStatisticControllerTest.java b/core/src/test/java/greencity/webcontroller/ManagementHabitStatisticControllerTest.java new file mode 100644 index 000000000..ccd46c5b5 --- /dev/null +++ b/core/src/test/java/greencity/webcontroller/ManagementHabitStatisticControllerTest.java @@ -0,0 +1,70 @@ +package greencity.webcontroller; + +import greencity.service.HabitStatisticService; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +@ExtendWith(MockitoExtension.class) +class ManagementHabitStatisticControllerTest { + private MockMvc mockMvc; + + @InjectMocks + private ManagementHabitStatisticController controller; + + @Mock + private HabitStatisticService habitStatisticService; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + } + + @Test + @SneakyThrows + void getStatisticsPageTest() { + mockMvc.perform(get("/management/habit/statistics")) + .andExpect(status().isOk()) + .andExpect(view().name("core/management_habit_statistics")); + } + + @Test + @SneakyThrows + void getUserInterestStatisticsTest() { + mockMvc.perform(get("/management/habit/statistics/interest") + .accept(MediaType.APPLICATION_JSON)); + + verify(habitStatisticService, times(1)).calculateUserInterest(); + } + + @Test + @SneakyThrows + void getHabitBehaviorStatisticsTest() { + mockMvc.perform(get("/management/habit/statistics/habit-behavior") + .accept(MediaType.APPLICATION_JSON)); + + verify(habitStatisticService, times(1)).calculateHabitBehaviorStatistic(); + } + + @Test + @SneakyThrows + void getUserHabitInteractionStatisticsTest() { + mockMvc.perform(get("/management/habit/statistics/user-interaction") + .param("range", "weekly") + .accept(MediaType.APPLICATION_JSON)); + + verify(habitStatisticService, times(1)).calculateInteractions("weekly"); + } +} diff --git a/dao/src/main/java/greencity/entity/Habit.java b/dao/src/main/java/greencity/entity/Habit.java index 47d565ce1..fff0a01e6 100644 --- a/dao/src/main/java/greencity/entity/Habit.java +++ b/dao/src/main/java/greencity/entity/Habit.java @@ -1,5 +1,6 @@ package greencity.entity; +import java.time.LocalDateTime; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -105,4 +106,7 @@ public class Habit { joinColumns = @JoinColumn(name = "habit_id"), inverseJoinColumns = @JoinColumn(name = "user_id")) private Set followers = new HashSet<>(); + + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; } diff --git a/dao/src/main/java/greencity/repository/HabitAssignRepo.java b/dao/src/main/java/greencity/repository/HabitAssignRepo.java index 36125c9e9..64931a2de 100644 --- a/dao/src/main/java/greencity/repository/HabitAssignRepo.java +++ b/dao/src/main/java/greencity/repository/HabitAssignRepo.java @@ -1,5 +1,6 @@ package greencity.repository; +import greencity.dto.habitstatistic.HabitStatusCount; import greencity.entity.Habit; import greencity.entity.HabitAssign; import greencity.entity.User; @@ -441,4 +442,15 @@ void updateProgressNotificationHasDisplayed(@Param("habitAssignId") Long habitAs AND n.id IS NULL """) List getHabitAssignsWithLastDayOfPrimaryDurationToMessage(); + + /** + * Group HabitAssign records by status and count occurrences. + */ + @Query(""" + SELECT new greencity.dto.habitstatistic.HabitStatusCount(ha.status, COUNT(ha)) + FROM HabitAssign ha + WHERE ha.user.userStatus IN (greencity.enums.UserStatus.ACTIVATED) + GROUP BY ha.status + """) + List countHabitAssignsByStatus(); } diff --git a/dao/src/main/java/greencity/repository/HabitRepo.java b/dao/src/main/java/greencity/repository/HabitRepo.java index 8a12b0cd6..80e3a8640 100644 --- a/dao/src/main/java/greencity/repository/HabitRepo.java +++ b/dao/src/main/java/greencity/repository/HabitRepo.java @@ -1,5 +1,6 @@ package greencity.repository; +import greencity.dto.habitstatistic.HabitDateCount; import greencity.entity.Habit; import greencity.entity.HabitAssign; import jakarta.transaction.Transactional; @@ -9,6 +10,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -100,4 +102,68 @@ fallback_habit AS ( LIMIT 1; """) Habit findRandomHabit(); + + /** + * Count distinct users who have created habits (non-deleted) and are active. + */ + @Query(""" + SELECT DISTINCT h.userId + FROM Habit h + JOIN User u ON h.userId = u.id + WHERE h.isDeleted = false AND u.userStatus IN (greencity.enums.UserStatus.ACTIVATED) + """) + List countActiveHabitCreators(); + + /** + * Count distinct users who are followers of non-deleted habits and are active. + */ + @Query(""" + SELECT DISTINCT f.id + FROM Habit h + JOIN h.followers f + WHERE h.isDeleted = false AND f.userStatus IN (greencity.enums.UserStatus.ACTIVATED) + """) + List countActiveHabitFollowers(); + + /** + * Counts the number of habit creations (habits) for a given date range. This + * method aggregates habit creation counts by date within the specified range. + * + * @param startDate The start date (inclusive) of the range. + * @param endDate The end date (inclusive) of the range. + * @return A list of {@link HabitDateCount} objects, each containing a date and + * the count of habit creations for that date. The list is ordered by + * the creation date. + */ + @Query(""" + SELECT new greencity.dto.habitstatistic.HabitDateCount(CAST(h.createdAt AS DATE), COUNT(h)) + FROM Habit h + WHERE h.createdAt BETWEEN :startDate AND :endDate AND h.createdAt IS NOT NULL + GROUP BY CAST(h.createdAt AS DATE) + ORDER BY CAST(h.createdAt AS DATE ) + """) + List countCreationsInRange(LocalDateTime startDate, LocalDateTime endDate); + + /** + * Counts the number of habit subscriptions (followers) for a given date range. + * This method aggregates the count of habit subscriptions by date within the + * specified range. + * + * @param startDate The start date (inclusive) of the range. + * @param endDate The end date (inclusive) of the range. + * @return A list of {@code Object[]} arrays where each element in the array + * represents: - The first element is the date (as a + * {@link java.sql.Date}). - The second element is the count of + * subscriptions for that date (as a {@link Long}). The list is ordered + * by the subscription date. + */ + @Query(value = """ + SELECT CAST(f.created_at AS DATE) AS date, CAST(COUNT(f) AS BIGINT) AS count + FROM habits_followers f + JOIN habits h ON f.habit_id = h.id + WHERE f.created_at BETWEEN :startDate AND :endDate AND f.created_at IS NOT NULL + GROUP BY CAST(f.created_at AS DATE) + ORDER BY CAST(f.created_at AS DATE) + """, nativeQuery = true) + List countSubscriptionsInRangeRaw(LocalDateTime startDate, LocalDateTime endDate); } diff --git a/dao/src/main/java/greencity/repository/UserRepo.java b/dao/src/main/java/greencity/repository/UserRepo.java index 6ec891579..600cc319d 100644 --- a/dao/src/main/java/greencity/repository/UserRepo.java +++ b/dao/src/main/java/greencity/repository/UserRepo.java @@ -894,4 +894,10 @@ uep.emailPreference, uep.periodicity, COUNT(uep.id) GROUP BY uep.emailPreference, uep.periodicity """) List getUserEmailPreferencesDistribution(); + + /** + * Count total active users in the system. + */ + @Query("SELECT COUNT(u) FROM User u WHERE u.userStatus IN (greencity.enums.UserStatus.ACTIVATED) ") + Long countActiveUsers(); } diff --git a/dao/src/main/resources/db/changelog/db.changelog-master.xml b/dao/src/main/resources/db/changelog/db.changelog-master.xml index c5e06e860..bce0abaa6 100644 --- a/dao/src/main/resources/db/changelog/db.changelog-master.xml +++ b/dao/src/main/resources/db/changelog/db.changelog-master.xml @@ -256,4 +256,6 @@ + + diff --git a/dao/src/main/resources/db/changelog/logs/ch-habits-add-created-at-column.xml b/dao/src/main/resources/db/changelog/logs/ch-habits-add-created-at-column.xml new file mode 100644 index 000000000..d46439aef --- /dev/null +++ b/dao/src/main/resources/db/changelog/logs/ch-habits-add-created-at-column.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/dao/src/main/resources/db/changelog/logs/ch-habits-followers-add-created-at-column.xml b/dao/src/main/resources/db/changelog/logs/ch-habits-followers-add-created-at-column.xml new file mode 100644 index 000000000..9cae57663 --- /dev/null +++ b/dao/src/main/resources/db/changelog/logs/ch-habits-followers-add-created-at-column.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/service-api/src/main/java/greencity/dto/habitstatistic/HabitDateCount.java b/service-api/src/main/java/greencity/dto/habitstatistic/HabitDateCount.java new file mode 100644 index 000000000..6681a578e --- /dev/null +++ b/service-api/src/main/java/greencity/dto/habitstatistic/HabitDateCount.java @@ -0,0 +1,9 @@ +package greencity.dto.habitstatistic; + +import java.time.LocalDate; + +public record HabitDateCount(LocalDate date, Long count) { + public HabitDateCount(java.sql.Date date, Long count) { + this(date.toLocalDate(), count); + } +} diff --git a/service-api/src/main/java/greencity/dto/habitstatistic/HabitStatusCount.java b/service-api/src/main/java/greencity/dto/habitstatistic/HabitStatusCount.java new file mode 100644 index 000000000..dd0ab5fc6 --- /dev/null +++ b/service-api/src/main/java/greencity/dto/habitstatistic/HabitStatusCount.java @@ -0,0 +1,6 @@ +package greencity.dto.habitstatistic; + +import greencity.enums.HabitAssignStatus; + +public record HabitStatusCount(HabitAssignStatus status, long count) { +} diff --git a/service-api/src/main/java/greencity/service/HabitStatisticService.java b/service-api/src/main/java/greencity/service/HabitStatisticService.java index 7233c688f..0569b6a46 100644 --- a/service-api/src/main/java/greencity/service/HabitStatisticService.java +++ b/service-api/src/main/java/greencity/service/HabitStatisticService.java @@ -1,13 +1,19 @@ package greencity.service; import greencity.dto.habit.HabitAssignVO; -import greencity.dto.habitstatistic.*; +import greencity.dto.habitstatistic.AddHabitStatisticDto; +import greencity.dto.habitstatistic.HabitDateCount; +import greencity.dto.habitstatistic.HabitStatisticDto; +import greencity.dto.habitstatistic.UpdateHabitStatisticDto; +import greencity.dto.habitstatistic.GetHabitStatisticDto; +import greencity.dto.habitstatistic.HabitItemsAmountStatisticDto; import java.util.List; +import java.util.Map; public interface HabitStatisticService { /** * Method for creating {@code HabitStatistic} by {@code Habit}, {@code User} - * id's and {@link AddHabitStatisticDto} instance. + * id's and {@link greencity.dto.habitstatistic.AddHabitStatisticDto} instance. * * @param habitId {@code Habit} id. * @param userId {@code User} id. @@ -89,4 +95,30 @@ public interface HabitStatisticService { * @param habitAssign {HabitAssign} instance. */ void deleteAllStatsByHabitAssign(HabitAssignVO habitAssign); + + /** + * Calculates user interest statistics, returning the count of interactions or + * engagements for each interest type. + * + * @return A {@link Map} of user interest types and their respective counts. + */ + Map calculateUserInterest(); + + /** + * Calculates statistics on how users interact with habits, such as creating or + * following habits. + * + * @return A {@link Map} of habit behavior types and their respective counts. + */ + Map calculateHabitBehaviorStatistic(); + + /** + * Calculates user interactions (creations and subscriptions) within a specified + * date range. + * + * @param range The date range (e.g., "weekly", "monthly", "yearly"). + * @return A {@link Map} of interaction types ("creations", "subscriptions") and + * their respective counts by date. + */ + Map> calculateInteractions(String range); } diff --git a/service/src/main/java/greencity/service/HabitStatisticServiceImpl.java b/service/src/main/java/greencity/service/HabitStatisticServiceImpl.java index d05f1e962..7eb61c075 100644 --- a/service/src/main/java/greencity/service/HabitStatisticServiceImpl.java +++ b/service/src/main/java/greencity/service/HabitStatisticServiceImpl.java @@ -4,23 +4,35 @@ import greencity.constant.ErrorMessage; import greencity.converters.DateService; import greencity.dto.habit.HabitAssignVO; -import greencity.dto.habitstatistic.*; +import greencity.dto.habitstatistic.HabitDateCount; +import greencity.dto.habitstatistic.HabitStatusCount; import greencity.entity.Habit; import greencity.entity.HabitAssign; import greencity.entity.HabitStatistic; +import greencity.enums.HabitAssignStatus; import greencity.exception.exceptions.BadRequestException; import greencity.exception.exceptions.NotFoundException; import greencity.exception.exceptions.NotSavedException; import greencity.repository.HabitAssignRepo; import greencity.repository.HabitRepo; import greencity.repository.HabitStatisticRepo; +import greencity.dto.habitstatistic.AddHabitStatisticDto; +import greencity.dto.habitstatistic.HabitStatisticDto; +import greencity.dto.habitstatistic.UpdateHabitStatisticDto; +import greencity.dto.habitstatistic.GetHabitStatisticDto; +import greencity.dto.habitstatistic.HabitItemsAmountStatisticDto; import java.time.LocalDate; +import java.time.LocalDateTime; import java.time.Period; import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import greencity.repository.UserRepo; import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.modelmapper.ModelMapper; import org.modelmapper.TypeToken; import org.springframework.cache.annotation.CacheEvict; @@ -30,7 +42,6 @@ import org.springframework.transaction.annotation.Transactional; @Service -@Slf4j @EnableCaching @AllArgsConstructor public class HabitStatisticServiceImpl implements HabitStatisticService { @@ -39,6 +50,7 @@ public class HabitStatisticServiceImpl implements HabitStatisticService { private final HabitRepo habitRepo; private final DateService dateService; private final ModelMapper modelMapper; + private final UserRepo userRepo; /** * {@inheritDoc} @@ -182,4 +194,75 @@ public Long getAmountOfAcquiredHabitsByUserId(Long userId) { public void deleteAllStatsByHabitAssign(HabitAssignVO habitAssignVO) { habitStatisticRepo.deleteAll(habitStatisticRepo.findAllByHabitAssignId(habitAssignVO.getId())); } -} + + /** + * {@inheritDoc} + */ + @Override + public Map calculateUserInterest() { + Long totalActiveUsers = userRepo.countActiveUsers(); + List creators = habitRepo.countActiveHabitCreators(); + List followers = habitRepo.countActiveHabitFollowers(); + Set participatingUsers = new HashSet<>(followers); + participatingUsers.addAll(creators); + + Long nonParticipatingUsers = totalActiveUsers - participatingUsers.size(); + + return Map.of("subscribed", (long) followers.size(), + "creators", (long) creators.size(), + "nonParticipants", nonParticipatingUsers); + } + + /** + * {@inheritDoc} + */ + @Override + public Map calculateHabitBehaviorStatistic() { + List habitStatusCounts = habitAssignRepo.countHabitAssignsByStatus(); + Map counts = habitStatusCounts.stream() + .collect(Collectors.toMap( + HabitStatusCount::status, + HabitStatusCount::count)); + + Arrays.stream(HabitAssignStatus.values()) + .forEach(status -> counts.putIfAbsent(status, 0L)); + + return Map.of( + "giveUp", counts.getOrDefault(HabitAssignStatus.CANCELLED, 0L) + + counts.getOrDefault(HabitAssignStatus.EXPIRED, 0L), + "successfullyComplete", counts.getOrDefault(HabitAssignStatus.ACQUIRED, 0L), + "stayWithHabit", counts.getOrDefault(HabitAssignStatus.INPROGRESS, 0L)); + } + + @Override + public Map> calculateInteractions(String range) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime startDate = calculateStartDate(range); + + List creationStats = habitRepo.countCreationsInRange(startDate, now); + List subscriptionStatsRaw = habitRepo.countSubscriptionsInRangeRaw(startDate, now); + List subscriptionStats = mapToHabitDateCount(subscriptionStatsRaw); + + return Map.of( + "creations", creationStats, + "subscriptions", subscriptionStats); + } + + /** + * Calculate the start date based on the range. + */ + private LocalDateTime calculateStartDate(String range) { + return switch (range.toLowerCase()) { + case "weekly" -> LocalDateTime.now().minusWeeks(1); + case "monthly" -> LocalDateTime.now().minusMonths(1); + case "yearly" -> LocalDateTime.now().minusYears(1); + default -> LocalDateTime.now().minusMonths(1); + }; + } + + private List mapToHabitDateCount(List results) { + return results.stream() + .map(result -> new HabitDateCount((java.sql.Date) result[0], (Long) result[1])) + .collect(Collectors.toList()); + } +} \ No newline at end of file diff --git a/service/src/test/java/greencity/service/HabitStatisticServiceImplTest.java b/service/src/test/java/greencity/service/HabitStatisticServiceImplTest.java index 5ce414d4f..b6b15abad 100644 --- a/service/src/test/java/greencity/service/HabitStatisticServiceImplTest.java +++ b/service/src/test/java/greencity/service/HabitStatisticServiceImplTest.java @@ -7,6 +7,7 @@ import greencity.entity.Habit; import greencity.entity.HabitAssign; import greencity.entity.HabitStatistic; +import greencity.enums.HabitAssignStatus; import greencity.enums.HabitRate; import greencity.exception.exceptions.BadRequestException; import greencity.exception.exceptions.NotFoundException; @@ -14,23 +15,35 @@ import greencity.repository.HabitAssignRepo; import greencity.repository.HabitRepo; import greencity.repository.HabitStatisticRepo; +import java.sql.Date; +import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; - +import greencity.repository.UserRepo; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InjectMocks; import org.mockito.Mock; + +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.*; + import org.modelmapper.ModelMapper; import org.modelmapper.TypeToken; import org.springframework.test.context.junit.jupiter.SpringExtension; +@Slf4j @ExtendWith(SpringExtension.class) class HabitStatisticServiceImplTest { @Mock @@ -45,6 +58,8 @@ class HabitStatisticServiceImplTest { private HabitStatisticRepo habitStatisticRepo; @InjectMocks private HabitStatisticServiceImpl habitStatisticService; + @Mock + private UserRepo userRepo; private ZonedDateTime zonedDateTime = ZonedDateTime.now(); @@ -74,7 +89,7 @@ class HabitStatisticServiceImplTest { @Test void saveByHabitIdAndCorrectUserIdTest() { when(habitStatisticRepo.findStatByDateAndHabitIdAndUserId(addhs.getCreateDate(), - 1L, 1L)).thenReturn(Optional.empty()); + 1L, 1L)).thenReturn(Optional.empty()); when(dateService.convertToDatasourceTimezone(addhs.getCreateDate())).thenReturn(zonedDateTime); when(modelMapper.map(addhs, HabitStatistic.class)).thenReturn(habitStatistic); @@ -89,27 +104,27 @@ void saveByHabitIdAndCorrectUserIdTest() { @Test void saveExceptionTest() { when(habitStatisticRepo.findStatByDateAndHabitIdAndUserId(addhs.getCreateDate(), - 1L, 1L)).thenReturn(Optional.of(new HabitStatistic())); + 1L, 1L)).thenReturn(Optional.of(new HabitStatistic())); assertThrows(NotSavedException.class, () -> habitStatisticService.saveByHabitIdAndUserId(1L, 1L, addhs)); } @Test void saveExceptionWrongHabitAssignTest() { when(habitStatisticRepo.findStatByDateAndHabitIdAndUserId(addhs.getCreateDate(), - 1L, 1L)).thenReturn(Optional.empty()); + 1L, 1L)).thenReturn(Optional.empty()); when(dateService.convertToDatasourceTimezone(addhs.getCreateDate())).thenReturn(zonedDateTime); when(modelMapper.map(addhs, HabitStatistic.class)).thenReturn(habitStatistic); when(habitAssignRepo.findByHabitIdAndUserId(1L, 1L)) - .thenReturn(Optional.empty()); + .thenReturn(Optional.empty()); assertThrows(NotFoundException.class, () -> habitStatisticService.saveByHabitIdAndUserId(1L, 1L, addhs)); } @Test void saveExceptionBadRequestTest() { when(habitStatisticRepo.findStatByDateAndHabitIdAndUserId(addhs.getCreateDate(), - 1L, 1L)).thenReturn(Optional.empty()); + 1L, 1L)).thenReturn(Optional.empty()); when(dateService.convertToDatasourceTimezone(addhs.getCreateDate())) - .thenReturn(zonedDateTime.plusDays(2)); + .thenReturn(zonedDateTime.plusDays(2)); assertThrows(BadRequestException.class, () -> habitStatisticService.saveByHabitIdAndUserId(1L, 1L, addhs)); } @@ -193,9 +208,9 @@ void findAllStatsByWrongHabitId() { @Test void getTodayStatisticsForAllHabitItemsTest() { when(habitStatisticRepo.getStatisticsForAllHabitItemsByDate(zonedDateTime, "en")) - .thenReturn(new ArrayList<>()); + .thenReturn(new ArrayList<>()); assertEquals(new ArrayList(), - habitStatisticService.getTodayStatisticsForAllHabitItems("en")); + habitStatisticService.getTodayStatisticsForAllHabitItems("en")); } @Test @@ -219,4 +234,120 @@ void getAmountOfAcquiredHabitsByUserIdTest() { when(habitStatisticRepo.getAmountOfAcquiredHabitsByUserId(1L)).thenReturn(4L); assertEquals(4L, habitStatisticRepo.getAmountOfAcquiredHabitsByUserId(1L)); } -} \ No newline at end of file + + @Test + void testCalculateUserInterest() { + when(userRepo.countActiveUsers()).thenReturn(100L); + when(habitRepo.countActiveHabitCreators()).thenReturn(List.of(1L, 2L, 3L)); + when(habitRepo.countActiveHabitFollowers()).thenReturn(List.of(4L, 5L)); + + Map result = habitStatisticService.calculateUserInterest(); + + assertNotNull(result); + assertEquals(2L, result.get("subscribed")); + assertEquals(3L, result.get("creators")); + assertEquals(95L, result.get("nonParticipants")); + } + + @Test + void calculateHabitBehaviorStatisticTest() { + List habitStatusCounts = List.of( + new HabitStatusCount(HabitAssignStatus.ACQUIRED, 10L), + new HabitStatusCount(HabitAssignStatus.CANCELLED, 5L), + new HabitStatusCount(HabitAssignStatus.EXPIRED, 3L), + new HabitStatusCount(HabitAssignStatus.INPROGRESS, 8L)); + when(habitAssignRepo.countHabitAssignsByStatus()).thenReturn(habitStatusCounts); + + Map result = habitStatisticService.calculateHabitBehaviorStatistic(); + + assertNotNull(result); + assertEquals(8L, result.get("giveUp")); + assertEquals(10L, result.get("successfullyComplete")); + assertEquals(8L, result.get("stayWithHabit")); + } + + @Test + void calculateInteractionsWeeklyTest() { + LocalDateTime now = LocalDateTime.now(); + + List creationStats = List.of(new HabitDateCount(Date.valueOf(now.toLocalDate()), 5L)); + Object[] row = new Object[] {Date.valueOf(now.toLocalDate()), 3L}; + List subscriptionStatsRaw = new ArrayList<>(); + subscriptionStatsRaw.add(row); + List subscriptionStats = List.of(new HabitDateCount(Date.valueOf(now.toLocalDate()), 3L)); + + when(habitRepo.countCreationsInRange(any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(creationStats); + when(habitRepo.countSubscriptionsInRangeRaw(any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(subscriptionStatsRaw); + + Map> result = habitStatisticService.calculateInteractions("weekly"); + + assertNotNull(result); + assertEquals(2, result.size()); + + assertTrue(result.containsKey("creations")); + assertEquals(creationStats, result.get("creations")); + + assertTrue(result.containsKey("subscriptions")); + assertEquals(subscriptionStats, result.get("subscriptions")); + } + + @ParameterizedTest + @ValueSource(strings = {"weekly", "monthly", "yearly", "invalid"}) + void calculateStartDateTest(String range) { + LocalDateTime startDate = invokeCalculateStartDate(range); + LocalDateTime expectedStartDate; + + switch (range.toLowerCase()) { + case "weekly": + expectedStartDate = LocalDateTime.now().minusWeeks(1); + break; + case "monthly": + expectedStartDate = LocalDateTime.now().minusMonths(1); + break; + case "yearly": + expectedStartDate = LocalDateTime.now().minusYears(1); + break; + default: + expectedStartDate = LocalDateTime.now().minusMonths(1); + break; + } + + assertEquals(expectedStartDate.toLocalDate(), startDate.toLocalDate()); + } + + @SneakyThrows + private LocalDateTime invokeCalculateStartDate(String range) { + var method = HabitStatisticServiceImpl.class.getDeclaredMethod("calculateStartDate", String.class); + method.setAccessible(true); + return (LocalDateTime) method.invoke(habitStatisticService, range); + } + + @Test + void mapToHabitDateCountTest() { + Object[] row1 = {Date.valueOf("2024-12-01"), 5L}; + Object[] row2 = {Date.valueOf("2024-12-02"), 10L}; + List results = List.of(row1, row2); + + List habitDateCounts = invokeMapToHabitDateCount(results); + + assertNotNull(habitDateCounts); + assertEquals(2, habitDateCounts.size()); + + HabitDateCount first = habitDateCounts.getFirst(); + assertEquals(Date.valueOf("2024-12-01").toLocalDate(), first.date()); + assertEquals(5L, first.count()); + + HabitDateCount second = habitDateCounts.get(1); + assertEquals(Date.valueOf("2024-12-02").toLocalDate(), second.date()); + assertEquals(10L, second.count()); + } + + @SneakyThrows + private List invokeMapToHabitDateCount(List results) { + var method = HabitStatisticServiceImpl.class.getDeclaredMethod("mapToHabitDateCount", List.class); + method.setAccessible(true); + return (List) method.invoke(habitStatisticService, results); + } +} From 48eb7aae57e5453fbae687dff9db2d6ccc2d5194 Mon Sep 17 00:00:00 2001 From: KostashchukIryna Date: Mon, 6 Jan 2025 11:03:43 +0200 Subject: [PATCH 34/43] Changed dynamic content localization in notifications (#7971) * added localization when forming notificationDto * change in COMMENT constant * extracted utility methods * changed email forming * formatted files * refactored --- .../greencity/service/CommentServiceImpl.java | 21 +------ .../service/NotificationServiceImpl.java | 10 ++- .../service/UserNotificationServiceImpl.java | 12 +++- .../greencity/utils/NotificationUtils.java | 31 ++++++++++ .../main/resources/notification.properties | 2 +- .../utils/NotificationUtilsTest.java | 62 +++++++++++++++++++ 6 files changed, 114 insertions(+), 24 deletions(-) diff --git a/service/src/main/java/greencity/service/CommentServiceImpl.java b/service/src/main/java/greencity/service/CommentServiceImpl.java index e3cea0541..bf50d1382 100644 --- a/service/src/main/java/greencity/service/CommentServiceImpl.java +++ b/service/src/main/java/greencity/service/CommentServiceImpl.java @@ -53,7 +53,6 @@ import java.util.List; import java.util.Locale; import java.util.Optional; -import java.util.ResourceBundle; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -316,18 +315,10 @@ private NotificationType getNotificationType(ArticleType articleType, CommentAct */ private void createCommentNotification(ArticleType articleType, Long articleId, UserVO userVO, Locale locale) { UserVO receiver = modelMapper.map(getArticleAuthor(articleType, articleId), UserVO.class); - String message = null; - ResourceBundle bundle = ResourceBundle.getBundle("notification", - Locale.forLanguageTag(locale.getLanguage()), - ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT)); long commentsCount = notificationRepo .countActionUsersByTargetUserIdAndNotificationTypeAndTargetIdAndViewedIsFalse(receiver.getId(), getNotificationType(articleType, CommentActionType.COMMENT), articleId); - if (commentsCount >= 1) { - message = (commentsCount + 1) + " " + bundle.getString("COMMENTS"); - } else if (commentsCount == 0) { - message = bundle.getString("COMMENT"); - } + String message = (commentsCount >= 1) ? (commentsCount + 1) + " COMMENTS" : "COMMENT"; userNotificationService.createNotification( receiver, userVO, @@ -374,21 +365,13 @@ private void createCommentLikeNotification(ArticleType articleType, Long article */ private void createCommentReplyNotification(ArticleType articleType, Long articleId, Comment comment, UserVO sender, UserVO receiver, Locale locale) { - ResourceBundle bundle = ResourceBundle.getBundle("notification", - Locale.forLanguageTag(locale.getLanguage()), - ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT)); long replyCount = notificationRepo .countUnviewedRepliesByTargetAndParent( receiver.getId(), getNotificationType(articleType, CommentActionType.COMMENT_REPLY), articleId, comment.getParentComment().getId()); - String message; - if (replyCount >= 1) { - message = (replyCount + 1) + " " + bundle.getString("REPLIES"); - } else { - message = bundle.getString("REPLY"); - } + String message = (replyCount >= 1) ? (replyCount + 1) + " REPLIES" : "REPLY"; userNotificationService.createNotification( receiver, sender, diff --git a/service/src/main/java/greencity/service/NotificationServiceImpl.java b/service/src/main/java/greencity/service/NotificationServiceImpl.java index 8b9031c1c..b7f5961a7 100644 --- a/service/src/main/java/greencity/service/NotificationServiceImpl.java +++ b/service/src/main/java/greencity/service/NotificationServiceImpl.java @@ -39,6 +39,8 @@ import org.springframework.transaction.annotation.Transactional; import static greencity.utils.NotificationUtils.resolveTimesInEnglish; import static greencity.utils.NotificationUtils.resolveTimesInUkrainian; +import static greencity.utils.NotificationUtils.isMessageLocalizationRequired; +import static greencity.utils.NotificationUtils.localizeMessage; @Slf4j @Service @@ -349,8 +351,9 @@ private List getUniqueCategoriesFromPlaces(List places) { private ScheduledEmailMessage createScheduledEmailMessage(Notification notification, String language) { ResourceBundle bundle = ResourceBundle.getBundle("notification", Locale.forLanguageTag(language), ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT)); - String subject = bundle.getString(notification.getNotificationType() + "_TITLE"); - String bodyTemplate = bundle.getString(notification.getNotificationType().toString()); + String notificationType = notification.getNotificationType().toString(); + String subject = bundle.getString(notificationType + "_TITLE"); + String bodyTemplate = bundle.getString(notificationType); String actionUserText; long actionUsersSize = notification.getActionUsers().stream().distinct().toList().size(); if (actionUsersSize > 1) { @@ -361,6 +364,9 @@ private ScheduledEmailMessage createScheduledEmailMessage(Notification notificat actionUserText = ""; } String customMessage = notification.getCustomMessage() != null ? notification.getCustomMessage() : ""; + if (!customMessage.isEmpty() && isMessageLocalizationRequired(notificationType)) { + customMessage = localizeMessage(customMessage, bundle); + } String secondMessage = notification.getSecondMessage() != null ? notification.getSecondMessage() : ""; int messagesCount = notification.getActionUsers().size(); String times = language.equals("ua") diff --git a/service/src/main/java/greencity/service/UserNotificationServiceImpl.java b/service/src/main/java/greencity/service/UserNotificationServiceImpl.java index 7306fbac4..995f21bf6 100644 --- a/service/src/main/java/greencity/service/UserNotificationServiceImpl.java +++ b/service/src/main/java/greencity/service/UserNotificationServiceImpl.java @@ -34,6 +34,8 @@ import java.util.ResourceBundle; import static greencity.utils.NotificationUtils.resolveTimesInEnglish; import static greencity.utils.NotificationUtils.resolveTimesInUkrainian; +import static greencity.utils.NotificationUtils.isMessageLocalizationRequired; +import static greencity.utils.NotificationUtils.localizeMessage; /** * Implementation of {@link UserNotificationService}. @@ -353,13 +355,15 @@ private NotificationDto createNotificationDto(Notification notification, String NotificationDto dto = modelMapper.map(notification, NotificationDto.class); ResourceBundle bundle = ResourceBundle.getBundle("notification", Locale.forLanguageTag(language), ResourceBundle.Control.getNoFallbackControl(ResourceBundle.Control.FORMAT_DEFAULT)); - dto.setTitleText(bundle.getString(dto.getNotificationType() + "_TITLE")); + String notificationType = dto.getNotificationType(); + dto.setTitleText(bundle.getString(notificationType + "_TITLE")); final List uniqueActionUsers = new ArrayList<>(notification.getActionUsers().stream().distinct().toList()); int size = new HashSet<>(uniqueActionUsers).size(); dto.setActionUserText(uniqueActionUsers.stream().map(User::getName).toList()); dto.setActionUserId(uniqueActionUsers.stream().map(User::getId).toList()); - String bodyTextTemplate = bundle.getString(dto.getNotificationType()); + + String bodyTextTemplate = bundle.getString(notificationType); String bodyText; switch (size) { case 1 -> bodyText = bodyTextTemplate; @@ -378,6 +382,10 @@ private NotificationDto createNotificationDto(Notification notification, String } } dto.setBodyText(bodyText); + String message = dto.getMessage(); + if (message != null && isMessageLocalizationRequired(notificationType)) { + dto.setMessage(localizeMessage(message, bundle)); + } return dto; } diff --git a/service/src/main/java/greencity/utils/NotificationUtils.java b/service/src/main/java/greencity/utils/NotificationUtils.java index 2c8854cb0..58117161b 100644 --- a/service/src/main/java/greencity/utils/NotificationUtils.java +++ b/service/src/main/java/greencity/utils/NotificationUtils.java @@ -2,9 +2,15 @@ import lombok.AccessLevel; import lombok.NoArgsConstructor; +import java.util.ResourceBundle; @NoArgsConstructor(access = AccessLevel.PRIVATE) public class NotificationUtils { + private static final String REPLIES = "REPLIES"; + private static final String REPLY = "REPLY"; + private static final String COMMENTS = "COMMENTS"; + private static final String COMMENT = "COMMENT"; + public static String resolveTimesInEnglish(final int number) { return switch (number) { case 1 -> ""; @@ -32,4 +38,29 @@ public static String resolveTimesInUkrainian(int number) { default -> number + " разів"; }; } + + public static boolean isMessageLocalizationRequired(String notificationType) { + return switch (notificationType) { + case "ECONEWS_COMMENT_REPLY", "ECONEWS_COMMENT", + "EVENT_COMMENT_REPLY", "EVENT_COMMENT", + "HABIT_COMMENT", "HABIT_COMMENT_REPLY" -> true; + default -> false; + }; + } + + public static String localizeMessage(String message, ResourceBundle bundle) { + if (message.contains(REPLIES)) { + message = message.replace(REPLIES, bundle.getString(REPLIES)); + } + if (message.contains(REPLY)) { + message = message.replace(REPLY, bundle.getString(REPLY)); + } + if (message.contains(COMMENTS)) { + message = message.replace(COMMENTS, bundle.getString(COMMENTS)); + } + if (message.contains(COMMENT)) { + message = message.replace(COMMENT, bundle.getString(COMMENT)); + } + return message; + } } diff --git a/service/src/main/resources/notification.properties b/service/src/main/resources/notification.properties index f4f343128..9301efe24 100644 --- a/service/src/main/resources/notification.properties +++ b/service/src/main/resources/notification.properties @@ -1,6 +1,6 @@ USERS=users -COMMENT=comment +COMMENT=a comment COMMENTS=comments REPLY=a reply diff --git a/service/src/test/java/greencity/utils/NotificationUtilsTest.java b/service/src/test/java/greencity/utils/NotificationUtilsTest.java index 5aaebfe48..ed5ecc7c2 100644 --- a/service/src/test/java/greencity/utils/NotificationUtilsTest.java +++ b/service/src/test/java/greencity/utils/NotificationUtilsTest.java @@ -4,9 +4,15 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.stream.Stream; +import java.util.ResourceBundle; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; class NotificationUtilsTest { + private static final ResourceBundle uaBundle = ResourceBundle.getBundle("notification_ua"); + private static final ResourceBundle defaultBundle = ResourceBundle.getBundle("notification"); + @ParameterizedTest(name = "Test resolveTimeInUkrainian with input {0}") @MethodSource("provideUkrainianTestCases") void testResolveTimeInUkrainian(int input, String expected) { @@ -48,4 +54,60 @@ private static Stream provideEnglishTestCases() { Arguments.of(11, "11 times"), Arguments.of(21, "21 times")); } + + @ParameterizedTest(name = "Test isMessageLocalizationRequired for valid notification type {0}") + @MethodSource("provideValidNotificationTypes") + void isMessageLocalizationRequiredForValidNotificationTypeTest(String notificationType) { + assertTrue(NotificationUtils.isMessageLocalizationRequired(notificationType)); + } + + private static Stream provideValidNotificationTypes() { + return Stream.of( + "ECONEWS_COMMENT_REPLY", "ECONEWS_COMMENT", + "EVENT_COMMENT_REPLY", "EVENT_COMMENT", + "HABIT_COMMENT", "HABIT_COMMENT_REPLY"); + } + + @ParameterizedTest(name = "Test isMessageLocalizationRequired for invalid notification type {0}") + @MethodSource("provideInvalidNotificationTypes") + void isMessageLocalizationRequiredForInvalidNotificationTypeTest(String notificationType) { + assertFalse(NotificationUtils.isMessageLocalizationRequired(notificationType)); + } + + private static Stream provideInvalidNotificationTypes() { + return Stream.of( + "ECONEWS_LIKE", "EVENT_COMMENT_LIKE", "HABIT_COMMENT_USER_TAG"); + } + + @ParameterizedTest(name = "Test localizeMessage for input {0}") + @MethodSource("provideUkrainianLocalizationTestCases") + void localizeMessageInUaTest(String input, String expected) { + String localizedMessage = NotificationUtils.localizeMessage(input, uaBundle); + assertEquals(expected, localizedMessage); + } + + private static Stream provideUkrainianLocalizationTestCases() { + return Stream.of( + Arguments.of("COMMENT", "коментар"), + Arguments.of("5 COMMENTS", "5 коментарів"), + Arguments.of("REPLY", "відповідь"), + Arguments.of("2 REPLIES", "2 відповіді"), + Arguments.of("comment", "comment")); + } + + @ParameterizedTest(name = "Test localizeMessage for input {0}") + @MethodSource("provideDefaultLocalizationTestCases") + void localizeMessageInDefaultTest(String input, String expected) { + String localizedMessage = NotificationUtils.localizeMessage(input, defaultBundle); + assertEquals(expected, localizedMessage); + } + + private static Stream provideDefaultLocalizationTestCases() { + return Stream.of( + Arguments.of("COMMENT", "a comment"), + Arguments.of("5 COMMENTS", "5 comments"), + Arguments.of("REPLY", "a reply"), + Arguments.of("2 REPLIES", "2 replies"), + Arguments.of("comment", "comment")); + } } \ No newline at end of file From cf8a25e38119e83bda3d2a87b26d56f7c2cee308 Mon Sep 17 00:00:00 2001 From: vnglnk <128087718+holotsvan@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:48:03 +0200 Subject: [PATCH 35/43] add hasAcceptedInvitation field (#7992) * add hasAcceptedInvitation field * formatter + checkstyle --- .../repository/HabitInvitationRepo.java | 42 ++++++++++--------- .../dto/friends/UserFriendHabitInviteDto.java | 1 + .../greencity/service/HabitServiceImpl.java | 2 + .../src/test/java/greencity/ModelUtils.java | 24 +++++++---- .../service/HabitServiceImplTest.java | 3 ++ 5 files changed, 43 insertions(+), 29 deletions(-) diff --git a/dao/src/main/java/greencity/repository/HabitInvitationRepo.java b/dao/src/main/java/greencity/repository/HabitInvitationRepo.java index d8cbb3b8d..b9cbf16e0 100644 --- a/dao/src/main/java/greencity/repository/HabitInvitationRepo.java +++ b/dao/src/main/java/greencity/repository/HabitInvitationRepo.java @@ -56,13 +56,10 @@ public interface HabitInvitationRepo extends JpaRepository findUserFriendsWithHabitInvites( Long userId, String name, Long habitId, Pageable pageable); diff --git a/service-api/src/main/java/greencity/dto/friends/UserFriendHabitInviteDto.java b/service-api/src/main/java/greencity/dto/friends/UserFriendHabitInviteDto.java index df1688e56..caeba9563 100644 --- a/service-api/src/main/java/greencity/dto/friends/UserFriendHabitInviteDto.java +++ b/service-api/src/main/java/greencity/dto/friends/UserFriendHabitInviteDto.java @@ -13,4 +13,5 @@ @SuperBuilder public class UserFriendHabitInviteDto extends UserFriendDto { private Boolean hasInvitation; + private Boolean hasAcceptedInvitation; } diff --git a/service/src/main/java/greencity/service/HabitServiceImpl.java b/service/src/main/java/greencity/service/HabitServiceImpl.java index b6dadb17a..c723adb72 100644 --- a/service/src/main/java/greencity/service/HabitServiceImpl.java +++ b/service/src/main/java/greencity/service/HabitServiceImpl.java @@ -770,6 +770,8 @@ private Page findUserFriendsWithHabitInvitesMapped( .email(tuple.get("email", String.class)) .profilePicturePath(tuple.get("profile_picture", String.class)) .hasInvitation(tuple.get("has_invitation", Boolean.class)) + .hasAcceptedInvitation(tuple.get("has_accepted_invitation", Boolean.class)) + .build()) .collect(Collectors.toList()); return new PageImpl<>(dtoList, pageable, dtoList.size()); diff --git a/service/src/test/java/greencity/ModelUtils.java b/service/src/test/java/greencity/ModelUtils.java index 426c8653a..d6946abc3 100644 --- a/service/src/test/java/greencity/ModelUtils.java +++ b/service/src/test/java/greencity/ModelUtils.java @@ -3282,10 +3282,12 @@ public static List getUserFriendInviteHabitDtoTuple1() { new TupleElementImpl<>(String.class, "email"), new TupleElementImpl<>(String.class, "name"), new TupleElementImpl<>(String.class, "profile_picture"), - new TupleElementImpl<>(Boolean.class, "has_invitation") + new TupleElementImpl<>(Boolean.class, "has_invitation"), + new TupleElementImpl<>(Boolean.class, "has_accepted_invitation") + }, - new String[] {"id", "email", "name", "profile_picture", "has_invitation"}), - new Object[] {2L, "john@example.com", "John", "/image/path/john.png", true}); + new String[] {"id", "email", "name", "profile_picture", "has_invitation", "has_accepted_invitation"}), + new Object[] {2L, "john@example.com", "John", "/image/path/john.png", true, true}); return List.of(tuple); } @@ -3298,10 +3300,12 @@ public static List getUserFriendInviteHabitDtoTuple2() { new TupleElementImpl<>(String.class, "email"), new TupleElementImpl<>(String.class, "name"), new TupleElementImpl<>(String.class, "profile_picture"), - new TupleElementImpl<>(Boolean.class, "has_invitation") + new TupleElementImpl<>(Boolean.class, "has_invitation"), + new TupleElementImpl<>(Boolean.class, "has_accepted_invitation") + }, - new String[] {"id", "email", "name", "profile_picture", "has_invitation"}), - new Object[] {2L, "john@example.com", "John", "/image/path/john.png", false}); + new String[] {"id", "email", "name", "profile_picture", "has_invitation", "has_accepted_invitation"}), + new Object[] {2L, "john@example.com", "John", "/image/path/john.png", false, false}); Tuple tuple2 = new TupleImpl( new TupleMetadata( @@ -3310,10 +3314,12 @@ public static List getUserFriendInviteHabitDtoTuple2() { new TupleElementImpl<>(String.class, "email"), new TupleElementImpl<>(String.class, "name"), new TupleElementImpl<>(String.class, "profile_picture"), - new TupleElementImpl<>(Boolean.class, "has_invitation") + new TupleElementImpl<>(Boolean.class, "has_invitation"), + new TupleElementImpl<>(Boolean.class, "has_accepted_invitation") + }, - new String[] {"id", "email", "name", "profile_picture", "has_invitation"}), - new Object[] {3L, "ivan@example.com", "Ivan", "/image/path/ivan.png", false}); + new String[] {"id", "email", "name", "profile_picture", "has_invitation", "has_accepted_invitation"}), + new Object[] {3L, "ivan@example.com", "Ivan", "/image/path/ivan.png", false, false}); return List.of(tuple1, tuple2); } } diff --git a/service/src/test/java/greencity/service/HabitServiceImplTest.java b/service/src/test/java/greencity/service/HabitServiceImplTest.java index ff809026f..76c0b505d 100644 --- a/service/src/test/java/greencity/service/HabitServiceImplTest.java +++ b/service/src/test/java/greencity/service/HabitServiceImplTest.java @@ -1613,6 +1613,8 @@ void testFindAllFriendsOfUserNoNameProvidedNoInvitations() { assertEquals(2, result.getPage().size()); assertFalse(result.getPage().get(0).getHasInvitation()); assertFalse(result.getPage().get(1).getHasInvitation()); + assertFalse(result.getPage().get(0).getHasAcceptedInvitation()); + assertFalse(result.getPage().get(1).getHasAcceptedInvitation()); assertEquals("John", result.getPage().get(0).getName()); assertEquals("Ivan", result.getPage().get(1).getName()); assertEquals("john@example.com", result.getPage().get(0).getEmail()); @@ -1639,6 +1641,7 @@ void testFindAllFriendsOfUserNameProvidedWithInvitations() { assertNotNull(result); assertTrue(result.getPage().getFirst().getHasInvitation()); + assertTrue(result.getPage().getFirst().getHasAcceptedInvitation()); assertEquals("John", result.getPage().getFirst().getName()); assertEquals("john@example.com", result.getPage().getFirst().getEmail()); assertEquals(2L, result.getPage().getFirst().getId()); From 89435bfb36c476b8a18c9150ef28355c02aff595 Mon Sep 17 00:00:00 2001 From: Yurii Osovskyi <85992215+urio999@users.noreply.github.com> Date: Tue, 7 Jan 2025 10:28:30 +0200 Subject: [PATCH 36/43] added back button on habit page (#7997) --- .../main/resources/templates/core/management_user_habit.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/main/resources/templates/core/management_user_habit.html b/core/src/main/resources/templates/core/management_user_habit.html index 806001b0a..b5f255af3 100644 --- a/core/src/main/resources/templates/core/management_user_habit.html +++ b/core/src/main/resources/templates/core/management_user_habit.html @@ -47,6 +47,9 @@

    + +
    Back
    +

    Habit [[${habit.id}]]

    From 781c8aecd2dd798c81f32fc559dea28587a6409c Mon Sep 17 00:00:00 2001 From: vnglnk <128087718+holotsvan@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:17:07 +0200 Subject: [PATCH 37/43] Bugfix/null invite glitch (#7993) * bugfix * format --- .../repository/HabitInvitationRepo.java | 19 ++++++++++++------- .../greencity/service/HabitServiceImpl.java | 1 - 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/dao/src/main/java/greencity/repository/HabitInvitationRepo.java b/dao/src/main/java/greencity/repository/HabitInvitationRepo.java index b9cbf16e0..e3a38b8f9 100644 --- a/dao/src/main/java/greencity/repository/HabitInvitationRepo.java +++ b/dao/src/main/java/greencity/repository/HabitInvitationRepo.java @@ -78,7 +78,10 @@ relevant_habit_assignments AS ( ), invitations AS ( SELECT DISTINCT - i.invitee_id AS friend_id, + CASE + WHEN i.inviter_id = :userId THEN i.invitee_id + ELSE i.inviter_id + END AS friend_id, CASE WHEN i.status = 'ACCEPTED' THEN TRUE ELSE FALSE @@ -86,20 +89,22 @@ invitations AS ( i.status IN ('PENDING', 'ACCEPTED') AS has_invitation FROM habit_invitations i WHERE i.status IN ('PENDING', 'ACCEPTED') - AND i.inviter_id = :userId - AND i.inviter_habit_assign_id IN ( - SELECT habit_assign_id FROM relevant_habit_assignments + AND ( + (i.inviter_id = :userId AND i.inviter_habit_assign_id + IN (SELECT habit_assign_id FROM relevant_habit_assignments)) + OR + (i.invitee_id = :userId AND i.inviter_habit_assign_id + IN (SELECT habit_assign_id FROM relevant_habit_assignments)) ) ) SELECT f.id, f.name, f.email, f.profile_picture, - inv.has_invitation, - inv.has_accepted_invitation + COALESCE(inv.has_invitation, FALSE) AS has_invitation, + COALESCE(inv.has_accepted_invitation, FALSE) AS has_accepted_invitation FROM filtered_friends f LEFT JOIN invitations inv ON f.id = inv.friend_id - LEFT JOIN relevant_habit_assignments ha ON f.id = ha.friend_id """) List findUserFriendsWithHabitInvites( Long userId, String name, Long habitId, Pageable pageable); diff --git a/service/src/main/java/greencity/service/HabitServiceImpl.java b/service/src/main/java/greencity/service/HabitServiceImpl.java index c723adb72..19c0fb2c6 100644 --- a/service/src/main/java/greencity/service/HabitServiceImpl.java +++ b/service/src/main/java/greencity/service/HabitServiceImpl.java @@ -771,7 +771,6 @@ private Page findUserFriendsWithHabitInvitesMapped( .profilePicturePath(tuple.get("profile_picture", String.class)) .hasInvitation(tuple.get("has_invitation", Boolean.class)) .hasAcceptedInvitation(tuple.get("has_accepted_invitation", Boolean.class)) - .build()) .collect(Collectors.toList()); return new PageImpl<>(dtoList, pageable, dtoList.size()); From e95d8413d911a068b6ba795dff6efe551daf379f Mon Sep 17 00:00:00 2001 From: Maryna Date: Tue, 7 Jan 2025 14:33:43 +0200 Subject: [PATCH 38/43] Fixed nested tables and refactored table_Modal_Pagination.css --- .../resources/static/css/create-event.css | 8 +- core/src/main/resources/static/css/econew.css | 24 +- core/src/main/resources/static/css/events.css | 12 +- core/src/main/resources/static/css/footer.css | 8 +- core/src/main/resources/static/css/global.css | 20 + core/src/main/resources/static/css/habit.css | 10 +- core/src/main/resources/static/css/habits.css | 44 +- core/src/main/resources/static/css/header.css | 66 - .../src/main/resources/static/css/sidebar.css | 49 +- core/src/main/resources/static/css/slider.css | 23 +- .../static/css/table_Modal_Pagination.css | 1560 ++--------------- core/src/main/resources/static/css/user.css | 316 ++++ .../resources/templates/core/about_us.html | 18 +- .../templates/core/management_eco_new.html | 9 +- .../templates/core/management_eco_news.html | 10 +- .../core/management_fact_of_the_day.html | 24 +- .../management_habit_to_do_list_item.html | 14 +- .../templates/core/management_places.html | 56 +- .../core/management_rating_calculation.html | 2 +- .../core/management_rating_deleted.html | 2 +- .../templates/core/management_tags.html | 53 +- .../core/management_to_do_list_items.html | 42 +- .../templates/core/management_user.html | 100 +- .../core/management_user_personal_page.html | 46 +- 24 files changed, 711 insertions(+), 1805 deletions(-) create mode 100644 core/src/main/resources/static/css/user.css diff --git a/core/src/main/resources/static/css/create-event.css b/core/src/main/resources/static/css/create-event.css index 6ee4b5f0f..05f3f1f36 100644 --- a/core/src/main/resources/static/css/create-event.css +++ b/core/src/main/resources/static/css/create-event.css @@ -3,20 +3,20 @@ } .quill-error, .form-error { - color: red; + color: var(--red); font-size: 12px; } .custom-carousel-control { - background-color: rgba(0, 0, 0, 0.5); - border: 2px solid white; + background-color: var(--black-translucent); + border: 2px solid var(--white); border-radius: 50%; width: 50px; height: 50px; display: flex; align-items: center; justify-content: center; - color: white; + color: var(--white); transition: background-color 0.3s ease; position: absolute; top: 50%; diff --git a/core/src/main/resources/static/css/econew.css b/core/src/main/resources/static/css/econew.css index dd2ea0743..32384ae62 100644 --- a/core/src/main/resources/static/css/econew.css +++ b/core/src/main/resources/static/css/econew.css @@ -16,7 +16,7 @@ a, a:active, a:focus, a:hover { } .button-link { - color: #494a49; + color: var(--gray-ash); } .button-content, .news-info, .news-links-images, .top-elements { @@ -39,13 +39,13 @@ a, a:active, a:focus, a:hover { } .tags-item { - color: #666; + color: var(--dark-gray); text-transform: capitalize; font-size: 14px; line-height: 16px; margin: 0 8px; padding: 8px 16px; - border: 1px solid #878787; + border: 1px solid var(--gray-light); border-radius: 25px; } @@ -64,7 +64,7 @@ a, a:active, a:focus, a:hover { .news-title { margin: 0 6.8% 24px; - color: #494a49; + color: var(--gray-ash); font-weight: 500; width: 100%; font-size: calc(19.42857px + 1.42857vw); @@ -83,7 +83,7 @@ a, a:active, a:focus, a:hover { } .news-info-author, .news-info-date { - color: #494a49; + color: var(--gray-ash); font-size: calc(13.71429px + .71429vw); line-height: calc(9.14286px + 2.14286vw); } @@ -107,7 +107,7 @@ a, a:active, a:focus, a:hover { .like_wr .numerosity_likes { font-family: Lato,sans-serif; font-size: 16px; - color: #878787; + color: var(--gray-light); opacity: .6; margin-left: 10px; } @@ -134,7 +134,7 @@ a, a:active, a:focus, a:hover { .news-text{ margin-left: 3rem; - color: #000; + color: var(--black); text-align: left; width: 100%; } @@ -146,7 +146,7 @@ a, a:active, a:focus, a:hover { } .id{ - color:#13AA57; + color: var(--green); margin-right: 10px; font-size: 18px; } @@ -156,12 +156,12 @@ a, a:active, a:focus, a:hover { font-weight: 500; font-size: 24px; line-height: 35px; - color: #313131; + color: var(--black-ash); margin-bottom: 18px; } .counter hr { - background: #e6e6e6; + background: var(--white-smoke); margin-top: 0; margin-bottom: 32px; } @@ -183,13 +183,13 @@ img { input[type=range] { -webkit-appearance: none; - background-color: #13AA57; + background-color: var(--green); height: 3px; border-radius: 2px; } input[type="range"]::-webkit-slider-thumb { - background-color: #13AA57; + background-color: var(--green); -webkit-appearance: none; width: 12px; height: 12px; diff --git a/core/src/main/resources/static/css/events.css b/core/src/main/resources/static/css/events.css index 8fa91894b..4fae4afd4 100644 --- a/core/src/main/resources/static/css/events.css +++ b/core/src/main/resources/static/css/events.css @@ -9,12 +9,12 @@ right: 10px; top: 50%; transform: translateY(-40%); - color: #9CA7B0; + color: var(--gray-gull); } .dropdown-btn { - background-color: #f1f1f1; - color: #333; + background-color: var(--white-smoke); + color: var(--black-ash); padding: 10px; font-size: 16px; border: none; @@ -50,9 +50,9 @@ .select-all-checkbox { width: 18px; height: 18px; - border: 2px solid #4CAF50; + border: 2px solid var(--green); border-radius: 2px; - background-color: #fff; + background-color: var(--white); cursor: pointer; position: relative; margin-right: 8px; @@ -61,6 +61,6 @@ .select-all-label { font-size: 16px; font-weight: 500; - color: #333; + color: var(--black-ash); cursor: pointer; } \ No newline at end of file diff --git a/core/src/main/resources/static/css/footer.css b/core/src/main/resources/static/css/footer.css index b2612b096..60072627a 100644 --- a/core/src/main/resources/static/css/footer.css +++ b/core/src/main/resources/static/css/footer.css @@ -16,7 +16,7 @@ footer { font-family: OpenSans, sans-serif; font-size: 14.07px; line-height: 16px; - color: #494a49 !important; + color: var(--gray-ash) !important; padding-top: 3px; padding-left: 20px; text-decoration: none !important; @@ -51,7 +51,7 @@ footer { font-family: OpenSans, sans-serif; font-size: 14.07px; line-height: 16px; - color: #494a49; + color: var(--gray-ash); margin-right: 32px; padding-top: 3px; } @@ -70,7 +70,7 @@ footer { line-height: 16px; margin: 0; padding-top: 6.9px; - color: #494a49; + color: var(--gray-ash); } .main-background-image .main .links .right-side .social-network-links { @@ -99,7 +99,7 @@ footer { font-family: OpenSans, sans-serif; font-size: 14px; line-height: 16px; - color: #878787; + color: var(--gray-light); } @media screen and (max-width: 1024px) { diff --git a/core/src/main/resources/static/css/global.css b/core/src/main/resources/static/css/global.css index 087332e8e..7f396be32 100644 --- a/core/src/main/resources/static/css/global.css +++ b/core/src/main/resources/static/css/global.css @@ -1,15 +1,35 @@ :root { --black: #000000; --black-shadow: rgba(73, 74, 73, 0.2); + --black-ash: #333; + --black-translucent: rgba(0, 0, 0, 0.5); + --black-chancoal: #ffffff00; --gray: #708090; --red: red; + --red-cherry: #E02116; + --red-cherry-dark: #B21D15; + --red-shadow: rgba(225,83,97,.5); + --orange: #FF9310; --green-emerald: #04AA6D; --green: #13AA57; --green-eucalyptus: #28a745; + --green-light: #B3E6C9; + --green-shadow: #336B4C; --gray-gull: #9CA7B0; + --gray-asphalt: #64727D; + --gray-dark-asphalt: #444E55; + --gray-ash: #494a49; + --gray-light: #878787; + --gray-blue: #566787; + --dark-gray: #666; --white: white; --white-smoke: #f1f1f1; --white-focus: #ddd; + --white-blue: #E3E6E8; + --white-green: #E7F7EE; + --white-gray-blue: #CACFD3; + --light-mustard: #fff7cf; + --light-pink: #FCE0DE; scroll-behavior: smooth; } diff --git a/core/src/main/resources/static/css/habit.css b/core/src/main/resources/static/css/habit.css index a5246f02a..e26185565 100644 --- a/core/src/main/resources/static/css/habit.css +++ b/core/src/main/resources/static/css/habit.css @@ -6,7 +6,7 @@ line-height: 44px; letter-spacing: 0.002em; text-align: left; - color: #000000; + color: var(--black); } .light-text, p { @@ -47,13 +47,13 @@ input[type=range] { -webkit-appearance: none; - background-color: #13AA57; + background-color: var(--green); height: 3px; border-radius: 2px; } input[type="range"]::-webkit-slider-thumb { - background-color: #13AA57; + background-color: var(--green); -webkit-appearance: none; width: 12px; height: 12px; @@ -79,7 +79,7 @@ input[type="range"]::-webkit-slider-thumb { } .column table, td, th { - border: 1px solid black; + border: 1px solid var(--black); border-collapse: collapse; width: 100%; } @@ -124,7 +124,7 @@ input[type="range"]::-webkit-slider-thumb { line-height: 40px; letter-spacing: 0.002em; text-align: left; - color: #000000; + color: var(--black); margin: 0; padding: 0; } diff --git a/core/src/main/resources/static/css/habits.css b/core/src/main/resources/static/css/habits.css index 248fe8579..79ee66809 100644 --- a/core/src/main/resources/static/css/habits.css +++ b/core/src/main/resources/static/css/habits.css @@ -1,16 +1,56 @@ input[type=range] { -webkit-appearance: none; - background-color: #13AA57; + background-color: var(--green); height: 2.5px; border-radius: 2px; } input[type="range"]::-webkit-slider-thumb { - background-color: #13AA57; + background-color: var(--green); -webkit-appearance: none; width: 12px; height: 12px; border-radius: 10px; overflow: visible; cursor: pointer; +} + +.habit_complexity { + white-space: nowrap; +} + + +.btn_modal { + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 8px 24px; + position: static; + left: 0%; + right: 0%; + top: 0%; + bottom: 0%; + border-radius: 4px; + flex: none; + order: 0; + flex-grow: 0; + margin: 0px 0px; + font-size: 16px; + border-color: var(--green); +} + +.save_changes_btn { + background: var(--green); + color: var(--white); +} + +.cancel_btn { + background-color: var(--white); + color: var(--green); +} + +.habit_img { + height: 34px; + width: auto; } \ No newline at end of file diff --git a/core/src/main/resources/static/css/header.css b/core/src/main/resources/static/css/header.css index b34bc7ff1..036b79255 100644 --- a/core/src/main/resources/static/css/header.css +++ b/core/src/main/resources/static/css/header.css @@ -119,22 +119,6 @@ header { .header .logo { margin-left: 14px; } - - .navigation-menu { - width: auto; - } - - .navigation-menu .navigation-menu-left { - display: none; - } - - .burger-b { - margin-left: 5px; - } - - .burger-b .menu-icon-wrapper { - display: inline; - } } @media screen and (max-width: 576px) { @@ -142,54 +126,4 @@ header { margin-top: 15px; margin-left: 16px; } - - .navigation-menu-left-col { - height: 100%; - } - - .navigation-menu-left-col ul .mobile-vis { - display: flex; - } - - .create-button { - min-width: 138px; - min-height: 48px; - } - - .create-button span { - padding: 0 1px; - } - - .navigation-menu-right { - display: flex; - justify-content: flex-end; - align-items: center; - } - - .navigation-menu-right ul .sign-in-link, .navigation-menu-right ul .sign-up-link { - display: none; - } -} - -@media screen and (max-width: 320px) { - .navigation-menu-left-col { - box-shadow: none; - } - - .navigation-menu-left-col ul li:nth-of-type(6) { - border-bottom: 1px solid rgba(73, 74, 73, 0.2); - } - - .create-button { - min-width: 138px; - min-height: 48px; - } - - .create-button span { - padding: 0 1px; - } - - li:nth-of-type(6) { - margin-bottom: 40px; - } } \ No newline at end of file diff --git a/core/src/main/resources/static/css/sidebar.css b/core/src/main/resources/static/css/sidebar.css index f472cfedd..7926978b0 100644 --- a/core/src/main/resources/static/css/sidebar.css +++ b/core/src/main/resources/static/css/sidebar.css @@ -1,4 +1,3 @@ - @font-face { font-family: 'Lato Regular'; src: url('Lato-Regular.eot'); @@ -15,12 +14,12 @@ width: 264px; position: absolute; top: 52px; - background: #444E55; + background: var(--gray-dark-asphalt); overflow:auto; scrollbar-width: none; font-size: 16px; - } + ::-webkit-scrollbar { display: none; } @@ -34,7 +33,6 @@ list-style: none; position: relative; transition: 0.4s; - } .menuVertical ul li { @@ -44,7 +42,6 @@ transition: 0.4s; height: 56px; justify-content: center; - } .menuVertical ul li a, dropdown { @@ -56,18 +53,16 @@ line-height: 2em; padding: 16px 0 16px 16px; text-decoration: none; - - } .menuVertical ul li:hover a, dropdown:hover{ - background: #9CA7B0; - color: #F7F9FA; + background: var(--gray-gull); + color: var(--white-smoke); } .menuVertical ul li:active a, .active_li{ - background: #64727D; - color: #F7F9FA; + background: var(--gray-asphalt); + color: var(--white-smoke); } .accordion { @@ -78,7 +73,7 @@ } .accordion .active { - background: #444E55; + background: var(--gray-dark-asphalt); } .panel{ @@ -106,7 +101,7 @@ width: content-box; box-sizing: border-box; font-weight: normal; - color: #F7F9FA; + color: var(--white-smoke); line-height: 2em; text-decoration: none; } @@ -116,13 +111,13 @@ } .panel li:hover a{ - background: #9CA7B0; - color: #F7F9FA; + background: var(--gray-gull); + color: var(--white-smoke); } .panel li:active a{ - background: #64727D; - color: #F7F9FA; + background: var(--gray-asphalt); + color: var(--white-smoke); } /* Style the accordion panel. Note: hidden by default */ @@ -149,14 +144,7 @@ margin-left: 5px; transition: .3s; } -/*block {*/ -/* display: block;*/ -/* width: 100%;*/ -/* position: relative;*/ -/* background: #444E55;*/ -/* cursor: pointer;*/ -/* justify-content: end;*/ -/*}*/ + .img_blc{ padding-left: 235px; z-index: 4; @@ -171,11 +159,6 @@ -o-transform: rotate(180deg); transform: rotate(180deg); } -/*block:hover img{*/ -/* transition: .3s;*/ -/* background: #444E55;*/ -/*}*/ - .narrow{ width:56px; @@ -195,10 +178,6 @@ display: none; } -#to_be_white{ - color: white; -} - button.close { padding: 2rem 1rem !important; } @@ -213,7 +192,7 @@ button.close:focus { } .cb-tags { - accent-color: #13AA57 + accent-color: var(--green); } diff --git a/core/src/main/resources/static/css/slider.css b/core/src/main/resources/static/css/slider.css index 2e811fb6e..51081c59a 100644 --- a/core/src/main/resources/static/css/slider.css +++ b/core/src/main/resources/static/css/slider.css @@ -1,10 +1,3 @@ - -/*.middle {*/ -/* position: relative;*/ -/* width: 100%;*/ -/* max-width: 500px;*/ -/*}*/ - .slider { position: relative; z-index: 1; @@ -19,7 +12,7 @@ top: 0; bottom: 0; border-radius: 5px; - background-color: #13AA57; + background-color: var(--green); } .slider > .range { position: absolute; @@ -29,14 +22,14 @@ top: 0; bottom: 0; border-radius: 5px; - background-color: #13AA57; + background-color: var(--green); } .slider > .thumb { position: absolute; z-index: 3; width: 17px; height: 17px; - background-color: #13AA57; + background-color: var(--green); border-radius: 50%; /*box-shadow: 0 0 0 0 rgba(98,0,238,.1);*/ /*transition: box-shadow .3s ease-in-out;*/ @@ -49,12 +42,6 @@ right: 25%; transform: translate(15px, -10px); } -/*.slider > .thumb.hover {*/ -/* box-shadow: 0 0 0 20px rgba(98,0,238,.1);*/ -/*}*/ -/*.slider > .thumb.active {*/ -/* box-shadow: 0 0 0 40px rgba(98,0,238,.2);*/ -/*}*/ input[type=range] { position: absolute; @@ -70,7 +57,7 @@ input[type=range]::-webkit-slider-thumb { width: 20px; height: 20px; border-radius: 0; - border: 10px solid white ; - background-color: red; + border: 10px solid var(--white); + background-color: var(--red); -webkit-appearance: none; } \ No newline at end of file diff --git a/core/src/main/resources/static/css/table_Modal_Pagination.css b/core/src/main/resources/static/css/table_Modal_Pagination.css index efe434065..ce179fcb9 100644 --- a/core/src/main/resources/static/css/table_Modal_Pagination.css +++ b/core/src/main/resources/static/css/table_Modal_Pagination.css @@ -1,245 +1,35 @@ -body { - color: #566787; - font-family: 'Open Sans', sans-serif; - font-size: 14px; -} - +body, .table { + font-family: 'Open Sans', sans-serif; font-size: 14px; } -.table-secondary, .table-secondary > td, .table-secondary > th { - background-color: #F5F6F6; -} - .filter-table { - background-color: white; - border: #4b9b25; + background-color: var(--white); + border: var(--green-eucalyptus); border-radius: 2px; font-size: 14px; - color: #4b9b25; + color: var(--green-eucalyptus); } .table-wrapper { - background: #fff; margin-top: 5px; - border-radius: 3px; - box-shadow: 0 1px 1px rgba(0, 0, 0, .05); } .table-title { - color: #212529; padding: 16px 30px; width: 100%; - border-radius: 3px 3px 0 0; -} - -.center { - margin: auto; - display: block; -} - -.page-title h1 { - padding-left: 7px; - font-weight: bold; - font-size: 32px; - color: #13AA57; } .content { display: flex; } -.user-info { - width: 261px; - min-width: 261px; - height: 100%; - padding: 24px; - margin-right: 16px; - overflow-x: auto; - box-shadow: 1px 4px 8px rgba(100, 114, 125, 0.18); -} - -.online-status { - position: relative; - left: 60px; - width: 16px; - height: 16px; - border-radius: 50px; - background: #13AA57; - border: 1px solid #FFFFFF; -} - -.profile-picture { - display: flex; - align-items: center; - justify-content: center; - width: 80px; - height: 80px; - border-radius: 50px; - margin-top: 20px; -} - -.profile-picture.default { - background: linear-gradient(180deg, #abe64d 0%, #4b9b25 100%); - font-style: normal; - font-weight: 600; - font-size: 22px; - line-height: 42px; - color: #fff; - box-sizing: border-box; -} - -.profile-picture.friend { - margin: 7px; - width: 56px; - height: 56px; - font-size: 20px; - line-height: 30px -} - -.friends-list { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: center; -} - -.user-info-text { - margin-top: 10px; - font-style: normal; - font-weight: normal; - font-size: 14px; - color: #64727D; -} - -.username-text { - font-style: normal; - font-weight: bold; - font-size: 18px; - line-height: 28px; - text-align: center; - margin-top: 10px; - letter-spacing: 0.2px; - color: #444E55; -} - -.info-title-text span { - font-style: normal; - font-weight: bold; - font-size: 16px; - line-height: 24px; - letter-spacing: 0.1px; - color: #444E55; -} - -.info-title-text h4 { - font-style: normal; - font-weight: normal; - font-size: 11px; - margin-top: 5px; - color: #9CA7B0; -} - -.info-title-text a { - float: right; - text-decoration: none; - font-style: normal; - font-weight: normal; - font-size: 12px; - line-height: 24px; - color: #9CA7B0; -} - -.block-green { - display: flex; - flex-direction: row; - margin: auto; - align-items: center; - justify-content: center; - width: 89px; - height: 24px; - margin-top: 12px; - background: #B3E6C9; - border-radius: 4px; - font-weight: 600; - font-size: 13px; - color: #336B4C; -} - -.social-networks { - display: flex; - justify-content: center; - align-items: center; - margin-top: 15px; -} - -.social-networks img { - width: 35px; - height: 35px; - padding: 4px; -} - -.divider { - margin: 12px auto auto; - height: 2px; - background-color: #E3E6E8; -} - -.user-tables { - display: flex; - min-width: 800px; -} - -/*.hoverable { - color: red; - cursor: pointer; -} -.hoverable:hover .tooltip { - display: block; -} -.tooltip { - position: absolute; - white-space: nowrap; - display: none; - background: #ffffcc; - border: 1px solid black; - padding: 5px; - z-index: 1000; - color: black; -}*/ - -.tooltip-container { - display: flex; - width: fit-content; - height: fit-content; - padding: 32px; - background: #FFFFFF; - box-shadow: 1px 4px 8px rgba(100, 114, 125, 0.18); - border-radius: 4px; - font-size: 13px; - color: #444E55; -} - -.tooltip-container h2 { - font-size: 13px; - line-height: 28px; - font-weight: bold; - letter-spacing: 0.1px; - color: #444E55; - margin: 0 -} - -.tooltip-container table { - margin-right: 30px; - font-size: 13px; -} - .table-title h2 { display: inline-block; font-weight: bold; font-size: 32px; - color: #13AA57; + color: var(--green); } .table-header { @@ -255,7 +45,7 @@ body { font-weight: bold; font-size: 25px; letter-spacing: 0.002em; - color: #444E55; + color: var(--gray-dark-asphalt); } .manager-user { @@ -282,14 +72,9 @@ body { width: 200px; } -.table-title .btn-group { - float: right; -} - .table-title .btn { - color: #fff; + color: var(--white); font-size: 14px; - border: none; min-width: 50px; border-radius: 5px; border: none; @@ -303,11 +88,6 @@ body { margin-right: 5px; } -.table-title .btn span { - float: left; - margin-top: 2px; -} - .table-responsive.userpage { max-height: 286px; } @@ -318,25 +98,16 @@ body { } .table-responsive.userpage::-webkit-scrollbar-track { - background: #f1f1f1; + background: var(--white-smoke); } .table-responsive.userpage::-webkit-scrollbar-thumb { - background: #64727D; + background: var(--gray-asphalt); border-radius: 50px; } .table-responsive.userpage::-webkit-scrollbar-thumb:hover { - background: #555; -} - -.table-primary { - position: sticky; - top: 0; -} - -.table-bordered.userpage { - margin-bottom: 0; + background: var(--dark-gray); } table.table tr th, table.table tr td { @@ -347,8 +118,8 @@ table.table tr th, table.table tr td { } table.table tr th { - background-color: #444E55; - color: #EFF0F1; + background-color: var(--gray-dark-asphalt); + color: var(--white-smoke); font-weight: normal; } @@ -375,12 +146,12 @@ table.table td:last-child i.fa-star { opacity: 0.9; font-size: 19px; margin: 0; - color: #13AA57; + color: var(--green); } table.table td a { font-weight: bold; - color: #566787; + color: var(--gray-blue); display: inline-block; text-decoration: none; outline: none !important; @@ -388,35 +159,23 @@ table.table td a { table.table td a.id-link { font-weight: normal; - color: #13AA57; + color: var(--green); } table.table td a:hover { - color: #13AA57; + color: var(--green); } table.table td a.edit { - color: #FF9310; + color: var(--orange); } table.table td a.delete { - color: #E02116; + color: var(--red); } table.table td a.add { - color: #008000; -} - -table.table td a.deactivate-user { - color: #000000; -} - -table.table td a.activate-user { - color: #000000; -} - -.statistic-style { - color: #13AA57; + color: var(--green-eucalyptus); } table.table td i { @@ -439,7 +198,7 @@ table.table .avatar { font-size: 14px; min-width: 30px; min-height: 30px; - color: #444E55; + color: var(--gray-dark-asphalt); margin: 0 2px; line-height: 30px; border-radius: 2px !important; @@ -448,21 +207,17 @@ table.table .avatar { } .pagination li a:hover { - color: #666; + color: var(--dark-gray); } .pagination li.active a, .pagination li.active a.page-link { - background: #FFFFFF; - color: #444E55; - border: 1px solid #444E55; -} - -.pagination li.active a:hover { - /*background: #245740;*/ + background: var(--white); + color: var(--gray-dark-asphalt); + border: 1px solid var(--gray-dark-asphalt); } .pagination li.disabled i { - color: #ccc; + color: var(--white-focus); } .pagination li i { @@ -485,133 +240,33 @@ table.table .avatar { flex-grow: 1; } - -.custom-checkbox-select-all { - padding-top: 10px; - margin-left: 40px; - position: relative; -} - -.custom-checkbox-select-all input[type="checkbox"] { - opacity: 0; - position: absolute; - margin: 5px 0 0 3px; - z-index: 9; - border: 1px solid; -} - -.custom-checkbox-select-all label:before { - content: ''; - display: inline-block; - vertical-align: text-top; - background: #f0f0f0; - border: 1px solid #f0f0f0; - border-radius: 2px; - box-sizing: border-box; - z-index: 2; - width: 18px; - height: 18px; -} - -.custom-checkbox-select-all input[type="checkbox"]:checked + label:after { - margin-top: 10px; - content: ''; - position: absolute; - left: 6px; - top: 3px; - width: 6px; - height: 11px; - border: solid #000; - border-width: 0 3px 3px 0; - transform: inherit; - z-index: 3; - transform: rotateZ(45deg); -} - -.custom-checkbox-select-all input[type="checkbox"]:checked + label:before { - border-color: #245740; - background: #245740; -} - -.custom-checkbox-select-all input[type="checkbox"]:checked + label:after { - border-color: #fff; -} - -.custom-checkbox-select-all input[type="checkbox"]:disabled + label:before { - color: #f0f0f0; - cursor: auto; - box-shadow: none; - background: #f0f0f0; -} - -.custom-checkbox { - position:relative; -} - -.custom-checkbox.userpage { - position: relative; - margin: 0px; -} - .custom-checkbox input[type="checkbox"] { - opacity: 0; - position: absolute; - z-index: 9; -} - -.custom-checkbox .modal-checkbox label:before { - border: 1px solid green; -} - -.custom-checkbox label { - position: relative; - display: inline-block; + appearance: none; + width: 20px; + height: 20px; cursor: pointer; - margin:0; -} - -.custom-checkbox label:before { - content: ''; - display: inline-block; - vertical-align: text-top; - background: #FFFFFF; - border: 1px solid #9CA7B0; - box-sizing: border-box; + position: relative; + background: var(--white); + border: 1px solid var(--gray-gull); border-radius: 3px; - z-index: 2; - width: 18px; - height: 18px; - transition: background-color 0.3s, border-color 0.3s; -} - -.custom-checkbox input[type="checkbox"]:checked + label:after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 6px; - height: 11px; - border: solid #ffffff 1px; - border-width: 0 3px 3px 0; - transform: translate(-50%, -50%) rotate(45deg); - z-index: 10; + vertical-align: middle; + text-align: center; } -.custom-checkbox input[type="checkbox"]:checked + label:before, -.custom-checkbox input[type="checkbox"].indeterminate + label:before{ - border-color: #13AA57; - background: #13AA57; +.custom-checkbox input[type="checkbox"]:checked { + background-color: var(--white); } -.custom-checkbox input[type="checkbox"].indeterminate + label:after { +.custom-checkbox input[type="checkbox"]:checked::after{ content: ''; position: absolute; - top: 50%; - left: 50%; - width: 10px; - height: 2px; - background: #ffffff; - transform: translate(-50%, -50%); + top: 1px; + left: 5px; + width: 8px; + height: 13px; + border: solid var(--green); + border-width: 0 4px 4px 0; + transform: rotate(45deg); } .column-role{ @@ -621,10 +276,10 @@ table.table .avatar { .dropbtn.role { width: auto; height: 25px; - background-color: #ffffff; + background-color: var(--white); text-align: center; border-radius: 0; - border: solid 1px #ffffff; + border: solid 1px var(--white); } .dropdown-content.role { @@ -634,7 +289,7 @@ table.table .avatar { position: absolute; margin-top: 25px; margin-left: -10px; - border: 1px solid #CACFD3; + border: 1px solid var(--white-gray-blue); border-top: none; border-radius: 0; } @@ -647,19 +302,18 @@ table.table .avatar { .dropdown:hover .dropdown-content.role { display: block; - background-color: #ffffff; + background-color: var(--white); z-index: 10000; border-radius: 0; } .dropdown-content.role a { - color: #000000; + color: var(--black); text-decoration: none; display: block; font-weight: normal; border-radius: 0; - } .dropdown.role { @@ -669,14 +323,8 @@ table.table .avatar { } .dropdown-content.role a:hover { - color: #ffffff; - background-color: #CACFD3; -} - -.button-container { - width: 79px; - align-items: center; - display: flex; + color: var(--white); + background-color: var(--white-gray-blue); } .button-arrow { @@ -696,16 +344,16 @@ table.table .avatar { padding-left: 15px; width: 104px; height: 26px; - background: #9CA7B0; + background: var(--gray-gull); border-radius: 4px; font-weight: 600; font-size: 13px; - color: #444E55; + color: var(--gray-dark-asphalt); } .dropbtn.userpage.green { - background: #B3E6C9; - color: #336B4C; + background: var(--green-light); + color: var(--green-shadow); } .dropdown.userpage { @@ -729,8 +377,8 @@ table.table .avatar { .dropdown-content.userpage input { width: 104px; text-align: left; - color: #000000; - background-color: #ffffff; + color: var(--black); + background-color: var(--white); border: none; border-radius: 5px; padding: 2px 0px 0px 15px; @@ -739,14 +387,14 @@ table.table .avatar { } .dropdown-content.userpage input:hover { - color: #ffffff; - background-color: #9CA7B0; + color: var(--white); + background-color: var(--gray-gull); } .dropdown:hover .dropbtn.userpage { - background-color: #CACFD3; - color: #ffffff; - border: solid 1px #ffffff; + background-color: var(--white-gray-blue); + color: var(--white); + border: solid 1px var(--white); } .dropdown.userpage.table-button { @@ -759,8 +407,8 @@ table.table .avatar { width: 90px; height: 25px; padding-left: 5px; - background: #ffffff; - color: #000000; + background: var(--white); + color: var(--black); font-size: 14px; font-weight: normal; } @@ -778,10 +426,10 @@ table.table .avatar { .dropbtn.status { width: 150px; height: 25px; - background-color: #ffffff00; + background-color: var(--black-chancoal); text-align: left; border-radius: 0; - border: solid 1px #ffffff00; + border: solid 1px var(--black-chancoal); } .dropdown-content.status { @@ -791,7 +439,7 @@ table.table .avatar { position: absolute; margin-top: 25px; margin-left: -10px; - border: 1px solid #CACFD3; + border: 1px solid var(--white-gray-blue); border-top: none; border-radius: 0; } @@ -802,33 +450,6 @@ table.table .avatar { height: 6px; } -/*.dropbtn.access { - display: flex; - align-items: center; - justify-content: center; - width: 80px; - height: 25px; -} -.dropdown-content.access { - width: 80px; - height: 44px; - margin-top: 25px; -} -.dropdown-content.access a { - line-height: 19px; - color: #000000; - text-decoration: none; - border-radius: 5px; - padding: 2px; - display: block; - font-weight: normal; -} -.dropdown.access { - position: static; - display: flex; - margin-left: 0; -}*/ - .table-filter-icon { background-image: url("/img/filter-icon.svg"); width: 16px; @@ -857,141 +478,44 @@ table.table .avatar { margin-top: 5px; } -.cut-text { - text-overflow: ellipsis; - overflow: hidden; - width: 100%; - max-width: 247px; - white-space: nowrap; - display: flex; - justify-content: space-between; +.form-search.filter { + width: 264px; + margin-left: 24px; } -.name-filter-form { - background-color: #FFFFFF; - position: absolute; - width: 313px; - height: 260px; - margin-top: 17px; - border-radius: 4px; - display: none; - z-index: 1; +.hide-button { + display: inline-block; + width: 75px; + padding-left: 20px; } -.filter-sorting-buttons { +.hide-button button { display: flex; - justify-content: center; - margin-bottom: 10px; - margin-top: 25px; -} - -.form-search.filter { - width: 264px; - margin-left: 24px; + border: none; + outline: none; + background: none; + align-items: center; } -.filter-buttons { - display: flex; - justify-content: flex-end; +.hide-button button span { + padding-right: 5px; + font-size: 13px; + color: var(--gray-asphalt); } -.sort-buttons.asc { - margin-right: 7px; - width: 124px; -} - -.sort-buttons { - border: 1px solid #13AA57; - background-color: #FFFFFF; - color: #13AA57; - border-radius: 5px; - width: 133px; - height: 40px; - font-weight: bold; - font-size: 16px; -} - -.filter-checkbox { - display: flex; - margin-top: 20px; -} - -.filter-checkbox-input { - height: 16px; - width: 16px; - margin-left: 25px; -} - -.filter-checkbox-text { - margin-left: 12px; - margin-bottom: 33px; - font-style: normal; - font-weight: normal; - font-size: 14px; - line-height: 20px; - color: #1D2830; -} - -.filter-buttons { - justify-content: center; -} - -.filter-cancel { - border: 1px solid #13AA57; - background-color: #FFFFFF; - color: #13AA57; - border-radius: 5px; - width: 98px; - height: 40px; - margin-right: 16px; - font-weight: bold; - font-size: 16px; -} - -.filter-apply { - border: 1px solid #13AA57; - background-color: #13AA57; - color: #FFFFFF; - border-radius: 5px; - width: 144px; - height: 40px; - font-weight: bold; - font-size: 16px; -} - -.hide-button { - display: inline-block; - width: 75px; - padding-left: 20px; -} - -.hide-button button { - display: flex; - border: none; - outline: none; - background: none; - align-items: center; -} - -.hide-button button span { - padding-right: 5px; - font-size: 13px; - color: #64727D; -} - -.hide-button:focus { - outline: none; +.hide-button:focus { + outline: none; } .dropdown:hover .dropdown-content.status { display: block; - background-color: #ffffff; + background-color: var(--white); z-index: 10000; border-radius: 0; } .dropdown-content.status a { - color: #000000; + color: var(--black); text-decoration: none; display: block; font-weight: normal; @@ -1004,9 +528,9 @@ table.table .avatar { } .dropdown-content.status a:hover { - color: #000000 !important; - background-color: #9CA7B0; - border-radius: 0px; + color: var(--black) !important; + background-color: var(--gray-gull); + border-radius: 0; } .dropbtn { @@ -1017,7 +541,7 @@ table.table .avatar { .size { margin-top: 4px; margin-left: 4.5px; - color: #9CA7B0; + color: var(--gray-gull); text-align: center; box-sizing: border-box; } @@ -1031,7 +555,7 @@ table.table .avatar { position: absolute; margin-top: 5px; margin-left: -1px; - border: solid 1px #336B4C; + border: solid 1px var(--green-shadow); } .change_color { @@ -1044,8 +568,7 @@ table.table .avatar { .dropdown:hover .dropdown-content.size { display: block; - background-color: #ffffff; - + background-color: var(--white); } .dropdown-content.size a { @@ -1065,7 +588,7 @@ table.table .avatar { height: 36px; text-align: center; border-radius: 4px; - border: solid 1px #9CA7B0; + border: solid 1px var(--gray-gull); position: relative; display: inline-block; margin-left: 0 !important; @@ -1074,8 +597,8 @@ table.table .avatar { } .dropdown-content.size a:hover { - color: #9CA7B0; - background-color: #336B4C; + color: var(--gray-gull); + background-color: var(--green-shadow); box-sizing: border-box; height: 35px; text-align: center; @@ -1092,13 +615,6 @@ table.table .avatar { font-size: 16px; } -. input[type="checkbox"]:disabled + label:before { - color: #b8b8b8; - cursor: auto; - box-shadow: none; - background: #ddd; -} - .modal .modal-dialog { width: 660px; max-width: 660px; @@ -1111,72 +627,14 @@ table.table .avatar { margin-top: 20px; } -.modal-title-deac { - font-style: normal; - font-weight: bold; - font-size: 20px; - line-height: 28px; - letter-spacing: 0.002em; - color: #313131; - padding-top: 40px; - margin-bottom: 0; -} - -.deactivation-reasons { - color: black; - margin-bottom: 0; - margin-left: 12px; -} - -.modal-reasons { - display: flex; - align-items: center; -} - -.modal-title-act { - text-align: center !important; - padding-top: 40px; - padding-bottom: 25px; -} - -.modal-body-act1 { - margin: 0 !important; - padding-left: 7px; -} - -.modal-body-act { - margin: 0 !important; -} - .reason { margin-left: 25px; - color: #000000; + color: var(--black); margin-top: 5px; width: 100%; display: inline-block; } -.user-lang { - width: 100%; - display: inline-block; - margin-top: 4px; -} - -.custom-reason { - width: 100%; - padding: 8px; - height: 94px; - background: #FFFFFF; - border: 1px solid #9CA7B0; - border-radius: 4px; - order: 1; - margin-bottom: 24px; -} - -.custom-reason:focus { - border-color: red; -} - .modal .modal-header, .modal .modal-body, .modal .modal-footer { padding: 0 40px; border: none !important; @@ -1201,13 +659,13 @@ table.table .avatar { .modal { border-radius: 3px; box-shadow: none; - border: 1px solid #878787; + border: 1px solid var(--gray-light); } .form-control { border-radius: 3px; box-shadow: none; - border: 1px solid #ced4da; + border: 1px solid var(--white-gray-blue); } .form-search { @@ -1216,24 +674,15 @@ table.table .avatar { justify-content: center; align-items: center; padding: 8px; - position: static; width: 500px; height: 36px; left: calc(50% - 500px / 2); top: calc(50% - 36px / 2); - - /* Greyscale/00.White */ - - background: #FFFFFF; - /* Greyscale/05 */ - - border: 1px solid #9CA7B0; + background: var(--white); + border: 1px solid var(--gray-gull); box-sizing: border-box; border-radius: 4px; - - /* Inside Auto Layout */ - flex: none; order: 1; align-self: stretch; @@ -1254,7 +703,7 @@ table.table .avatar { } .errorSpan { - color: red; + color: var(--red); } .main-search { @@ -1272,53 +721,27 @@ table.table .avatar { visibility: hidden; } -.search-box { - position: relative; - float: left; - margin-top: 10px; -} - -.search-box input { - height: 34px; - border-radius: 20px; - padding-left: 35px; - border-color: #ddd; - box-shadow: none; -} - .search-box-right { position: relative; float: right; margin-left: 10px; } -.search-box i { - color: #a0a5b1; - position: absolute; - font-size: 19px; - top: 8px; - left: 10px; -} - -.search-box input:focus { - border-color: #245740; -} - .search-box-right input { height: 34px; border: none; border-radius: 0; padding-left: 8px; - border-bottom: #E6E6E6 1px solid; + border-bottom: var(--white-blue) 1px solid; } .search-box-right input:focus { - border-color: #245740; + border-color: var(--green-shadow); outline: none; } .search-box-right i { - color: #a0a5b1; + color: var(--gray-gull); position: absolute; font-size: 19px; top: 8px; @@ -1335,7 +758,7 @@ table.table .avatar { } .filter-button { - border: 1px solid #9CA7B0; + border: 1px solid var(--gray-gull); display: flex; flex-direction: row; justify-content: center; @@ -1357,18 +780,17 @@ table.table .avatar { flex: none; order: 0; flex-grow: 0; - margin-left: 20px; height: 36px; - color: #9CA7B0; - background-color: white; + color: var(--gray-gull); + background-color: var(--white); margin-top: 5px; } td input[type=search] { border: none; border-bottom: 1px solid; - background-color: #f2f5f7; + background-color: transparent; width: 110px; margin: 0; box-sizing: border-box; @@ -1380,11 +802,16 @@ td input[type=search] { cursor: pointer; } +/*Button*/ + .btn { min-width: 122px; min-height: 48px; border-radius: 5px; font-weight: 600; + align-content: center; + justify-content: center; + text-align: center; } .btn div { @@ -1394,8 +821,8 @@ td input[type=search] { } .btn-primary { - background-color: #13AA57; - border: 1px solid #13AA57; + background-color: var(--green); + border: 1px solid var(--green); box-sizing: border-box; border-radius: 4px; width: 98px; @@ -1404,108 +831,90 @@ td input[type=search] { } .btn-primary:hover { - background-color: #245740; + background-color: var(--green-shadow); } .btn-primary:focus { - background-color: #245740; + background-color: var(--green-shadow); outline: none; } .btn.btn-secondary { - border: 1px solid #13AA57; - background-color: #ffffff; - color: #13AA57; + border: 1px solid var(--green); + background-color: var(--white); + color: var(--green); border-radius: 5px; width: 175px; height: 40px; - align-content: center; } .btn.btn-secondary:hover { - border: 1px solid #13AA57; - background-color: #13AA57; - color: #ffffff; + border: 1px solid var(--green); + background-color: var(--green); + color: var(--white); } .btn.btn-secondary:focus { - border: 1px solid #10804E; - background-color: #ffffff !important; - color: #245740 !important; + border: 1px solid var(--green-shadow); + background-color: var(--white) !important; + color: var(--green-shadow) !important; outline: none; } .btn.btn-tertiary { - color: #13AA57; - background-color: #ffffff; - border: 1px solid #13AA57; + color: var(--green); + background-color: var(--white); + border: 1px solid var(--green); box-sizing: border-box; border-radius: 4px; - width: 98px; + min-width: 98px; height: 40px; display: flex; - justify-content: center; } .btn.btn-tertiary:hover { - background-color: #13AA57; - color: #ffffff; + background-color: var(--green); + color: var(--white); } .btn-tertiary:focus { - color: #13AA57; + color: var(--green); box-shadow: 0 0 0 0.2rem rgb(76 175 80 / 25%); } -.btn-remove { - color: #ffffff; +.table-title .btn-remove { + border: 1px solid var(--red-cherry); + background-color: var(--white); + border-radius: 5px; + color: var(--red-cherry); width: 130px; height: 40px; - align-items: baseline; - display: flex; - justify-content: center; - background: #13AA57; - border-radius: 4px; - } #deactivateOneSubmit{ margin-top: 25px; } -.btn-remove:hover { - color: #fff; - background-color: #B21D15; - +.table-title .btn-remove:hover { + color: var(--white); + background-color: var(--red-cherry); } .btn-remove:focus { - color: #fff; - background-color: #B21D15; + color: var(--white); + background-color: var(--red-cherry-dark); outline: none; - box-shadow: 0 0 0 .2rem rgba(225,83,97,.5); + box-shadow: 0 0 0 .2rem var(--red-shadow); } .btn-remove.disabled { - /*border: 1px solid #E02116;*/ - /*background-color: #fff;*/ - /*border-radius: 5px;*/ - /*color: #E02116;*/ - /*width: 130px;*/ - /*height: 40px;*/ display: none; } -.button-delete:focus{ - box-shadow: 0 0 0 .2rem rgba(225,83,97,.5); -} - .btn-danger{ height: 40px; min-width: 98px; } .btn-success, .btn-danger{ - text-align: center; - align-content: center; margin: 5px; } @@ -1513,19 +922,6 @@ td input[type=search] { outline: none; } -a#deactivateAllSubmit.btn.btn-danger { - height: 59px; -} - -a#deactivateOneSubmit.btn.btdeactivateUserModaln-danger { - vertical-align: middle; -} - -.table-primary > th { - background-color: #F5F6F6; - height: 39px; -} - .graph { margin-bottom: 0; margin-left: 5px; @@ -1533,12 +929,7 @@ a#deactivateOneSubmit.btn.btdeactivateUserModaln-danger { vertical-align: middle; } -.arrow.fas.fa-chevron-right { - text-align: center; - padding-top: 9px; - font-size: 12px; -} - +.arrow.fas.fa-chevron-right, .arrow.fas.fa-chevron-left { font-size: 12px; padding-top: 9px; @@ -1550,34 +941,7 @@ a#deactivateOneSubmit.btn.btdeactivateUserModaln-danger { margin-right: 5px; } -.deactivate-warning { - font-style: normal; - font-weight: normal; - font-size: 14px; - line-height: 20px; - color: #64727D; - padding-top: 25px; - margin-bottom: 0; -} - -.deactivate-checkbox { - height: 16px; - width: 16px; - margin: 10px 0; - padding-bottom: 5px; - padding-top: 5px; -} - -.table-hover thead tr:hover th { - background-color: #13AA57 !important; - color: #fff; -} - -button#searchBy { - border: none; - background-color: transparent; -} - +button#searchBy, button#searchByForUser { border: none; background-color: transparent; @@ -1588,11 +952,7 @@ button#searchByForUser { word-wrap: break-word; } -.col.title { - width: 140px; - word-wrap: break-word; -} - +.col.title, .col.source { width: 140px; word-wrap: break-word; @@ -1602,83 +962,6 @@ button#searchByForUser { width: 90px; } -.habit_complexity { - white-space: nowrap; -} - -.habit_edit_descriptions { - position: fixed; - display: none; - height: 400px; - width: 660px; - margin-top: 180px; - margin-left: 390px; - background: #FFFFFF; - border-radius: 4px; - z-index: 1000; -} - -.input_translation { - display: flex; - flex-direction: row; - align-items: flex-start; - padding: 8px; - position: static; - width: 612px; - height: 86px; - left: calc(50% - 612px / 2); - top: calc(50% - 86px / 2 + 10px); - background: #FFFFFF; - border: 1px solid #CACFD3; - box-sizing: border-box; - border-radius: 4px; -} - -.btn_modal { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - padding: 8px 24px; - position: static; - left: 0%; - right: 0%; - top: 0%; - bottom: 0%; - border-radius: 4px; - flex: none; - order: 0; - flex-grow: 0; - margin: 0px 0px; - font-size: 16px; - border-color: #13AA57; -} - -.save_changes_btn { - background: #13AA57; - color: #ffffff; - -} - -.cancel_btn { - - background-color: #ffffff; - color: #13AA57; -} - -.habit_img { - height: 34px; - width: auto; -} - -.column-credo-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - height: 39px; - max-width: 195px; -} - #upload_image { height: 212px; width: auto; @@ -1689,46 +972,26 @@ button#searchByForUser { } #upload_image:hover { - background: #FFFFFF; + background: var(--white); opacity: 0.8; - border: 1px dashed #9CA7B0; + border: 1px dashed var(--gray-gull); box-sizing: border-box; border-radius: 4px; } .dropdown:hover .dropbtn.size { - background: white; - color: #9CA7B0; + background: var(--white); + color: var(--gray-gull); } a.lightgrey { - color: #9CA7B0 !important; + color: var(--gray-gull) !important; } .dropdown.status:hover .rotate { transform: rotateX(180deg); } -.cyan { - background-color: #E5F2FF; -} - -.lightgreen { - background-color: #E7F7EE; -} - -.lightmustard { - background-color: #fff7cf; -} - -.lightpink { - background-color: #FCE0DE; -} - -.lightgreen:hover, .lightpink:hover, .lightmustard:hover, .cyan:hover { - background: white; -} - .fas.fa-chevron-up { float: right; margin-top: 11px; @@ -1747,8 +1010,8 @@ a.lightgrey { margin-top: 11px; margin-right: 5px; } -.table-child tr:nth-child(odd) { - border-bottom: 1px solid #ddd; +.table-child tr:not(:last-child) { + border-bottom: 1px solid var(--white-focus); } .table-child td { @@ -1758,32 +1021,6 @@ a.lightgrey { .align-content-center { align-content: center; } -.alternative-checkbox input[type="checkbox"] { - appearance: none; - width: 20px; - height: 20px; - cursor: pointer; - position: relative; - background: #FFFFFF; - border: 1px solid #9CA7B0; - border-radius: 3px; -} - -.alternative-checkbox input[type="checkbox"]:checked { - background-color: white; -} - -.alternative-checkbox input[type="checkbox"]:checked::after { - content: ''; - position: absolute; - top: 1px; - left: 5px; - width: 8px; - height: 13px; - border: solid #13AA57; - border-width: 0 4px 4px 0; - transform: rotate(45deg); -} .table-responsive { overflow-x: auto; @@ -1814,30 +1051,14 @@ a.lightgrey { } } -.table-container { - overflow-x: scroll; - margin-bottom: 1rem; -} - -.table-container::-webkit-scrollbar { - display: block; - height: 10px; - background-color: #f1f1f1; -} - -.table-container::-webkit-scrollbar-thumb { - background-color: #888; - border-radius: 5px; -} - -.table-container .table-header-container { - display: flex; - justify-content: space-between; +.table-header-container { + display: flex; + justify-content: space-between; } .filter-container { - background-color: #ffffff; - border: 1px solid #9CA7B0; + background-color: var(--white); + border: 1px solid var(--gray-gull); border-bottom-right-radius: 5px; border-bottom-left-radius: 5px; display: flex; @@ -1860,15 +1081,15 @@ a.lightgrey { .filter-container__search input[type="radio"]:checked+label, .Checked+label { - border: 1px solid #13AA57; - background-color: #13AA57; - color: #ffffff; + border: 1px solid var(--green); + background-color: var(--green); + color: var(--white); } .filter-container__search .visibility-label { - border: 1px solid #13AA57; - background-color: #ffffff; - color: #13AA57; + border: 1px solid var(--green); + background-color: var(--white); + color: var(--green); border-radius: 5px; width: 140px; height: 40px; @@ -1888,24 +1109,17 @@ a.lightgrey { .filter-container__search .form-search { width: 100%; - padding-right: 30px; /* Add padding to the right to make space for the icon */ + padding-right: 30px; box-sizing: border-box; } -.eco-news .search-icon { - position: absolute; - right: 10px; - top: 50%; - transform: translateY(-40%); - color: #9CA7B0; -} - +.eco-news .search-icon, .places .search-icon { position: absolute; right: 10px; top: 50%; transform: translateY(-40%); - color: #9CA7B0; + color: var(--gray-gull); } .filter-container__search .material-icons { @@ -1933,15 +1147,10 @@ a.lightgrey { } .material-icons.sorted { - color: #13AA57; -} - -.eco-news .form-inline.searching { - position: relative; - display: flex; - align-items: center; + color: var(--green); } +.eco-news .form-inline.searching, .places .form-inline.searching { position: relative; display: flex; @@ -1965,17 +1174,6 @@ a.lightgrey { gap: 10px; } -.custom-checkbox.tag span, .custom-checkbox.date span { - margin-left: 10px; - color: #13AA57; - font-weight: 600; -} - -.custom-checkbox.tag input[type="checkbox"]:checked + label:after, -.custom-checkbox.date input[type="checkbox"]:checked + label:after { - transform: translateY(10px); -} - .form-control.end-date { margin-top: 10px; } @@ -1989,420 +1187,4 @@ a.lightgrey { width: 100%; height: 65vh; transition: height 0.3s ease; -} - -.navigation-menu { - display: flex; - align-items: center; - background: var(--white); - justify-content: space-between; - width: 100%; - font-family: "Open Sans"; -} - -.navigation-menu-left { - width: 100%; - max-width: 930px; -} - -.navigation-menu-left ul { - list-style: none; - margin: 0; - padding: 0; - display: flex; - justify-content: space-between; -} - -.navigation-menu-left ul li { - list-style-type: none; - display: inline-block; -} - -.navigation-menu-left ul li:nth-of-type(6) { - margin-right: 0; - height: 20px; -} - -.navigation-menu-left ul li a { - font-size: 14px; - line-height: 16px; - color: #494a49; - text-decoration: none; -} - -.navigation-menu-left ul .mobile-vis { - display: none; - font-weight: 600; -} - -.navigation-menu-left-col { - display: flex; - justify-content: center; - align-items: flex-start; - position: fixed; - top: 53px; - left: 0; - width: 100%; - height: 100%; - background: white; - transition: transform 0.2s ease-in, top 0.2s linear 0.2s; - box-shadow: 0 4px 5px -2px rgba(73, 74, 73, 0.2); - overflow-y: auto; -} - -.navigation-menu-left-col ul { - display: flex; - flex-direction: column; - list-style: none; - margin: 0; - overflow: hidden; - padding: 0; - height: 550px; -} - -.navigation-menu-left-col ul li { - list-style-type: none; - display: flex; - justify-content: center; - align-items: center; - height: 52px; -} - -.navigation-menu-left-col ul li a { - display: block; - font-size: 20px; - line-height: 24px; - color: #494a49; -} - -.navigation-menu-left-col ul li:nth-of-type(6) { - height: 0px; - width: 900px; - border-bottom: 2px solid rgba(73, 74, 73, 0.2); -} - -.navigation-menu-left-col ul .mobile-vis { - display: none; - margin: 0; - list-style-type: none; - justify-content: center; - align-items: center; - height: 52px; -} - -.navigation-menu-left-col ul .mobile-vis .sign-in-link { - color: #056b33; - font-size: 16px; - font-weight: 600; -} - -.navigation-menu-left-col ul .mobile-vis .sign-in-link:hover { - text-decoration-line: none; -} - -.navigation-menu-left-col ul .name { - font-weight: 600; - font-size: 18px; -} - -.navigation-menu-left-col ul .mobile-vis .create-button { - cursor: pointer; - width: auto; - min-width: 138px; - padding: 0 4px 0; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #13aa57; -} - -.navigation-menu-left-col ul .mobile-vis .create-button span { - display: block; - font-size: 16px; - line-height: 16px; - font-weight: 600; -} - -.navigation-menu-right { - width: auto; -} - -.navigation-menu-right ul { - display: flex; - justify-content: space-between; - align-items: center; - margin: 0; - padding: 0; - width: auto; -} - -.navigation-menu-right ul .destroy { - display: none; -} - -.navigation-menu-right ul > li { - list-style-type: none; - margin-right: 20px; -} - -.navigation-menu-right ul > li:nth-of-type(4) { - margin-right: 5px; -} - -.navigation-menu-right ul > li:nth-of-type(5) { - margin-right: 0; -} - -.navigation-menu-right ul .language-switcher { - width: 33px; - cursor: pointer; - outline: none; - appearance: none; - background-color: transparent; - border: none; - -moz-appearance: none; - -webkit-appearance: none; -} - -.navigation-menu-right ul .language-switcher option { - background: white; -} - -.navigation-menu-right ul .switcher-wrapper { - position: relative; - width: 61px; - min-height: 26px; -} - -.navigation-menu-right ul .switcher-wrapper .arrow { - position: absolute; - top: 10px; - right: 19px; - width: 8px; - height: 5px; - z-index: 1; -} - -.navigation-menu-right ul .switcher-wrapper .reverse { - transform: rotate(180deg); - transition: 0.4s ease-out; - transition-delay: 0.1s; -} - -.navigation-menu-right ul .switcher-wrapper ul { - position: absolute; - display: flex; - flex-direction: column; - justify-content: space-between; - top: -8px; - width: 61px; -} - -.navigation-menu-right ul .switcher-wrapper ul li { - cursor: pointer; - height: 44px; - font-size: 14px; - padding-top: 10px; -} - -.navigation-menu-right ul .switcher-wrapper .add-shadow { - background: #fff; - box-shadow: 0px 0px 8px rgba(73, 74, 73, 0.2); - border-radius: 2px; -} - -.navigation-menu-right ul .sign-in-link { - font-size: 12px; - font-weight: 600; -} - -.navigation-menu-right ul .sign-in-link a { - display: block; - min-width: 40px; - word-break: break-all; -} - -.navigation-menu-right ul .sign-up-link { - position: relative; - font-size: 12px; -} - -.navigation-menu-right ul .sign-up-link a { - color: #056b33; -} - -.navigation-menu-right ul .sign-up-link a:hover { - text-decoration-line: none; -} - -.navigation-menu-right ul .sign-up-link .create-button { - cursor: pointer; - width: auto; - min-width: 86px; - padding: 0 4px 0; - height: 32px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid #13aa57; -} - -.navigation-menu-right ul .sign-up-link .create-button span { - display: block; - font-size: 12px; - line-height: 12px; - font-weight: 600; -} - -.navigation-menu-right ul li .search { - line-height: 10px; -} - -.secondary-global-button { - border: 1px solid #13AA57; - box-sizing: border-box; - border-radius: 3px; - font-family: Open Sans; - font-style: normal; - font-weight: bold; - font-size: 16px; - line-height: 16px; - color: #13AA57; - text-align: center; -} - -.secondary-global-button:hover, -.secondary-global-button:active { - border: 1px solid #056B33; - line-height: 22px; - color: #056B33; -} - -.tertiary-global-button { - font-family: Open Sans; - font-size: 16px; - font-stretch: normal; - font-style: normal; - font-weight: bold; - line-height: 16px; - color: #056B33; -} - -.tertiary-global-button:hover, -.tertiary-global-button:focus { - color: #13AA57; -} - -.burger-b .menu-icon-wrapper { - width: 24px; - height: 18px; - justify-content: center; - align-items: center; - display: none; -} - -.burger-b .menu-icon-wrapper .menu-icon { - position: relative; - width: 24px; - height: 2px; - border-radius: 14px; - background-color: #494a49; -} - -.tertiary-global-button { - font-family: Open Sans; - font-size: 16px; - font-stretch: normal; - font-style: normal; - font-weight: bold; - line-height: 16px; - color: #056B33; -} - -.burger-b .menu-icon-wrapper .menu-icon:before { - position: absolute; - left: 0; - top: -6px; - content: ''; - width: 24px; - height: 2px; - border-radius: 14px; - background-color: #494a49; - transition: transform 0.2s ease-in, top 0.2s linear 0.2s; -} - -.burger-b .menu-icon-wrapper .menu-icon:after { - position: absolute; - left: 0; - top: 6px; - content: ''; - width: 24px; - height: 2px; - border-radius: 14px; - background-color: #494a49; - transition: transform 0.2s ease-in, top 0.2s linear 0.2s; -} - -.burger-b .menu-icon-wrapper .menu-icon-active { - background-color: transparent; -} - -.burger-b .menu-icon-wrapper .menu-icon-active:before { - transform: rotate(45deg); - top: 0; - transition: top 0.2s linear, transform 0.2s ease-in 0.2s; -} - -.burger-b .menu-icon-wrapper .menu-icon-active:after { - transform: rotate(-45deg); - top: 0; - transition: top 0.2s linear, transform 0.2s ease-in 0.2s; -} - -#user-avatar-wrapper, #lang-wrapper { - position: relative; - width: auto; - min-width: 135px; - height: 36px; -} - -#user-avatar-wrapper ul, #lang-wrapper ul { - display: flex; - flex-direction: column; - position: absolute; - margin: 0; - padding: 10px 0 0 0; - background: #fff; -} - -#user-avatar-wrapper ul li, #lang-wrapper ul li { - width: 100%; - height: 30px; - font-size: 14px; - line-height: 16px; - margin: 0 0 3px 0; - padding: 0 10px 0; - color: #494a49; -} - -#user-avatar-wrapper ul li:first-of-type, #lang-wrapper ul li:first-of-type { - color: #056b33; - font-size: 12px; - font-weight: 600; -} - -#user-avatar-wrapper ul li:first-of-type .reverse, #lang-wrapper ul li:first-of-type .reverse { - transform: rotate(180deg); - transition: 0.4s ease-out; - transition-delay: 0.1s; -} - -#user-avatar-wrapper .text-hidde, #lang-wrapper .text-hidde { - display: none; -} - -#user-avatar-wrapper .add-shadow, #lang-wrapper .add-shadow { - box-shadow: 0 0 8px rgba(73, 74, 73, 0.2); - height: 176px; } \ No newline at end of file diff --git a/core/src/main/resources/static/css/user.css b/core/src/main/resources/static/css/user.css new file mode 100644 index 000000000..d3e1d74c7 --- /dev/null +++ b/core/src/main/resources/static/css/user.css @@ -0,0 +1,316 @@ +.page-title h1 { + padding-left: 7px; + font-weight: bold; + font-size: 32px; + color: var(--green); +} + +.user-info { + width: 261px; + min-width: 261px; + height: 100%; + padding: 24px; + margin-right: 16px; + overflow-x: auto; + box-shadow: 1px 4px 8px rgba(100, 114, 125, 0.18); +} + +.online-status { + position: relative; + left: 60px; + width: 16px; + height: 16px; + border-radius: 50px; + background: var(--green); + border: 1px solid var(--white); +} + +.profile-picture { + display: flex; + align-items: center; + justify-content: center; + width: 80px; + height: 80px; + border-radius: 50px; + margin-top: 20px; +} + +.profile-picture.default { + background: linear-gradient(180deg, #abe64d 0%, #4b9b25 100%); + font-style: normal; + font-weight: 600; + font-size: 22px; + line-height: 42px; + color: var(--white); + box-sizing: border-box; +} + +.profile-picture.friend { + margin: 7px; + width: 56px; + height: 56px; + font-size: 20px; + line-height: 30px +} + +.friends-list { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; +} + +.user-info-text { + margin-top: 10px; + font-style: normal; + font-weight: normal; + font-size: 14px; + color: var(--gray-asphalt); +} + +.username-text { + font-style: normal; + font-weight: bold; + font-size: 18px; + line-height: 28px; + text-align: center; + margin-top: 10px; + letter-spacing: 0.2px; + color: var(--gray-dark-asphalt); +} + +.info-title-text span { + font-style: normal; + font-weight: bold; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: var(--gray-dark-asphalt); +} + +.info-title-text h4 { + font-style: normal; + font-weight: normal; + font-size: 11px; + margin-top: 5px; + color: var(--gray-gull); +} + +.info-title-text a { + float: right; + text-decoration: none; + font-style: normal; + font-weight: normal; + font-size: 12px; + line-height: 24px; + color: var(--gray-gull); +} + +.block-green { + display: flex; + flex-direction: row; + margin: auto; + align-items: center; + justify-content: center; + width: 89px; + height: 24px; + margin-top: 12px; + background: var(--green-light); + border-radius: 4px; + font-weight: 600; + font-size: 13px; + color: var(--green-shadow); +} + +.social-networks { + display: flex; + justify-content: center; + align-items: center; + margin-top: 15px; +} + +.social-networks img { + width: 35px; + height: 35px; + padding: 4px; +} + +.divider { + margin: 12px auto auto; + height: 2px; + background-color: var(--white-blue); +} + +.user-tables { + display: flex; + min-width: 800px; +} + + +.tooltip-container { + display: flex; + width: fit-content; + height: fit-content; + padding: 32px; + background: var(--white); + box-shadow: 1px 4px 8px rgba(100, 114, 125, 0.18); + border-radius: 4px; + font-size: 13px; + color: var(--gray-dark-asphalt); +} + +.tooltip-container h2 { + font-size: 13px; + line-height: 28px; + font-weight: bold; + letter-spacing: 0.1px; + color: var(--gray-dark-asphalt); + margin: 0 +} + +.tooltip-container table { + margin-right: 30px; + font-size: 13px; +} + + +.table-bordered.userpage { + margin-bottom: 0; +} + +table.table td a.deactivate-user { + color: var(--black); +} + +.button-container { + width: 79px; + align-items: center; + display: flex; +} + +.modal-title-deac { + font-style: normal; + font-weight: bold; + font-size: 20px; + line-height: 28px; + letter-spacing: 0.002em; + color: var(--black-ash); + padding-top: 40px; + margin-bottom: 0; +} + +.deactivation-reasons { + color: var(--black); + margin-bottom: 0; + margin-left: 12px; +} + +.modal-reasons { + display: flex; + align-items: center; +} + +.modal-title-act { + text-align: center !important; + padding-top: 40px; + padding-bottom: 25px; +} + +.modal-body-act1 { + margin: 0 !important; + padding-left: 7px; +} + +.modal-body-act { + margin: 0 !important; +} + +.user-lang { + width: 100%; + display: inline-block; + margin-top: 4px; +} + +.custom-reason { + width: 100%; + padding: 8px; + height: 94px; + background: var(--white); + border: 1px solid var(--gray-gull); + border-radius: 4px; + order: 1; + margin-bottom: 24px; +} + +.custom-reason:focus { + border-color: red; +} + +a#deactivateAllSubmit.btn.btn-danger { + height: 59px; +} + +a#deactivateOneSubmit.btn.btdeactivateUserModaln-danger { + vertical-align: middle; +} + +.table-primary { + position: sticky; + top: 0; +} + +.table-primary > th { + background-color: var(--white-smoke); + height: 39px; +} + +.deactivate-warning { + font-style: normal; + font-weight: normal; + font-size: 14px; + line-height: 20px; + color: var(--gray-asphalt); + padding-top: 25px; + margin-bottom: 0; +} + +.deactivate-checkbox { + height: 16px; + width: 16px; + margin: 10px 0; + padding-bottom: 5px; + padding-top: 5px; +} + +.table-hover thead tr:hover th { + background-color: var(--green) !important; + color: var(--white); +} + +.column-credo-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + height: 39px; + max-width: 195px; +} + +.cyan { + background-color: var(--white-blue); +} + +.lightgreen { + background-color: var(--white-green); +} + +.lightmustard { + background-color: var(--light-mustard); +} + +.lightpink { + background-color: var(--light-pink); +} + +.lightgreen:hover, .lightpink:hover, .lightmustard:hover, .cyan:hover { + background: var(--white); +} \ No newline at end of file diff --git a/core/src/main/resources/templates/core/about_us.html b/core/src/main/resources/templates/core/about_us.html index 63a06a9f7..c627e3997 100644 --- a/core/src/main/resources/templates/core/about_us.html +++ b/core/src/main/resources/templates/core/about_us.html @@ -6,11 +6,7 @@ GreenCity - - - - - + @@ -25,16 +21,17 @@ - + -
    +
    - logo - +
    + logo +

    Local or organic? Hybrid or electric? Paper or plastic or neither? Nearly all @@ -56,9 +53,8 @@

    -
    - + diff --git a/core/src/main/resources/templates/core/management_eco_new.html b/core/src/main/resources/templates/core/management_eco_new.html index 0147a1299..3948de2f6 100644 --- a/core/src/main/resources/templates/core/management_eco_new.html +++ b/core/src/main/resources/templates/core/management_eco_new.html @@ -184,9 +184,9 @@ @@ -262,11 +262,8 @@ - - - + - \ No newline at end of file diff --git a/core/src/main/resources/templates/core/management_eco_news.html b/core/src/main/resources/templates/core/management_eco_news.html index b0c01829f..3b1802571 100644 --- a/core/src/main/resources/templates/core/management_eco_news.html +++ b/core/src/main/resources/templates/core/management_eco_news.html @@ -85,15 +85,14 @@

    [[#{greenCity.econews.page.h}]]

    -
  • [[#{greenCity.pages.table.id}]] [[#{greenCity.pages.table.event.name}]] [[#{greenCity.pages.table.user.id}]]
    - - - - + + + +
    @@ -435,7 +434,6 @@

    [[#{greenCity.econews.page.h}]]

    -
    diff --git a/core/src/main/resources/templates/core/management_fact_of_the_day.html b/core/src/main/resources/templates/core/management_fact_of_the_day.html index 39e068568..39d84a0c5 100644 --- a/core/src/main/resources/templates/core/management_fact_of_the_day.html +++ b/core/src/main/resources/templates/core/management_fact_of_the_day.html @@ -41,11 +41,11 @@

    [[#{greenCity.factsOfTheDay.page.h}]]

    - - [[#{greenCity.factsOfTheDay.page.add.factsOfTheDay}]] - [[#{greenCity.pages.delete}]] + +
    [[#{greenCity.factsOfTheDay.page.add.factsOfTheDay}]]
    + +
    [[#{greenCity.pages.delete}]]
    @@ -53,7 +53,7 @@

    [[#{greenCity.factsOfTheDay.page.h}]]

    - + @@ -92,7 +92,7 @@

    [[#{greenCity.factsOfTheDay.page.h}]]

    [[#{greenCity.pages.table.tags}]] [[#{greenCity.pages.table.actions}]]
    [[#{greenCity.pages.table.translation.id}]] [[#{greenCity.pages.table.content}]] @@ -111,7 +111,7 @@

    [[#{greenCity.factsOfTheDay.page.h}]]

    - + @@ -124,7 +124,7 @@

    [[#{greenCity.factsOfTheDay.page.h}]]

    1 - +
    - + @@ -276,7 +276,7 @@

    - + @@ -122,9 +122,9 @@

    id="btnAdd">
    [[#{greenCity.habitToDoListItem.page.addAll.toDoListItem}]]
    -

    [[#{greenCity.habitToDoListItem.page.translationId.toDoListItem}]]
    +
    - +
    @@ -155,14 +155,14 @@

    - - + - - diff --git a/core/src/main/resources/templates/core/management_places.html b/core/src/main/resources/templates/core/management_places.html index ddbad30d9..d553a33bf 100644 --- a/core/src/main/resources/templates/core/management_places.html +++ b/core/src/main/resources/templates/core/management_places.html @@ -82,17 +82,16 @@

    [[#{greenCity.places.page.h}]]

    -
    +
    [[#{greenCity.habitToDoListItem.page.translationId.toDoListItem}]] + [[#{greenCity.habitToDoListItem.page.translation.content.toDoListItem}]] + [[#{greenCity.habitToDoListItem.page.translation.code.toDoListItem}]]
    - + - - - - - - - - - - - + + + + + @@ -241,16 +239,9 @@

    [[#{greenCity.places.page.h}]]

    -
    + +
    [[#{greenCity.pages.table.id}]] @@ -116,7 +115,7 @@

    [[#{greenCity.places.page.h}]]

    +
    [[#{greenCity.pages.table.name}]] @@ -140,7 +139,7 @@

    [[#{greenCity.places.page.h}]]

    +
    [[#{greenCity.pages.table.status}]] @@ -164,12 +163,10 @@

    [[#{greenCity.places.page.h}]]

    -
    - [[#{greenCity.pages.table.opening.hours}]] -
    +
    + [[#{greenCity.pages.table.opening.hours}]] +
    [[#{greenCity.pages.table.author}]] @@ -193,7 +190,7 @@

    [[#{greenCity.places.page.h}]]

    +
    [[#{greenCity.pages.table.address}]] @@ -209,23 +206,24 @@

    [[#{greenCity.places.page.h}]]

    -
    - Lat -
    +
    + Lat -
    - Lng -
    +
    + Lng + [[#{greenCity.pages.table.image}]] + [[#{greenCity.pages.table.actions}]]
    DayOpen TimeClose Time
    +
    - - - - - - - - +
    DayOpen TimeClose Time
    @@ -289,7 +280,6 @@

    [[#{greenCity.places.page.h}]]

    -
    [[#{greenCity.page.paging.show}]] 5 @@ -405,7 +395,7 @@
    -
    diff --git a/core/src/main/resources/templates/core/management_rating_calculation.html b/core/src/main/resources/templates/core/management_rating_calculation.html index d1d420288..f76deeab8 100644 --- a/core/src/main/resources/templates/core/management_rating_calculation.html +++ b/core/src/main/resources/templates/core/management_rating_calculation.html @@ -132,7 +132,7 @@
    - + diff --git a/core/src/main/resources/templates/core/management_rating_deleted.html b/core/src/main/resources/templates/core/management_rating_deleted.html index c013a605c..4042ba8ad 100644 --- a/core/src/main/resources/templates/core/management_rating_deleted.html +++ b/core/src/main/resources/templates/core/management_rating_deleted.html @@ -54,7 +54,7 @@

    [[#{greenCity.sidebar.ratings.calculation.setup}]]

    -
    [[#{greenCity.pages.table.id}]] + [[#{greenCity.pages.table.id}]] diff --git a/core/src/main/resources/templates/core/management_tags.html b/core/src/main/resources/templates/core/management_tags.html index 163bd70cc..8799afa07 100644 --- a/core/src/main/resources/templates/core/management_tags.html +++ b/core/src/main/resources/templates/core/management_tags.html @@ -56,23 +56,25 @@

    [[#{greenCity.tags.page.h}]]

    - + - - - - - + + + + + + + + + - - - @@ -84,11 +86,11 @@

    [[#{greenCity.tags.page.h}]]

    type="search"/> - -
    + [[#{greenCity.pages.table.id}]][[#{greenCity.pages.table.type}]][[#{greenCity.pages.table.translations}]][[#{greenCity.pages.table.actions}]][[#{greenCity.pages.table.id}]][[#{greenCity.pages.table.type}]][[#{greenCity.pages.table.translations}]][[#{greenCity.pages.table.actions}]]
    [[#{greenCity.pages.table.translation.id}]][[#{greenCity.pages.table.name}]][[#{greenCity.pages.table.language.code}]]
    - + @@ -104,31 +106,24 @@

    [[#{greenCity.tags.page.h}]]

    - - - - + + + + - - - - - - - - +
    [[#{greenCity.pages.table.translation.id}]][[#{greenCity.pages.table.name}]][[#{greenCity.pages.table.language.code}]]
    + - - - + + + diff --git a/core/src/main/resources/templates/core/management_to_do_list_items.html b/core/src/main/resources/templates/core/management_to_do_list_items.html index 8610fe886..05954a39e 100644 --- a/core/src/main/resources/templates/core/management_to_do_list_items.html +++ b/core/src/main/resources/templates/core/management_to_do_list_items.html @@ -59,17 +59,25 @@

    [[#{greenCity.toDoListItem.page.h}]]

    - + - - - - + + + + + + + + @@ -83,8 +91,8 @@

    [[#{greenCity.toDoListItem.page.h}]]

    - -
    + [[#{greenCity.pages.table.id}]][[#{greenCity.pages.table.translations}]][[#{greenCity.pages.table.actions}]][[#{greenCity.pages.table.id}]][[#{greenCity.pages.table.translations}]][[#{greenCity.pages.table.actions}]]
    + [[#{greenCity.pages.table.translation.id}]] + [[#{greenCity.pages.table.content}]][[#{greenCity.pages.table.language.code}]] +
    - + @@ -106,25 +114,15 @@

    [[#{greenCity.toDoListItem.page.h}]]

    - - - - - - - - +
    - [[#{greenCity.pages.table.translation.id}]] - [[#{greenCity.pages.table.content}]][[#{greenCity.pages.table.language.code}]] -
    + - - - + + + diff --git a/core/src/main/resources/templates/core/management_user.html b/core/src/main/resources/templates/core/management_user.html index c7b3c67bc..ad8b05191 100644 --- a/core/src/main/resources/templates/core/management_user.html +++ b/core/src/main/resources/templates/core/management_user.html @@ -5,6 +5,7 @@ User management + [[#{greenCity.user.page.add.user}]] [[#{greenCity.pages.table.userName}]] arrow-icon - - - - - - - - - - - - - - - - - - - - - - - + @@ -595,7 +509,7 @@ - + \ No newline at end of file diff --git a/core/src/main/resources/templates/core/management_user_personal_page.html b/core/src/main/resources/templates/core/management_user_personal_page.html index 513db3590..f1fa1cdda 100644 --- a/core/src/main/resources/templates/core/management_user_personal_page.html +++ b/core/src/main/resources/templates/core/management_user_personal_page.html @@ -4,6 +4,7 @@ User's personal page + [[#{greenCity.personal.page.h}]]
    - -
    [[#{greenCity.pages.table.email}]] arrow-icon - - - - - - - - - - - - - - - - - - - - - - [[#{greenCity.pages.table.credo}]] - arrow-icon - - - - - - - - - - - - - - - - - - - - - - + + [[#{greenCity.pages.table.credo}]] + arrow-icon + [[#{greenCity.pages.table.role}]] arrow-icon - - - - - - - - - - - - - - - - - - - - - - - - [[#{greenCity.pages.table.status}]] @@ -310,7 +224,7 @@ [[#{greenCity.pages.table.actions}]]