diff --git a/src/main/java/com/penguineering/gartenplus/GartenplusApplication.java b/src/main/java/com/penguineering/gartenplus/GartenplusApplication.java index dd3b653..dc74635 100644 --- a/src/main/java/com/penguineering/gartenplus/GartenplusApplication.java +++ b/src/main/java/com/penguineering/gartenplus/GartenplusApplication.java @@ -2,6 +2,7 @@ import com.vaadin.flow.component.page.AppShellConfigurator; import com.vaadin.flow.component.page.Push; +import com.vaadin.flow.server.AppShellSettings; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @@ -16,4 +17,10 @@ public class GartenplusApplication implements AppShellConfigurator { public static void main(String[] args) { SpringApplication.run(GartenplusApplication.class, args); } + + @Override + public void configurePage(AppShellSettings settings) { + settings.addFavIcon("icon", "favicon.ico", "256x256"); + settings.addLink("shortcut icon", "favicon.ico"); + } } diff --git a/src/main/java/com/penguineering/gartenplus/auth/CurrentUserSupplierFactory.java b/src/main/java/com/penguineering/gartenplus/auth/CurrentUserSupplierFactory.java new file mode 100644 index 0000000..99d90e6 --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/auth/CurrentUserSupplierFactory.java @@ -0,0 +1,38 @@ +package com.penguineering.gartenplus.auth; + +import com.penguineering.gartenplus.auth.user.UserDTO; +import com.vaadin.flow.server.VaadinSession; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.web.context.WebApplicationContext; + +import java.util.Optional; +import java.util.function.Supplier; + +@Configuration +public class CurrentUserSupplierFactory { + @Bean(name = "user") + @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) + public Supplier getCurrentUserSupplier() { + return () -> retrieveUserFromSession().orElse(null); + } + + private Optional retrieveUserFromSession() { + return retrieveSecurityContext() + .map(SecurityContext::getAuthentication) + .map(Authentication::getPrincipal) + .map(GartenplusUser.class::cast) + .map(GartenplusUser::getUser); + } + + private Optional retrieveSecurityContext() { + return Optional.ofNullable(VaadinSession.getCurrent()) + .map(VaadinSession::getSession) + .map(s -> s.getAttribute("SPRING_SECURITY_CONTEXT")) + .map(SecurityContext.class::cast); + } +} diff --git a/src/main/java/com/penguineering/gartenplus/ui/appframe/AppFrameLayout.java b/src/main/java/com/penguineering/gartenplus/ui/appframe/AppFrameLayout.java new file mode 100644 index 0000000..e485fcb --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/ui/appframe/AppFrameLayout.java @@ -0,0 +1,48 @@ +package com.penguineering.gartenplus.ui.appframe; + +import com.penguineering.gartenplus.auth.user.UserDTO; +import com.vaadin.flow.component.HasElement; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.RouterLayout; +import org.springframework.beans.factory.annotation.Qualifier; + +import java.util.function.Supplier; + +public class AppFrameLayout extends VerticalLayout implements RouterLayout { + private final Div content; + + public AppFrameLayout(@Qualifier("user") Supplier currentUser) { + setId("gartenplus-app"); + + + GartenplusHeader header = new GartenplusHeader(currentUser); + add(header); + + content = new Div(); + content.setId("gartenplus-content"); + content.setSizeFull(); + add(content); + } + + private HorizontalLayout createHeader(Supplier currentUser) { + HorizontalLayout header = new HorizontalLayout(); + header.setId("gartenplus-header"); + header.setSizeFull(); + + header.add(new LoggedUserView(currentUser)); + + return header; + } + + @Override + public void showRouterLayoutContent(HasElement newContent) { + // Previous content is automatically removed + + newContent.getElement() + .getComponent() + .ifPresent(content::add); + } + +} diff --git a/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusHeader.java b/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusHeader.java new file mode 100644 index 0000000..98734c6 --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusHeader.java @@ -0,0 +1,31 @@ +package com.penguineering.gartenplus.ui.appframe; + +import com.penguineering.gartenplus.auth.user.UserDTO; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; + +import java.util.function.Supplier; + +public class GartenplusHeader extends Div { + public GartenplusHeader(Supplier currentUser) { + setWidthFull(); + getStyle().set("display", "contents"); + + HorizontalLayout headerLayout = new HorizontalLayout(); + headerLayout.setId("gartenplus-header"); + headerLayout.setWidthFull(); + + headerLayout.add(new GartenplusLogo()); + + headerLayout.add(new H1("GartenPlus")); + + Div spacer = new Div(); + spacer.setWidthFull(); + headerLayout.add(spacer); + + headerLayout.add(new LoggedUserView(currentUser)); + + this.add(headerLayout); + } +} diff --git a/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusLogo.java b/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusLogo.java new file mode 100644 index 0000000..ae3afbd --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusLogo.java @@ -0,0 +1,21 @@ +package com.penguineering.gartenplus.ui.appframe; + +import com.vaadin.flow.component.Unit; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.Image; + +public class GartenplusLogo extends Div { + private static final String PATH = "assets/GartenPlusLogoRounded.png"; + private static final String ALT = "GartenPlus Logo"; + private static final int DIMENSIONS = 48; + + public GartenplusLogo() { + getStyle().set("display", "contents"); + + Image logo = new Image(PATH, ALT); + logo.setWidth(DIMENSIONS, Unit.PIXELS); + logo.setHeight(DIMENSIONS, Unit.PIXELS); + + add(logo); + } +} diff --git a/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusPage.java b/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusPage.java new file mode 100644 index 0000000..a3bbd39 --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/ui/appframe/GartenplusPage.java @@ -0,0 +1,13 @@ +package com.penguineering.gartenplus.ui.appframe; + +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.RouterLayout; + +public class GartenplusPage extends VerticalLayout implements RouterLayout { + public GartenplusPage() { + setId("gartenplus-app"); + setPadding(false); + setMargin(false); + } +} diff --git a/src/main/java/com/penguineering/gartenplus/ui/appframe/LoggedUserView.java b/src/main/java/com/penguineering/gartenplus/ui/appframe/LoggedUserView.java new file mode 100644 index 0000000..f585a84 --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/ui/appframe/LoggedUserView.java @@ -0,0 +1,46 @@ +package com.penguineering.gartenplus.ui.appframe; + +import com.penguineering.gartenplus.auth.user.UserDTO; +import com.vaadin.flow.component.avatar.Avatar; +import com.vaadin.flow.component.avatar.AvatarVariant; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.orderedlayout.FlexComponent; +import com.vaadin.flow.component.orderedlayout.HorizontalLayout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.Optional; +import java.util.function.Supplier; + +public class LoggedUserView extends Div { + Logger logger = LoggerFactory.getLogger(LoggedUserView.class); + + final Avatar avatar; + + public LoggedUserView(Supplier currentUser) { + Optional optUser = Optional.ofNullable(currentUser.get()); + + HorizontalLayout avatarLayout = new HorizontalLayout(); + avatarLayout.setAlignItems(FlexComponent.Alignment.CENTER); + + avatar = new Avatar(); + avatar.setId("logged-user-avatar"); + avatar.addThemeVariants(AvatarVariant.LUMO_LARGE); + avatar.setTooltipEnabled(true); + + avatarLayout.add(avatar); + add(avatarLayout); + + // Set the user's name + optUser. + map(UserDTO::displayName) + .ifPresent(avatar::setName); + + // load the avatar image + optUser + .map(UserDTO::avatarUrl) + .map(URI::toASCIIString) + .ifPresent(avatar::setImage); + } +} diff --git a/src/main/java/com/penguineering/gartenplus/views/HomeView.java b/src/main/java/com/penguineering/gartenplus/views/HomeView.java new file mode 100644 index 0000000..b5a8bfd --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/views/HomeView.java @@ -0,0 +1,19 @@ +package com.penguineering.gartenplus.views; + +import com.penguineering.gartenplus.ui.appframe.AppFrameLayout; +import com.penguineering.gartenplus.ui.appframe.GartenplusPage; +import com.vaadin.flow.component.html.H1; +import com.vaadin.flow.component.html.Paragraph; +import com.vaadin.flow.component.orderedlayout.VerticalLayout; +import com.vaadin.flow.router.Route; +import com.vaadin.flow.router.RouterLayout; +import jakarta.annotation.security.PermitAll; + +@Route(value = "", layout = AppFrameLayout.class) +@PermitAll +public class HomeView extends GartenplusPage { + + public HomeView() { + add(new Paragraph("Der Garten wächst noch …")); + } +} diff --git a/src/main/resources/META-INF/resources/assets/GartenPlusLogo.png b/src/main/resources/META-INF/resources/assets/GartenPlusLogo.png new file mode 100644 index 0000000..e668744 Binary files /dev/null and b/src/main/resources/META-INF/resources/assets/GartenPlusLogo.png differ diff --git a/src/main/resources/META-INF/resources/assets/GartenPlusLogoRounded.png b/src/main/resources/META-INF/resources/assets/GartenPlusLogoRounded.png new file mode 100644 index 0000000..1abb98b Binary files /dev/null and b/src/main/resources/META-INF/resources/assets/GartenPlusLogoRounded.png differ diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..c35bd55 Binary files /dev/null and b/src/main/resources/static/favicon.ico differ