mirror of
https://github.com/dedicatedcode/reitti.git
synced 2026-01-09 17:37:57 -05:00
UI fixes (#385)
This commit is contained in:
@@ -2,14 +2,11 @@ package com.dedicatedcode.reitti.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.servlet.LocaleResolver;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
|
||||
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
@@ -32,25 +29,15 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
resolver.setCookiePath("/");
|
||||
return resolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
|
||||
interceptor.setParamName("lang");
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ReloadableResourceBundleMessageSource messageSource() {
|
||||
ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||
messageSource.setBasename("classpath:messages");
|
||||
messageSource.setDefaultEncoding("UTF-8");
|
||||
messageSource.setCacheSeconds(3600);
|
||||
return messageSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localeChangeInterceptor());
|
||||
}
|
||||
//
|
||||
//
|
||||
// @Bean
|
||||
// public ReloadableResourceBundleMessageSource messageSource() {
|
||||
// ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
|
||||
// messageSource.setBasename("classpath:messages");
|
||||
// messageSource.setDefaultEncoding("UTF-8");
|
||||
// messageSource.setCacheSeconds(3600);
|
||||
// return messageSource;
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -13,12 +13,15 @@ import com.dedicatedcode.reitti.model.security.TokenUser;
|
||||
import com.dedicatedcode.reitti.model.security.User;
|
||||
import com.dedicatedcode.reitti.repository.ProcessedVisitJdbcService;
|
||||
import com.dedicatedcode.reitti.repository.TripJdbcService;
|
||||
import com.dedicatedcode.reitti.service.I18nService;
|
||||
import com.dedicatedcode.reitti.service.MagicLinkTokenService;
|
||||
import com.dedicatedcode.reitti.service.MemoryService;
|
||||
import com.dedicatedcode.reitti.service.RequestHelper;
|
||||
import com.dedicatedcode.reitti.service.integration.ImmichIntegrationService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@@ -36,23 +39,27 @@ import static com.dedicatedcode.reitti.model.Role.USER;
|
||||
@Controller
|
||||
@RequestMapping("/memories")
|
||||
public class MemoryController {
|
||||
private static final Logger log = LoggerFactory.getLogger(MemoryController.class);
|
||||
|
||||
private final MemoryService memoryService;
|
||||
private final TripJdbcService tripJdbcService;
|
||||
private final ProcessedVisitJdbcService processedVisitJdbcService;
|
||||
private final ImmichIntegrationService immichIntegrationService;
|
||||
private final MagicLinkTokenService magicLinkTokenService;
|
||||
private final I18nService i18n;
|
||||
|
||||
public MemoryController(MemoryService memoryService,
|
||||
TripJdbcService tripJdbcService,
|
||||
ProcessedVisitJdbcService processedVisitJdbcService,
|
||||
ImmichIntegrationService immichIntegrationService,
|
||||
MagicLinkTokenService magicLinkTokenService) {
|
||||
MagicLinkTokenService magicLinkTokenService,
|
||||
I18nService i18n) {
|
||||
this.memoryService = memoryService;
|
||||
this.tripJdbcService = tripJdbcService;
|
||||
this.processedVisitJdbcService = processedVisitJdbcService;
|
||||
this.immichIntegrationService = immichIntegrationService;
|
||||
this.magicLinkTokenService = magicLinkTokenService;
|
||||
this.i18n = i18n;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -156,12 +163,13 @@ public class MemoryController {
|
||||
@RequestParam LocalDate startDate,
|
||||
@RequestParam LocalDate endDate,
|
||||
@RequestParam(required = false) String headerImageUrl,
|
||||
@RequestParam(required = false) String year,
|
||||
@RequestParam(required = false, defaultValue = "UTC") ZoneId timezone,
|
||||
Model model,
|
||||
HttpServletResponse response) {
|
||||
|
||||
if (title == null || title.trim().isEmpty()) {
|
||||
model.addAttribute("error", "memory.validation.title.required");
|
||||
model.addAttribute("error", i18n.translate("memory.validation.title.required"));
|
||||
model.addAttribute("title", title);
|
||||
model.addAttribute("description", description);
|
||||
model.addAttribute("startDate", startDate);
|
||||
@@ -177,23 +185,27 @@ public class MemoryController {
|
||||
|
||||
// Validate dates are not in the future
|
||||
if (start.isAfter(today) || end.isAfter(today)) {
|
||||
model.addAttribute("error", "memory.validation.date.future");
|
||||
model.addAttribute("error", i18n.translate("memory.validation.date.future"));
|
||||
model.addAttribute("title", title);
|
||||
model.addAttribute("description", description);
|
||||
model.addAttribute("startDate", startDate);
|
||||
model.addAttribute("endDate", endDate);
|
||||
model.addAttribute("headerImageUrl", headerImageUrl);
|
||||
model.addAttribute("year", year);
|
||||
|
||||
return "memories/new :: new-memory";
|
||||
}
|
||||
|
||||
// Validate end date is not before start date
|
||||
if (end.isBefore(start)) {
|
||||
model.addAttribute("error", "memory.validation.end.date.before.start");
|
||||
model.addAttribute("error", i18n.translate("memory.validation.end.date.before.start"));
|
||||
model.addAttribute("title", title);
|
||||
model.addAttribute("description", description);
|
||||
model.addAttribute("startDate", startDate);
|
||||
model.addAttribute("endDate", endDate);
|
||||
model.addAttribute("headerImageUrl", headerImageUrl);
|
||||
model.addAttribute("year", year);
|
||||
|
||||
return "memories/new :: new-memory";
|
||||
}
|
||||
|
||||
@@ -212,12 +224,15 @@ public class MemoryController {
|
||||
return "memories/fragments :: empty";
|
||||
|
||||
} catch (Exception e) {
|
||||
model.addAttribute("error", "memory.validation.start.date.required");
|
||||
log.error("Error creating memory", e);
|
||||
model.addAttribute("error", i18n.translate("memory.creation.error", e.getMessage()));
|
||||
model.addAttribute("title", title);
|
||||
model.addAttribute("description", description);
|
||||
model.addAttribute("startDate", startDate);
|
||||
model.addAttribute("endDate", endDate);
|
||||
model.addAttribute("headerImageUrl", headerImageUrl);
|
||||
model.addAttribute("year", year);
|
||||
|
||||
return "memories/new :: new-memory";
|
||||
}
|
||||
}
|
||||
@@ -261,7 +276,7 @@ public class MemoryController {
|
||||
|
||||
// Add validation similar to create method
|
||||
if (title == null || title.trim().isEmpty()) {
|
||||
model.addAttribute("error", "memory.validation.title.required");
|
||||
model.addAttribute("error", i18n.translate("memory.validation.title.required"));
|
||||
model.addAttribute("memory", memory);
|
||||
|
||||
model.addAttribute("cancelEndpoint", "/memories/" + id);
|
||||
@@ -276,7 +291,7 @@ public class MemoryController {
|
||||
Instant today = Instant.now();
|
||||
|
||||
if (start.isAfter(today) || end.isAfter(today)) {
|
||||
model.addAttribute("error", "memory.validation.date.future");
|
||||
model.addAttribute("error", i18n.translate("memory.validation.date.future"));
|
||||
model.addAttribute("memory", memory);
|
||||
|
||||
model.addAttribute("cancelEndpoint", "/memories/" + id);
|
||||
@@ -286,7 +301,7 @@ public class MemoryController {
|
||||
}
|
||||
|
||||
if (end.isBefore(start)) {
|
||||
model.addAttribute("error", "memory.validation.end.date.before.start");
|
||||
model.addAttribute("error", i18n.translate("memory.validation.end.date.before.start"));
|
||||
model.addAttribute("memory", memory);
|
||||
|
||||
model.addAttribute("cancelEndpoint", "/memories/" + id);
|
||||
@@ -310,7 +325,7 @@ public class MemoryController {
|
||||
return "memories/view :: memory-header";
|
||||
|
||||
} catch (Exception e) {
|
||||
model.addAttribute("error", "memory.validation.start.date.required");
|
||||
model.addAttribute("error", i18n.translate("memory.validation.start.date.required"));
|
||||
model.addAttribute("memory", memory);
|
||||
|
||||
model.addAttribute("cancelEndpoint", "/memories/" + id);
|
||||
|
||||
@@ -246,7 +246,7 @@ public class MemoryBlockGenerationService {
|
||||
}
|
||||
|
||||
log.info("Generated {} memory block parts", blockParts.size());
|
||||
|
||||
|
||||
return blockParts;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ logging.level.com.dedicatedcode.reitti = INFO
|
||||
spring.messages.basename=messages
|
||||
spring.messages.encoding=UTF-8
|
||||
spring.messages.cache-duration=3600
|
||||
spring.messages.fallback-to-system-locale=false
|
||||
|
||||
# PostgreSQL configuration
|
||||
spring.datasource.url=jdbc:postgresql://localhost:5432/reittidb
|
||||
|
||||
@@ -738,6 +738,7 @@ language.english=English
|
||||
language.finnish=Finnish
|
||||
language.german=German
|
||||
language.french=French
|
||||
language.russian=Russian
|
||||
language.brazilian_portuguese=Portuguese (Brazil)
|
||||
|
||||
# Login page
|
||||
@@ -826,6 +827,7 @@ error.action.back=Go Back
|
||||
error.action.retry=Try Again
|
||||
|
||||
# Memory validation
|
||||
memory.creation.error=Error creating your memory: {0}
|
||||
memory.validation.start.date.required=Start date is required
|
||||
memory.validation.end.date.required=End date is required
|
||||
memory.validation.end.date.before.start=End date cannot be before start date
|
||||
|
||||
@@ -24,14 +24,22 @@
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
font-size: 1.6rem;
|
||||
background: var(--color-highlight);
|
||||
font-family: var(--serif-font);
|
||||
}
|
||||
|
||||
.avatar-marker-fallback {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
line-height: 1;
|
||||
|
||||
.avatar-marker-img:after {
|
||||
content: attr(alt);
|
||||
top: 0;
|
||||
position: absolute;
|
||||
background: white;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 2px;
|
||||
padding-left: 1px;
|
||||
font-family: var(--serif-font);
|
||||
}
|
||||
|
||||
@keyframes avatar-pulse {
|
||||
|
||||
@@ -1548,6 +1548,7 @@ button:disabled {
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar.small {
|
||||
@@ -1560,14 +1561,16 @@ button:disabled {
|
||||
}
|
||||
.avatar::after {
|
||||
content: attr(alt);
|
||||
font-size: 36px;
|
||||
font-family: var(--serif-font);
|
||||
font-weight: lighter;
|
||||
color: white;
|
||||
font-size: 2.4rem;
|
||||
top: 0;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: var(--color-background-dark);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 10px;
|
||||
font-family: var(--serif-font);
|
||||
}
|
||||
|
||||
.avatar:not(:has(img))::after {
|
||||
@@ -1855,6 +1858,10 @@ button:disabled {
|
||||
.coordinate-input {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
.auto-update-overlay {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.memories-overview {
|
||||
@@ -2345,14 +2352,6 @@ button:disabled {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-highlight), #ffd700);
|
||||
width: 20%;
|
||||
transition: width 2s ease;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
color: #ccc;
|
||||
font-size: 0.9rem;
|
||||
@@ -2371,4 +2370,5 @@ button:disabled {
|
||||
transform: translateX(300%);
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,11 @@
|
||||
<span class="language-flag">🇫🇷</span>
|
||||
<span th:text="#{language.french}" class="language-name">Français</span>
|
||||
</label>
|
||||
<label class="btn language-btn">
|
||||
<input type="radio" name="preferred_language" value="ru" th:checked="${selectedLanguage == 'ru'}" style="display: none;">
|
||||
<span class="language-flag">🇷🇺</span>
|
||||
<span th:text="#{language.russian}" class="language-name">Russian</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<div id="edit-fragment" class="memory-header" th:fragment="edit-memory">
|
||||
<div th:if="${error}" class="alert alert-error">
|
||||
<i class="lni lni-warning"></i>
|
||||
<span th:text="#{${error}}">Error message</span>
|
||||
<span th:text="${error}">Error message</span>
|
||||
</div>
|
||||
|
||||
<form th:attr="hx-post=@{/memories/{id}(id=${memory.id})}" hx-vals="js:{timezone: getUserTimezone()}" method="post" class="memory-form" hx-swap="outerHTML" hx-target=".memory-header">
|
||||
|
||||
@@ -33,13 +33,14 @@
|
||||
<div th:fragment="new-memory" class="settings-content-area">
|
||||
<div th:if="${error}" class="alert alert-error">
|
||||
<i class="lni lni-warning"></i>
|
||||
<span th:text="#{${error}}">Error message</span>
|
||||
<span th:text="${error}">Error message</span>
|
||||
</div>
|
||||
|
||||
<form th:attr="hx-post=@{/memories}" class="memory-form" hx-indicator="#memory-processing-overlay" hx-vals='js:{"timezone": getUserTimezone()}' onsubmit="return validateDates()">
|
||||
<form th:attr="hx-post=@{/memories}" class="memory-form" hx-indicator="#memory-processing-overlay" hx-swap="outerHTML" hx-target="closest .settings-content-area" hx-vals='js:{"timezone": getUserTimezone()}' onsubmit="return validateDates()">
|
||||
<div class="form-group">
|
||||
<label for="title" th:text="#{memory.form.title.label}">Title *</label>
|
||||
<input type="text" id="title" name="title" required class="form-control" th:placeholder="#{memory.form.title.placeholder}" th:value="${title}">
|
||||
<input type="hidden" name="year" th:value="${year}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
|
||||
@@ -592,12 +592,7 @@
|
||||
rawLocationLoader.init(userConfigs);
|
||||
|
||||
// Load location data for the memory date range without bounds filtering
|
||||
rawLocationLoader.loadForDateRange(
|
||||
memory.startDate,
|
||||
memory.endDate,
|
||||
false, // autoUpdateMode
|
||||
false // withBounds - don't filter by current map bounds
|
||||
);
|
||||
rawLocationLoader.loadForDateRange(false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user