Skip to content

Commit

Permalink
Merge pull request #992 from City-of-Helsinki/hakuvahti
Browse files Browse the repository at this point in the history
UHF-10103: Hakuvahti React Frontend.
  • Loading branch information
sundflux authored May 29, 2024
2 parents bcd7efd + b096005 commit f2b3f0b
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 3 deletions.
2 changes: 1 addition & 1 deletion dist/js/job-search.min.js

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions hdbt.theme
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ function hdbt_preprocess(&$variables): void {
}
}

// Hakuvahti server url, expose to react if it's enabled or not.
$hakuvahtiUrl = getenv('HAKUVAHTI_URL');
$variables['#attached']['drupalSettings']['helfi_react_search']['hakuvahti_url_set'] = !empty($hakuvahtiUrl);

// Required for allowing subtheming for HDBT theme.
$variables['active_theme'] = $active_theme = \Drupal::theme()->getActiveTheme()->getName();
$variables['theme_prefix'] = $active_theme !== 'hdbt' ? $active_theme : '';
Expand Down
8 changes: 6 additions & 2 deletions src/js/react/apps/job-search/containers/ResultsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ import { useAtomValue } from 'jotai';
import SimpleResultsContainer from './SimpleResultsContainer';
import PromotedResultsContainer from './PromotedResultsContainer';
import { configurationsAtom } from '../store';
import SearchMonitorContainer from './SearchMonitorContainer';

const ResultsContainer = () => {
const { promoted } = useAtomValue(configurationsAtom);

return promoted?.length ?
<PromotedResultsContainer /> :
<SimpleResultsContainer />;
<PromotedResultsContainer /> :
<>
{drupalSettings?.helfi_react_search?.hakuvahti_url_set && <SearchMonitorContainer />}
<SimpleResultsContainer />
</>;
};

export default ResultsContainer;
231 changes: 231 additions & 0 deletions src/js/react/apps/job-search/containers/SearchMonitorContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { atom, useAtom, useAtomValue } from 'jotai';

import { Button, Checkbox, TextInput, Notification, IconAngleUp, IconAngleDown } from 'hds-react';
import React from 'react';
import { Buffer } from 'buffer';
import URLParams from '../types/URLParams';
import useQueryString from '../hooks/useQueryString';
import { urlAtom } from '../store';

const SearchMonitorContainer = () => {
const urlParams: URLParams = useAtomValue(urlAtom);
const query = useQueryString(urlParams);

// Form validation states
const [termsAgreed, setTermsAgreed] = useAtom(receiveNewsletterAtom);
const [email, setEmail] = useAtom(emailAtom);
const [submitted, setSubmitted] = useAtom(submittedAtom);
const [errorMessage, seterrorMessage] = useAtom(errorAtom);
const [isFormVisible, setIsFormVisible] = useAtom(isFormVisibleAtom);

// ElasticSearch query base64 encoded
const queryEncoded = Buffer.from(query).toString('base64');
const searchDescription = '-';
const lang = '';

// Relative url for "query" parameter
const currentPath = window.location.pathname;
const currentParams = window.location.search;
const currentRelativeUrl = currentPath + currentParams;

const requestBody = {
elastic_query: queryEncoded,
query: currentRelativeUrl,
email,
search_description: searchDescription,
// lang is filled by Drupal module
lang
};

const onSubmit = async (
event: React.FormEvent<HTMLFormElement>,
): Promise<void> => {
event.preventDefault();

// Validate stuff to submit form
if (!termsAgreed) {
seterrorMessage(Drupal.t('The choice is mandatory: Terms of service', {}, { context: 'Search monitor error terms' }));
return;
}

if (!email) {
seterrorMessage(Drupal.t('This field is mandatory: Email address', {}, { context: 'Search monitor error email' }));
return;
}

// Make submit button disabled after submitting to prevent double submits
const submitButton = document.getElementById('job-search-form__search-monitor__submit-button');
if (submitButton) {
submitButton.setAttribute('disabled', 'true');
}

// Get csrf token from Drupal
let sessionToken = '';
try {
const response = await fetch('/session/token', {
method: 'GET',
});

if (!response.ok) {
seterrorMessage(
`Error getting session token: ${response.statusText}`,
);
if (submitButton) {
submitButton.removeAttribute('disabled');
}
return;
}

sessionToken = await response.text();
} catch (error) {
seterrorMessage(`Error getting session token: ${error}`);
if (submitButton) {
submitButton.removeAttribute('disabled');
}
return;
}

// Send form to Hakuvahti subscribe service
const body = JSON.stringify(requestBody);
const response = await fetch('/hakuvahti/subscribe', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': sessionToken,
},
body
});

// Oops, error from backend
if (!response.ok) {
seterrorMessage(
`Error submitting form: ${response.statusText}`,
);
if (submitButton) {
submitButton.removeAttribute('disabled');
}
return;
}

// Release submit locks and show success page
setSubmitted(true);
seterrorMessage('');
if (submitButton) {
submitButton.removeAttribute('disabled');
}
};

const formHeader: string = Drupal.t('Receive search results by email', {}, { context: 'Search monitor header' });
const openLabel: string = Drupal.t('Open', {}, { context: 'Search monitor open label' });
const closeLabel: string = Drupal.t('Close', {}, { context: 'Search monitor close label' });
const descriptionHeader: string = Drupal.t('Saved search', {}, { context: 'Search monitor content title' });
const descriptionFirstPart: string = Drupal.t('Save the search you make so that you can receive an email notification of new results matching your search criteria.', {}, { context: 'Search monitor content' });
const descriptionSecondPart: string = Drupal.t('You can save as many searches as you like. You can delete the saved search via the link in the email messages.', {}, { context: 'Search monitor content' });
const emailLabel: string = Drupal.t('Email address', {}, { context: 'Search monitor email label' });
const acceptTermsLabel: string = Drupal.t('Accept the terms and conditions.', {}, { context: 'Search monitor terms label' });
const buttonLabel: string = Drupal.t('Save your search', {}, { context: 'Search monitor submit button label' });
const thankYouHeader: string = Drupal.t('Your search has been saved', {}, { context: 'Search monitor thank you header' });
const thankYouMessage: string = Drupal.t('You will receive a confirmation link by email. You can activate the saved search via the link.', {}, { context: 'Search monitor thank you message' });
const errorLabel: string = Drupal.t('Please check these selections', {}, { context: 'Search monitor error label' });

const customCheckboxStyles = {
'--background-unselected': 'var(--color-white)',
'--background-selected': 'var(--color-black)',
marginTop: 'var(--spacing-m)',
};

return (
<form onSubmit={onSubmit} className="job-search-form__search-monitor">
{!submitted && (
<>
<h3 className='job-search-form__search-monitor__heading'>{formHeader}</h3>
<Button
type="button"
aria-controls='job-search-form__search-monitor__content'
variant="supplementary"
theme="black"
iconLeft={isFormVisible ? <IconAngleUp /> : <IconAngleDown />}
onClick={(event: React.MouseEvent) => {
event.preventDefault();
setIsFormVisible(!isFormVisible);
}}
style={{
backgroundColor: 'transparent',
}}
>
{isFormVisible ? closeLabel : openLabel}
</Button>

<div id='job-search-form__search-monitor__content' className='job-search-form__search-monitor__content' aria-hidden={!isFormVisible}>
<h4 className='job-search-form__search-monitor__content__heading'>{descriptionHeader}</h4>
<p>{descriptionFirstPart}</p>
<p>{descriptionSecondPart}</p>

{errorMessage &&
<Notification
type='error'
size='default'
label={errorLabel}
className='job-search-form__search-monitor__error'
>
{errorMessage}
</Notification>
}

<TextInput
className='job-search-form__search-monitor__email'
id='job-search-form__search_monitor__email'
label={emailLabel}
name='job-search-form__search_monitor__email'
type='email'
onChange={(event) => setEmail(event.target.value)}
value={email}
required
style={{
marginTop: 'var(--spacing-m)',
}}
/>

<Checkbox
className='job-search-form__search-monitor__terms'
label={acceptTermsLabel}
id='job-search-form__search_monitor__terms'
onChange={(event) => setTermsAgreed(event.target.checked)}
checked={termsAgreed}
name='job-search-form__search_monitor__terms'
required
style={customCheckboxStyles}
/>

<Button
className='hdbt-search--react__submit-button job-search-form__search-monitor__submit-button'
type='submit'
id='job-search-form__search-monitor__submit-button'
style={{
marginBottom: '0',
marginTop: 'var(--spacing-l)',
}}
>
{buttonLabel}
</Button>
</div>
</>
)}

{submitted &&
<>
<h3 className='job-search-form__search-monitor__heading'>{thankYouHeader}</h3>
<p>{thankYouMessage}</p>
</>
}
</form>
);
};

const emailAtom = atom('');
const receiveNewsletterAtom = atom(false);
const submittedAtom = atom(false);
const isFormVisibleAtom = atom(false);
const errorAtom = atom('');

export default SearchMonitorContainer;
1 change: 1 addition & 0 deletions src/js/types/drupalSettings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare namespace drupalSettings {
cookie_privacy_url: string;
elastic_proxy_url: string;
sentry_dsn_react: string;
hakuvahti_url_set: boolean;
};
const helfi_rekry_job_search: {
results_page_path: string;
Expand Down
56 changes: 56 additions & 0 deletions translations/fi.po
Original file line number Diff line number Diff line change
Expand Up @@ -1217,3 +1217,59 @@ msgstr "Näytä vain"
msgctxt "Label for news card published time"
msgid "Published"
msgstr "Julkaistu"

msgctxt "Search monitor header"
msgid "Receive search results by email"
msgstr "Tilaa hakuvahti sähköpostiisi"

msgctxt "Search monitor open label"
msgid "Open"
msgstr "Avaa"

msgctxt "Search monitor close label"
msgid "Close"
msgstr "Sulje"

msgctxt "Search monitor content title"
msgid "Saved search"
msgstr "Hakuvahti"

msgctxt "Search monitor content"
msgid "Save the search you make so that you can receive an email notification of new results matching your search criteria."
msgstr "Tallenna tekemäsi haku hakuvahdiksi, niin saat sähköpostiisi tiedon uusista osumista."

msgctxt "Search monitor content"
msgid "You can save as many searches as you like. You can delete the saved search via the link in the email messages."
msgstr "Voit luoda niin monta hakuvahtia kuin haluat ja poistaa hakuvahdin sähköpostien mukana tulevasta linkistä."

msgctxt "Search monitor email label"
msgid "Email address"
msgstr "Sähköpostiosoite"

msgctxt "Search monitor terms label"
msgid "Accept the terms and conditions."
msgstr "Hyväksy käyttöehdot."

msgctxt "Search monitor submit button label"
msgid "Save your search"
msgstr "Tilaa hakuvahti"

msgctxt "Search monitor thank you header"
msgid "Your search has been saved"
msgstr "Hakuvahti on luotu"

msgctxt "Search monitor thank you message"
msgid "You will receive a confirmation link by email. You can activate the saved search via the link."
msgstr "Saat vielä sähköpostiisi vahvistuslinkin, jolla voit aktivoida hakuvahdin."

msgctxt "Search monitor error label"
msgid "Please check these selections"
msgstr "Tarkistathan vielä nämä kohdat"

msgctxt "Search monitor error terms"
msgid "The choice is mandatory: Terms of service"
msgstr "Valinta on pakollinen: Käyttöehdot"

msgctxt "Search monitor error email"
msgid "This field is mandatory: Email address"
msgstr "Kenttä on pakollinen: Sähköpostiosoite"
56 changes: 56 additions & 0 deletions translations/sv.po
Original file line number Diff line number Diff line change
Expand Up @@ -1219,3 +1219,59 @@ msgstr "Visa endast"
msgctxt "Label for news card published time"
msgid "Published"
msgstr "Publicerad"

msgctxt "Search monitor header"
msgid "Receive search results by email"
msgstr "Beställ sökvakten till din e-post"

msgctxt "Search monitor open label"
msgid "Open"
msgstr "Öppna"

msgctxt "Search monitor close label"
msgid "Close"
msgstr "Stäng"

msgctxt "Search monitor content title"
msgid "Saved search"
msgstr "Sökvakten"

msgctxt "Search monitor content"
msgid "Save the search you make so that you can receive an email notification of new results matching your search criteria."
msgstr "Spara din sökning som en sökvakt, så får du information om nya träffar till din e-post."

msgctxt "Search monitor content"
msgid "You can save as many searches as you like. You can delete the saved search via the link in the email messages."
msgstr "Du kan skapa så många sökvakter du vill och ta bort sökvakten via länken som finns i e-postmeddelandena. "

msgctxt "Search monitor email label"
msgid "Email address"
msgstr "E-postadress"

msgctxt "Search monitor terms label"
msgid "Accept the terms and conditions."
msgstr "Godkänn användarvillkoren."

msgctxt "Search monitor submit button label"
msgid "Save your search"
msgstr "Beställ sökvakten"

msgctxt "Search monitor thank you header"
msgid "Your search has been saved"
msgstr "Sökvakten har skapats"

msgctxt "Search monitor thank you message"
msgid "You will receive a confirmation link by email. You can activate the saved search via the link."
msgstr "Du får ännu en bekräftelselänk till din e-post genom vilken du kan aktivera sökvakten."

msgctxt "Search monitor error label"
msgid "Please check these selections"
msgstr "Kontrollera ännu dessa punkter"

msgctxt "Search monitor error terms"
msgid "The choice is mandatory: Terms of service"
msgstr "Valet är obligatoriskt: Användningsvillkor"

msgctxt "Search monitor error email"
msgid "This field is mandatory: Email address"
msgstr "Fältet är obligatoriskt: E-postadress"

0 comments on commit f2b3f0b

Please sign in to comment.