From afba24a22f8803c10884ce259d18a60b013ee5e3 Mon Sep 17 00:00:00 2001
From: Chris <chris@chrissnijder.nl>
Date: Thu, 18 Feb 2021 02:32:42 +0100
Subject: [PATCH] Too much to begin to list..

---
 package.json                                  |   4 +-
 pnpm-lock.yaml                                |  61 +++++++-
 src/App.module.scss                           |   3 +
 src/App.test.tsx                              |  14 --
 src/App.tsx                                   | 103 +++++++++-----
 src/__mocks__/config.json                     |  56 ++++----
 .../contact-form/ContactForm.module.scss      |   3 +
 .../contact-form/ContactForm.test.tsx         |  41 ++++++
 src/components/contact-form/ContactForm.tsx   | 134 ++++++++++++++++++
 src/components/fieldset/Fieldset.test.tsx     |  20 +++
 src/components/fieldset/Fieldset.tsx          |  18 +++
 src/components/input/Input.tsx                |  42 ++++++
 .../problem-dropdown/ProblemDropdown.test.tsx |  16 +--
 .../problem-dropdown/ProblemDropdown.tsx      |  65 +++++----
 src/components/spinner/Spinner.test.tsx       |  38 +++--
 src/components/spinner/Spinner.tsx            |   2 +-
 .../urgent-request/UrgentRequest.module.scss  |  13 --
 .../urgent-request/UrgentRequest.test.tsx     |  24 +---
 .../urgent-request/UrgentRequest.tsx          |  58 +++-----
 src/config.yml                                |  84 ++++++-----
 src/styles/transitions/panel.module.scss      |  27 ++++
 src/tests/App.test.tsx                        | 127 +++++++++++++++++
 src/types.ts                                  |   6 +-
 23 files changed, 719 insertions(+), 240 deletions(-)
 create mode 100644 src/App.module.scss
 delete mode 100644 src/App.test.tsx
 create mode 100644 src/components/contact-form/ContactForm.module.scss
 create mode 100644 src/components/contact-form/ContactForm.test.tsx
 create mode 100644 src/components/contact-form/ContactForm.tsx
 create mode 100644 src/components/fieldset/Fieldset.test.tsx
 create mode 100644 src/components/fieldset/Fieldset.tsx
 create mode 100644 src/components/input/Input.tsx
 create mode 100644 src/styles/transitions/panel.module.scss
 create mode 100644 src/tests/App.test.tsx

diff --git a/package.json b/package.json
index 298b1a6..e590c60 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,6 @@
   "license": "Apache-2.0",
   "dependencies": {
     "i18next": "^19.8.7",
-    "identity-obj-proxy": "^3.0.0",
     "js-yaml": "^4.0.0",
     "react": "^17.0.0",
     "react-dom": "^17.0.0",
@@ -39,6 +38,7 @@
     "@types/jest": "^26.0.20",
     "@types/react": "^17.0.0",
     "@types/react-dom": "^17.0.0",
+    "@types/react-transition-group": "^4.4.0",
     "@types/snowpack-env": "^2.3.2",
     "@typescript-eslint/eslint-plugin": "^4.15.0",
     "@typescript-eslint/parser": "^4.15.0",
@@ -47,9 +47,11 @@
     "eslint-plugin-jest-dom": "^3.6.5",
     "eslint-plugin-react": "^7.22.0",
     "eslint-plugin-testing-library": "^3.10.1",
+    "identity-obj-proxy": "^3.0.0",
     "jest-cli": "^26.6.3",
     "markdown-to-jsx": "^7.1.1",
     "prettier": "^2.0.5",
+    "react-transition-group": "^4.4.1",
     "rxjs": "^6.6.3",
     "sass": "^1.32.6",
     "snowpack": "^3.0.11",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6f441de..7caa641 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -15,11 +15,12 @@ devDependencies:
   '@snowpack/plugin-typescript': 1.2.1_typescript@4.1.3
   '@testing-library/jest-dom': 5.11.9
   '@testing-library/react': 11.2.5_react-dom@17.0.1+react@17.0.1
-  '@testing-library/react-hooks': 5.0.3_react-dom@17.0.1+react@17.0.1
+  '@testing-library/react-hooks': 5.0.3_884e1b78f106e7ec36cdc9eac308ff33
   '@testing-library/user-event': 12.6.3
   '@types/jest': 26.0.20
   '@types/react': 17.0.1
   '@types/react-dom': 17.0.0
+  '@types/react-transition-group': 4.4.0
   '@types/snowpack-env': 2.3.3
   '@typescript-eslint/eslint-plugin': 4.15.0_97ae0b5fdeb2dd2a56389613b9d78781
   '@typescript-eslint/parser': 4.15.0_eslint@7.20.0+typescript@4.1.3
@@ -31,6 +32,8 @@ devDependencies:
   jest-cli: 26.6.3_ts-node@9.1.1
   markdown-to-jsx: 7.1.1_react@17.0.1
   prettier: 2.2.1
+  react-test-renderer: 17.0.1_react@17.0.1
+  react-transition-group: 4.4.1_react-dom@17.0.1+react@17.0.1
   rxjs: 6.6.3
   sass: 1.32.6
   snowpack: 3.0.11
@@ -2576,7 +2579,7 @@ packages:
       yarn: '>=1'
     resolution:
       integrity: sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ==
-  /@testing-library/react-hooks/5.0.3_react-dom@17.0.1+react@17.0.1:
+  /@testing-library/react-hooks/5.0.3_884e1b78f106e7ec36cdc9eac308ff33:
     dependencies:
       '@babel/runtime': 7.12.13
       '@types/react': 17.0.2
@@ -2586,6 +2589,7 @@ packages:
       react: 17.0.1
       react-dom: 17.0.1_react@17.0.1
       react-error-boundary: 3.1.0_react@17.0.1
+      react-test-renderer: 17.0.1_react@17.0.1
     dev: true
     peerDependencies:
       react: '>=16.9.0'
@@ -2727,6 +2731,12 @@ packages:
     dev: true
     resolution:
       integrity: sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw==
+  /@types/react-transition-group/4.4.0:
+    dependencies:
+      '@types/react': 17.0.2
+    dev: true
+    resolution:
+      integrity: sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==
   /@types/react/17.0.1:
     dependencies:
       '@types/prop-types': 15.7.3
@@ -3813,6 +3823,13 @@ packages:
     dev: true
     resolution:
       integrity: sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ==
+  /dom-helpers/5.2.0:
+    dependencies:
+      '@babel/runtime': 7.12.13
+      csstype: 3.0.6
+    dev: true
+    resolution:
+      integrity: sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==
   /domexception/2.0.1:
     dependencies:
       webidl-conversions: 5.0.0
@@ -6401,6 +6418,42 @@ packages:
       node: '>=0.10.0'
     resolution:
       integrity: sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==
+  /react-shallow-renderer/16.14.1_react@17.0.1:
+    dependencies:
+      object-assign: 4.1.1
+      react: 17.0.1
+      react-is: 17.0.1
+    dev: true
+    peerDependencies:
+      react: ^16.0.0 || ^17.0.0
+    resolution:
+      integrity: sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==
+  /react-test-renderer/17.0.1_react@17.0.1:
+    dependencies:
+      object-assign: 4.1.1
+      react: 17.0.1
+      react-is: 17.0.1
+      react-shallow-renderer: 16.14.1_react@17.0.1
+      scheduler: 0.20.1
+    dev: true
+    peerDependencies:
+      react: 17.0.1
+    resolution:
+      integrity: sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA==
+  /react-transition-group/4.4.1_react-dom@17.0.1+react@17.0.1:
+    dependencies:
+      '@babel/runtime': 7.12.13
+      dom-helpers: 5.2.0
+      loose-envify: 1.4.0
+      prop-types: 15.7.2
+      react: 17.0.1
+      react-dom: 17.0.1_react@17.0.1
+    dev: true
+    peerDependencies:
+      react: '>=16.6.0'
+      react-dom: '>=16.6.0'
+    resolution:
+      integrity: sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==
   /react/17.0.1:
     dependencies:
       loose-envify: 1.4.0
@@ -6752,7 +6805,6 @@ packages:
     dependencies:
       loose-envify: 1.4.0
       object-assign: 4.1.1
-    dev: false
     resolution:
       integrity: sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
   /semver/5.7.1:
@@ -7650,6 +7702,7 @@ specifiers:
   '@types/jest': ^26.0.20
   '@types/react': ^17.0.0
   '@types/react-dom': ^17.0.0
+  '@types/react-transition-group': ^4.4.0
   '@types/snowpack-env': ^2.3.2
   '@typescript-eslint/eslint-plugin': ^4.15.0
   '@typescript-eslint/parser': ^4.15.0
@@ -7667,6 +7720,8 @@ specifiers:
   react: ^17.0.0
   react-dom: ^17.0.0
   react-i18next: ^11.8.5
+  react-test-renderer: ^17.0.1
+  react-transition-group: ^4.4.1
   rxjs: ^6.6.3
   sass: ^1.32.6
   snowpack: ^3.0.11
diff --git a/src/App.module.scss b/src/App.module.scss
new file mode 100644
index 0000000..9d7ba47
--- /dev/null
+++ b/src/App.module.scss
@@ -0,0 +1,3 @@
+.troubleshooting {
+    margin-bottom: 4rem;
+}
\ No newline at end of file
diff --git a/src/App.test.tsx b/src/App.test.tsx
deleted file mode 100644
index c018e32..0000000
--- a/src/App.test.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import * as React from 'react';
-import { render } from '@testing-library/react';
-import App from './App';
-import config from "./config.yml";
-
-jest.mock("./config.yml", ()=>require("./__mocks__/config.json" ) );
-
-describe('<App>', () => {
-  it('renders with mocked config', () => {
-    const { getByText } = render(<App />);
-    const title = getByText("Report a problem");
-    expect(title).toBeDefined();
-  });
-});
diff --git a/src/App.tsx b/src/App.tsx
index a069b20..58ac19a 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,15 +6,14 @@ import {
   ProblemGroupDropdown,
 } from './components/problem-dropdown/ProblemDropdown';
 import ProblemSuggestionList from './components/suggestion-list/ProblemSuggestionList';
-import UrgentRequest from './components/urgent-request/UrgentRequest';
 import config from './config.yml';
-import Markdown from "markdown-to-jsx";
+import Markdown from 'markdown-to-jsx';
 import { useLanguage } from './hooks';
+import type { Problem, Suggestion } from './types';
+import ContactForm from './components/contact-form/ContactForm';
+import styles from './App.module.scss';
 
-interface AppProps {
-}
-
-function App({}: AppProps) {
+function App(): JSX.Element {
   const { t } = useTranslation();
   const [problemGroup, _setProblemGroup] = useState<string>('');
   const [problem, setProblem] = useState<string>('');
@@ -22,30 +21,59 @@ function App({}: AppProps) {
   const localized =
     config.localisations[language == 'dev' ? 'en' : language].translation;
   const problemGroups = localized.problemGroups;
-  let problems = [];
-  if (problemGroup) problems = localized.problemGroups[problemGroup].problems;
-  let suggestions = [];
-  if (problemGroup && problem)
-    suggestions =
-      localized.problemGroups[problemGroup].problems[problem].suggestions;
+  let problems: Record<string, Problem> = {};
+  let suggestions: Suggestion[] = [];
+  let problemDescribe = '';
+  let suggestionDescription = '';
+
+  // A problemgroup was selected?
+  if (problemGroup) {
+    // Does the problem group have problems?
+    if (localized.problemGroups[problemGroup].problems) {
+      problems = localized.problemGroups[problemGroup].problems;
+      problemDescribe = localized.problemGroups[problemGroup].problemDescribe;
+    }
+    // Does the problem group have suggestions?
+    if (localized.problemGroups[problemGroup].suggestions)
+      suggestions = localized.problemGroups[problemGroup].suggestions;
+    suggestionDescription =
+      localized.problemGroups[problemGroup].suggestionDescription;
+    // If a problem is selected, and the problem has suggestions..
+    if (
+      problem &&
+      localized.problemGroups[problemGroup].problems[problem].suggestions
+    ) {
+      // Add the suggestions to the suggestions array
+      suggestions = suggestions.concat(
+        localized.problemGroups[problemGroup].problems[problem].suggestions,
+      );
+      suggestionDescription =
+        localized.problemGroups[problemGroup].problems[problem]
+          .suggestionDescription;
+    }
+  }
 
   function setProblemGroup(newGroup: string) {
     setProblem('');
     _setProblemGroup(newGroup);
   }
 
-  function sendUrgent() {alert("SENT!")}
-
   return (
     <Suspense fallback="Loading...">
       <LocaleSwitcher />
       <header>
         <img src="" />
-        <h1><Markdown>{t('title')}</Markdown></h1>
-        <p><Markdown>{t('subtitle')}</Markdown></p>
+        <h1>
+          <Markdown>{t('title')}</Markdown>
+        </h1>
+        <p>
+          <Markdown>{t('subtitle')}</Markdown>
+        </p>
       </header>
-      <form>
+      <form className={styles.troubleshooting}>
         <ProblemGroupDropdown
+          id="problemGroup"
+          intro={localized.problemDescribe}
           selectProblemGroup={t('selectProblemGroup')}
           problemGroup={problemGroup}
           setProblemGroup={setProblemGroup}
@@ -53,29 +81,40 @@ function App({}: AppProps) {
         />
         {problemGroup && (
           <>
-            <p>{t(`problemGroups.${problemGroup}.problemDescribe`)}</p>
-            <ProblemDropdown
-              selectProblem={t('selectProblem')}
-              problem={problem}
-              setProblem={setProblem}
-              problems={problems}
-            />
-            {problem && (
+            {Object.keys(problems).length > 0 && (
+              <>
+                <ProblemDropdown
+                  id="problem"
+                  intro={problemDescribe}
+                  selectProblem={t('selectProblem')}
+                  problem={problem}
+                  setProblem={setProblem}
+                  problems={problems}
+                />
+              </>
+            )}
+            {suggestions.length > 0 && (
               <ProblemSuggestionList
-                intro={t(
-                  `problemGroups.${problemGroup}.problems.${problem}.suggestionDescription`,
-                )}
+                intro={suggestionDescription}
                 suggestions={suggestions}
               />
             )}
           </>
         )}
       </form>
-      {problemGroup && problem && problemGroup == 'technical' && (
-        <UrgentRequest sendUrgent={sendUrgent} />
-      )}
+      <h1>
+        <Markdown>{t('contact.title')}</Markdown>
+      </h1>
+      <ContactForm {...{ problem, problemGroup }} />
+      <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+      <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+      <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+      <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+      <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+      <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
+
     </Suspense>
   );
 }
 
-export default App;
\ No newline at end of file
+export default App;
diff --git a/src/__mocks__/config.json b/src/__mocks__/config.json
index dc40c09..755c9a6 100644
--- a/src/__mocks__/config.json
+++ b/src/__mocks__/config.json
@@ -4,21 +4,40 @@
     "en": {
       "translation": {
         "language": "English",
-        "title": "Report a problem",
+        "title": "Report a problem (mocked)",
         "subtitle": "Let's troubleshoot together. What type of issue are you experiencing?",
         "selectProblemGroup": "Select an issue type..",
         "selectProblem": "Select issue..",
+        "problemDescribe": "The nature of my problem is:",
+        "submit": "Submit request",
         "urgency": {
           "legend": "Confirm urgent request",
           "foldText": "Urgent request?",
           "intro": "__CAUTION__: by selecting this, your request will escalate to highest priority and immediately alert on-call operators, 24/7.\n\nYou will be billed {{ urgencyFee }} for this type of request, unless we find that the urgency of your issue was justified through risk of immediate and irreparable damage.\n\nIf you are okay with this, type: `URGENT` below and then click on \"Submit Urgent Request\"",
           "submit": "Submit Urgent Request",
-          "urgent": "URGENT"
+          "urgent": "URGENT",
+          "typeUrgentHere": "Type URGENT in capital letters here to unlock the urgent submit button."
+        },
+        "contact": {
+          "title": "Tell us about your problem",
+          "name": "Name:",
+          "phone": "Phone number:",
+          "email": "E-mail address:",
+          "message": "Your description of the problem:",
+          "tell-us": "What do you want to tell us?",
+          "details": "Contact details"
         },
         "problemGroups": {
           "technical": {
             "description": "Technical",
-            "problemDescribe": "How would you best describe your adminstrative question?",
+            "problemDescribe": "How would you best describe your technical problem?",
+            "suggestionDescription": "Here are some solutions to some of the most common technical issues:",
+            "suggestions": [
+              {
+                "link": "https://greenhost.net/helpdesk/website/hosting-management/#maintenance",
+                "description": "Generic technical problems fix"
+              }
+            ],
             "problems": {
               "hosting": {
                 "description": "My website is not reachable or is not working properly",
@@ -38,29 +57,18 @@
           },
           "administrative": {
             "description": "Administrative question",
-            "problemDescribe": "How would you best describe your adminstrative question?",
-            "problems": {
-              "invoicing": {
-                "description": "I have a question regarding an invoice",
-                "suggestionDescription": "Here are some of the most common examples of invoice questions:",
-                "suggestions": [
-                  {
-                    "link": "https://greenhost.net/contact/#payment-details",
-                    "description": "I need Greenhost's payment information."
-                  }
-                ]
+            "problemDescribe": "How would you best describe your adminstrative problem?",
+            "suggestionDescription": "Here are some of the most common examples of administrative questions:",
+            "suggestions": [
+              {
+                "link": "https://greenhost.net/contact/#payment-details",
+                "description": "I need Greenhost's payment information."
               },
-              "legal": {
-                "description": "I have a legal question",
-                "suggestionDescription": "Here are some of the most common examples of legal questions:",
-                "suggestions": [
-                  {
-                    "link": "https://greenhost.net/legal/terms-and-conditions/",
-                    "description": "Where can I find your terms and conditions?"
-                  }
-                ]
+              {
+                "link": "https://greenhost.net/legal/terms-and-conditions/",
+                "description": "Where can I find your terms and conditions?"
               }
-            }
+            ]
           }
         }
       }
diff --git a/src/components/contact-form/ContactForm.module.scss b/src/components/contact-form/ContactForm.module.scss
new file mode 100644
index 0000000..7ef3113
--- /dev/null
+++ b/src/components/contact-form/ContactForm.module.scss
@@ -0,0 +1,3 @@
+.ContactForm fieldset {
+  margin: 1rem 0 1rem 0;
+}
diff --git a/src/components/contact-form/ContactForm.test.tsx b/src/components/contact-form/ContactForm.test.tsx
new file mode 100644
index 0000000..87e93f6
--- /dev/null
+++ b/src/components/contact-form/ContactForm.test.tsx
@@ -0,0 +1,41 @@
+import * as React from 'react';
+import { render } from '@testing-library/react';
+import ContactForm from './ContactForm';
+import userEvent from '@testing-library/user-event';
+
+const mockSetUrgentIntent = jest.fn();
+const mockSendUrgent = jest.fn();
+const problem = ''; // for now makes no difference
+
+describe.each([
+  ['', false],
+  ['administrative', false],
+  ['technical', false],
+  ['technical', true],
+])('<ContactForm problemGroup={%s}>', (problemGroup) => {
+  it(`renders the contact form with problemGroup: ${problemGroup}`, () => {
+    const { getByRole } = render(
+      <ContactForm {...{ problemGroup, problem }} />,
+    );
+    const contactDetailFieldset = getByRole('group', {
+      name: /Contact details/i,
+    });
+    const messageFieldset = getByRole('group', {
+      name: /what do you want to tell us/i,
+    });
+    const regularSubmit = getByRole('button', { name: /submit request/i });
+
+    expect(contactDetailFieldset).toBeInTheDocument();
+    expect(messageFieldset).toBeInTheDocument();
+    expect(regularSubmit).toBeInTheDocument();
+
+    /**
+     *  To test: 
+     *  - Fields are rendered
+     *  - Fill in some fields
+     *  - Not filling in required fields leads to errors -> integration test or mocked, or end-to-end?
+     *  - Form submission, urgent and regular, triggers a call to a function
+     */
+     
+  });
+});
diff --git a/src/components/contact-form/ContactForm.tsx b/src/components/contact-form/ContactForm.tsx
new file mode 100644
index 0000000..d58b01b
--- /dev/null
+++ b/src/components/contact-form/ContactForm.tsx
@@ -0,0 +1,134 @@
+import Markdown from 'markdown-to-jsx';
+import React, { ChangeEvent, FormEvent, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import UrgentRequest from '../urgent-request/UrgentRequest';
+import { CSSTransition, SwitchTransition } from 'react-transition-group';
+import styles from './ContactForm.module.scss';
+import panelTransition from '../../styles/transitions/panel.module.scss';
+import Fieldset from '../fieldset/Fieldset';
+import Input from '../input/Input';
+
+export interface ContactFormProps {
+  problemGroup: string;
+  problem: string;
+}
+
+function ContactForm({ problemGroup, problem }: ContactFormProps): JSX.Element {
+  const [urgentIntent, setUrgentIntent] = useState<boolean>(false);
+  const [state, setState] = useState<Record<string, string>>({});
+  const { t } = useTranslation();
+
+  const changeHandler = (
+    event: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLInputElement>,
+  ) => {
+    setState({ ...state, ...{ [event.target.name]: event.target.value } });
+  };
+
+  const changeUrgentIntent = (event: ChangeEvent<HTMLInputElement>) => {
+    setUrgentIntent(event.target.checked);
+  };
+
+  function submit(event: FormEvent<HTMLFormElement>) {
+    event.preventDefault();
+    sendRequest();
+  }
+
+  const sendRequest = (urgent = false) => {
+    const formData = { ...state, ...{ urgent: urgent ? 'yes' : 'no' } };
+
+    Object.fromEntries(
+      Object.entries(formData).map(([name, value]) => [
+        `SupportForm[${name}]`,
+        value,
+      ]),
+    ),
+      console.log(formData);
+  };
+
+  return (
+    <form onSubmit={submit} className={`${styles.ContactForm}`}>
+      
+      <Fieldset legend={t('contact.details')}>
+        <Input
+          label={t('contact.name')}
+          id="name"
+          name="name"
+          required
+          onChange={changeHandler}
+        />
+        <Input
+          label={t('contact.phone')}
+          id="phone"
+          name="phone"
+          onChange={changeHandler}
+        />
+        <Input
+          label={t('contact.identification')}
+          id="identification"
+          name="identification"
+          onChange={changeHandler}
+          required
+        />
+        <Input
+          label={t('contact.email')}
+          id="email"
+          name="email"
+          onChange={changeHandler}
+          required
+        />
+      </Fieldset>
+      <Fieldset legend={t('contact.tell-us')}>
+        <Input
+          label={t('Message')}
+          id="message"
+          name="message"
+          onChange={changeHandler}
+          type="textarea"
+          required
+        />
+      </Fieldset>
+      {problemGroup == 'technical' && (
+        <div className="field.switch">
+          <input
+            id="urgency-checkbox"
+            type="checkbox"
+            className="switch fold-button"
+            onChange={changeUrgentIntent}
+          />
+          <label htmlFor="urgency-checkbox">{t('urgency.foldText')}</label>
+        </div>
+      )}
+      <div className="field">
+        <SwitchTransition>
+          <CSSTransition
+            key={urgentIntent ? 'urgent' : 'normal'}
+            in={urgentIntent}
+            classNames={{ ...panelTransition }}
+            unmountOnExit={true}
+            mountOnEnter={true}
+            addEndListener={(node: HTMLElement, done: EventListener) => {
+              // use the css transitionend event to mark the finish of a transition
+              node.addEventListener('transitionend', done, false);
+            }}
+          >
+            {urgentIntent ? (
+              <UrgentRequest sendUrgent={() => sendRequest(true)} />
+            ) : (
+              <button
+                className="good"
+                onClick={(event) => {
+                  event?.preventDefault();
+                  sendRequest();
+                }}
+              >
+                {t('submit')}
+              </button>
+            )}
+          </CSSTransition>
+        </SwitchTransition>
+      </div>
+    </form>
+  );
+}
+
+export default ContactForm;
diff --git a/src/components/fieldset/Fieldset.test.tsx b/src/components/fieldset/Fieldset.test.tsx
new file mode 100644
index 0000000..bc176da
--- /dev/null
+++ b/src/components/fieldset/Fieldset.test.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import Fieldset from './Fieldset';
+
+describe('<Fieldset>', () => {
+  it('renders a fieldset with a legend and children elements', () => {
+    const legend = () => <strong>Zelda</strong>;
+
+    const { getByRole, getByText } = render(
+      <Fieldset legend={legend()}>
+        <blockquote>It&apos;s dangerous to go alone! take this.🗡️</blockquote>
+      </Fieldset>,
+    );
+    const fieldset = getByRole('group');
+    expect(fieldset).toBeInTheDocument();
+    expect(getByText(/zelda/i)).toBeInTheDocument();
+    expect(getByText(/take this/i)).toBeInTheDocument();
+    expect(getByText(/🗡️/)).toBeInTheDocument();
+  });
+});
diff --git a/src/components/fieldset/Fieldset.tsx b/src/components/fieldset/Fieldset.tsx
new file mode 100644
index 0000000..067f797
--- /dev/null
+++ b/src/components/fieldset/Fieldset.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+
+interface FieldsetProps {
+  children?: React.ReactNode;
+  legend?: React.ReactNode;
+}
+
+export default function Fieldset({
+  children,
+  legend,
+}: FieldsetProps): JSX.Element {
+  return (
+    <fieldset>
+      <legend>{legend}</legend>
+      {children}
+    </fieldset>
+  );
+}
diff --git a/src/components/input/Input.tsx b/src/components/input/Input.tsx
new file mode 100644
index 0000000..dae027c
--- /dev/null
+++ b/src/components/input/Input.tsx
@@ -0,0 +1,42 @@
+import React, { ReactEventHandler } from 'react';
+
+interface InputProps {
+  id: string;
+  name?: string;
+  label: React.ReactNode;
+  classes?: string[];
+  value?: string | undefined;
+  required?: boolean;
+  type?: string;
+  onChange?: undefined | ReactEventHandler;
+  ref?: React.RefObject<unknown>;
+}
+export default function Input({
+  id,
+  name,
+  label,
+  classes = [],
+  // Applying this binds value to input.value, making it a controlled component
+  // You must also supply onChange in this case, or the field will be read-only
+  value = undefined, 
+  required = false,
+  type = 'text',
+  onChange = undefined,
+  ref = undefined
+}: InputProps): JSX.Element {
+  classes = ['field', ...classes];
+  if (required) classes.push('mandatory');
+
+  const attributes = { id, name, type, onChange, value, ref};
+
+  return (
+    <div className={classes.join(' ')}>
+      <label htmlFor={id}>{label}</label>
+      {type == 'textarea' ? (
+        <textarea {...attributes}></textarea>
+      ) : (
+        <input {...attributes} />
+      )}
+    </div>
+  );
+}
diff --git a/src/components/problem-dropdown/ProblemDropdown.test.tsx b/src/components/problem-dropdown/ProblemDropdown.test.tsx
index c9e36a8..d7f958f 100644
--- a/src/components/problem-dropdown/ProblemDropdown.test.tsx
+++ b/src/components/problem-dropdown/ProblemDropdown.test.tsx
@@ -17,6 +17,8 @@ const selectProblem = translation.selectProblem;
 const mockSetProblem: CallableFunction = jest.fn();
 
 const problemDropdownProps: ProblemDropdownProps = {
+  id: 'problem',
+  intro: problemGroups.technical.problemDescribe,
   problems,
   selectProblem,
   problem: '', // none selected
@@ -24,6 +26,8 @@ const problemDropdownProps: ProblemDropdownProps = {
 };
 
 const problemGroupDropdownProps: ProblemGroupDropdownProps = {
+  id: 'problemGroup',
+  intro: translation.problemDescribe,
   problemGroups,
   selectProblemGroup,
   problemGroup: '', // none selected
@@ -46,26 +50,22 @@ const getComponentByProps = (
   return ProblemGroupDropdown;
 };
 
-
 describe.each([
   [
     'ProblemDropdown', // Name for decribe()
     'problems', // Name of item type
     problemDropdownProps, // Props to render with
   ],
-  [
-    'ProblemGroupDropdown',
-    'problemroups',
-    problemGroupDropdownProps,
-  ],
+  ['ProblemGroupDropdown', 'problemgroups', problemGroupDropdownProps],
 ])('<%s>', (_, type, props) => {
   const Component = getComponentByProps(props);
   const firstProblem = getFirstItem(props);
   it(`renders a list of ${type}`, () => {
-    render(<Component {...props as any} />);
+    render(<Component {...(props as any)} />);
     // Check the default value is selected
     const elSelect = screen.getByRole('combobox');
-    if ('selectProblem' in props) {
+    if ("selectProblem" in props) {
+      expect(screen.getByText(props.intro)).toBeInTheDocument();
       expect(elSelect).toHaveDisplayValue(props.selectProblem);
     } else {
       expect(elSelect).toHaveDisplayValue(props.selectProblemGroup);
diff --git a/src/components/problem-dropdown/ProblemDropdown.tsx b/src/components/problem-dropdown/ProblemDropdown.tsx
index 34089d8..79d1e7e 100644
--- a/src/components/problem-dropdown/ProblemDropdown.tsx
+++ b/src/components/problem-dropdown/ProblemDropdown.tsx
@@ -1,62 +1,69 @@
 import React from 'react';
+import type { Problem, ProblemGroup } from 'src/types';
 
-export function ProblemDropdown({
-  selectProblem,
-  problem,
-  setProblem,
-  problems
-}: ProblemDropdownProps) {
+export interface ProblemDropdownProps extends GenericProblemDropdownProps {
+  intro: string;
+  problems: Record<string, Problem>;
+}
 
-  return (
-    <GenericProblemDropdown 
-    problem={problem}
-    problems={problems}
-    selectProblem={selectProblem}
-    setProblem={setProblem}
-    />
-  )
+export function ProblemDropdown(props: ProblemDropdownProps): JSX.Element {
+  return <GenericProblemDropdown {...props} />;
 }
 
 export interface ProblemGroupDropdownProps {
+  id: string;
+  intro: string;
   selectProblemGroup: string;
   problemGroup: string;
   setProblemGroup: CallableFunction;
-  problemGroups: object;
+  problemGroups: Record<string, ProblemGroup>;
 }
 
 export function ProblemGroupDropdown({
+  id,
+  intro,
   selectProblemGroup,
   problemGroup,
   setProblemGroup,
-  problemGroups
-}: ProblemGroupDropdownProps) {
+  problemGroups,
+}: ProblemGroupDropdownProps): JSX.Element {
   return (
-    <GenericProblemDropdown 
-    problem={problemGroup}
-    problems={problemGroups}
-    selectProblem={selectProblemGroup}
-    setProblem={setProblemGroup}
+    <GenericProblemDropdown
+      id={id}
+      intro={intro}
+      problem={problemGroup}
+      problems={problemGroups}
+      selectProblem={selectProblemGroup}
+      setProblem={setProblemGroup}
     />
-  )
+  );
 }
 
-
-export interface ProblemDropdownProps {
+export interface GenericProblemDropdownProps {
+  id: string;
+  intro: string;
   selectProblem: string;
   problem: string;
-  problems: object,
+  problems: Record<string, Problem | ProblemGroup>;
   setProblem: CallableFunction;
 }
 
 function GenericProblemDropdown({
+  id,
+  intro,
   selectProblem,
   problems,
   problem,
   setProblem,
-}: ProblemDropdownProps) {
+}: GenericProblemDropdownProps) {
   return (
     <div className="field">
-      <select value={problem} onChange={(e) => setProblem(e.target.value)}>
+      <label htmlFor={id}>{intro}</label>
+      <select
+        id={id}
+        value={problem}
+        onChange={(e) => setProblem(e.target.value)}
+      >
         <option key="_default" value="">
           {selectProblem}
         </option>
@@ -68,4 +75,4 @@ function GenericProblemDropdown({
       </select>
     </div>
   );
-}
\ No newline at end of file
+}
diff --git a/src/components/spinner/Spinner.test.tsx b/src/components/spinner/Spinner.test.tsx
index 3ce3ad5..beca3cc 100644
--- a/src/components/spinner/Spinner.test.tsx
+++ b/src/components/spinner/Spinner.test.tsx
@@ -1,35 +1,29 @@
 import React from 'react';
 import { render } from '@testing-library/react';
 import Spinner from './Spinner';
-import each from 'jest-each';
 
 describe('<Spinner>', () => {
   it('renders spinning loading indicator: default', () => {
     const { getByRole } = render(<Spinner />);
     const spinner = getByRole('progressbar');
-    expect(spinner).toBeVisible();
+    expect(spinner).toBeInTheDocument();
     expect(spinner).not.toHaveClass('big');
   });
+});
 
-  const variations = [
-    ['small', false],
-    ['big', true],
-  ];
-  each(variations).it(
-    `renders spinning loading indicator: %s`,
-    (big: boolean) => {
-      const { getByRole } = render(<Spinner loading={true} big={big} />);
-      const spinner = getByRole('progressbar');
-      expect(spinner).toBeVisible();
-    },
-  );
+describe.each([
+  ['small', false],
+  ['big', true],
+])('<Spinner>', (_, big: boolean) => {
+  it(`renders spinning loading indicator: %s`, () => {
+    const { getByRole } = render(<Spinner loading={true} big={big} />);
+    const spinner = getByRole('progressbar');
+    expect(spinner).toBeInTheDocument();
+  });
 
-  each(variations).it(
-    `Does not render spinning loading indicator: %s`,
-    (big: boolean) => {
-      const { queryByRole } = render(<Spinner loading={false} big={big} />);
-      const spinners = queryByRole('progressbar');
-      expect(spinners).toBe(null);
-    },
-  );
+  it(`Does not render spinning loading indicator: %s`, () => {
+    const { queryByRole } = render(<Spinner loading={false} big={big} />);
+    const spinners = queryByRole('progressbar');
+    expect(spinners).toBe(null);
+  });
 });
diff --git a/src/components/spinner/Spinner.tsx b/src/components/spinner/Spinner.tsx
index 12698b0..6bff6b3 100644
--- a/src/components/spinner/Spinner.tsx
+++ b/src/components/spinner/Spinner.tsx
@@ -5,7 +5,7 @@ export interface SpinnerProps {
   loading?: boolean;
 }
 
-function Spinner({ big = false, loading = true }: SpinnerProps) {
+function Spinner({ big = false, loading = true }: SpinnerProps): JSX.Element {
   if (!loading) return <div className="spinner"></div>;
   const classes = ['spinner', 'loading'];
   if (big) classes.push('big');
diff --git a/src/components/urgent-request/UrgentRequest.module.scss b/src/components/urgent-request/UrgentRequest.module.scss
index 0f50588..b0ae6d2 100644
--- a/src/components/urgent-request/UrgentRequest.module.scss
+++ b/src/components/urgent-request/UrgentRequest.module.scss
@@ -1,16 +1,3 @@
 .UrgentRequest {
-  margin: 1rem 0 1rem 0;
   border: 1px rgb(113, 0, 10) solid;
-  position: relative;
-  height: 0;
-  overflow: hidden;
-  transition: all 0.2s ease-in-out;
-  opacity: 0;
-  transform-origin: top left;
-  transform: rotate3d(1, 0.5, 0.1, -90deg) scale(0);
-  &.open {
-    opacity: 1;
-    transform: rotate(0deg);
-    height: auto;
-  }
 }
diff --git a/src/components/urgent-request/UrgentRequest.test.tsx b/src/components/urgent-request/UrgentRequest.test.tsx
index 7169b71..422a9c9 100644
--- a/src/components/urgent-request/UrgentRequest.test.tsx
+++ b/src/components/urgent-request/UrgentRequest.test.tsx
@@ -1,29 +1,22 @@
 import * as React from 'react';
-import { fireEvent, render } from '@testing-library/react';
+import { render } from '@testing-library/react';
 import UrgentRequest from './UrgentRequest';
 import userEvent from '@testing-library/user-event';
 import styles from './UrgentRequest.module.scss';
 
-
 const mockSendUrgent = jest.fn();
 
 describe('<UrgentRequest>', () => {
   it('renders the option to make a contact request urgent', () => {
-    const { getByRole, getByText } = render(
-      <UrgentRequest sendUrgent={mockSendUrgent} />,
+    const { getByRole } = render(
+      <UrgentRequest
+        sendUrgent={mockSendUrgent}
+      />,
     );
     const urgentFieldset = getByRole('group');
     const urgentInput = getByRole('textbox');
     const urgentSubmit = getByRole('button');
 
-    const urgentFold = getByText('Urgent request?');
-    // Folded urgent form has invisible content
-    expect(urgentFieldset).not.toHaveClass('open');
-
-    // Unfolding the urgency form
-    userEvent.click(urgentFold);
-    expect(urgentFieldset).toHaveClass(styles.open);
-
     // See expected elements
     expect(document.body.contains(urgentFieldset));
 
@@ -36,13 +29,8 @@ describe('<UrgentRequest>', () => {
     expect(urgentInput).toHaveValue('NOT URGENT');
     expect(urgentSubmit).toBeDisabled();
 
-    // Close the fold again.. this should clear the input field too.
-    userEvent.click(urgentFold);
-    expect(urgentFieldset).not.toHaveClass('open');
-    userEvent.click(urgentFold);
-    expect(urgentInput).toHaveValue('');
-    
     // Fill in URGENT to change the urgent status
+    userEvent.clear(urgentInput);
     userEvent.type(urgentInput, 'URGENT');
     // State was updated..
     expect(urgentInput).toHaveValue('URGENT');
diff --git a/src/components/urgent-request/UrgentRequest.tsx b/src/components/urgent-request/UrgentRequest.tsx
index cf4e7b3..5e373fc 100644
--- a/src/components/urgent-request/UrgentRequest.tsx
+++ b/src/components/urgent-request/UrgentRequest.tsx
@@ -7,16 +7,10 @@ export interface UrgentRequestProps {
   sendUrgent: CallableFunction;
 }
 
-function UrgentRequest({ sendUrgent }: UrgentRequestProps) {
-  const [open, setOpen] = useState<boolean>(false);
-  const [urgent, setUrgent] = useState<boolean>(true);
+function UrgentRequest({ sendUrgent }: UrgentRequestProps): JSX.Element {
+  const [urgent, setUrgent] = useState<boolean>(false);
   const [urgentText, setUrgentText] = useState<string>('');
   const { t } = useTranslation();
-  const changeFold = (event: ChangeEvent<HTMLInputElement>) => {
-    setOpen(event.target.checked);
-    if (!open) setUrgentText('');
-  };
-
   const changeUrgentText = (event: ChangeEvent<HTMLInputElement>): void => {
     setUrgentText(event.target.value);
   };
@@ -25,37 +19,29 @@ function UrgentRequest({ sendUrgent }: UrgentRequestProps) {
     setUrgent(urgentText == t('urgency.urgent'));
   }, [urgentText]);
 
-  function submitUrgent(event: React.SyntheticEvent<HTMLFormElement>) {
-    event.preventDefault();
-    sendUrgent();
-  }
-
   return (
-    <form onSubmit={submitUrgent}>
-      <div className="field.switch">
+    <fieldset className={`${styles.UrgentRequest}`}>
+      <legend>{t('urgency.legend')}</legend>
+      <Markdown>{t('urgency.intro')}</Markdown>
+      <div className="field">
         <input
-          id="urgency-checkbox"
-          type="checkbox"
-          className="switch fold-button"
-          onChange={changeFold}
-        />
-        <label htmlFor="urgency-checkbox">{t('urgency.foldText')}</label>
+          type="text"
+          value={urgentText}
+          onChange={changeUrgentText}
+          aria-label={t('urgency.typeUrgentHere')}
+        ></input>
       </div>
-      <fieldset className={`${styles.UrgentRequest} ${open ? styles.open : ''}`}>
-        <legend>{t('urgency.legend')}</legend>
-        <Markdown>{t('urgency.intro')}</Markdown>
-        <div className="field">
-          <input
-            type="text"
-            value={urgentText}
-            onChange={changeUrgentText}
-          ></input>
-        </div>
-        <button disabled={!urgent} className="bad" type="submit">
-          {t('urgency.submit')}
-        </button>
-      </fieldset>
-    </form>
+      <button
+        disabled={!urgent}
+        className="bad"
+        onClick={(event) => {
+          event?.preventDefault();
+          sendUrgent();
+        }}
+      >
+        {t('urgency.submit')}
+      </button>
+    </fieldset>
   );
 }
 
diff --git a/src/config.yml b/src/config.yml
index 5a75edd..f4a2403 100644
--- a/src/config.yml
+++ b/src/config.yml
@@ -1,5 +1,5 @@
 # This determines where to mount the "app" in the DOM.
-mountingElementId: "helpful-contact-form"
+mountingElementId: 'helpful-contact-form'
 urgentRequestFee: 420
 localisations:
   en:
@@ -9,6 +9,8 @@ localisations:
       subtitle: Let's troubleshoot together. What type of issue are you experiencing?
       selectProblemGroup: Select an issue type..
       selectProblem: Select issue..
+      problemDescribe: 'The nature of my problem is:'
+      submit: Submit request
       urgency:
         legend: Confirm urgent request
         foldText: Urgent request?
@@ -24,26 +26,32 @@ localisations:
           Urgent Request"
         submit: Submit Urgent Request
         urgent: URGENT
+        typeUrgentHere: Type URGENT in capital letters here to unlock the urgent submit button.
+      contact:
+        title: Tell us about your problem
+        name: "Name:"
+        phone: "Phone number:"
+        email: "E-mail address:"
+        message: "Your description of the problem:"
+        tell-us: What do you want to tell us?
+        details: Contact details
+        identification: Client identification (can be your company name, an invoice number, or a domain name)
       problemGroups:
         administrative:
           description: Administrative question
-          problemDescribe: How would you best describe your adminstrative question?
-          problems:
-            invoicing:
-              description: I have a question regarding an invoice
-              suggestionDescription: 'Here are some of the most common examples of invoice questions:'
-              suggestions:
-                - link: https://greenhost.net/contact/#payment-details
-                  description: I need Greenhost's payment information.
-            legal:
-              description: I have a legal question
-              suggestionDescription: 'Here are some of the most common examples of legal questions:'
-              suggestions:
-                - link: https://greenhost.net/legal/terms-and-conditions/
-                  description: Where can I find your terms and conditions?
+          suggestionDescription: 'Here are some of the most common examples of administrative questions:'
+          suggestions:
+            - link: https://greenhost.net/contact/#payment-details
+              description: I need Greenhost's payment information.
+            - link: https://greenhost.net/legal/terms-and-conditions/
+              description: Where can I find your terms and conditions?
         technical:
           description: Technical
-          problemDescribe: How would you best describe your adminstrative question?
+          problemDescribe: How would you best describe your technical question?
+          suggestionDescription: "Here are some solutions to some of the most common technical issues:"
+          suggestions: 
+            - link: "https://greenhost.net/helpdesk/website/hosting-management/#maintenance"
+              description: Generic technical problems fix
           problems:
             hosting:
               description: My website is not reachable or is not working properly
@@ -63,11 +71,13 @@ localisations:
         probleem ervaar je?
       selectProblemGroup: Selecteer een type issue..
       selectProblem: Selecteer issue..
+      problemDescribe: 'De aard van mijn probleem is:'
+      submit: Verstuur verzoek
       urgency:
         legend: Urgent verzoek bevestigen
         foldText: Urgent verzoek?
         intro: >-
-          LET OP: wanneer u deze optie kiest, zal uw verzoek naar het hoogste
+          __LET OP__: wanneer u deze optie kiest, zal uw verzoek naar het hoogste
           escalatieniveau worden gebracht en onmiddelijk mensen van de
           storingsdienst alarmeren, 24/7.
 
@@ -75,32 +85,32 @@ localisations:
           tenzij we van mening zijn dat de urgentie van het issue
           gerechtvaardigd is door het risico op onmiddelijke, onherstelbare
           schade.
-          
-          Wanneer u hiermee akkoort gaat, type: "URGENT" en klik op "Verstuur urgent
+
+          Wanneer u hiermee akkoort gaat, type: `URGENT` en klik op "Verstuur urgent
           verzoek"
         submit: Verstuur urgent verzoek
         urgent: URGENT
+        typeUrgentHere: Type hier URGENT in hoofdletters om een urgent verzoek te kunnen sturen.
+      contact:
+        title: Vertel ons over uw probleem
+        name: "Naam:"
+        phone: "Telefoonnumer:"
+        email: "E-mailadres:"
+        message: "Uw omschrijving van het probleem:"
+        tell-us: Wat wilt u ons vertellen?
+        details: Contactgegevens
+        identification: Klant identificatie (dit mag uw bedrijfsnaam, een factuurnummer, of een domein zijn)
       problemGroups:
         administrative:
           description: Administratieve vraag
-          problemDescribe: Hoe zou je jouw administratieve vraag het beste omschrijven?
-          problems:
-            invoicing:
-              description: Ik heb een vraag over een factuur
-              suggestionDescription:
-                'Hier zijn een paar van de meest voorkomende voorbeelden
-                van vragen over facturen:'
-              suggestions:
-                - link: https://greenhost.nl/contact/#betalingsgegevens
-                  description: Ik heb betalingsgegevens van Greenhost nodig
-            legal:
-              description: Ik heb een juridische vraag
-              suggestionDescription:
-                'Hier zijn een paar van de meest voorkomende voorbeelden
-                juridische vragen:'
-              suggestions:
-                - link: https://greenhost.nl/juridisch/algemene-voorwaarden/
-                  description: Waar vind ik de algemene voorwaarden?
+          suggestionDescription:
+            'Hier zijn een paar van de meest voorkomende voorbeelden
+            van administratieve vragen:'
+          suggestions:
+            - link: https://greenhost.nl/contact/#betalingsgegevens
+              description: Ik heb betalingsgegevens van Greenhost nodig
+            - link: https://greenhost.nl/juridisch/algemene-voorwaarden/
+              description: Waar vind ik de algemene voorwaarden?
         technical:
           description: Technisch
           problemDescribe: Hoe zou je jouw technische vraag of probleem het beste omschrijven?
diff --git a/src/styles/transitions/panel.module.scss b/src/styles/transitions/panel.module.scss
new file mode 100644
index 0000000..ad59aca
--- /dev/null
+++ b/src/styles/transitions/panel.module.scss
@@ -0,0 +1,27 @@
+.enter {
+  height: 20%;
+  opacity: 0;
+  transform: perspective(200px) rotateX(-90deg);
+  transform-origin: top center;
+}
+.exit {
+  opacity: 1;
+  height: auto;
+  transform: rotateX(0deg);
+  transform-origin: top center;
+}
+
+.enterActive {
+  height: auto;
+  opacity: 1;
+  transform: rotateX(0deg);
+  transform-origin: top center;
+  transition: all 200ms ease-in-out, transform 200ms cubic-bezier(.74,.36,.61,1.53);//cubic-bezier(0.980, 0.755, 0.830, 1.515); // bounce
+}
+.exitActive {
+  height: 20%;
+  opacity: 0;
+  transform:  perspective(200px) rotateX(-90deg);
+  transition: all 200ms ease-in-out, transform 200ms ease-in-out;
+  transform-origin: top center;
+}
diff --git a/src/tests/App.test.tsx b/src/tests/App.test.tsx
new file mode 100644
index 0000000..aea792a
--- /dev/null
+++ b/src/tests/App.test.tsx
@@ -0,0 +1,127 @@
+import * as React from 'react';
+import { act, render } from '@testing-library/react';
+import App from '../App';
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+import config from '../config.yml';
+import userEvent from '@testing-library/user-event';
+import urgencyStyles from '../components/urgent-request/UrgentRequest.scss';
+
+// Use a fixture instead of the real config file.
+jest.mock('../config.yml', () => require('../__mocks__/config.json'));
+
+describe('<App> with mocked config', () => {
+  it('renders with a mocked config', () => {
+    const { getByRole, getByText } = render(<App />);
+    expect(getByText('Report a problem (mocked)')).toBeInTheDocument();
+    expect(getByText(/Let's troubleshoot together/i)).toBeInTheDocument();
+    expect(
+      getByRole('button', { name: /Submit request/i }),
+    ).toBeInTheDocument();
+  });
+
+  it('Helps a user find suggestions to administrative problems', () => {
+    const { getByRole, getByText, queryByRole } = render(<App />);
+
+    const problemGroupInput = getByRole('combobox', {
+      name: 'The nature of my problem is:',
+    });
+    expect(problemGroupInput).toBeInTheDocument();
+
+    expect(
+      queryByRole('listitem', { name: /payment info/i }),
+    ).not.toBeInTheDocument();
+
+    userEvent.selectOptions(problemGroupInput, ['administrative']);
+
+    expect(
+      getByText(/examples of administrative questions/i),
+    ).toBeInTheDocument();
+    expect(getByText(/payment info/i)).toBeInTheDocument();
+    expect(getByText(/terms and conditions/i)).toBeInTheDocument();
+
+    // There should not be any option to select problems (hierarchically under
+    // problemGroups) because there is no prolem list in the fixture.
+    expect(
+      queryByRole('combobox', {
+        name: /How would you best describe your .* problem/i,
+      }),
+    ).not.toBeInTheDocument();
+
+    // For administrative issues, an urgent request can't be made.
+    expect(
+      queryByRole('checkbox', {
+        name: /Urgent request/i,
+      }),
+    ).not.toBeInTheDocument();
+  });
+
+  it('Helps a user find suggestions to technical problems', () => {
+    const { getByText, getByRole, queryByRole, queryByText } = render(<App />);
+    const problemGroupInput = getByRole('combobox', {
+      name: 'The nature of my problem is:',
+    });
+
+    // There are no suggestions yet..
+    expect(queryByRole('listitem')).not.toBeInTheDocument();
+
+    userEvent.selectOptions(problemGroupInput, ['technical']);
+
+    // A list of suggestions exists under the technical problem group in the
+    // fixture.
+    expect(
+      getByText(
+        /Here are some solutions to some of the most common technical issues:/,
+      ),
+    ).toBeInTheDocument();
+    expect(getByText(/Generic technical problems fix/i)).toBeInTheDocument();
+
+    // But a dropdown with problems is visible
+    const problemInput = getByRole('combobox', {
+      name: 'How would you best describe your technical problem?',
+    });
+
+    // We tell the form we have a hosting problem
+    userEvent.selectOptions(problemInput, ['hosting']);
+
+    expect(
+      getByText(/ solutions to some of the most common website issues/i),
+    ).toBeInTheDocument();
+
+    expect(getByText(/Restart server processes/i)).toBeInTheDocument();
+    expect(getByText(/Recyling the gremlin states/i)).toBeInTheDocument();
+    expect(getByText(/Generic technical problems fix/i)).toBeInTheDocument();
+
+    // For technical issues, an urgent request can be made.
+    const urgencyCheckbox = getByRole('checkbox', {
+      name: /Urgent request/,
+    });
+    expect(urgencyCheckbox).toBeInTheDocument();
+
+    // Assert that the urgency form is still hidden
+    expect(queryByText(/Confirm urgent request/i)).not.toBeInTheDocument();
+
+    userEvent.click(urgencyCheckbox);
+
+    // Assert that the urgency form is now visible
+    expect(getByText(/Confirm urgent request/i)).toBeInTheDocument();
+
+    // This also proves markdown works..
+    expect(getByText(/CAUTION/)).toHaveStyle('font-weight: bold');
+
+    const input = getByRole('textbox', {
+      name: /Type URGENT in capital letters/i,
+    });
+    expect(input).toBeInTheDocument();
+    expect(input).toHaveValue('');
+
+    const submit = getByRole('button', { name: /Submit urgent request/i });
+    expect(submit).toBeInTheDocument();
+    expect(submit).toBeDisabled();
+
+    userEvent.type(input, 'NOTURGENT');
+    expect(submit).toBeDisabled();
+    userEvent.clear(input);
+    userEvent.type(input, 'URGENT');
+    expect(submit).toBeEnabled();
+  });
+});
diff --git a/src/types.ts b/src/types.ts
index 04518cd..b527fe6 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,14 +7,16 @@ export interface ProblemHierarchy {
 export interface ProblemGroup {
   description: string;
   problemDescribe: string;
-  problems: {
+  suggestionDescription?: string;
+  suggestions?: Suggestion[];
+  problems?: {
     [problem: string]: Problem;
   };
 }
 
 export interface Problem {
   description: string;
-  suggestionDescription: string;
+  suggestionDescription?: string;
   suggestions?: Suggestion[];
 }
 
-- 
GitLab