-
Notifications
You must be signed in to change notification settings - Fork 4k
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(web): reusable sms preview component #5173
feat(web): reusable sms preview component #5173
Conversation
|
||
type ErrorType = { error: string; message: string; statusCode: number }; | ||
|
||
export const usePreviewSms = (options: UseMutationOptions<ResultType, ErrorType, PayloadType> = {}) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hook to compile SMS template.
export function LocaleSelect({ | ||
locales, | ||
value, | ||
isLoading, | ||
onLocaleChange, | ||
className, | ||
dropdownPosition, | ||
}: { | ||
locales: { langName: string; langIso: string }[]; | ||
value?: string; | ||
isLoading?: boolean; | ||
onLocaleChange: (val: string) => void; | ||
className?: string; | ||
dropdownPosition?: ISelectProps['dropdownPosition']; | ||
}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added missing types and a new property dropdownPosition
&::before { | ||
content: ''; | ||
position: absolute; | ||
bottom: -2px; | ||
right: -7px; | ||
height: 20px; | ||
border-right: 20px solid ${({ isError }) => (isError ? colors.error : '#51ba52')}; | ||
border-bottom-left-radius: 16px 14px; | ||
transform: translate(0, -2px); | ||
} | ||
|
||
&::after { | ||
content: ''; | ||
position: absolute; | ||
z-index: 1; | ||
bottom: -2px; | ||
right: -46px; | ||
width: 16px; | ||
height: 20px; | ||
background: ${({ theme }) => (theme.colorScheme === 'dark' ? '#4b4b51' : colors.white)}; | ||
border-bottom-left-radius: 10px; | ||
transform: translate(-30px, -2px); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the CSS magic to render the message bubble tail
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 suggestion: Could you please add some comments to the code to explain the purpose / use case of this magic?
min-width: 160px; | ||
min-height: 36px; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set the min-width because on the hover we show blurred background and edit button which needs some space
min-height: 36px; | ||
padding: 8px 12px; | ||
border-radius: 20px; | ||
background: ${({ isError }) => (isError ? colors.error : '#51ba52')}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#51ba52
this is the light green color unfortunately not from the color theme we have
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 suggestion: Could we please check with Nik to make sure this will be part of our next color palette then? (I think we already have the base palette defined somewhere)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will talk to him
const [previewContent, setPreviewContent] = useState(templateContent as string); | ||
const [selectedLocale, setSelectedLocale] = useState(''); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previewContent
is a compiled template content shown as the text
selectedLocale
is the locale to which the template would be compiled
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 thought: These would be helpful in code too!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll refactor this code so additional comments won't be needed.
useEffect(() => { | ||
getLocalesFromContent({ | ||
content: previewData.current.templateContent, | ||
}); | ||
}, [getLocalesFromContent, previewData]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetch locales from the template, will be called only once during the initial mount
useEffect(() => { | ||
getSmsPreview({ | ||
content: previewData.current.templateContent, | ||
payload: '', | ||
locale: selectedLocale, | ||
}); | ||
}, [selectedLocale, previewData, getSmsPreview]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
compile the template for the preview
export const MobileSimulator: React.FC = ({ children }) => { | ||
const [isIOS, setIsIOS] = useState(true); | ||
const { colorScheme } = useMantineColorScheme(); | ||
|
||
return ( | ||
<MobileSimulatorBody> | ||
{isIOS ? <Notch /> : <Camera />} | ||
<IndicatorsContainer> | ||
<TimeIconStyled isVisible={isIOS} /> | ||
{isIOS ? <IOSIndicatorsIcon /> : <AndroidIndicatorsIcon />} | ||
</IndicatorsContainer> | ||
<SwitchContainer> | ||
<PhonePlatformSwitch checked={isIOS} onChange={() => setIsIOS((old) => !old)} /> | ||
</SwitchContainer> | ||
{children} | ||
{isIOS ? ( | ||
<IOSKeyboard isDarkMode={colorScheme === 'dark'} /> | ||
) : ( | ||
<AndroidKeyboard isDarkMode={colorScheme === 'dark'} /> | ||
)} | ||
</MobileSimulatorBody> | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tried a few approaches here, exported phone svg and webp images, but the problem that I faced is with changing it for our needs, like for ex. in the Push preview we would need to have a background... also there are different themes and platforms, so in total would be 4 images just for sms, and 4 for push then...
that's why I decided to code it with css and only have some parts included with svg... it's less scalable, but for now we don't need this...
<SwitchInput id="phonePlatformSwitch" ref={ref} type="checkbox" checked={checked} onChange={onChange} /> | ||
<ButtonsContainer htmlFor="phonePlatformSwitch"> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
old good css trick to use input + label
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code looks pretty good overall! Will do behavior validation tomorrow morning in another pass.
Some high-level thoughts:
- We should aim for
rem
where possible IMO - The assets related to the Mobile views looked not insignificantly large... when we begin chunking the application, I think we will want to make sure those are not in the main bundle
}: { | ||
content?: string | IEmailBlock[]; | ||
payload: string; | ||
locale?: string; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❔ question: Do we have a more restricted type for Locale
that could be used here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately, we don't :/ might be hard to introduce now without refactoring
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good, created a ticket for the future: https://linear.app/novu/issue/NV-3499/create-a-reusable-typescript-type-for-locale
|
||
const getSmsPreview = useCallback( | ||
({ content, payload, locale }: PayloadType) => { | ||
if (IS_DOCKER_HOSTED) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 suggestion: Could you please add a comment in-code explaining why we do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a general pattern we use when something should not be available for the self-hosted users
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it reads logically, it might be good to extract this structure to a single helper function to ensure we apply it consistently and with documentation / comments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's have a tech debt ticket for this as we have many places around the code ;)
apps/web/src/pages/templates/components/phone-simulator/PhonePlatformSwitch.tsx
Outdated
Show resolved
Hide resolved
apps/web/src/pages/templates/components/phone-simulator/PhonePlatformSwitch.tsx
Outdated
Show resolved
Hide resolved
apps/web/src/pages/templates/components/phone-simulator/PhonePlatformSwitch.tsx
Outdated
Show resolved
Hide resolved
import { errorMessage, successMessage } from '@novu/design-system'; | ||
import { errorMessage as errorMessageRoot, successMessage as successMessageRoot } from '@novu/design-system'; | ||
|
||
export { errorMessage, successMessage }; | ||
export const errorMessage = errorMessageRoot; | ||
export const successMessage = successMessageRoot; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❔ question: Why make this abstraction here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had an error in the console that somehow it wasn't able to find these methods when doing imports, it was weird and only this helped
const isVariant = !!variantUuid; | ||
let navigatePath = `${basePath}/${channel}/${stepUuid}`; | ||
if (isVariant) { | ||
navigatePath += `/variants/${variantUuid}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤓 nitpick: Is there a constant we can derive this from?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unfortunately, we don't, because it's a dynamic path
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, by that I meant something more like /${VARIANTS_PATH}/${variantUuid}
…reusable-sms-preview
These are the things I discussed with Nikolay and we came up with this implementation. Mainly because of the issues we had with the overlay showing on the entire phone (it doesn't allow change language, frustration on hover, etc.). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the changes and responses!
} | ||
); | ||
} = useMutation<ILocale[], IResponseError, Payload>(({ content }) => getLocalesFromContent({ content }), { | ||
onError: (e: any) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤓 nitpick: unknown
is technically recommended by TypeScript here (introduced first in 4.4)
onError: (e: any) => { | |
onError: (e: unknown) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should be defined tho IResponseError
, I will remove that :)
|
||
const getSmsPreview = useCallback( | ||
({ content, payload, locale }: PayloadType) => { | ||
if (IS_DOCKER_HOSTED) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While it reads logically, it might be good to extract this structure to a single helper function to ensure we apply it consistently and with documentation / comments
} | ||
|
||
export const SmsBubble: React.FC<ISmsBubbleProps> = ({ className, text = '', isLoading, onEditClick, error }) => { | ||
const [isHovered, setIsHovered] = useState(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
export const SmsBubble: React.FC<ISmsBubbleProps> = ({ className, text = '', isLoading, onEditClick, error }) => { | |
const [isHovered, setIsHovered] = useState(false); | |
} | |
export const SmsBubble: React.FC<ISmsBubbleProps> = ({ className, text = '', isLoading, onEditClick, error }) => { | |
const [isHovered, setIsHovered] = useState<boolean>(false); |
In my previous position, we ended up with typing issues everywhere we didn't have explicit type params, so my preference is to always use them to be safe. Especially because in theory this could be false | null
or false | undefined
instead of boolean
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type is derived here from the value and it won't allow setting the state for undefined
or null
. But I'm not sure whether you mean something else.
onMouseEnter={() => setIsHovered(true)} | ||
onMouseLeave={() => setIsHovered(false)} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💭 thought: Sorry it didn't catch my eye the first time around, but this could be useful to extract to its own hook since we may have to re-use useHover
behavior elsewhere
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sure, will do that 🙌
`; | ||
|
||
export const TimeIconStyled = styled(({ isVisible }: { isVisible: boolean }) => ( | ||
<TimeIcon style={{ opacity: isVisible ? 1 : 0 }} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
❔ question: Do we specifically want this to still "occupy" its space even when not visible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, otherwise we would have to position the indicators component differently in the flex container
What change does this PR introduce?
Reusable SMS preview component based on the new designs from the Translation Management project.
Why was this change needed?
Other information (Screenshots)
Screen.Recording.2024-02-08.at.23.53.01.mov