diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/pull_request_template.md similarity index 95% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/pull_request_template.md index e36c0ed53..969acf8b4 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/pull_request_template.md @@ -6,7 +6,7 @@ Please provide a brief summary of the changes you’ve made. Explain the purpose ## Visual Overview (Image/Video) -If applicable, please include screenshots or a short video showcasing the changes you’ve made. +If applicable, please include screenshots or a short video showcasing the changes you have made. *Insert images or videos here.* diff --git a/src/app/(main)/[slug]/page.tsx b/src/app/(main)/[slug]/page.tsx index 0e9deffa3..cf9988578 100644 --- a/src/app/(main)/[slug]/page.tsx +++ b/src/app/(main)/[slug]/page.tsx @@ -2,12 +2,12 @@ import { Metadata } from "next"; import { redirect } from "next/navigation"; import { Blog } from "src/blog/Blog"; import BlogPreview from "src/blog/BlogPreview"; -import SalaryAndBenefits from 'src/salaryAndBenefits/SalaryAndBenefits'; +import SalaryAndBenefits from "src/salaryAndBenefits/SalaryAndBenefits"; import { getDraftModeInfo } from "src/utils/draftmode"; import SectionRenderer from "src/utils/renderSection"; import { fetchSeoData, generateMetadataFromSeo } from "src/utils/seo"; import { BlogPage, PageBuilder, Post } from "studio/lib/payloads/pages"; -import { SalaryAndBenefits as SalaryAndBenefitsPayload } from 'studio/lib/payloads/salaryAndBenefits'; +import { SalaryAndBenefitsPage } from "studio/lib/payloads/salaryAndBenefits"; import { BLOG_PAGE_QUERY, POSTS_QUERY, @@ -36,13 +36,22 @@ async function Page({ params }: Props) { const { slug } = params; const { perspective, isDraftMode } = getDraftModeInfo(); - const [initialPage, initialBlogPage, initialSalaryAndBenefitsPage] = await Promise.all([ - loadQuery(SLUG_QUERY, { slug }, { perspective }), - loadQuery(BLOG_PAGE_QUERY, { slug }, { perspective }), - loadQuery(SALARY_AND_BENEFITS_PAGE_QUERY, { slug }, { perspective }), - ]); + const [initialPage, initialBlogPage, initialSalaryAndBenefitsPage] = + await Promise.all([ + loadQuery(SLUG_QUERY, { slug }, { perspective }), + loadQuery(BLOG_PAGE_QUERY, { slug }, { perspective }), + loadQuery( + SALARY_AND_BENEFITS_PAGE_QUERY, + { slug }, + { perspective }, + ), + ]); - if (!initialPage.data && !initialBlogPage.data && !initialSalaryAndBenefitsPage.data) { + if ( + !initialPage.data && + !initialBlogPage.data && + !initialSalaryAndBenefitsPage.data + ) { console.log(`Page ${slug} not found`); // TODO: add error snackbar redirect("/"); @@ -53,7 +62,7 @@ async function Page({ params }: Props) { const initialPosts = await loadQuery( POSTS_QUERY, { slug }, - { perspective } + { perspective }, ); if (!initialPosts) { @@ -95,9 +104,13 @@ async function Page({ params }: Props) { if (initialSalaryAndBenefitsPage.data) { return isDraftMode ? ( - + ) : ( - + ); } diff --git a/src/components/forms/inputField/InputField.tsx b/src/components/forms/inputField/InputField.tsx index 07d5ac340..78f17aa8d 100644 --- a/src/components/forms/inputField/InputField.tsx +++ b/src/components/forms/inputField/InputField.tsx @@ -10,9 +10,11 @@ interface InputFieldProps { autoComplete?: HTMLInputAutoCompleteAttribute; autoCorrect?: string; type?: HTMLInputTypeAttribute; + max?: number; + min?: number; spellCheck?: "true" | "false"; autoCapitalize?: string; - value: string; + value: string | number; onChange: (name: string, value: string) => void; required?: boolean; } @@ -24,6 +26,8 @@ const InputField = ({ autoComplete, autoCorrect = "off", type = "text", + max, + min, spellCheck, autoCapitalize, value, @@ -56,6 +60,8 @@ const InputField = ({ autoComplete={autoComplete} autoCorrect={autoCorrect} type={type} + max={max} + min={min} className={styles.input} spellCheck={spellCheck} value={value} diff --git a/src/components/forms/radioButtonGroup/RadioButtonGroup.tsx b/src/components/forms/radioButtonGroup/RadioButtonGroup.tsx index 155b838cf..98bb57490 100644 --- a/src/components/forms/radioButtonGroup/RadioButtonGroup.tsx +++ b/src/components/forms/radioButtonGroup/RadioButtonGroup.tsx @@ -3,11 +3,11 @@ import styles from "src/components/forms/radioButtonGroup/radioButtonGroup.modul import { RadioButton } from "./components/RadioButton"; import textStyles from "src/components/text/text.module.css"; -interface IOption { +export interface IOption { id: string; label: string; - disabled: boolean; - currentChecked: boolean; + disabled?: boolean; + currentSelected: boolean; } interface RenderOptionsProps { @@ -41,8 +41,8 @@ interface RadioButtonGroupProps { * * ``` * const options = [ - * { id: 'radio1', label: 'Option 1', value: '1', currentChecked: false }, - * { id: 'radio2', label: 'Option 2', value: '2', currentChecked: true }, + * { id: 'radio1', label: 'Option 1', value: '1', currentSelected: false }, + * { id: 'radio2', label: 'Option 2', value: '2', currentSelected: true }, * ]; * * { return ( <> - {options.map(({ id, label, disabled, currentChecked }) => ( + {options.map(({ id, label, disabled, currentSelected }) => ( { name="radio" disabled={disabled} value={label} - defaultChecked={currentChecked} + defaultChecked={currentSelected} onChange={onChange} /> ))} diff --git a/src/components/forms/radioButtonGroup/radioButtonGroup.module.css b/src/components/forms/radioButtonGroup/radioButtonGroup.module.css index fc95242ef..12547de2b 100644 --- a/src/components/forms/radioButtonGroup/radioButtonGroup.module.css +++ b/src/components/forms/radioButtonGroup/radioButtonGroup.module.css @@ -1,5 +1,6 @@ .fieldset{ border: 0 none; + padding: 0; } .wrapper { diff --git a/src/salaryAndBenefits/SalaryAndBenefits.tsx b/src/salaryAndBenefits/SalaryAndBenefits.tsx index a462e8086..a8bba6c1f 100644 --- a/src/salaryAndBenefits/SalaryAndBenefits.tsx +++ b/src/salaryAndBenefits/SalaryAndBenefits.tsx @@ -1,23 +1,96 @@ +"use client"; + import styles from "./salaryAndBenefits.module.css"; import Text from "src/components/text/Text"; -import {SalaryAndBenefits as SalaryAndBenefitsPayload} from "studio/lib/payloads/salaryAndBenefits"; -import Benefit from './components/benefit/Benefit'; +import SalaryCalculator, { + Degree, +} from "./components/salaryCalculator/SalaryCalculator"; +import { useState } from "react"; +import { + calculatePension, + calculateSalary, + maxExperience, +} from "./utils/calculateSalary"; +import { SalaryAndBenefitsPage } from "studio/lib/payloads/salaryAndBenefits"; +import { RichText } from "src/components/richText/RichText"; interface SalaryAndBenefitsProps { - salaryAndBenefits: SalaryAndBenefitsPayload + salaryAndBenefits: SalaryAndBenefitsPage; +} + +interface SalaryCalculatorFormState { + examinationYear: number; + selectedDegree: Degree; } -const SalaryAndBenefits = ({salaryAndBenefits}: SalaryAndBenefitsProps) => { +const SalaryAndBenefits = ({ salaryAndBenefits }: SalaryAndBenefitsProps) => { + const currentYear = new Date().getFullYear(); + const minExaminationYear = maxExperience(currentYear); + const maxExaminationYear = currentYear - 1; + + const [formState, setFormState] = useState({ + examinationYear: currentYear - 1, + selectedDegree: "bachelor", + }); + const [salary, setSalary] = useState(null); + + const updateSelectedDegree = (newDegree: Degree) => { + setFormState((prevState) => ({ + ...prevState, + selectedDegree: newDegree, + })); + }; + + const updateExaminationYear = (newYear: number) => { + setFormState((prevState) => ({ + ...prevState, + examinationYear: newYear, + })); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + setSalary( + calculateSalary( + currentYear, + formState.examinationYear, + formState.selectedDegree, + ), + ); + }; + return (
{salaryAndBenefits.basicTitle} + {salaryAndBenefits.showSalaryCalculator && ( + + )} + {salary !== null ? ( +
+ Du vil få en årlig lønn på {salary} + + Du vil få en årlig pensjon på omtrent {calculatePension(salary)} + +
+ ) : null}
{salaryAndBenefits.benefits.map((benefit) => ( - +
+ {benefit.basicTitle} + +
))}
- ) -} + ); +}; -export default SalaryAndBenefits; \ No newline at end of file +export default SalaryAndBenefits; diff --git a/src/salaryAndBenefits/SalaryAndBenefitsPreview.tsx b/src/salaryAndBenefits/SalaryAndBenefitsPreview.tsx index 3994dc4d4..246d209fa 100644 --- a/src/salaryAndBenefits/SalaryAndBenefitsPreview.tsx +++ b/src/salaryAndBenefits/SalaryAndBenefitsPreview.tsx @@ -2,17 +2,17 @@ import { Suspense } from "react"; import SalaryAndBenefits from "./SalaryAndBenefits"; import { QueryResponseInitial, useQuery } from "@sanity/react-loader"; -import { SalaryAndBenefits as SalaryAndBenefitsPayload } from "studio/lib/payloads/salaryAndBenefits"; +import { SalaryAndBenefitsPage } from "studio/lib/payloads/salaryAndBenefits"; import { SALARY_AND_BENEFITS_PAGE_QUERY } from "studio/lib/queries/pages"; interface SalaryAndBenefitsPreviewProps { - initialSalaryAndBenefits: QueryResponseInitial; + initialSalaryAndBenefits: QueryResponseInitial; } const SalaryAndBenefitsPreview = ({ initialSalaryAndBenefits, }: SalaryAndBenefitsPreviewProps) => { - const { data: newData } = useQuery( + const { data: newData } = useQuery( SALARY_AND_BENEFITS_PAGE_QUERY, { slug: initialSalaryAndBenefits.data.slug.current }, { initial: initialSalaryAndBenefits }, diff --git a/src/salaryAndBenefits/components/benefit/Benefit.tsx b/src/salaryAndBenefits/components/benefit/Benefit.tsx deleted file mode 100644 index ca31af62f..000000000 --- a/src/salaryAndBenefits/components/benefit/Benefit.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import Text from "src/components/text/Text"; -import { RichText } from '../../../components/richText/RichText'; -import {Benefit as BenefitPayload} from 'studio/lib/payloads/salaryAndBenefits'; -import styles from "./benefit.module.css"; - - -interface BenefitProps { - benefit: BenefitPayload; -} - -export default function Benefit({benefit}: BenefitProps) { - return ( -
- {benefit.basicTitle} - -
- ) -} \ No newline at end of file diff --git a/src/salaryAndBenefits/components/salaryCalculator/SalaryCalculator.tsx b/src/salaryAndBenefits/components/salaryCalculator/SalaryCalculator.tsx new file mode 100644 index 000000000..f6fb28f9d --- /dev/null +++ b/src/salaryAndBenefits/components/salaryCalculator/SalaryCalculator.tsx @@ -0,0 +1,68 @@ +import styles from "./salaryCalculator.module.css"; +import InputField from "src/components/forms/inputField/InputField"; +import { + IOption, + RadioButtonGroup, +} from "src/components/forms/radioButtonGroup/RadioButtonGroup"; +import Button from "src/components/buttons/Button"; + +export type Degree = "bachelor" | "master"; + +const degreeOptions: IOption[] = [ + { + id: "bachelor", + label: "Bachelor", + currentSelected: true, + disabled: false, + }, + { id: "master", label: "Master", currentSelected: false, disabled: false }, +]; + +interface SalaryCalculatorProps { + examinationYear: number; + minExaminationYear: number; + maxExaminationYear: number; + onDegreeChanged: (degree: Degree) => void; + onExaminationYearChanged: (examinationYear: number) => void; + onSubmit: (event: React.FormEvent) => void; +} + +export default function SalaryCalculator({ + examinationYear, + minExaminationYear, + maxExaminationYear, + onDegreeChanged, + onExaminationYearChanged, + onSubmit, +}: SalaryCalculatorProps) { + return ( +
+ + (value.id === "bachelor" || value.id === "master") && + onDegreeChanged(value.id) + } + /> + onExaminationYearChanged(parseInt(value))} + required + /> + + + ); +} diff --git a/src/salaryAndBenefits/components/benefit/benefit.module.css b/src/salaryAndBenefits/components/salaryCalculator/salaryCalculator.module.css similarity index 63% rename from src/salaryAndBenefits/components/benefit/benefit.module.css rename to src/salaryAndBenefits/components/salaryCalculator/salaryCalculator.module.css index e6df59537..99961ac01 100644 --- a/src/salaryAndBenefits/components/benefit/benefit.module.css +++ b/src/salaryAndBenefits/components/salaryCalculator/salaryCalculator.module.css @@ -1,5 +1,5 @@ -.wrapper { +.calculator{ display: flex; flex-direction: column; - gap: 1rem; + gap: 2rem; } \ No newline at end of file diff --git a/src/salaryAndBenefits/salaryAndBenefits.module.css b/src/salaryAndBenefits/salaryAndBenefits.module.css index e07e765c4..c53fdbc23 100644 --- a/src/salaryAndBenefits/salaryAndBenefits.module.css +++ b/src/salaryAndBenefits/salaryAndBenefits.module.css @@ -10,4 +10,10 @@ flex-direction: column; gap: 5rem; max-width: var(--max-content-width-medium); +} + +.benefitWrapper { + display: flex; + flex-direction: column; + gap: 1rem; } \ No newline at end of file diff --git a/src/salaryAndBenefits/utils/calculateSalary.ts b/src/salaryAndBenefits/utils/calculateSalary.ts new file mode 100644 index 000000000..b23dc7290 --- /dev/null +++ b/src/salaryAndBenefits/utils/calculateSalary.ts @@ -0,0 +1,29 @@ +import { payscale } from "./salaryData"; + +interface PayScale { + [year: number]: { + [examinationYear: number]: number; + }; + } + + const salaryPayscale: PayScale = payscale; + + export function calculateSalary( + currentYear: number, + examinationYear: number, + degree: string + ): number { + + const degreeValue = degree === "bachelor" ? 1 : 0; + const adjustedYear = (examinationYear + degreeValue); + return salaryPayscale[currentYear][adjustedYear]; + } + + export function calculatePension(salary: number): number { + return Math.round(salary * 0.07); + } + + export function maxExperience(thisYear: number): number { + const years = Object.keys(salaryPayscale[thisYear]).map(Number) + return Math.min(...years); + } \ No newline at end of file diff --git a/src/salaryAndBenefits/utils/salaryData.ts b/src/salaryAndBenefits/utils/salaryData.ts new file mode 100644 index 000000000..24a8b85f6 --- /dev/null +++ b/src/salaryAndBenefits/utils/salaryData.ts @@ -0,0 +1,41 @@ +export const payscale = { + 2024: { + 2024 : 600000, + 2023 : 635833, + 2022 : 681829, + 2021 : 734982, + 2020 : 789982, + 2019 : 838539, + 2018 : 879553, + 2017 : 916886, + 2016 : 949000, + 2015 : 977333, + 2014 : 1005324, + 2013 : 1031405, + 2012 : 1064738, + 2011 : 1091489, + 2010 : 1113742, + 2009 : 1138742, + 2008 : 1166667, + 2007 : 1192460, + 2006 : 1210126, + 2005 : 1233560, + 2004 : 1264767, + 2003 : 1289780, + 2002 : 1299680, + 2001 : 1295953, + 2000 : 1305501, + 1999 : 1328501, + 1998 : 1349349, + 1997 : 1365121, + 1996 : 1384832, + 1995 : 1399711, + 1994 : 1422069, + 1993 : 1429358, + 1992 : 1452891, + 1991 : 1458021, + 1990 : 1467321, + 1989 : 1484721 + } + } ; + \ No newline at end of file diff --git a/studio/lib/payloads/salaryAndBenefits.ts b/studio/lib/payloads/salaryAndBenefits.ts index f80dfb46d..ff6636c49 100644 --- a/studio/lib/payloads/salaryAndBenefits.ts +++ b/studio/lib/payloads/salaryAndBenefits.ts @@ -1,5 +1,5 @@ -import { PortableTextBlock } from 'src/components/richText/RichText'; -import { Slug } from './global'; +import { PortableTextBlock } from "src/components/richText/RichText"; +import { Slug } from "./global"; export interface Benefit { _type: string; @@ -8,8 +8,7 @@ export interface Benefit { richText: PortableTextBlock[]; } - -export interface SalaryAndBenefits { +export interface SalaryAndBenefitsPage { _createdAt: string; _id: string; _rev: string; @@ -19,4 +18,5 @@ export interface SalaryAndBenefits { page: string; slug: Slug; benefits: Benefit[]; -} \ No newline at end of file + showSalaryCalculator: boolean; +}