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(permission): add notification permission experience #19

Merged
merged 11 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ The main idea is to collect all the disgusting features from today's website in
- [x] Wheel of fortune coupon modal
- [x] Add confirmation when trying to leave the page
- [x] Update title while the user is on a different tab
- [ ] Age verification on some images
- [x] Asking for notification: no worries, the website won't send any notification
- [ ] Funny, silly contents (inspired Onion news)
- [ ] Newsletter modal when the user leaves the screen or scrolls down a bit
- [ ] Asking for notification: no worries, the website won't send any notification
- [ ] Asking for location permission: no worries, the website won't use your location
- [ ] Sticky video player obscuring the page visibility. (+audio)
- [ ] Randomly loading images while scrolling.
- [ ] Low Quality images
- [ ] Funny, silly contents (inspired Onion news)
- [ ] Age verification on some images

**Stretch goal experiments**:

Expand Down
2 changes: 1 addition & 1 deletion next-i18next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const isDev = process.env.NODE_ENV === 'development';
/** @type {import('next-i18next').UserConfig} */
module.exports = {
reloadOnPrerender: isDev,
debug: isDev,
debug: false, // isDev,
i18n: {
defaultLocale: 'en',
locales: ['en', 'hu'],
Expand Down
3 changes: 3 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@ module.exports = {
poweredByHeader: false,
trailingSlash: true,
i18n,
compiler: {
styledComponents: true,
},
};
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,13 @@
"@vercel/analytics": "^1.2.2",
"color": "^4.2.3",
"eslint-config-next": "^14",
"i18next": "^23.10.1",
"modern-normalize": "^2.0.0",
"next": "^14.1.3",
"next-i18next": "^15.2.0",
"react": "^18.2.0",
"react-confetti": "^6.1.0",
"react-dom": "^18.2.0",
"react-fast-marquee": "^1.6.4",
"react-i18next": "^14.1.0",
"react-redux": "^9.1.0",
"react-share": "^5.1.0",
"react-timeago": "^7.2.0",
Expand Down Expand Up @@ -78,5 +76,9 @@
"prettier": "^3.2.5",
"tsx": "^4.7.1",
"typescript": "^5.3"
},
"peerDependencies": {
"react-i18next": "^14.1.0",
"i18next": "^23.10.1"
}
}
18 changes: 17 additions & 1 deletion public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
"yes": "Yes",
"no": "No"
},
"actions": {
"dismiss": "Dismiss",
"cancel": "Cancel",
"accept": "Accept",
"close": "Close",
"ok": "OK"
},
"status": {
"unknown": "Unknown",
"not_set": "Not set"
},
"navigation": {
"home": "Home",
"hotThings": "Hot things",
Expand All @@ -17,6 +28,11 @@
},
"experiences": {
"marquee_variants": ["📣 Come back please 🏃‍♀️🏃 We have candy!! 🚐"],
"array_paged_variants": ["⭐️ HEY YOU 🫵", "😜 YES YOU 😱", "📣 COME BACK 🏃"]
"array_paged_variants": ["⭐️ HEY YOU 🫵", "😜 YES YOU 😱", "📣 COME BACK 🏃"],
"notification_permission_manual": {
"title": "You can manually change the notification permission up here",
"description": "We would like to send you notifications sometimes, could you be so kind to allow us to do so? 🙏🥺🙏"
},
"exit_prompt": "I'd reconsider leaving before some bad things happend to you. Are you sure?"
}
}
9 changes: 5 additions & 4 deletions public/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
"adult_filter": "Filter adult contents"
},
"consent_section": {
"title": "Consent",
"allow_cookies": "Allow non-essential cookies",
"allow_notification": "Allow notification",
"enable_location": "Allow location"
"title": "Consent and permissions",
"permission_disclaimer": "Permission settings are managed by the browser, if you want to change theme you can do it from the site settings in your browser.",
"essential_cookies": "Allow essential cookies",
"notification_permission": "Notification permission",
"location_permission": "Location permission"
},
"experience_section": {
"title": "Experience",
Expand Down
1 change: 1 addition & 0 deletions public/locales/hu/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
40 changes: 20 additions & 20 deletions src/components/atoms/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,11 @@ import { ButtonHTMLAttributes, FunctionComponent } from 'react';
import { cssVars } from '@/styles/theme';

type Props = ButtonHTMLAttributes<HTMLButtonElement> & {
variant: 'primary' | 'secondary' | 'tertiary';
variant?: 'primary' | 'secondary' | 'tertiary';
};

const StyledButton = styled.button<{
$background: string;
$backgroundAlt: string;
$textColor: string;
}>`
cursor: pointer;
background: ${(props) => props.$background};
color: ${(props) => props.$textColor};
transition: background 0.1s ease-in-out;
&:hover {
background: ${(props) => props.$backgroundAlt};
}
&:disabled {
filter: grayscale(100%);
cursor: default;
}
`;

const Button: FunctionComponent<Props> = ({
variant,
variant = 'primary',
children,
onClick,
disabled,
Expand Down Expand Up @@ -71,4 +53,22 @@ const Button: FunctionComponent<Props> = ({
);
};

const StyledButton = styled.button<{
$background: string;
$backgroundAlt: string;
$textColor: string;
}>`
cursor: pointer;
background: ${(props) => props.$background};
color: ${(props) => props.$textColor};
transition: background 0.1s ease-in-out;
&:hover {
background: ${(props) => props.$backgroundAlt};
}
&:disabled {
filter: grayscale(100%);
cursor: default;
}
`;

export default Button;
12 changes: 6 additions & 6 deletions src/components/atoms/DimmerOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { styled } from 'styled-components';
import { cssVars } from '@/styles/theme';

export type DimmerOverlayProps = PropsWithChildren<{
show: boolean;
visible: boolean;
onClose?: () => void;
closeOnEsc?: boolean;
closeOnClickOutside?: boolean;
Expand All @@ -22,7 +22,7 @@ export type DimmerOverlayProps = PropsWithChildren<{
*/
const DimmerOverlay: FunctionComponent<DimmerOverlayProps> = ({
children,
show,
visible,
onClose,
closeOnEsc = true,
closeOnClickOutside = true,
Expand All @@ -41,15 +41,15 @@ const DimmerOverlay: FunctionComponent<DimmerOverlayProps> = ({
}, [handleKeyDown]);

return (
<Dimmer
className={show ? 'modal-show' : 'modal-hide'}
<Wrapper
className={visible ? 'modal-show' : 'modal-hide'}
onClick={() => closeOnClickOutside && onClose?.()}>
{children}
</Dimmer>
</Wrapper>
);
};

const Dimmer = styled.div`
const Wrapper = styled.div`
position: fixed;
display: flex;
top: 0;
Expand Down
18 changes: 18 additions & 0 deletions src/components/atoms/FormCheckbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { DetailedHTMLProps, InputHTMLAttributes } from 'react';

export type FormCheckbox = DetailedHTMLProps<
InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> & {
onValueChange?: (value: boolean) => void;
};
const FormCheckbox = ({ onChange, onValueChange, ...rest }: FormCheckbox) => {
const onChangeProxy = (e: React.ChangeEvent<HTMLInputElement>) => {
onChange?.(e);
onValueChange?.(e.target.checked);
};

return <input type="checkbox" onChange={onChangeProxy} {...rest} />;
};

export default FormCheckbox;
33 changes: 33 additions & 0 deletions src/components/atoms/FormSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { DetailedHTMLProps, SelectHTMLAttributes } from 'react';

export type SelectRowProps = DetailedHTMLProps<
SelectHTMLAttributes<HTMLSelectElement>,
HTMLSelectElement
> & {
values: { value: string; label: string }[];
selected: string;
onValueChange: (value: string) => void;
};
const FormSelect = ({
values,
onChange,
onValueChange,
...rest
}: SelectRowProps) => {
const onChangeProxy = (e: React.ChangeEvent<HTMLSelectElement>) => {
onChange?.(e);
onValueChange?.(values[e.target.selectedIndex].value);
};

return (
<select onChange={onChangeProxy} {...rest}>
{values.map(({ value, label }) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
);
};

export default FormSelect;
2 changes: 1 addition & 1 deletion src/components/organisms/ShareModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const ShareModal: FunctionComponent<Props> = ({ show, handleClose }) => {
</EmailShareButton>
</>
}
show={show}
visible={show}
onClose={handleClose}>
Sharing is caring, please show this awefully anoying website to your
friends.
Expand Down
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const config = {
contactEmail: '[email protected]',
defaultColorScheme: 'dark' as const,
isBrowser: typeof window !== 'undefined',
};

export default config;
15 changes: 14 additions & 1 deletion src/features/chat_bubble/components/ActionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useAppSelector } from '@/redux/hooks';
import { cssVars } from '@/styles/theme';
import { selectEnableSound } from '@/redux/selectors/preference';
import { selectInteractionUnlocked } from '@/redux/selectors/runtime';
import useSendNotification from '@/hooks/useSendNotification';

const zIndexBase = 20;

Expand Down Expand Up @@ -96,6 +97,7 @@ const ActionButton: FunctionComponent = () => {
const [history, setHistory] = useState([initialMessage()] as HistoryItem[]);
const [isOpen, setIsOpen] = useState(false);
const [badgeCounter, setBadgeCounter] = useState(1);
const notification = useSendNotification();
const notificationSfx = useAudio('/assets/sfx/notification_chord1.wav');

const preventClose: MouseEventHandler = (e) => e.stopPropagation();
Expand All @@ -112,6 +114,16 @@ const ActionButton: FunctionComponent = () => {
notificationSfx.play();
}, [enableSound, notificationSfx]);

const sendNotification = useCallback(
(message: string) => {
notification.send({
title: 'New message!',
body: message,
});
},
[notification],
);

const addRandomBotMessage = useCallback(() => {
const pool = messages.filter(
(message) => !history.some((item) => item.text === message),
Expand All @@ -124,8 +136,9 @@ const ActionButton: FunctionComponent = () => {
if (!isOpen) {
setBadgeCounter((prev) => prev + 1);
playSound();
sendNotification(randomMessage);
}
}, [addHistory, history, isOpen, playSound]);
}, [addHistory, history, isOpen, playSound, sendNotification]);

const closeHistory = () => setIsOpen(false);
const toggleHistory: MouseEventHandler<HTMLDivElement> = useCallback(() => {
Expand Down
47 changes: 47 additions & 0 deletions src/features/notification/components/ManualModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use client';

import { FunctionComponent } from 'react';
import styled from 'styled-components';
import { useTranslation } from 'next-i18next';

import { cssVars } from '@/styles/theme';
import DimmerOverlay from '@/components/atoms/DimmerOverlay';
import Button from '@/components/atoms/Button';

type ManualModalProps = {
visible?: boolean;
onDismiss: () => void;
};
const ManualModal: FunctionComponent<ManualModalProps> = ({
visible = false,
onDismiss,
}) => {
const { t } = useTranslation('common');

return (
<DimmerOverlay visible={visible}>
<Wrapper>
<Title>{t('experiences.notification_permission_manual.title')}</Title>
<p>{t('experiences.notification_permission_manual.description')}</p>
<Button onClick={onDismiss}>{t('actions.dismiss')}</Button>
</Wrapper>
</DimmerOverlay>
);
};

const Wrapper = styled.div`
position: fixed;
top: 1rem;
left: 5rem;
padding: 1rem 2rem;
max-width: min(calc(100vw - 10rem), 30rem);
background: ${cssVars.color.surface};
color: ${cssVars.color.onSurface};
border-radius: 0.5rem;
`;
const Title = styled.h3`
font-size: 1.5rem;
margin: 0 0 1rem 0;
`;

export default ManualModal;
Loading
Loading