all repos — caroster @ 4c733c75c7cd8b445013383eee26c32cc30621c6

[Octree] Group carpool to your event https://caroster.io

feat: ✨ Managing notifications from user profile
Maylie maylie@octree.ch
Mon, 12 Feb 2024 14:06:50 +0000
commit

4c733c75c7cd8b445013383eee26c32cc30621c6

parent

d880deea29e93447a5cfc03f48ec9d377eb0628d

M backend/src/api/email/services/email.tsbackend/src/api/email/services/email.ts

@@ -46,6 +46,9 @@ );

if (!emailTemplate) throw new Error(`No locale found for ${notifType} in ${lang}`); + strapi.log.debug( + `Send email notification of type ${notifType} to ${to} (lang: ${lang})` + ); await strapi.plugins["email"].services.email.send({ to, ...emailTemplate,
M backend/src/api/notification/content-types/notification/lifecycles.tsbackend/src/api/notification/content-types/notification/lifecycles.ts

@@ -19,13 +19,14 @@ },

} ); const { user, event, payload = {} } = notification; - await strapi - .service("api::email.email") - .sendEmailNotif(user.email, notification.type, user.lang, { - user, - event, - ...(payload as object), - }); + if (user.notificationEnabled) + await strapi + .service("api::email.email") + .sendEmailNotif(user.email, notification.type, user.lang, { + user, + event, + ...(payload as object), + }); } }, };
M backend/src/extensions/users-permissions/content-types/user/schema.jsonbackend/src/extensions/users-permissions/content-types/user/schema.json

@@ -100,13 +100,14 @@ "default": false

}, "lang": { "type": "enumeration", - "enum": [ - "fr", - "en" - ], + "enum": ["fr", "en"], "default": "fr" }, "newsletterConsent": { + "type": "boolean", + "default": false + }, + "notificationEnabled": { "type": "boolean", "default": true },
M backend/types/generated/contentTypes.d.tsbackend/types/generated/contentTypes.d.ts

@@ -772,7 +772,8 @@ lastName: Attribute.String;

onboardingUser: Attribute.Boolean & Attribute.DefaultTo<false>; onboardingCreator: Attribute.Boolean & Attribute.DefaultTo<false>; lang: Attribute.Enumeration<['fr', 'en']> & Attribute.DefaultTo<'fr'>; - newsletterConsent: Attribute.Boolean & Attribute.DefaultTo<true>; + newsletterConsent: Attribute.Boolean & Attribute.DefaultTo<false>; + notificationEnabled: Attribute.Boolean & Attribute.DefaultTo<true>; notifications: Attribute.Relation< 'plugin::users-permissions.user', 'oneToMany',
A frontend/containers/Profile/ContentSwitch.tsx

@@ -0,0 +1,21 @@

+import Switch from '@mui/material/Switch'; +import Typography from '@mui/material/Typography'; + +const ContentSwitch = ({ + isEditing, + checked, + onChange, + trueLabel, + falseLabel, + t, +}) => { + return isEditing ? ( + <Switch checked={checked} onChange={onChange} /> + ) : ( + <Typography variant="h6"> + {checked ? t(trueLabel) : t(falseLabel)} + </Typography> + ); +}; + +export default ContentSwitch;
A frontend/containers/Profile/ManagingNotificationsField.tsx

@@ -0,0 +1,51 @@

+import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import {useTranslation} from 'react-i18next'; +import ContentSwitch from './ContentSwitch'; + +interface Props { + toggleNotification: () => void; + toggleNewsletter: () => void; + notificationChecked: boolean; + newsletterChecked: boolean; + isEditing: boolean; +} + +const ManagingNotificationsField = ({ + isEditing, + toggleNotification, + toggleNewsletter, + notificationChecked, + newsletterChecked, +}: Props) => { + const {t} = useTranslation(); + + return ( + <Box padding={2}> + <Box display="flex" alignItems="center" justifyContent="space-between"> + <Typography variant="h6">{t('profile.notification.label')}</Typography> + <ContentSwitch + isEditing={isEditing} + checked={notificationChecked} + onChange={toggleNotification} + trueLabel="profile.notification.value.yes" + falseLabel="profile.notification.value.no" + t={t} + /> + </Box> + <Box display="flex" alignItems="center" justifyContent="space-between"> + <Typography variant="h6">{t('profile.newsletter.label')}</Typography> + <ContentSwitch + isEditing={isEditing} + checked={newsletterChecked} + onChange={toggleNewsletter} + trueLabel="profile.newsletter.value.yes" + falseLabel="profile.newsletter.value.no" + t={t} + /> + </Box> + </Box> + ); +}; + +export default ManagingNotificationsField;
M frontend/containers/Profile/index.tsxfrontend/containers/Profile/index.tsx

@@ -1,17 +1,26 @@

-import {useState} from 'react'; +import {useReducer, useState} from 'react'; import Container from '@mui/material/Container'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; import CardActions from '@mui/material/CardActions'; import Button from '@mui/material/Button'; -import { useTheme } from '@mui/material/styles'; +import {useTheme} from '@mui/material/styles'; import {useTranslation} from 'react-i18next'; import EditPassword from './EditPassword'; import ProfileField from './ProfileField'; import useToastStore from '../../stores/useToastStore'; -import {useUpdateMeMutation} from '../../generated/graphql'; +import { + UsersPermissionsUser, + useUpdateMeMutation, +} from '../../generated/graphql'; +import ManagingNotificationsField from './ManagingNotificationsField'; -const Profile = ({profile, logout}) => { +interface Props { + profile: UsersPermissionsUser; + logout: () => void; +} + +const Profile = ({profile, logout}: Props) => { const {t} = useTranslation(); const theme = useTheme(); const addToast = useToastStore(s => s.addToast);

@@ -22,6 +31,15 @@ const [isEditingPassword, setIsEditingPassword] = useState(false);

const [firstName, setFirstName] = useState(profile.firstName); const [lastName, setLastName] = useState(profile.lastName); const [email, setEmail] = useState(profile.email); + const [newsletterConsent, toggleNewsletter] = useReducer( + i => !i, + profile.newsletterConsent + ); + const [notificationEnabled, toggleNotification] = useReducer( + i => !i, + profile.notificationEnabled + ); + const [oldPassword, setOldPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); const [errorPassword, setErrorPassword] = useState('');

@@ -55,7 +73,13 @@ const onSave = async () => {

try { await updateProfile({ variables: { - userUpdate: {firstName, lastName, email}, + userUpdate: { + firstName, + lastName, + email, + newsletterConsent, + notificationEnabled, + }, }, }); setIsEditing(false);

@@ -122,10 +146,18 @@ isEditing={isEditing}

disabled={!isStrapiUser} /> </CardContent> + <ManagingNotificationsField + isEditing={isEditing} + notificationChecked={notificationEnabled} + newsletterChecked={newsletterConsent} + toggleNotification={toggleNotification} + toggleNewsletter={toggleNewsletter} + /> + <CardActions sx={{justifyContent: 'flex-end'}}> {!isEditing && ( <> - <Button type="button" onClick={() => logout()}> + <Button type="button" onClick={logout}> {t('profile.actions.logout')} </Button> <Button
M frontend/generated/graphql.tsxfrontend/generated/graphql.tsx

@@ -1854,6 +1854,7 @@ firstName?: Maybe<Scalars['String']['output']>;

lang?: Maybe<Enum_Userspermissionsuser_Lang>; lastName?: Maybe<Scalars['String']['output']>; newsletterConsent?: Maybe<Scalars['Boolean']['output']>; + notificationEnabled?: Maybe<Scalars['Boolean']['output']>; notifications?: Maybe<NotificationRelationResponseCollection>; onboardingCreator?: Maybe<Scalars['Boolean']['output']>; onboardingUser?: Maybe<Scalars['Boolean']['output']>;

@@ -1916,6 +1917,7 @@ lang?: InputMaybe<StringFilterInput>;

lastName?: InputMaybe<StringFilterInput>; newsletterConsent?: InputMaybe<BooleanFilterInput>; not?: InputMaybe<UsersPermissionsUserFiltersInput>; + notificationEnabled?: InputMaybe<BooleanFilterInput>; notifications?: InputMaybe<NotificationFiltersInput>; onboardingCreator?: InputMaybe<BooleanFilterInput>; onboardingUser?: InputMaybe<BooleanFilterInput>;

@@ -1940,6 +1942,7 @@ firstName?: InputMaybe<Scalars['String']['input']>;

lang?: InputMaybe<Enum_Userspermissionsuser_Lang>; lastName?: InputMaybe<Scalars['String']['input']>; newsletterConsent?: InputMaybe<Scalars['Boolean']['input']>; + notificationEnabled?: InputMaybe<Scalars['Boolean']['input']>; notifications?: InputMaybe<Array<InputMaybe<Scalars['ID']['input']>>>; oldPassword?: InputMaybe<Scalars['String']['input']>; onboardingCreator?: InputMaybe<Scalars['Boolean']['input']>;

@@ -2159,19 +2162,19 @@

export type DeleteTravelMutation = { __typename?: 'Mutation', deleteTravel?: { __typename?: 'TravelEntityResponse', data?: { __typename?: 'TravelEntity', id?: string | null } | null } | null }; -export type UserFieldsFragment = { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null }; +export type UserFieldsFragment = { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, newsletterConsent?: boolean | null, notificationEnabled?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null }; export type ProfileQueryVariables = Exact<{ [key: string]: never; }>; -export type ProfileQuery = { __typename?: 'Query', me?: { __typename?: 'UsersPermissionsMe', id: string, username: string, profile?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null }; +export type ProfileQuery = { __typename?: 'Query', me?: { __typename?: 'UsersPermissionsMe', id: string, username: string, profile?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, newsletterConsent?: boolean | null, notificationEnabled?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null }; export type UpdateMeMutationVariables = Exact<{ userUpdate: UsersPermissionsUserInput; }>; -export type UpdateMeMutation = { __typename?: 'Mutation', updateMe: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null } }; +export type UpdateMeMutation = { __typename?: 'Mutation', updateMe: { __typename?: 'UsersPermissionsUserEntityResponse', data?: { __typename?: 'UsersPermissionsUserEntity', id?: string | null, attributes?: { __typename?: 'UsersPermissionsUser', username: string, email: string, confirmed?: boolean | null, lastName?: string | null, firstName?: string | null, lang?: Enum_Userspermissionsuser_Lang | null, onboardingUser?: boolean | null, onboardingCreator?: boolean | null, newsletterConsent?: boolean | null, notificationEnabled?: boolean | null, provider?: string | null, events?: { __typename?: 'EventRelationResponseCollection', data: Array<{ __typename?: 'EventEntity', id?: string | null, attributes?: { __typename?: 'Event', uuid?: string | null, name: string, date?: any | null, address?: string | null } | null }> } | null } | null } | null } }; export type VehicleFieldsFragment = { __typename?: 'VehicleEntity', id?: string | null, attributes?: { __typename?: 'Vehicle', name: string, seats?: number | null, phone_number?: string | null } | null };

@@ -2332,6 +2335,8 @@ firstName

lang onboardingUser onboardingCreator + newsletterConsent + notificationEnabled provider events(pagination: {limit: 500}) { data {
M frontend/graphql/user.gqlfrontend/graphql/user.gql

@@ -7,6 +7,8 @@ firstName

lang onboardingUser onboardingCreator + newsletterConsent + notificationEnabled provider events(pagination: {limit: 500}) { data {
M frontend/locales/en.jsonfrontend/locales/en.json

@@ -148,6 +148,12 @@ "profile.actions.logout": "Logout",

"profile.actions.save": "Save", "profile.actions.save_new_password": "Update", "profile.current_password": "Current password", + "profile.notification.label": "Notifications", + "profile.notification.value.yes": "Activated", + "profile.notification.value.no": "Disabled", + "profile.newsletter.label": "Receive our newsletter", + "profile.newsletter.value.yes": "Yes", + "profile.newsletter.value.no": "No", "profile.email": "Email", "profile.errors.password_nomatch": "Wrong password", "profile.firstName": "First name",
M frontend/locales/fr.jsonfrontend/locales/fr.json

@@ -148,6 +148,12 @@ "profile.actions.logout": "Se déconnecter",

"profile.actions.save": "Enregistrer", "profile.actions.save_new_password": "Mettre à jour", "profile.current_password": "Mot de passe actuel", + "profile.notification.label": "Notifications", + "profile.notification.value.yes": "Activées", + "profile.notification.value.no": "Désactivées", + "profile.newsletter.label": "Recevoir la newsletter", + "profile.newsletter.value.yes": "Oui", + "profile.newsletter.value.no": "Non", "profile.email": "Email", "profile.errors.password_nomatch": "Mot de passe erroné", "profile.firstName": "Prénom",