diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index ef74cbdbf973cfbfe0a0c596ae4e97961d923f76..0557dd77520ee8597aa6213c09a952ee8e5499d4 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -22,6 +22,7 @@ "plugins": ["react", "@typescript-eslint"], "rules": { "semi": "off", + "react/jsx-props-no-spreading": "off", "@typescript-eslint/semi": ["error", "never"] } } diff --git a/frontend/public/index.html b/frontend/public/index.html index aa069f27cbd9d53394428171c3989fd03db73c76..2bc0e084ce0bca917aa25836d941d54011b64d6a 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -10,34 +10,11 @@ content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> - <!-- - manifest.json provides metadata used when your web app is installed on a - user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ - --> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> - <!-- - Notice the use of %PUBLIC_URL% in the tags above. - It will be replaced with the URL of the `public` folder during the build. - Only files inside the `public` folder can be referenced from the HTML. - - Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will - work correctly both with client-side routing and a non-root public URL. - Learn how to configure a non-root public URL by running `npm run build`. - --> - <title>React App</title> + <title>Guest Registration</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> - <!-- - This HTML file is a template. - If you open it directly in the browser, you will see an empty page. - - You can add webfonts, meta tags, or analytics to this file. - The build step will place the bundled scripts into the <body> tag. - - To begin the development, run `npm start` or `yarn start`. - To create a production bundle, use `npm run build` or `yarn build`. - --> </body> </html> diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index 713a15ba11df4cc1687f3576cf281ae912279930..4c2ab711ff2501d634315c3041da3394a142b2eb 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -14,5 +14,8 @@ }, "loading": "Loading...", "termsHeader": "Terms", - "staging": "Staging" + "staging": "Staging", + "firstName": "First name", + "lastName": "Last name", + "dateOfBirth": "Date of birth" } diff --git a/frontend/public/locales/nb/common.json b/frontend/public/locales/nb/common.json index 57d358c0dd322350f45ef0216625428a6e76b6ea..11a25642f5fb9b4214464761810703467700b845 100644 --- a/frontend/public/locales/nb/common.json +++ b/frontend/public/locales/nb/common.json @@ -7,12 +7,15 @@ "change": "Bytt språk til {{lang}}" }, "fnr": "Fødselsnummer", - "header":{ + "header": { "applicationTitle": "Gjesteregistrering", "applicationDescription": "Registreringstjeneste for gjester", "selectLanguage": "Velg språk" }, "loading": "Laster...", "termsHeader": "Vilkår", - "staging": "Staging" + "staging": "Staging", + "firstName": "Fornavn", + "lastName": "Etternavn", + "dateOfBirth": "Fødselsdato" } diff --git a/frontend/public/locales/nn/common.json b/frontend/public/locales/nn/common.json index 2e76d48dfcb7f5e3e9582083fdffa687b333cb54..21039b90560962d4338ef53f60348eb0d8edba48 100644 --- a/frontend/public/locales/nn/common.json +++ b/frontend/public/locales/nn/common.json @@ -8,12 +8,15 @@ "change": "Bytt språk til {{lang}}" }, "fnr": "National identity number", - "header":{ + "header": { "applicationTitle": "Gjesteregistrering", "applicationDescription": "Registreringsteneste for gjester", "selectLanguage": "Velg språk" }, "loading": "Lastar...", "termsHeader": "Vilkår", - "staging": "Staging" + "staging": "Staging", + "firstName": "Fornamn", + "lastName": "Etternamn", + "dateOfBirth": "Fødselsdato" } diff --git a/frontend/src/components/button/index.tsx b/frontend/src/components/button/index.tsx index 9eb200519a6b51d5342bd15a8a5b9a1b188e2fe7..7054c37015069fd236de9a1312f0979977135538 100644 --- a/frontend/src/components/button/index.tsx +++ b/frontend/src/components/button/index.tsx @@ -2,6 +2,7 @@ import styled from 'styled-components/macro' export const Button = styled.a` display: inline-block; + cursor: pointer; border-radius: 3px; padding: 0.5rem 0; margin: 0.5rem 1rem; diff --git a/frontend/src/components/dateinput/index.tsx b/frontend/src/components/dateinput/index.tsx index 215661edc9b8076bbddff1fe61a6654da2e40605..a12c37a7dd0260d5190847a53849dc3e01923f40 100644 --- a/frontend/src/components/dateinput/index.tsx +++ b/frontend/src/components/dateinput/index.tsx @@ -1,29 +1,25 @@ -import { addDays } from 'date-fns' -import nb from 'date-fns/locale/nb' -import nn from 'date-fns/locale/nn' -import React, { useState } from 'react' -import DatePicker, { registerLocale } from 'react-datepicker' +import React from 'react' +import DatePicker, { + ReactDatePickerProps, + registerLocale, +} from 'react-datepicker' + import 'react-datepicker/dist/react-datepicker.css' import { useTranslation } from 'react-i18next' +import nb from 'date-fns/locale/nb' +import nn from 'date-fns/locale/nn' + registerLocale('nb', nb) registerLocale('nn', nn) // CSS Modules, react-datepicker-cssmodules.css // import 'react-datepicker/dist/react-datepicker-cssmodules.css'; -const DateInput = () => { - const [startDate, setStartDate] = useState(new Date()) +const DateInput = (props: ReactDatePickerProps) => { const { i18n } = useTranslation() return ( - <DatePicker - dateFormat="yyyy-MM-dd" - locale={i18n.language} - selected={startDate} - minDate={startDate} - maxDate={addDays(new Date(), 365)} - onChange={(date: any) => setStartDate(date)} - /> + <DatePicker dateFormat="yyyy-MM-dd" locale={i18n.language} {...props} /> ) } diff --git a/frontend/src/components/form/fnr.tsx b/frontend/src/components/form/fnr.tsx index 16e356125bdef88dfbae3e90d18164ef31e6b318..ee7cd1f2a920bc3b443d11f4c5a084f0321b5065 100644 --- a/frontend/src/components/form/fnr.tsx +++ b/frontend/src/components/form/fnr.tsx @@ -1,11 +1,7 @@ import React from 'react' -import validator from '@navikt/fnrvalidator' -import { UseFormReturn } from 'react-hook-form' -export function isValidIdnr(data: string) { - const validationResult = validator.idnr(data) - return validationResult.status === 'valid' -} +import { UseFormReturn } from 'react-hook-form' +import { isValidFnr } from 'utils' interface FnrProps extends Partial<Pick<UseFormReturn, 'register'>> { name: string @@ -26,7 +22,7 @@ function Fnr(props: FnrProps) { // eslint-disable-next-line react/jsx-props-no-spreading {...register(name, { required: 'Fnr is required', - validate: isValidIdnr, + validate: isValidFnr, })} id="fnr" /> diff --git a/frontend/src/components/form/form.tsx b/frontend/src/components/form/form.tsx index 305eb2d302e1eb138480cff9399447c69d1c0e92..2964e7f2591152ec5e6fb2d5e83fe2ac40861538 100644 --- a/frontend/src/components/form/form.tsx +++ b/frontend/src/components/form/form.tsx @@ -22,6 +22,7 @@ export default function Form(props: FormProps) { ...{ // eslint-disable-next-line react/jsx-props-no-spreading ...child.props, + control: methods.control, register: methods.register, errors, key: child.props.name, diff --git a/frontend/src/components/form/input.tsx b/frontend/src/components/form/input.tsx index 231a731c4b4be4fd8e98e684373dbba1bbca134f..94e7332f465c98a0fa1561ffb3756ed4c37c8846 100644 --- a/frontend/src/components/form/input.tsx +++ b/frontend/src/components/form/input.tsx @@ -1,12 +1,25 @@ import React from 'react' +import styled from 'styled-components/macro' import { UseFormReturn } from 'react-hook-form' -interface InputProps extends Partial<Pick<UseFormReturn, 'register'>> { +interface InputProps + extends Partial<React.InputHTMLAttributes<HTMLInputElement>>, + Partial<UseFormReturn> { name: string errors?: any type?: 'text' | 'email' | 'number' } +export const StyledInput = styled.input` + width: 50%; + border: 1px solid; + border-width: 0.013rem; + border-radius: 0.3rem; + border-color: #ccc; + box-shadow: inset 0 0 0 0.013rem #4d4d4d; + padding: 0.625rem; +` + function Input(props: InputProps) { // eslint-disable-next-line react/jsx-props-no-spreading const { register, name, errors, type, ...rest } = props @@ -14,7 +27,7 @@ function Input(props: InputProps) { return <></> } // eslint-disable-next-line react/jsx-props-no-spreading - return <input {...register(name)} {...rest} /> + return <StyledInput {...register(name)} {...rest} /> } Input.defaultProps = { diff --git a/frontend/src/components/page/page.tsx b/frontend/src/components/page/page.tsx index 6a2792ecd1a150c8acb9c49016e3f2f6b7c69467..2d95a68cc813c3a214ec92d67108016c3c431d13 100644 --- a/frontend/src/components/page/page.tsx +++ b/frontend/src/components/page/page.tsx @@ -6,6 +6,11 @@ import styled from 'styled-components/macro' const StyledPage = styled.main` display: block; + justify-content: space-between; + margin: 0 auto; + max-width: ${(props) => props.theme.appMaxWidth}; + padding: ${(props) => + `0.5rem ${props.theme.horizontalPadding} 1rem ${props.theme.horizontalPadding}`}; ` const StyledPageHeader = styled.h2` diff --git a/frontend/src/globalStyles.ts b/frontend/src/globalStyles.ts index f81dda019fb53dbc61108181c833a0187aa06164..d158638476ed156b47e87db0e1e30432cd210ae0 100644 --- a/frontend/src/globalStyles.ts +++ b/frontend/src/globalStyles.ts @@ -1,6 +1,10 @@ import { createGlobalStyle } from 'styled-components/macro' const GlobalStyle = createGlobalStyle` + html { + font-size: clamp(24px, calc(24px * 1vw), 36px); + } + body { margin: 0; min-height: 100vh; diff --git a/frontend/src/routes/register/index.tsx b/frontend/src/routes/register/index.tsx index 42193d0c537d9d8824db10238aa5223a201cc0a8..629080bdea3a7580c42a5e41c25f53a569aa1c94 100644 --- a/frontend/src/routes/register/index.tsx +++ b/frontend/src/routes/register/index.tsx @@ -1,24 +1,78 @@ import React from 'react' import { useTranslation } from 'react-i18next' +import { StyledInput } from 'components/form/input' import DateInput from 'components/dateinput' -import { Fnr, Form, Input, Select } from 'components/form' +import Button from 'components/button' import Page from 'components/page' +import { useForm, Controller, SubmitHandler } from 'react-hook-form' +import format from 'date-fns/format' +import { postJsonOpts } from 'utils' + +type RegisterFormData = { + first_name: string + last_name: string + date_of_birth: Date +} export default function Register() { - const { t } = useTranslation(['common', 'footer']) + const { t } = useTranslation(['common']) + + const submit: SubmitHandler<RegisterFormData> = (data) => { + const payload = { + first_name: data.first_name, + last_name: data.last_name, + date_of_birth: format(data.date_of_birth, 'yyyy-MM-dd'), + } + console.log('submitting', JSON.stringify(payload)) + fetch('http://localhost:3000/api/ui/v1/register/', postJsonOpts(payload)) + .then((res) => res.text()) + .then((result) => { + console.log('result', result) + }) + .catch((error) => { + console.log('error', error) + }) + } + + const { + register, + control, + handleSubmit, + // setValue, + // formState: { errors }, + } = useForm<RegisterFormData>() + const onSubmit = handleSubmit(submit) return ( <Page header="Register as a guest"> - <Form onSubmit={() => {}}> - <Input name="firstName" /> - <Input name="lastName" /> - <DateInput /> - <Select name="gender" options={['female', 'male', 'other']} /> - <Fnr name={t('common:fnr')} /> - <button type="submit">Submit</button> - </Form> - <br /> + <form onSubmit={onSubmit}> + <StyledInput + {...register(`first_name`)} + placeholder={t('common:firstName')} + /> + <StyledInput + {...register(`last_name`)} + placeholder={t('common:lastName')} + /> + <Controller + name="date_of_birth" + control={control} + render={({ field }) => ( + <DateInput + customInput={<StyledInput />} + placeholderText={t('common:dateOfBirth')} + onChange={(date) => field.onChange(date)} + selected={field.value} + /> + )} + /> + {/* <Select name="gender" options={['female', 'male', 'other']} /> + <Fnr name={t('common:fnr')} /> */} + <Button as="button" type="submit"> + Submit + </Button> + </form> </Page> ) } diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..80e6cd182ccc39586653a9f3200ba64bcd862505 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,43 @@ +import validator from '@navikt/fnrvalidator' + +export function getCookie(name: string) { + if (!document.cookie) { + return null + } + + const cookies = document.cookie + .split(';') + .map((c) => c.trim()) + .filter((c) => c.startsWith(`${name}=`)) + + if (cookies.length === 0) { + return null + } + return decodeURIComponent(cookies[0].split('=')[1]) +} + +export function maybeCsrfToken() { + const csrfToken = getCookie('csrftoken') + if (!csrfToken) { + return null + } + return { + 'X-CSRFToken': csrfToken, + } +} + +export function postJsonOpts(data: object): RequestInit { + return { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...maybeCsrfToken(), + }, + body: JSON.stringify(data), + credentials: 'same-origin', + } +} + +export function isValidFnr(data: string): boolean { + return validator.idnr(data).status === 'valid' +} diff --git a/gregsite/settings/base.py b/gregsite/settings/base.py index 758c3ed9db51bb3632d2b35bb5000a2d7b6f610f..de2c8a1be8fa12a4043e71b491b5e9e4859b8444 100644 --- a/gregsite/settings/base.py +++ b/gregsite/settings/base.py @@ -64,7 +64,7 @@ MIDDLEWARE = [ REST_FRAMEWORK = { "DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning", "DEFAULT_VERSION": "v1", - "ALLOWED_VERSIONS": ("v1",), + "ALLOWED_VERSIONS": ("v1", "gregui-v1"), "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework.authentication.TokenAuthentication", "rest_framework.authentication.SessionAuthentication", diff --git a/gregsite/urls.py b/gregsite/urls.py index e0f9859117293eb3708c6123a95cc8c0802b9e4a..649c186c85e3fcee051a11e82371e39f09ee3d13 100644 --- a/gregsite/urls.py +++ b/gregsite/urls.py @@ -17,10 +17,12 @@ from django.contrib import admin from django.urls import include, path from greg import urls as greg_urls +from gregui import urls as gregui_urls admin.autodiscover() urlpatterns = [ path("admin/", admin.site.urls), path("", include(greg_urls.urlpatterns)), + path("", include(gregui_urls.urlpatterns)), ] diff --git a/gregui/api/__init__.py b/gregui/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gregui/api/serializers/__init__.py b/gregui/api/serializers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gregui/api/serializers/guest.py b/gregui/api/serializers/guest.py new file mode 100644 index 0000000000000000000000000000000000000000..ecdb652c5de590faede392fc0c269c44b9755df0 --- /dev/null +++ b/gregui/api/serializers/guest.py @@ -0,0 +1,21 @@ +from rest_framework import serializers + +from greg.models import Person + + +class GuestRegisterSerializer(serializers.ModelSerializer): + def create(self, validated_data): + obj = super().create(validated_data) + return obj + + class Meta: + model = Person + fields = ("id", "first_name", "last_name", "date_of_birth") + read_only_fields = ("id",) + extra_kwargs = { + "first_name": {"required": True}, + "last_name": {"required": True}, + "date_of_birth": {"required": True}, + # 'email': {'required': True}, + # 'phone_number': {'required': True}, + } diff --git a/gregui/api/urls.py b/gregui/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..0d39bbd34b782108f49262803e7722186bba22a7 --- /dev/null +++ b/gregui/api/urls.py @@ -0,0 +1,12 @@ +from django.urls import re_path + +from rest_framework.routers import DefaultRouter + +from gregui.api.views.guest import GuestRegisterView + +router = DefaultRouter(trailing_slash=False) + +urlpatterns = router.urls +urlpatterns += [ + re_path(r"register/$", GuestRegisterView.as_view(), name="guest-register"), +] diff --git a/gregui/api/views/__init__,py b/gregui/api/views/__init__,py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gregui/api/views/guest.py b/gregui/api/views/guest.py new file mode 100644 index 0000000000000000000000000000000000000000..4d9370203b94acd1563b9a8a1669964841721cc1 --- /dev/null +++ b/gregui/api/views/guest.py @@ -0,0 +1,10 @@ +from rest_framework import viewsets, filters, permissions +from rest_framework.generics import CreateAPIView + +from greg.models import Person +from gregui.api.serializers.guest import GuestRegisterSerializer + +class GuestRegisterView(CreateAPIView): + queryset = Person.objects.all() + permission_classes = [permissions.AllowAny] + serializer_class = GuestRegisterSerializer \ No newline at end of file diff --git a/gregui/tests.py b/gregui/tests.py deleted file mode 100644 index 7ce503c2dd97ba78597f6ff6e4393132753573f6..0000000000000000000000000000000000000000 --- a/gregui/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/gregui/tests/__init__.py b/gregui/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gregui/tests/api/__init__.py b/gregui/tests/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/gregui/tests/api/test_guest.py b/gregui/tests/api/test_guest.py new file mode 100644 index 0000000000000000000000000000000000000000..17102f7634fb8973f86a34d8e41715bb930025d4 --- /dev/null +++ b/gregui/tests/api/test_guest.py @@ -0,0 +1,13 @@ +import pytest + +from rest_framework import status +from rest_framework.reverse import reverse + + +@pytest.mark.django_db +def test_register_guest(client): + data = {"first_name": "Foo", "last_name": "Bar", "date_of_birth": "2020-09-21"} + url = reverse("gregui-v1:guest-register") + response = client.post(url, data) + assert response.status_code == status.HTTP_201_CREATED + assert response.json() == {"id": 1, **data} diff --git a/gregui/tests/conftest.py b/gregui/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..a59673226660f89957c51362bfc0499d0b546832 --- /dev/null +++ b/gregui/tests/conftest.py @@ -0,0 +1,22 @@ +from rest_framework.test import APIClient +from django.contrib.auth import get_user_model + +import pytest + +from greg.models import ( + Consent, + Notification, + Person, + Sponsor, + SponsorOrganizationalUnit, + Identity, + Role, + RoleType, + OrganizationalUnit, + ConsentType, +) + +@pytest.fixture +def client() -> APIClient: + client = APIClient() + return client diff --git a/gregui/urls.py b/gregui/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..394acdecb7c466dfdb5c5bf79f715b4c6301ea0b --- /dev/null +++ b/gregui/urls.py @@ -0,0 +1,10 @@ +from typing import List +from django.urls import path, include +from django.urls.resolvers import URLResolver +from rest_framework.versioning import NamespaceVersioning + +from gregui.api import urls as api_urls + +urlpatterns: List[URLResolver] = [ + path("api/ui/v1/", include((api_urls.urlpatterns, "gregui"), namespace="gregui-v1")), +]