Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat jalezi/sozial marie banner 466 #474

Merged
merged 44 commits into from
Apr 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
bac08ed
feat: ✨ adds useTimer custom hook for timer countdown
jalezi Mar 12, 2024
2ca6bf4
feat: ✨ adds getTimeDifference function
jalezi Mar 12, 2024
37ec905
feat: ✨ adds useLocalStorage hook
jalezi Mar 12, 2024
1a75183
feat: ✨ adds SozialMarie alert
jalezi Mar 13, 2024
880f08d
fix: 🚑️ timer does not restart on date change
jalezi Mar 13, 2024
a0f7983
fix: 🚑️ timer resets to soon
jalezi Mar 13, 2024
f41726e
refactor: ♻️ creates VotingButton component
jalezi Mar 13, 2024
9312b03
refactor: 🌐 Capitalize start and end strings for SozialMarie
jalezi Mar 13, 2024
3632924
refactor: ♻️ Add AlertCountDown component
jalezi Mar 13, 2024
8719f84
refactor: ♻️ adds SozialMarieLink component
jalezi Mar 13, 2024
e70f198
refactor: ♻️ adds AlertFooterContent component
jalezi Mar 13, 2024
a5cdb15
refactor: ♻️ replace days, hours, min and sec prop with time prop
jalezi Mar 13, 2024
1fcbedc
refactor: 🚚 rename file due to typo
jalezi Mar 13, 2024
98eb72b
fix: 🐛 not perfect handling of negative time left value
jalezi Mar 13, 2024
0c0f71b
perf: ⚡️ use useCallback hook in SozialMarie component
jalezi Mar 14, 2024
1396065
refactor: ♻️ no need to hold initial time in ref
jalezi Mar 14, 2024
a2f065c
feat: ✨ show voting date range
jalezi Mar 14, 2024
c1fd36b
refactor: ♻️ adds AlertHeaderContent component
jalezi Mar 14, 2024
b1d0944
perf: ⚡️ memoize AlertFooterContent
jalezi Mar 14, 2024
5a1c301
refactor: ♻️ extract date range to separate file
jalezi Mar 14, 2024
5096b15
feat: ✨ adds variant prop to AlertCountDown component
jalezi Mar 14, 2024
9d42b5e
refactor: ♻️ handle voting expiration
jalezi Mar 14, 2024
fb70002
refactor: ♻️ SozialMarie component to use configurable delay values
jalezi Mar 14, 2024
b99dcbd
fix: 🐛 initial load does not respect localStorage
jalezi Mar 14, 2024
1674b66
refactor: 🚸 better remind-me/no-show label
jalezi Mar 14, 2024
acf52aa
refactor: 🎨 extract "localStorage" related vars and func to separate …
jalezi Mar 14, 2024
7d85727
fix: 🚑️ safari & iOS does not support date format and breaks the page
jalezi Mar 14, 2024
b22ab3c
refactor: ⚰️ remove unused CountDown component
jalezi Mar 16, 2024
375c257
fix: ♿️ wrong datetime attr value
jalezi Mar 16, 2024
5351ca7
refactor: ♻️ adds time constants and import them in relevant files
jalezi Mar 16, 2024
394337c
feat: ✨ adds FullCountDown component
jalezi Mar 16, 2024
6def0f5
test: 🔧 adds mobile browser tests
jalezi Mar 16, 2024
9a7c428
test: ✅ adds e2e test for Sozial Marie voting button
jalezi Mar 16, 2024
5593271
refactor: ♻️ duplicate vars
jalezi Mar 16, 2024
6b07772
chore: 🧑‍💻 allow unused vars starting with "_"
jalezi Mar 16, 2024
b458aa7
feat: ✨ trigger button for SozialMarie will render after specific date
jalezi Mar 16, 2024
b4f749b
Merge branch 'master' into feat-jalezi/sozial-marie-banner-466
stefanb Mar 16, 2024
71e6019
feat: ⚗️ show SozialMarie trigger immediately, not on/after specific …
jalezi Mar 17, 2024
1496e41
fix: 🚑️ translations prersist on locale change
jalezi Mar 17, 2024
25f1b7e
fix: 🚑️ translations prersist on locale change
jalezi Mar 17, 2024
8a964ed
fix: 🐛 new voting date range and voting link
jalezi Apr 4, 2024
78d1654
Merge branch 'master' into feat-jalezi/sozial-marie-banner-466
jalezi Apr 4, 2024
602759f
fix: ⚰️ remove about SM text
jalezi Apr 8, 2024
80a72fc
fix: 🐛 link always points to sl lang
jalezi Apr 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ REACT_APP_GOOGLE_FORM_INPUT_PHONE=1724437941
REACT_APP_GOOGLE_FORM_INPUT_EMAIL=
REACT_APP_GOOGLE_FORM_INPUT_ORDERFORM=
REACT_APP_GOOGLE_FORM_INPUT_NOTE=1714314704
# SozialMarie set to true when running tests
REACT_APP_SM_SHOW_TRIGGER_BUTTON_IMMEDIATELY=true
2 changes: 2 additions & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ REACT_APP_GOOGLE_FORM_INPUT_PHONE=1724437941
REACT_APP_GOOGLE_FORM_INPUT_EMAIL=1408261095
REACT_APP_GOOGLE_FORM_INPUT_ORDERFORM=1910910180
REACT_APP_GOOGLE_FORM_INPUT_NOTE=1714314704
# SozialMarie, when going live (before 2024-4-2), change to false, otherwise it doesn't matter
REACT_APP_SM_SHOW_TRIGGER_BUTTON_IMMEDIATELY=true
8 changes: 8 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ module.exports = {

// @TODO: These should be turned "ON" one by one
'react/jsx-props-no-spreading': 'warn',
'no-unused-vars': [
'error', // or "error"
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
settings: {
'import/resolver': {
Expand Down
16 changes: 8 additions & 8 deletions playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ module.exports = defineConfig({
},

/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},

/* Test against branded browsers. */
// {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Header/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IconButton, TextField, Toolbar } from '@mui/material';
import * as Icons from 'components/Shared/Icons';
import i18next, { languages } from 'i18n';
import { useFilter } from 'context/filterContext';
import SozialMarie from 'components/SozialMarie';
import TemporaryDrawer from './Drawer';
import NavLinks from './NavLinks';
import SocialLinks from './SocialLinks';
Expand Down Expand Up @@ -68,6 +69,7 @@ const Header = function Header() {
>
<Icons.Icon name="Logo" style={{ height: '40px' }} />
</NavLink>
<SozialMarie />
<Styled.StackLarge ref={ref} id="nav-links" onClick={eventHandler}>
<NavLinks containerId="nav-links" />
</Styled.StackLarge>
Expand Down
65 changes: 65 additions & 0 deletions src/components/Shared/CountDown/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import PropTypes from 'prop-types';
import i18n from 'i18next';
import { getTimeDurationAttrValue } from 'utils';

const INTL_LANGS = {
en: 'en-GB',
de: 'de-DE',
sl: 'sl-SI',
hr: 'hr-HR',
it: 'it-IT',
hu: 'hu-HU',
};

export const SimpleCountDown = function SimpleCountDown({ days, hours, minutes, seconds }) {
const d = days.toString();
const h = hours.toString();
const m = minutes.toString();
const s = seconds.toString();

const timeDuration = getTimeDurationAttrValue({ days, hours, minutes, seconds });

return (
<time dateTime={timeDuration} aria-live="polite">
{d.padStart(2, '0')}:{h.padStart(2, '0')}:{m.padStart(2, '0')}:{s.padStart(2, '0')}
</time>
);
};

SimpleCountDown.propTypes = {
days: PropTypes.number.isRequired,
hours: PropTypes.number.isRequired,
minutes: PropTypes.number.isRequired,
seconds: PropTypes.number.isRequired,
};

export const FullCountDown = function FullCountDown({ days, hours, minutes, seconds }) {
const rtf = new Intl.RelativeTimeFormat(INTL_LANGS[i18n.language], {
numeric: 'always',
style: 'narrow',
});

const daysParts = rtf.formatToParts(days, 'day');
const hoursParts = rtf.formatToParts(hours, 'hour');
const minutesParts = rtf.formatToParts(minutes, 'minute');
const secondsParts = rtf.formatToParts(seconds, 'second');

const value = [daysParts, hoursParts, minutesParts, secondsParts]
.map(part => `${part[1].value} ${part[2].value}`)
.join(', ');

const timeDuration = getTimeDurationAttrValue({ days, hours, minutes, seconds });

return (
<time dateTime={timeDuration} aria-live="polite">
{value}
</time>
);
};

FullCountDown.propTypes = {
days: PropTypes.number.isRequired,
hours: PropTypes.number.isRequired,
minutes: PropTypes.number.isRequired,
seconds: PropTypes.number.isRequired,
};
2 changes: 1 addition & 1 deletion src/components/Shared/ExpandMore.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import IconButton from '@mui/material/IconButton';
import { ExpandMore as ExpandMoreIcon } from '@mui/icons-material';
import PropTypes from 'prop-types';

const ExpandMoreButton = styled(({ expand, ...other }) => <IconButton {...other} />)(
const ExpandMoreButton = styled(({ _expand, ...other }) => <IconButton {...other} />)(
({ theme, expand }) => ({
transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)',
marginLeft: 'auto',
Expand Down
23 changes: 23 additions & 0 deletions src/components/SozialMarie/AlertCountDown.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { FullCountDown, SimpleCountDown } from 'components/Shared/CountDown';
import PropTypes from 'prop-types';
import { getTimeDifference } from 'utils';

const AlertCountDown = function AlertCountDown({ time, variant = 'simple' }) {
const { days, hours, minutes, seconds } = getTimeDifference(time);
if (variant === 'simple') {
return <SimpleCountDown days={days} hours={hours} minutes={minutes} seconds={seconds} />;
}

return <FullCountDown days={days} hours={hours} minutes={minutes} seconds={seconds} />;
};

AlertCountDown.defaultProps = {
variant: 'simple',
};

AlertCountDown.propTypes = {
time: PropTypes.number.isRequired,
variant: PropTypes.oneOf(['simple', 'full']),
};

export default AlertCountDown;
43 changes: 43 additions & 0 deletions src/components/SozialMarie/AlertFooterContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { FormControlLabel, Checkbox } from '@mui/material';
import { t } from 'i18next';
import PropTypes from 'prop-types';
import { memo } from 'react';

const AlertFooterContent = function AlertFooter({ checked, handleChecked, isBefore, lang }) {
const sozialMarieTranslations = t('sozialMarie', { returnObjects: true });
const label = isBefore
? sozialMarieTranslations.noShowBefore
: sozialMarieTranslations.noShowDuring;

return (
<>
<FormControlLabel
key={lang}
labelPlacement="start"
control={
<Checkbox name="no-show" checked={checked} onChange={handleChecked} size="small" />
}
label={label}
sx={{
marginInline: 0,
'& .MuiFormControlLabel-label': { fontSize: '0.875rem' },
}}
/>
<p>{sozialMarieTranslations.seeAlert}</p>
</>
);
};

AlertFooterContent.propTypes = {
checked: PropTypes.bool.isRequired,
handleChecked: PropTypes.func.isRequired,
isBefore: PropTypes.bool.isRequired,
lang: PropTypes.string.isRequired,
};

const areEqual = (prevProps, nextProps) =>
prevProps.checked === nextProps.checked &&
prevProps.isBefore === nextProps.isBefore &&
prevProps.lang === nextProps.lang;

export default memo(AlertFooterContent, areEqual);
69 changes: 69 additions & 0 deletions src/components/SozialMarie/AlertHeaderContent.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Typography } from '@mui/material';
import { t } from 'i18next';
import PropTypes from 'prop-types';
import { memo } from 'react';

import { ONE_DAY_IN_MILLISECONDS } from 'const/time';

const INTL_LANGS = {
en: 'en-GB',
de: 'de-DE',
sl: 'sl-SI',
hr: 'hr-HR',
it: 'it-IT',
hu: 'hu-HU',
};

function getIntlFormatOptions(dateRangeInMilliseconds) {
if (dateRangeInMilliseconds > ONE_DAY_IN_MILLISECONDS) {
return {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
};
}

// for dev purposes
return {
year: 'numeric',
month: 'numeric',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
};
}

const AlertContentHeader = function AlertContentHeader({ endDate, startDate, lang }) {
const sozialMarieTranslations = t('sozialMarie', { returnObjects: true });
const intlDate = Intl.DateTimeFormat(INTL_LANGS[lang], getIntlFormatOptions(endDate - startDate));

const dateRange = intlDate.formatRange(startDate, endDate);

return (
<>
<Typography component="h2" fontWeight={600}>
{sozialMarieTranslations.title}
</Typography>

<Typography
component="time"
dateTime={`${intlDate.format(startDate)}-${intlDate.format(endDate)}`}
fontSize="0.875rem"
>
{dateRange}
</Typography>
</>
);
};

AlertContentHeader.propTypes = {
endDate: PropTypes.instanceOf(Date).isRequired,
startDate: PropTypes.instanceOf(Date).isRequired,
lang: PropTypes.string.isRequired,
};

const areEqual = (prev, next) =>
prev.endDate === next.endDate && prev.startDate === next.startDate && prev.lang === next.lang;
export default memo(AlertContentHeader, areEqual);
26 changes: 26 additions & 0 deletions src/components/SozialMarie/SozialMarieLink.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { t } from 'i18next';
import PropTypes from 'prop-types';

const SozialMarieLink = function SozialMarieLink({ href }) {
const sozialMarieTranslations = t('sozialMarie', { returnObjects: true });

return (
<p>
{sozialMarieTranslations.clicking}{' '}
<a href={href} target="_blank" rel="noopener noreferrer">
{sozialMarieTranslations.thisLink}
</a>{' '}
{sozialMarieTranslations.inNewTab}
</p>
);
};

SozialMarieLink.defaultProps = {
href: '#',
};

SozialMarieLink.propTypes = {
href: PropTypes.string,
};

export default SozialMarieLink;
57 changes: 57 additions & 0 deletions src/components/SozialMarie/VotingButton.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Box, Button, Tooltip } from '@mui/material';
import { SimpleCountDown } from 'components/Shared/CountDown';
import { t } from 'i18next';
import PropTypes from 'prop-types';
import { getTimeDifference } from 'utils';

const VotingButton = function VotingButton({
date,
handleClick,
isBeforeVoting,
isVoting,
isAfterVoting,
time,
}) {
const { days, hours, minutes, seconds } = getTimeDifference(time);

const sozialMarieTranslations = t('sozialMarie', { returnObjects: true });

return (
<Tooltip
title={
<Box display="flex" flexDirection="column" justifyContent="center" alignItems="center">
<span>
{isBeforeVoting ? `${sozialMarieTranslations.untilVotingStarts}:` : null}
{isVoting ? `${sozialMarieTranslations.untilVotingEnds}:` : null}
{isAfterVoting ? `${sozialMarieTranslations.votingHasEnded}!` : null}
</span>

{isAfterVoting ? null : (
<SimpleCountDown
date={date}
days={days}
hours={hours}
minutes={minutes}
seconds={seconds}
/>
)}
</Box>
}
>
<Button type="button" aria-label="vote" onClick={handleClick} color="inherit">
{sozialMarieTranslations.vote}!
</Button>
</Tooltip>
);
};

VotingButton.propTypes = {
date: PropTypes.instanceOf(Date).isRequired,
handleClick: PropTypes.func.isRequired,
isBeforeVoting: PropTypes.bool.isRequired,
isVoting: PropTypes.bool.isRequired,
isAfterVoting: PropTypes.bool.isRequired,
time: PropTypes.number.isRequired,
};

export default VotingButton;
24 changes: 24 additions & 0 deletions src/components/SozialMarie/date-range.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ONE_SECOND_MILLISECONDS } from '../../const/time';
import { getDevVotingDateRange } from './getDevVotingDateRange';

// Safari and iOS don't support the date format 'YYYY-MM-DD HH:MM GMT+0200' https://www.coditty.com/code/javascript-new-date-not-working-on-ie-and-safari
const SM_VOTING_STARTS = 'Tue Apr 08 2024 08:00:00 GMT+0200';
const SM_VOTING_ENDS = 'Wed Apr 15 2024 23:55:00 GMT+0200';
const SM_DO_NOT_SHOW_BEFORE = 'Tue Apr 02 2024 00:00:00 GMT+0200';

const delayToVotingStart = ONE_SECOND_MILLISECONDS * 5;
const votingTime = ONE_SECOND_MILLISECONDS * 30;
// Test for SozialMarie will fail if this delay is too big.
// For testing purposes set env variable REACT_APP_SM_SHOW_TRIGGER_BUTTON_IMMEDIATELY to true
// It will show the button immediately and you can test the button functionality.
const delayToNotShowBefore = ONE_SECOND_MILLISECONDS * 10;

const now = new Date(new Date().setMilliseconds(0));
const dateRange =
process.env.NODE_ENV === 'development'
? getDevVotingDateRange(now, delayToVotingStart, votingTime, delayToNotShowBefore)
: [new Date(SM_VOTING_STARTS), new Date(SM_VOTING_ENDS), new Date(SM_DO_NOT_SHOW_BEFORE)];

export const startDate = dateRange[0];
export const endDate = dateRange[1];
export const doNotShowBefore = dateRange[2];
Loading
Loading