;
+ acceptTermsOfUse?: boolean;
+ setAcceptTermsOfUse?: (value: boolean) => void;
+}): JSX.Element {
+ const { t } = useTranslation();
+
+ const tos2 = application.applicationRound?.termsOfUse;
+
+ return (
+ <>
+
+
+ {t("application:preview.basicInfoSubHeading")}
+
+
+
+
+ {tos && (
+
+ }
+ />
+
+ )}
+ {tos2 && (
+
+ }
+ /* TODO TermsBox has accepted and checkbox we could use but for now leaving the single
+ * page specfici checkbox to accept all terms */
+ />
+
+ )}
+ {acceptTermsOfUse != null && setAcceptTermsOfUse != null && (
+
+
+ setAcceptTermsOfUse(e.target.checked)}
+ label={t("application:preview.userAcceptsTerms")}
+ // NOTE I'm assuming we can just hide the whole checkbox in View
+ />
+
+
+ )}
+
+ {/* Wrap the notification in a div, since HDS-notification has as the root element and we need section:last-of-type to hit the last ApplicationSection */}
+
+ {t("application:preview.notification.body")}
+
+
+ >
+ );
+}
diff --git a/apps/ui/components/application/styled.tsx b/apps/ui/components/application/styled.tsx
index e5e3d8f78..b0aa98922 100644
--- a/apps/ui/components/application/styled.tsx
+++ b/apps/ui/components/application/styled.tsx
@@ -1,14 +1,9 @@
import styled from "styled-components";
import { Notification } from "hds-react";
import LabelValue from "../common/LabelValue";
-
-export const TimePreviewContainer = styled.div`
- margin: var(--spacing-xl) 0;
-
- svg {
- margin-top: 2px;
- }
-`;
+import TermsBox from "common/src/termsbox/TermsBox";
+import { breakpoints, fontMedium } from "common";
+import { AccordionWithState } from "@/components/common/Accordion";
export const CheckboxContainer = styled.div`
margin-top: var(--spacing-m);
@@ -27,3 +22,146 @@ export const StyledNotification = styled(Notification)`
`;
export const StyledLabelValue = styled(LabelValue).attrs({ theme: "thin" })``;
+
+export const ApplicationSection = styled.section`
+ display: flex;
+ flex-flow: column;
+ padding: 0;
+ grid-template-columns: repeat(3, 1fr);
+ border: 1px solid var(--color-black-90);
+ /* --application-content-border is the dashed border used between items within an ApplicationSection */
+ --application-content-border: 1px dashed var(--color-black-30);
+ border-bottom: 0;
+
+ &:last-of-type {
+ border-bottom: 1px solid var(--color-black-90);
+ margin-bottom: var(--spacing-layout-s);
+ }
+`;
+
+export const ApplicationSectionHeader = styled.h2`
+ display: flex;
+ justify-content: space-between;
+ width: calc(100% - 2 * var(--spacing-m));
+ height: 26px;
+ margin: 0 var(--spacing-m);
+ padding: var(--spacing-m) 0;
+ border-bottom: var(--application-content-border);
+ font-size: var(--fontsize-heading-s);
+ [class*="statusLabel__"] {
+ position: relative;
+ transform: translateY(-4px);
+ }
+`;
+
+export const ApplicationInfoContainer = styled.div`
+ margin: 0 var(--spacing-m);
+ display: flex;
+ flex-flow: column;
+ @media (min-width: ${breakpoints.m}) {
+ flex-flow: row;
+ }
+`;
+
+export const InfoItemContainer = styled.div`
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: column;
+ align-content: flex-end;
+ position: relative;
+ background: var(--color-white);
+ width: 100%;
+ font-size: var(--fontsize-body-m);
+ border-top: var(--application-content-border);
+
+ > [class^="styled__InfoItem"] {
+ box-sizing: border-box;
+ width: 100%;
+ display: flex;
+ gap: var(--spacing-m);
+ flex-direction: column;
+ align-items: stretch;
+ padding: 0;
+ }
+
+ ol {
+ padding-inline: 40px;
+ }
+
+ li span {
+ white-space: nowrap;
+ ${fontMedium};
+ }
+
+ .info-label {
+ display: flex;
+ gap: var(--spacing-m);
+ margin: 0;
+ line-height: 1;
+ ${fontMedium};
+ font-size: inherit;
+ [class*="Tooltip-module_root"] {
+ /* Position the tooltip button and prevent it from stretching the info-label heading */
+ margin-top: -4px;
+ margin-bottom: -8px;
+ }
+ }
+
+ p {
+ font-size: var(--fontsize-body-m);
+ margin: 0;
+ }
+
+ /* Make InfoItemContainers rows of three columns, with application-content-border between the cols */
+ @media (min-width: ${breakpoints.m}) {
+ width: calc(100% / 3);
+ > [class^="styled__InfoItem"] {
+ padding-inline: var(--spacing-m);
+ }
+
+ /* first child in the row */
+ &:nth-child(3n-2) > [class^="styled__InfoItem"] {
+ padding-left: 0;
+ }
+
+ /* every child except the last one in each row */
+ &:not(:nth-child(3n)) > [class^="styled__InfoItem"] {
+ border-right: var(--application-content-border);
+ }
+
+ /* last child in each row */
+ &:nth-child(-n + 3) {
+ border-top: 0;
+ }
+ }
+`;
+
+export const InfoItem = styled.div`
+ margin: var(--spacing-m) 0;
+ height: 100%;
+ display: flex;
+ ul,
+ ol {
+ margin: 0;
+ padding-inline: var(--spacing-m);
+ }
+`;
+
+export const ScheduleDay = styled.div`
+ display: grid;
+ grid-template-columns: 3rem 1fr 1fr;
+ span:last-child {
+ padding-left: var(--spacing-xs);
+ }
+`;
+
+export const TermsAccordion = styled(AccordionWithState)`
+ --accordion-border-color: var(--color-black-90);
+ [class^="Button-module_label"] div {
+ font-size: var(--fontsize-heading-s);
+ }
+`;
+
+export const CompactTermsBox = styled(TermsBox)`
+ margin-bottom: 0;
+`;
diff --git a/apps/ui/components/common/Accordion.tsx b/apps/ui/components/common/Accordion.tsx
index ced0107d3..1ffefd2df 100644
--- a/apps/ui/components/common/Accordion.tsx
+++ b/apps/ui/components/common/Accordion.tsx
@@ -19,6 +19,7 @@ const AccordionElement = styled.div`
margin-bottom: var(--spacing-layout-xs);
margin-left: 0;
padding-left: 0;
+ --accordion-border-color: var(--color-black-50);
`;
const HeadingButton = styled(Button).attrs({
@@ -32,7 +33,7 @@ const HeadingButton = styled(Button).attrs({
width: 100%;
padding-left: 0;
border-left: 0;
- border-bottom: 1px solid var(--color-black-50) !important;
+ border-bottom: 1px solid var(--accordion-border-color) !important;
span {
${({ theme }) => {
diff --git a/apps/ui/pages/application/[id]/view.tsx b/apps/ui/pages/application/[id]/view.tsx
new file mode 100644
index 000000000..24639064f
--- /dev/null
+++ b/apps/ui/pages/application/[id]/view.tsx
@@ -0,0 +1,97 @@
+import React from "react";
+import Error from "next/error";
+import { useTranslation } from "next-i18next";
+import { useRouter } from "next/router";
+import { getTranslation } from "common/src/common/util";
+import type { GetServerSidePropsContext } from "next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { BlackButton } from "@/styles/util";
+import { ApplicationPageWrapper } from "@/components/application/ApplicationPage";
+import { createApolloClient } from "@/modules/apolloClient";
+import { ViewApplication } from "@/components/application/ViewApplication";
+import { ButtonContainer, CenterSpinner } from "@/components/common/common";
+import {
+ getCommonServerSideProps,
+ getGenericTerms,
+} from "@/modules/serverUtils";
+import { base64encode } from "common/src/helpers";
+import { useApplicationQuery } from "@gql/gql-types";
+
+const View = ({ id: pk, tos }: Props): JSX.Element => {
+ const { t } = useTranslation();
+
+ const router = useRouter();
+
+ const id = base64encode(`ApplicationNode:${pk}`);
+ const {
+ data,
+ error,
+ loading: isLoading,
+ } = useApplicationQuery({
+ variables: { id },
+ skip: !pk,
+ });
+ const { application } = data ?? {};
+
+ if (id == null) {
+ return ;
+ }
+ if (error) {
+ // eslint-disable-next-line no-console -- TODO use logger (sentry)
+ console.error("application query error: ", error);
+ return ;
+ }
+ if (isLoading) {
+ return ;
+ }
+ if (!application) {
+ return ;
+ }
+
+ const round = application.applicationRound;
+ const applicationRoundName =
+ round != null ? getTranslation(round, "name") : "-";
+
+ return (
+
+
+
+ router.back()}>
+ {t("common:prev")}
+
+
+
+ );
+};
+
+type Props = Awaited>["props"];
+
+export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
+ const { locale } = ctx;
+ const commonProps = getCommonServerSideProps();
+ const apolloClient = createApolloClient(commonProps.apiBaseUrl, ctx);
+
+ const tos = await getGenericTerms(apolloClient);
+
+ // TODO should fetch on SSR but we need authentication for it
+ const { query } = ctx;
+ const { id } = query;
+ const pkstring = Array.isArray(id) ? id[0] : id;
+ const pk = Number.isNaN(Number(pkstring)) ? undefined : Number(pkstring);
+
+ return {
+ props: {
+ ...commonProps,
+ key: locale ?? "fi",
+ id: pk ?? null,
+ tos,
+ ...(await serverSideTranslations(locale ?? "fi")),
+ },
+ };
+};
+
+export default View;
diff --git a/apps/ui/pages/applications/[id]/preview.tsx b/apps/ui/pages/applications/[id]/preview.tsx
index 4ece9c4bd..055e7123b 100644
--- a/apps/ui/pages/applications/[id]/preview.tsx
+++ b/apps/ui/pages/applications/[id]/preview.tsx
@@ -10,7 +10,7 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
import { default as ErrorComponent } from "next/error";
import { MediumButton } from "@/styles/util";
import { ButtonContainer, CenterSpinner } from "@/components/common/common";
-import { ViewInner } from "@/components/application/ViewInner";
+import { ViewApplication } from "@/components/application/ViewApplication";
import { createApolloClient } from "@/modules/apolloClient";
import { ApplicationPageWrapper } from "@/components/application/ApplicationPage";
import {
@@ -93,7 +93,7 @@ function Preview(props: PropsNarrowed): JSX.Element {
application={application}
>