Skip to content

Commit 8a9e9b9

Browse files
committed
i dont have time to write commit message. thanks. bye
1 parent bf82c1f commit 8a9e9b9

File tree

6 files changed

+375
-118
lines changed

6 files changed

+375
-118
lines changed
Lines changed: 202 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { createDateTimeInput } from "@/components/forms/DateTimeInput"
21
import { useFormBuilder } from "@/components/forms/Form"
32
import { createSelectInput } from "@/components/forms/SelectInput"
43
import {
@@ -9,20 +8,62 @@ import {
98
getMembershipTypeName,
109
getSpecializationName,
1110
} from "@dotkomonline/types"
12-
import { getNextSemesterStart, getCurrentSemesterStart } from "@dotkomonline/utils"
13-
import { isBefore } from "date-fns"
11+
import {
12+
getCurrentSemesterStart,
13+
getNextSemesterStart,
14+
getStudyGrade,
15+
getCurrentUTC,
16+
getPreviousSemesterStart,
17+
isSpringSemester,
18+
} from "@dotkomonline/utils"
19+
import { isBefore, roundToNearestHours } from "date-fns"
1420
import type { z } from "zod"
15-
import { createNumberInput } from "@/components/forms/NumberInput"
16-
import { Code, Stack, Text } from "@mantine/core"
21+
import { ActionIcon, Button, Group, NumberInput, Stack } from "@mantine/core"
22+
import { Controller } from "react-hook-form"
23+
import { ErrorMessage } from "@hookform/error-message"
24+
import { DatePickerInput } from "@mantine/dates"
25+
import { IconArrowLeft, IconArrowRight, IconX } from "@tabler/icons-react"
1726

1827
export const MembershipWriteFormSchema = MembershipWriteSchema.superRefine((data, ctx) => {
19-
if (data.end && isBefore(data.end, data.start)) {
28+
if (data.end !== null && isBefore(data.end, data.start)) {
2029
ctx.addIssue({
2130
code: "custom",
22-
message: "Sluttdato må være etter startdato",
31+
message: "Sluttdato må være etter startdato.",
2332
path: ["end"],
2433
})
2534
}
35+
36+
if (data.end === null && data.type !== "KNIGHT") {
37+
ctx.addIssue({
38+
code: "custom",
39+
message: "Sluttdato må oppgis for ikke-Ridder-medlemskap.",
40+
path: ["end"],
41+
})
42+
}
43+
44+
if (data.end !== null && data.type === "KNIGHT") {
45+
ctx.addIssue({
46+
code: "custom",
47+
message: "Riddermedlemskap skal ikke ha sluttdato.",
48+
path: ["end"],
49+
})
50+
}
51+
52+
if (data.specialization !== null && data.type !== "MASTER") {
53+
ctx.addIssue({
54+
code: "custom",
55+
message: "Spesialisering kan kun oppgis for mastermedlemskap.",
56+
path: ["specialization"],
57+
})
58+
}
59+
60+
if (data.specialization === null && data.type === "MASTER") {
61+
ctx.addIssue({
62+
code: "custom",
63+
message: "Spesialisering må oppgis for mastermedlemskap.",
64+
path: ["specialization"],
65+
})
66+
}
2667
})
2768

2869
type MembershipWriteFormSchema = z.infer<typeof MembershipWriteFormSchema>
@@ -61,8 +102,7 @@ export const useMembershipWriteForm = ({
61102
})),
62103
}),
63104
specialization: createSelectInput({
64-
label: "Spesialisering",
65-
description: "Masterspesialisering",
105+
label: "Masterspesialisering",
66106
required: false,
67107
clearable: true,
68108
placeholder: "Velg spesialisering",
@@ -74,48 +114,159 @@ export const useMembershipWriteForm = ({
74114
})),
75115
disabled: false,
76116
}),
77-
start: createDateTimeInput({
78-
label: "Startdato",
79-
required: true,
80-
}),
81-
end: createDateTimeInput({
82-
label: "Sluttdato",
83-
required: true,
84-
}),
85-
semester: createNumberInput({
86-
label: "Semester",
87-
description: (
88-
<Stack gap="xs">
89-
<Text size="xs" c="dimmed">
90-
Hvilket semester medlemskapet innebærer. 0-indeksert.
91-
</Text>
92-
<Stack gap="0.25rem">
93-
<Text size="xs" c="dimmed">
94-
<Code>0</Code> → 1. semester (1. årstrinn)
95-
</Text>
96-
<Text size="xs" c="dimmed">
97-
<Code>1</Code> → 2. semester (1. årstrinn)
98-
</Text>
99-
<Text size="xs" c="dimmed">
100-
<Code>2</Code> → 3. semester (2. årstrinn)
101-
</Text>
102-
<Text size="xs" c="dimmed">
103-
...
104-
</Text>
105-
<Text size="xs" c="dimmed">
106-
<Code>8</Code> → 9. semester (5. årstrinn)
107-
</Text>
108-
<Text size="xs" c="dimmed">
109-
<Code>9</Code> → 10. semester (5. årstrinn)
110-
</Text>
111-
</Stack>
112-
</Stack>
113-
),
114-
required: false,
115-
min: 0,
116-
max: 9,
117-
allowDecimal: false,
118-
}),
117+
start: ({ state, control }) => {
118+
const name = "start"
119+
120+
return (
121+
<Controller
122+
control={control}
123+
name={name}
124+
render={({ field }) => (
125+
<Stack gap="0.25rem">
126+
<DatePickerInput
127+
label="Startdato"
128+
valueFormat="YYYY-MM-DD"
129+
description={
130+
field.value
131+
? isSpringSemester(field.value)
132+
? `Vår ${field.value.getFullYear()}`
133+
: `Høst ${field.value.getFullYear()}`
134+
: undefined
135+
}
136+
style={{ flexGrow: 1 }}
137+
defaultValue={
138+
state.defaultValues?.[name] ?? roundToNearestHours(getCurrentUTC(), { roundingMethod: "ceil" })
139+
}
140+
value={field.value}
141+
onChange={field.onChange}
142+
error={state.errors[name] && <ErrorMessage errors={state.errors} name={name} />}
143+
required
144+
/>
145+
<Group>
146+
<Button
147+
w="fit-content"
148+
fw="normal"
149+
color="gray"
150+
size="compact-xs"
151+
variant="subtle"
152+
onClick={() => field.onChange(getPreviousSemesterStart(field.value ?? getCurrentUTC()))}
153+
leftSection={<IconArrowLeft size="0.85rem" />}
154+
styles={{ section: { marginRight: "0.35rem" } }}
155+
>
156+
Forrige semester
157+
</Button>
158+
<Button
159+
w="fit-content"
160+
fw="normal"
161+
color="gray"
162+
size="compact-xs"
163+
variant="subtle"
164+
onClick={() => field.onChange(getNextSemesterStart(field.value ?? getCurrentUTC()))}
165+
leftSection={<IconArrowRight size="0.85rem" />}
166+
styles={{ section: { marginRight: "0.35rem" } }}
167+
>
168+
Neste semester
169+
</Button>
170+
</Group>
171+
</Stack>
172+
)}
173+
/>
174+
)
175+
},
176+
end: ({ state, control }) => {
177+
const name = "end"
178+
179+
return (
180+
<Controller
181+
control={control}
182+
name={name}
183+
render={({ field }) => (
184+
<Stack gap="0.25rem">
185+
<DatePickerInput
186+
label="Sluttdato"
187+
description={
188+
field.value
189+
? isSpringSemester(field.value)
190+
? `Vår ${field.value.getFullYear()}`
191+
: `Høst ${field.value.getFullYear()}`
192+
: undefined
193+
}
194+
valueFormat="YYYY-MM-DD"
195+
style={{ flexGrow: 1 }}
196+
defaultValue={
197+
state.defaultValues?.[name] ?? roundToNearestHours(getCurrentUTC(), { roundingMethod: "ceil" })
198+
}
199+
value={field.value}
200+
onChange={field.onChange}
201+
error={state.errors[name] && <ErrorMessage errors={state.errors} name={name} />}
202+
rightSection={
203+
<ActionIcon w="fit-content" color="gray" variant="subtle" onClick={() => field.onChange(null)}>
204+
<IconX size="0.85rem" />
205+
</ActionIcon>
206+
}
207+
/>
208+
<Group>
209+
<Button
210+
w="fit-content"
211+
fw="normal"
212+
color="gray"
213+
size="compact-xs"
214+
variant="subtle"
215+
onClick={() => field.onChange(getPreviousSemesterStart(field.value ?? getCurrentUTC()))}
216+
leftSection={<IconArrowLeft size="0.85rem" />}
217+
styles={{ section: { marginRight: "0.35rem" } }}
218+
>
219+
Forrige semester
220+
</Button>
221+
<Button
222+
w="fit-content"
223+
fw="normal"
224+
color="gray"
225+
size="compact-xs"
226+
variant="subtle"
227+
onClick={() => field.onChange(getNextSemesterStart(field.value ?? getCurrentUTC()))}
228+
leftSection={<IconArrowRight size="0.85rem" />}
229+
styles={{ section: { marginRight: "0.35rem" } }}
230+
>
231+
Neste semester
232+
</Button>
233+
</Group>
234+
</Stack>
235+
)}
236+
/>
237+
)
238+
},
239+
semester: ({ state, control }) => {
240+
const name = "semester"
241+
const label = "Semester"
242+
243+
return (
244+
<Controller
245+
control={control}
246+
name={name}
247+
render={({ field }) => {
248+
const oneIndexedValue = field.value !== null ? field.value + 1 : null
249+
const studyGrade = field.value !== null ? getStudyGrade(field.value) : null
250+
251+
return (
252+
<NumberInput
253+
label={label}
254+
description={`${oneIndexedValue}. semester innebærer ${studyGrade}. årsgang`}
255+
min={1}
256+
max={10}
257+
allowDecimal={false}
258+
value={field.value !== null ? field.value + 1 : undefined}
259+
onChange={(value) => {
260+
const zeroIndexedValue = value !== undefined ? Number(value) - 1 : null
261+
field.onChange(zeroIndexedValue)
262+
}}
263+
error={state.errors[name] && <ErrorMessage errors={state.errors} name={name} />}
264+
/>
265+
)
266+
}}
267+
/>
268+
)
269+
},
119270
},
120271
})
121272
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { getCurrentUTC } from "@dotkomonline/utils"
2+
import { ErrorMessage } from "@hookform/error-message"
3+
import { DatePickerInput, type DatePickerInputProps } from "@mantine/dates"
4+
import { roundToNearestHours } from "date-fns"
5+
import { Controller, type FieldValues } from "react-hook-form"
6+
import type { InputProducerResult } from "./types"
7+
import { ActionIcon, Button, Stack } from "@mantine/core"
8+
import { IconX } from "@tabler/icons-react"
9+
10+
export function createDateInput<F extends FieldValues>({
11+
...props
12+
}: Omit<DatePickerInputProps, "error">): InputProducerResult<F> {
13+
return function FormDateInput({ name, state, control, defaultValue }) {
14+
return (
15+
<Controller
16+
control={control}
17+
name={name}
18+
render={({ field }) => (
19+
<Stack gap="0.25rem">
20+
<DatePickerInput
21+
{...props}
22+
valueFormat="YYYY-MM-DD"
23+
style={{ flexGrow: 1, ...props.style }}
24+
defaultValue={defaultValue ?? roundToNearestHours(getCurrentUTC(), { roundingMethod: "ceil" })}
25+
value={field.value}
26+
onChange={field.onChange}
27+
error={state.errors[name] && <ErrorMessage errors={state.errors} name={name} />}
28+
rightSection={
29+
props.required !== true && (
30+
<ActionIcon w="fit-content" color="gray" variant="subtle" onClick={() => field.onChange(null)}>
31+
<IconX size="0.85rem" />
32+
</ActionIcon>
33+
)
34+
}
35+
/>
36+
</Stack>
37+
)}
38+
/>
39+
)
40+
}
41+
}

apps/dashboard/src/components/forms/DateTimeInput.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { DateTimePicker, type DateTimePickerProps } from "@mantine/dates"
44
import { roundToNearestHours } from "date-fns"
55
import { Controller, type FieldValues } from "react-hook-form"
66
import type { InputProducerResult } from "./types"
7+
import { Button, Stack } from "@mantine/core"
8+
import { IconX } from "@tabler/icons-react"
79

810
export function createDateTimeInput<F extends FieldValues>({
911
...props
@@ -16,10 +18,20 @@ export function createDateTimeInput<F extends FieldValues>({
1618
render={({ field }) => (
1719
<DateTimePicker
1820
{...props}
21+
valueFormat="YYYY-MM-DD HH:mm"
22+
locale="nb"
23+
style={{ flexGrow: 1, ...props.style }}
1924
defaultValue={defaultValue ?? roundToNearestHours(getCurrentUTC(), { roundingMethod: "ceil" })}
2025
value={field.value}
2126
onChange={field.onChange}
2227
error={state.errors[name] && <ErrorMessage errors={state.errors} name={name} />}
28+
rightSection={
29+
props.required !== true && (
30+
<ActionIcon w="fit-content" color="gray" variant="subtle" onClick={() => field.onChange(null)}>
31+
<IconX size="0.85rem" />
32+
</ActionIcon>
33+
)
34+
}
2335
/>
2436
)}
2537
/>

apps/dashboard/src/components/forms/FileInput.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function createFileInput<F extends FieldValues>(
2727
control={control}
2828
name={name}
2929
render={({ field, fieldState }) => (
30-
<Stack gap="0.5rem">
30+
<Stack gap="0.25rem">
3131
<FileInput
3232
{...fileInputProps}
3333
description={description}
@@ -52,17 +52,19 @@ export function createFileInput<F extends FieldValues>(
5252
field.onChange(result)
5353
}}
5454
/>
55-
<Button
56-
w="fit-content"
57-
color="gray"
58-
size="compact-xs"
59-
variant="subtle"
60-
onClick={() => field.onChange(null)}
61-
leftSection={<IconX size="1rem" />}
62-
styles={{ section: { marginRight: "0.35rem" } }}
63-
>
64-
Fjern fil
65-
</Button>
55+
{props.required !== true && (
56+
<Button
57+
w="fit-content"
58+
color="gray"
59+
size="compact-xs"
60+
variant="subtle"
61+
onClick={() => field.onChange(null)}
62+
leftSection={<IconX size="0.85rem" />}
63+
styles={{ section: { marginRight: "0.35rem" } }}
64+
>
65+
Fjern fil
66+
</Button>
67+
)}
6668
</Stack>
6769
)}
6870
/>

0 commit comments

Comments
 (0)