diff --git a/api-gateway/package.json b/api-gateway/package.json index 23606f3..ac7ca40 100644 --- a/api-gateway/package.json +++ b/api-gateway/package.json @@ -20,14 +20,14 @@ "@types/express-http-proxy": "^1.6.6", "@types/node": "^20.9.0", "@types/source-map-support": "^0.5.10", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", "concurrently": "^8.2.2", "eslint": "^8.53.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-prettier": "^5.0.1", "nodemon": "^3.0.1", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "ts-node": "^10.9.1", "typescript": "^5.2.2" }, diff --git a/api-gateway/yarn.lock b/api-gateway/yarn.lock index 4638216..0c42952 100644 --- a/api-gateway/yarn.lock +++ b/api-gateway/yarn.lock @@ -251,16 +251,16 @@ dependencies: source-map "^0.6.0" -"@typescript-eslint/eslint-plugin@^6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.10.0.tgz#cfe2bd34e26d2289212946b96ab19dcad64b661a" - integrity sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg== +"@typescript-eslint/eslint-plugin@^6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz#52aae65174ff526576351f9ccd41cea01001463f" + integrity sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.10.0" - "@typescript-eslint/type-utils" "6.10.0" - "@typescript-eslint/utils" "6.10.0" - "@typescript-eslint/visitor-keys" "6.10.0" + "@typescript-eslint/scope-manager" "6.11.0" + "@typescript-eslint/type-utils" "6.11.0" + "@typescript-eslint/utils" "6.11.0" + "@typescript-eslint/visitor-keys" "6.11.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -268,72 +268,72 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/parser@^6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.10.0.tgz#578af79ae7273193b0b6b61a742a2bc8e02f875a" - integrity sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog== +"@typescript-eslint/parser@^6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.11.0.tgz#9640d9595d905f3be4f278bf515130e6129b202e" + integrity sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ== dependencies: - "@typescript-eslint/scope-manager" "6.10.0" - "@typescript-eslint/types" "6.10.0" - "@typescript-eslint/typescript-estree" "6.10.0" - "@typescript-eslint/visitor-keys" "6.10.0" + "@typescript-eslint/scope-manager" "6.11.0" + "@typescript-eslint/types" "6.11.0" + "@typescript-eslint/typescript-estree" "6.11.0" + "@typescript-eslint/visitor-keys" "6.11.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.10.0.tgz#b0276118b13d16f72809e3cecc86a72c93708540" - integrity sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg== +"@typescript-eslint/scope-manager@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz#621f603537c89f4d105733d949aa4d55eee5cea8" + integrity sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A== dependencies: - "@typescript-eslint/types" "6.10.0" - "@typescript-eslint/visitor-keys" "6.10.0" + "@typescript-eslint/types" "6.11.0" + "@typescript-eslint/visitor-keys" "6.11.0" -"@typescript-eslint/type-utils@6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.10.0.tgz#1007faede067c78bdbcef2e8abb31437e163e2e1" - integrity sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg== +"@typescript-eslint/type-utils@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz#d0b8b1ab6c26b974dbf91de1ebc5b11fea24e0d1" + integrity sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA== dependencies: - "@typescript-eslint/typescript-estree" "6.10.0" - "@typescript-eslint/utils" "6.10.0" + "@typescript-eslint/typescript-estree" "6.11.0" + "@typescript-eslint/utils" "6.11.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.10.0.tgz#f4f0a84aeb2ac546f21a66c6e0da92420e921367" - integrity sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg== +"@typescript-eslint/types@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.11.0.tgz#8ad3aa000cbf4bdc4dcceed96e9b577f15e0bf53" + integrity sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA== -"@typescript-eslint/typescript-estree@6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.10.0.tgz#667381eed6f723a1a8ad7590a31f312e31e07697" - integrity sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg== +"@typescript-eslint/typescript-estree@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz#7b52c12a623bf7f8ec7f8a79901b9f98eb5c7990" + integrity sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ== dependencies: - "@typescript-eslint/types" "6.10.0" - "@typescript-eslint/visitor-keys" "6.10.0" + "@typescript-eslint/types" "6.11.0" + "@typescript-eslint/visitor-keys" "6.11.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.10.0.tgz#4d76062d94413c30e402c9b0df8c14aef8d77336" - integrity sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg== +"@typescript-eslint/utils@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.11.0.tgz#11374f59ef4cea50857b1303477c08aafa2ca604" + integrity sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.10.0" - "@typescript-eslint/types" "6.10.0" - "@typescript-eslint/typescript-estree" "6.10.0" + "@typescript-eslint/scope-manager" "6.11.0" + "@typescript-eslint/types" "6.11.0" + "@typescript-eslint/typescript-estree" "6.11.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@6.10.0": - version "6.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.10.0.tgz#b9eaf855a1ac7e95633ae1073af43d451e8f84e3" - integrity sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg== +"@typescript-eslint/visitor-keys@6.11.0": + version "6.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz#d991538788923f92ec40d44389e7075b359f3458" + integrity sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ== dependencies: - "@typescript-eslint/types" "6.10.0" + "@typescript-eslint/types" "6.11.0" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -1621,10 +1621,10 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" -prettier@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" - integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== +prettier@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e" + integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw== proxy-addr@~2.0.7: version "2.0.7" diff --git a/frontend/package.json b/frontend/package.json index cb73f5a..8f416cd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,8 @@ "dependencies": { "babel-plugin-styled-components": "^2.1.4", "core-js": "^3.33.0", + "date-fns": "^2.30.0", + "lodash": "^4.17.21", "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.18.0", @@ -23,13 +25,15 @@ }, "devDependencies": { "@babel/core": "^7.23.2", + "@types/lodash": "^4.14.201", + "@types/node": "^20.9.0", "@babel/preset-env": "^7.23.2", "@types/react": "^18.2.36", "@types/react-dom": "^18.2.14", "@types/react-router-dom": "^5.3.3", "@types/styled-components": "^5.1.29", - "@typescript-eslint/eslint-plugin": "^6.9.0", - "@typescript-eslint/parser": "^6.9.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "@typescript-eslint/parser": "^6.11.0", "axios": "^1.6.0", "babel-loader": "^9.1.3", "concurrently": "^8.2.2", @@ -46,11 +50,10 @@ "eslint-plugin-react-hooks": "^4.6.0", "express": "^4.18.2", "express-http-proxy": "^2.0.0", - "lodash": "^4.17.21", "postcss": "^8.4.31", "postcss-loader": "^7.3.3", "postcss-preset-env": "^9.3.0", - "prettier": "^3.0.3", + "prettier": "^3.1.0", "style-loader": "^3.3.3", "ts-loader": "^9.5.0", "ts-node": "^10.9.1", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4641931..c82fe0f 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,10 +1,43 @@ import React from 'react' -import { Navigate, createBrowserRouter, Outlet } from 'react-router-dom' +import { Navigate, createBrowserRouter, Outlet, Link } from 'react-router-dom' +import styled from 'styled-components' -import { HelloWorldPage } from './hello/HelloWorldPage' +import { CreateStudentPage } from './students/CreateStudentPage' +import { StudentCasesSearchPage } from './students/StudentCasesSearchPage' +import { StudentPage } from './students/StudentPage' +import { StudentsSearchPage } from './students/StudentsSearchPage' + +const AppContainer = styled.div` + padding: 0 16px; + max-width: 1024px; + margin: 0 auto; + background-color: #fff; + min-height: 600px; +` + +const Header = styled.nav` + height: 64px; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + border-bottom: 2px double #888; + margin-bottom: 16px; + > * { + margin-right: 32px; + } +` function App() { - return + return ( + +
+ Tapaukset + Oppivelvolliset +
+ +
+ ) } export const appRouter = createBrowserRouter([ @@ -13,16 +46,28 @@ export const appRouter = createBrowserRouter([ element: , children: [ { - path: '/children', - element: + path: '/oppivelvolliset', + element: + }, + { + path: '/tapaukset', + element: + }, + { + path: '/oppivelvolliset/uusi', + element: + }, + { + path: '/oppivelvolliset/:id', + element: }, { path: '/*', - element: + element: }, { index: true, - element: + element: } ] } diff --git a/frontend/src/hello/HelloWorldPage.tsx b/frontend/src/hello/HelloWorldPage.tsx deleted file mode 100644 index a387f3d..0000000 --- a/frontend/src/hello/HelloWorldPage.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React, { useEffect, useState } from 'react' - -import { apiGetHellos, HelloResponse } from './api' - -export const HelloWorldPage = React.memo(function HelloWorldPage() { - const [helloResponse, setHelloResponse] = useState(null) - useEffect(() => { - void apiGetHellos().then(setHelloResponse) - }, []) - - return ( -
-

Oppivelvollisuus

- {helloResponse &&
Rivejä kannassa: {helloResponse.rows}
} -
- ) -}) diff --git a/frontend/src/hello/api.ts b/frontend/src/hello/api.ts deleted file mode 100644 index 076ccbb..0000000 --- a/frontend/src/hello/api.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { apiClient } from '../api-client' - -export interface HelloResponse { - rows: number -} - -export const apiGetHellos = (): Promise => - apiClient.get('/hello').then((res) => res.data) diff --git a/frontend/src/index.css b/frontend/src/index.css index d86c7c7..8a9bfc4 100755 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -35,3 +35,7 @@ a { cursor: pointer; text-decoration: none; } + +h1, h2, h3, p { + margin: 0; +} diff --git a/frontend/src/shared/dates.ts b/frontend/src/shared/dates.ts new file mode 100644 index 0000000..3d310cf --- /dev/null +++ b/frontend/src/shared/dates.ts @@ -0,0 +1,10 @@ +import { format, parse } from 'date-fns' + +export const parseDate = (date: string) => { + try { + return parse(date, 'dd.MM.yyyy', new Date()) + } catch (e) { + return undefined + } +} +export const formatDate = (date: Date) => format(date, 'dd.MM.yyyy') diff --git a/frontend/src/shared/layout.tsx b/frontend/src/shared/layout.tsx new file mode 100644 index 0000000..2d1dc3a --- /dev/null +++ b/frontend/src/shared/layout.tsx @@ -0,0 +1,46 @@ +import styled from 'styled-components' + +export const FlexCol = styled.div` + display: flex; + flex-direction: column; +` + +export const FlexRow = styled.div` + display: flex; + flex-direction: row; +` + +export const FlexColWithGaps = styled(FlexCol)<{ $gapSize?: 's' | 'm' | 'L' }>` + > * { + margin-bottom: ${(p) => + p.$gapSize === 'L' ? '32px' : p.$gapSize === 'm' ? '16px' : '8px'}; + } +` + +export const FlexRowWithGaps = styled(FlexRow)<{ $gapSize?: 's' | 'm' | 'L' }>` + > * { + margin-right: ${(p) => + p.$gapSize === 'L' ? '32px' : p.$gapSize === 'm' ? '16px' : '8px'}; + } +` + +export const FlexLeftRight = styled(FlexRow)` + justify-content: space-between; + align-items: center; +` + +export const VerticalGap = styled.div<{ $size?: 's' | 'm' | 'L' }>` + height: ${(p) => + p.$size === 'L' ? '32px' : p.$size === 'm' ? '16px' : '8px'}; +` + +export const Table = styled.table` + td { + border-top: 1px solid #888; + border-right: 1px solid #888; + } + + td:last-child { + border-right: none; + } +` diff --git a/frontend/src/shared/typography.tsx b/frontend/src/shared/typography.tsx new file mode 100644 index 0000000..3a34448 --- /dev/null +++ b/frontend/src/shared/typography.tsx @@ -0,0 +1,13 @@ +import styled from 'styled-components' + +export const Label = styled.label` + font-weight: bold; +` + +export const H1 = styled.h1`` + +export const H2 = styled.h2`` + +export const H3 = styled.h3`` + +export const P = styled.p`` diff --git a/frontend/src/students/CreateStudentPage.tsx b/frontend/src/students/CreateStudentPage.tsx new file mode 100644 index 0000000..d6927f2 --- /dev/null +++ b/frontend/src/students/CreateStudentPage.tsx @@ -0,0 +1,60 @@ +import React, { useState } from 'react' +import { useNavigate } from 'react-router-dom' + +import { FlexColWithGaps, VerticalGap } from '../shared/layout' +import { H1, Label } from '../shared/typography' + +import { apiPostStudent } from './api' + +export const CreateStudentPage = React.memo(function CreateStudentPage() { + const navigate = useNavigate() + const [firstName, setFirstName] = useState('') + const [lastName, setLastName] = useState('') + const [submitting, setSubmitting] = useState(false) + + const valid = firstName.trim() && lastName.trim() + + return ( +
+

Uusi oppivelvollinen

+ + + + + + + setFirstName(e.target.value)} + value={firstName} + /> + + + + setLastName(e.target.value)} + value={lastName} + /> + + + + + + +
+ ) +}) diff --git a/frontend/src/students/StudentCaseForm.tsx b/frontend/src/students/StudentCaseForm.tsx new file mode 100644 index 0000000..97a1b50 --- /dev/null +++ b/frontend/src/students/StudentCaseForm.tsx @@ -0,0 +1,118 @@ +import React, { useState } from 'react' + +import { formatDate, parseDate } from '../shared/dates' +import { FlexColWithGaps, FlexRowWithGaps, VerticalGap } from '../shared/layout' +import { H3, Label, P } from '../shared/typography' + +import { apiPostStudentCase, apiPutStudentCase, StudentCase } from './api' + +interface CreateProps { + studentId: string + onSaved: () => void + onCancelled: () => void +} +interface ViewProps { + studentCase: StudentCase + editing: false + onStartEdit: () => void +} +interface EditProps { + studentCase: StudentCase + editing: true + onSaved: () => void + onCancelled: () => void +} +type Props = CreateProps | ViewProps | EditProps + +function isCreating(p: Props): p is CreateProps { + return !('studentCase' in p) +} + +function isViewing(p: Props): p is ViewProps { + return 'studentCase' in p && !p.editing +} + +export const StudentCaseForm = React.memo(function StudentCaseForm( + props: Props +) { + const [openedAt, setOpenedAt] = useState( + formatDate(isCreating(props) ? new Date() : props.studentCase.openedAt) + ) + const [info, setInfo] = useState( + isCreating(props) ? '' : props.studentCase.info + ) + const [submitting, setSubmitting] = useState(false) + + const valid = parseDate(openedAt) !== undefined + + return ( +
+ +

+ {isCreating(props) + ? 'Uusi tapaus' + : `Tapaus ${formatDate(props.studentCase.openedAt)}`} +

+ {isViewing(props) && ( + + )} +
+ + + + + + + {isViewing(props) ? ( + {formatDate(props.studentCase.openedAt)} + ) : ( + setOpenedAt(e.target.value)} + value={openedAt} + /> + )} + + + + {isViewing(props) ? ( +

{props.studentCase.info}

+ ) : ( +