From 2db4fe29e828cce2248fa6c77177e5d8b54d9b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eik=20Hvattum=20R=C3=B8geberg?= Date: Thu, 19 Oct 2023 21:29:27 +0200 Subject: [PATCH] Add redux actions and form for creating lendable objects --- app/actions/ActionTypes.ts | 10 ++ app/actions/LendableObjectActions.ts | 74 +++++++++++ app/reducers/index.ts | 3 + app/reducers/lendableObjects.ts | 26 ++++ .../lending/LendableObjectAdminDetail.tsx | 125 ++++++++++++++++++ app/routes/lending/LendableObjectDetail.css | 0 app/routes/lending/LendableObjectDetail.tsx | 16 +-- app/routes/lending/LendableObjectEdit.tsx | 94 +++++++++++++ app/routes/lending/LendableObjectsAdmin.tsx | 46 +++++-- app/routes/lending/LendableObjectsList.css | 47 +++---- app/routes/lending/LendableObjectsList.tsx | 3 +- app/routes/lending/index.tsx | 13 ++ app/store/createRootReducer.ts | 2 + app/store/models/LendableObject.d.ts | 26 +++- app/store/models/entities.ts | 3 + 15 files changed, 439 insertions(+), 49 deletions(-) create mode 100644 app/actions/LendableObjectActions.ts create mode 100644 app/reducers/lendableObjects.ts create mode 100644 app/routes/lending/LendableObjectAdminDetail.tsx delete mode 100644 app/routes/lending/LendableObjectDetail.css create mode 100644 app/routes/lending/LendableObjectEdit.tsx diff --git a/app/actions/ActionTypes.ts b/app/actions/ActionTypes.ts index 2a1ac47c67..01eaccea28 100644 --- a/app/actions/ActionTypes.ts +++ b/app/actions/ActionTypes.ts @@ -408,3 +408,13 @@ export const Reaction = { ADD: generateStatuses('Reaction.ADD') as AAT, DELETE: generateStatuses('Reaction.DELETE') as AAT, }; + +/** + * + */ +export const LendableObject = { + FETCH: generateStatuses('LendableObject.FETCH') as AAT, + CREATE: generateStatuses('LendableObject.CREATE') as AAT, + EDIT: generateStatuses('LendableObject.EDIT') as AAT, + DELETE: generateStatuses('LendableObject.DELETE') as AAT, +}; diff --git a/app/actions/LendableObjectActions.ts b/app/actions/LendableObjectActions.ts new file mode 100644 index 0000000000..36bf9a6a58 --- /dev/null +++ b/app/actions/LendableObjectActions.ts @@ -0,0 +1,74 @@ +import callAPI from 'app/actions/callAPI'; +import { lendableObjectSchema } from 'app/reducers'; +import type { + EntityType, + NormalizedEntityPayload, +} from 'app/store/models/entities'; +import type { Thunk } from 'app/types'; +import { LendableObject } from './ActionTypes'; + +export function fetchAllLendableObjects(): Thunk< + Promise> +> { + return callAPI({ + types: LendableObject.FETCH, + endpoint: '/lendableobject/', + schema: [lendableObjectSchema], + meta: { + errorMessage: 'Henting av utlånsobjekter failet', + }, + propagateError: true, + }); +} + +export function fetchLendableObject(id: number): Thunk { + return callAPI({ + types: LendableObject.FETCH, + endpoint: `/lendableobject/${id}/`, + schema: lendableObjectSchema, + meta: { + errorMessage: 'Henting av utlånsobjekt feilet', + }, + propagateError: true, + }); +} + +export function deleteLendableObject(id: number): Thunk { + return callAPI({ + types: LendableObject.DELETE, + endpoint: `/lendableobject/${id}/`, + method: 'DELETE', + meta: { + id, + errorMessage: 'Sletting av utlånsobjekt feilet', + }, + }); +} + +export function createLendableObject(data: any): Thunk { + return callAPI({ + types: LendableObject.CREATE, + endpoint: '/lendableobject/', + method: 'POST', + body: data, + schema: lendableObjectSchema, + meta: { + errorMessage: 'Opprettelse av utlånsobjekt feilet', + }, + }); +} + +export function editLendableObject({ + id, + ...data +}: Record): Thunk { + return callAPI({ + types: LendableObject.EDIT, + endpoint: `/lendableobject/${id}/`, + method: 'PUT', + body: data, + meta: { + errorMessage: 'Endring av utlånsobjekt feilet', + }, + }); +} diff --git a/app/reducers/index.ts b/app/reducers/index.ts index 1dc88bdfd5..b4679a0990 100644 --- a/app/reducers/index.ts +++ b/app/reducers/index.ts @@ -132,3 +132,6 @@ export const followersCompanySchema = new schema.Entity( export const followersUserSchema = new schema.Entity(followersKeyGen('user'), { follower: userSchema, }); +export const lendableObjectSchema = new schema.Entity('lendableObjects', { + responsibleGroups: [groupSchema], +}); diff --git a/app/reducers/lendableObjects.ts b/app/reducers/lendableObjects.ts new file mode 100644 index 0000000000..33136dc8b4 --- /dev/null +++ b/app/reducers/lendableObjects.ts @@ -0,0 +1,26 @@ +import { createSelector } from '@reduxjs/toolkit'; +import { LendableObject } from 'app/actions/ActionTypes'; +import type { RootState } from 'app/store/createRootReducer'; +import createEntityReducer from 'app/utils/createEntityReducer'; +import type { EntityId } from '@reduxjs/toolkit'; + +export default createEntityReducer({ + key: 'lendableObjects', + types: { + fetch: LendableObject.FETCH, + mutate: LendableObject.CREATE, + delete: LendableObject.DELETE, + }, +}); +export const selectLendableObjects = createSelector( + (state: RootState) => state.lendableObjects.byId, + (state: RootState) => state.lendableObjects.items, + (lendableObjectsById, lendableObjectIds) => + lendableObjectIds.map((id) => lendableObjectsById[id]) +); +export const selectLendableObjectById = createSelector( + (state: RootState) => state.lendableObjects.byId, + (_: RootState, id: EntityId) => id, + (lendableObjectsById, lendableObjectId) => + lendableObjectsById[lendableObjectId] +); diff --git a/app/routes/lending/LendableObjectAdminDetail.tsx b/app/routes/lending/LendableObjectAdminDetail.tsx new file mode 100644 index 0000000000..a8ee78c2e4 --- /dev/null +++ b/app/routes/lending/LendableObjectAdminDetail.tsx @@ -0,0 +1,125 @@ +import dayGridPlugin from '@fullcalendar/daygrid'; +import interactionPlugin from '@fullcalendar/interaction'; +import FullCalendar from '@fullcalendar/react'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import moment from 'moment-timezone'; +import { Helmet } from 'react-helmet-async'; +import { useParams, Link } from 'react-router-dom'; +import { Content } from 'app/components/Content'; +import NavigationTab from 'app/components/NavigationTab'; +import type { DetailedLendableObject } from 'app/store/models/LendableObject'; + +type Params = { + lendableObjectId: string; +}; + +const LendableObjectAdminDetail = () => { + const { lendableObjectId } = useParams(); + + const lendingRequest = { + id: 1, + user: { + id: 1, + username: 'Eik', + fullName: 'Test Testesen', + }, + message: 'Jeg vil gjerne låne Soundboks til hyttetur:)', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + approved: false, + }; + + const lendableObject: DetailedLendableObject = { + id: lendableObjectId, + title: 'Soundbox', + description: 'En soundbox som kan brukes til å spille av lyder', + lendingCommentPrompt: 'Hvorfor ønsker du å låne soundboks', + image: + 'https://www.tntpyro.no/wp-content/uploads/2021/08/141_1283224098.jpg', + }; + + const otherLoans = [ + { + id: 2, + startTime: moment().subtract({ days: 1, hours: 2 }), + endTime: moment().subtract({ hours: 8 }), + }, + { + id: 3, + startTime: moment().subtract({ hours: 6 }), + endTime: moment().subtract({ hours: 2 }), + }, + ]; + + const requestEvent = { + id: String(lendingRequest.id), + title: lendingRequest.user.fullName, + start: lendingRequest.startTime.toISOString(), + end: lendingRequest.endTime.toISOString(), + backgroundColor: '#e11617', + borderColor: '#e11617', + }; + + const otherLoanEvents = otherLoans.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#999999', + borderColor: '#999999', + })); + + const otherLoanRequests = [ + { + id: 5, + startTime: moment().subtract({ hours: 2 }), + endTime: moment().add({ hours: 2 }), + }, + ]; + + const otherLoanRequestEvents = otherLoanRequests.map((loan) => ({ + id: String(loan.id), + title: 'Test', + start: loan.startTime.toISOString(), + end: loan.endTime.toISOString(), + backgroundColor: '#f57676', + borderColor: '#f57676', + })); + + return ( + + + +

+ {lendingRequest.message} -{' '} + + {lendingRequest.user.fullName} + {' '} +

+ +
+ ); +}; + +export default LendableObjectAdminDetail; diff --git a/app/routes/lending/LendableObjectDetail.css b/app/routes/lending/LendableObjectDetail.css deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/app/routes/lending/LendableObjectDetail.tsx b/app/routes/lending/LendableObjectDetail.tsx index 6ce0cd6b4d..4a280b99dd 100644 --- a/app/routes/lending/LendableObjectDetail.tsx +++ b/app/routes/lending/LendableObjectDetail.tsx @@ -2,18 +2,18 @@ import dayGridPlugin from '@fullcalendar/daygrid'; import interactionPlugin from '@fullcalendar/interaction'; import FullCalendar from '@fullcalendar/react'; import timeGridPlugin from '@fullcalendar/timegrid'; +import moment from 'moment-timezone'; +import { useState } from 'react'; +import { Field } from 'react-final-form'; import { Helmet } from 'react-helmet-async'; import { useParams } from 'react-router-dom'; import { Content } from 'app/components/Content'; +import { Button, TextArea, TextInput } from 'app/components/Form'; +import LegoFinalForm from 'app/components/Form/LegoFinalForm'; +import Modal from 'app/components/Modal'; import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; import type { DetailedLendableObject } from 'app/store/models/LendableObject'; -import { useState } from 'react'; -import LegoFinalForm from 'app/components/Form/LegoFinalForm'; import { createValidator, required } from 'app/utils/validation'; -import { Field } from 'react-final-form'; -import { Button, DatePicker, TextArea, TextInput } from 'app/components/Form'; -import Modal from 'app/components/Modal'; -import moment from 'moment-timezone'; type Params = { lendableObjectId: string; @@ -91,9 +91,7 @@ const LendableObjectDetail = () => { diff --git a/app/routes/lending/LendableObjectEdit.tsx b/app/routes/lending/LendableObjectEdit.tsx new file mode 100644 index 0000000000..46f3fed642 --- /dev/null +++ b/app/routes/lending/LendableObjectEdit.tsx @@ -0,0 +1,94 @@ +import { Field, FormSpy } from 'react-final-form'; +import { useParams } from 'react-router-dom'; +import { createLendableObject } from 'app/actions/LendableObjectActions'; +import { Content } from 'app/components/Content'; +import { + Button, + EditorField, + Form, + SelectInput, + TextInput, +} from 'app/components/Form'; +import LegoFinalForm from 'app/components/Form/LegoFinalForm'; +import SubmissionError from 'app/components/Form/SubmissionError'; +import { useAppDispatch } from 'app/store/hooks'; +import { roleOptions } from 'app/utils/constants'; +import { spySubmittable } from 'app/utils/formSpyUtils'; + +type Params = { + lendableObjectId: string | undefined; +}; + +const LendableObjectEdit = () => { + const { lendableObjectId } = useParams(); + const isNew = lendableObjectId === undefined; + + const dispatch = useAppDispatch(); + + const onSubmit = (values) => + dispatch( + createLendableObject({ + ...values, + responsibleGroups: values.responsibleGroups.map((group) => group.id), + responsibleRoles: values.responsibleRoles.map((role) => role.value), + }) + ); + + return ( + + + {({ handleSubmit }) => ( +
+ + {(form) => { + return
{JSON.stringify(form.values, undefined, 2)}
; + }} +
+ + + + + + + {spySubmittable((submittable) => ( + + ))} + + )} +
+
+ ); +}; + +export default LendableObjectEdit; diff --git a/app/routes/lending/LendableObjectsAdmin.tsx b/app/routes/lending/LendableObjectsAdmin.tsx index 0b0a62b420..d855493612 100644 --- a/app/routes/lending/LendableObjectsAdmin.tsx +++ b/app/routes/lending/LendableObjectsAdmin.tsx @@ -1,21 +1,49 @@ +import moment from 'moment-timezone'; +import { Helmet } from 'react-helmet-async'; import Card from 'app/components/Card'; import { Content } from 'app/components/Content'; import NavigationTab from 'app/components/NavigationTab'; -import { Helmet } from 'react-helmet-async'; import styles from './LendableObjectsAdmin.css'; -const PendingLendingRequest = () => { - return test; +type LendingRequestProps = { + pending: boolean; + request: any; }; -const ApprovedLendingRequest = () => { - return test; +const LendingRequest = ({ request }: LendingRequestProps) => { + return ( + + {request.lendableObject.title} - {request.user} + + ); }; const LendableObjectsAdmin = () => { const lendingRequests = [ - { user: '', startTime: '', endTime: '', approved: false, bitch: true }, - { user: '', startTime: '', endTime: '', approved: false }, + { + id: 1, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + approved: false, + lendableObject: { + id: 1, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, + { + id: 2, + user: 'Test Testesen', + startTime: moment().subtract({ hours: 2 }), + endTime: moment(), + approved: false, + lendableObject: { + id: 2, + title: 'Grill', + image: 'https://food.unl.edu/newsletters/images/grilled-kabobs.jpg', + }, + }, ]; return ( @@ -26,13 +54,13 @@ const LendableObjectsAdmin = () => { {lendingRequests .filter((request) => !request.approved) .map((request) => ( - + ))}

Godkjente utlånsforespørsler

{lendingRequests .filter((request) => request.approved) .map((request) => ( - + ))} ); diff --git a/app/routes/lending/LendableObjectsList.css b/app/routes/lending/LendableObjectsList.css index f6ca193daf..f89666897b 100644 --- a/app/routes/lending/LendableObjectsList.css +++ b/app/routes/lending/LendableObjectsList.css @@ -1,43 +1,44 @@ @import url('~app/styles/variables.css'); .searchBar { - margin-bottom: 2rem; + margin-bottom: 2rem; } .lendableObjectsContainer { - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-gap: 2rem; + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-gap: 2rem; } .lendableObjectCard { - display: flex; - flex-direction: column; - align-items: stretch; - padding: 0; + display: flex; + flex-direction: column; + align-items: stretch; + padding: 0; } .lendableObjectImage { - height: 15rem; - object-fit: cover; + height: 15rem; + object-fit: cover; } .lendableObjectFooter { - display: flex; - justify-content: center; - color: var(--lego-font-color); - font-size: 1.1rem; - font-weight: bold; - background-color: var(--color-gray-1); + display: flex; + justify-content: center; + color: var(--lego-font-color); + font-size: 1.1rem; + font-weight: bold; + background-color: var(--color-gray-1); } @media (--medium-viewport) { - .lendableObjectsContainer { - grid-template-columns: repeat(2, 1fr); - } + .lendableObjectsContainer { + grid-template-columns: repeat(2, 1fr); + } } + @media (--small-viewport) { - .lendableObjectsContainer { - grid-template-columns: 1fr; - } -} \ No newline at end of file + .lendableObjectsContainer { + grid-template-columns: 1fr; + } +} diff --git a/app/routes/lending/LendableObjectsList.tsx b/app/routes/lending/LendableObjectsList.tsx index 4cb1773a4e..a1f0549058 100644 --- a/app/routes/lending/LendableObjectsList.tsx +++ b/app/routes/lending/LendableObjectsList.tsx @@ -1,13 +1,12 @@ import { usePreparedEffect } from '@webkom/react-prepare'; import qs from 'qs'; -import React, { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link, useHistory, useLocation } from 'react-router-dom'; import Card from 'app/components/Card'; import { Content } from 'app/components/Content'; import TextInput from 'app/components/Form/TextInput'; import { Image } from 'app/components/Image'; -import NavigationTab, { NavigationLink } from 'app/components/NavigationTab'; +import NavigationTab from 'app/components/NavigationTab'; import type { ListLendableObject } from 'app/store/models/LendableObject'; import styles from './LendableObjectsList.css'; diff --git a/app/routes/lending/index.tsx b/app/routes/lending/index.tsx index f0789fe0c9..72016352c3 100644 --- a/app/routes/lending/index.tsx +++ b/app/routes/lending/index.tsx @@ -1,7 +1,9 @@ import { Route, Switch } from 'react-router-dom'; import LendableObjectDetail from 'app/routes/lending/LendableObjectDetail'; +import LendableObjectEdit from 'app/routes/lending/LendableObjectEdit'; import LendableObjectsList from 'app/routes/lending/LendableObjectsList'; import PageNotFound from 'app/routes/pageNotFound'; +import LendableObjectAdminDetail from './LendableObjectAdminDetail'; import LendableObjectsAdmin from './LendableObjectsAdmin'; const lendingRoute = ({ @@ -13,11 +15,22 @@ const lendingRoute = ({ }) => ( + + + galleryPictures, groups, joblistings, + lendableObjects, meetingInvitations, meetings, meetingsToken, diff --git a/app/store/models/LendableObject.d.ts b/app/store/models/LendableObject.d.ts index 3296874820..6f149c2e60 100644 --- a/app/store/models/LendableObject.d.ts +++ b/app/store/models/LendableObject.d.ts @@ -1,16 +1,30 @@ -import type { ID } from 'app/store/models/index'; +import type { RoleType } from 'app/utils/constants'; +import type { EntityId } from '@reduxjs/toolkit'; import type { Duration } from 'moment-timezone'; interface LendableObject { - id: ID; + id: EntityId; + image: string; title: string; description: string; - image: string; - lendingCommentPromt: string; + location: string; hasContract: boolean; - maxLendingPeriod: Duration | string; + maxLendingPeriod: null | string | Duration; + //lendingCommentPrompt: string; + responsibleRoles: RoleType[]; + responsibleGroups: EntityId[]; } export type ListLendableObject = Pick; export type DetailedLendableObject = ListLendableObject & - Pick; + Pick< + LendableObject, + | 'description' + | 'location' + | 'hasContract' + | 'maxLendingPeriod' + | 'responsibleRoles' + | 'responsibleGroups' + >; + +export type UnknownLendableObject = ListLendableObject | DetailedLendableObject; diff --git a/app/store/models/entities.ts b/app/store/models/entities.ts index 6e7e94956a..3b01852bfa 100644 --- a/app/store/models/entities.ts +++ b/app/store/models/entities.ts @@ -14,6 +14,7 @@ import type { UnknownGallery } from 'app/store/models/Gallery'; import type { UnknownGalleryPicture } from 'app/store/models/GalleryPicture'; import type { UnknownGroup } from 'app/store/models/Group'; import type { UnknownJoblisting } from 'app/store/models/Joblisting'; +import type { UnknownLendableObject } from 'app/store/models/LendableObject'; import type { UnknownMeeting } from 'app/store/models/Meeting'; import type { MeetingInvitation } from 'app/store/models/MeetingInvitation'; import type Membership from 'app/store/models/Membership'; @@ -50,6 +51,7 @@ export enum EntityType { GalleryPictures = 'galleryPictures', Groups = 'groups', Joblistings = 'joblistings', + LendableObjects = 'lendableObjects', MeetingInvitations = 'meetingInvitations', Meetings = 'meetings', Memberships = 'memberships', @@ -90,6 +92,7 @@ export default interface Entities { [EntityType.GalleryPictures]: Record; [EntityType.Groups]: Record; [EntityType.Joblistings]: Record; + [EntityType.LendableObjects]: Record; [EntityType.MeetingInvitations]: Record; [EntityType.Meetings]: Record; [EntityType.Memberships]: Record;