Skip to content

Commit

Permalink
Feat: UI facelift SwitchLanguageModal & AppFeedbackModal (#654)
Browse files Browse the repository at this point in the history
## Ticket

corona-school/project-user#1323
Partially: corona-school/project-user#1305

## What was done?

- Updated the SwitchLanguageModal & AppFeedbackModal to use the new
components
- Fixed toast behavior when a modal is open _(for the
`AppFeedbackModal`)_ by adding a portal for the toaster to render it on
the body

## Preview
<img width="1440" alt="Bildschirmfoto 2024-09-13 um 13 33 09"
src="https://github.com/user-attachments/assets/7b96eee0-0925-45d7-8d01-9825141f324b">
<img width="1440" alt="Bildschirmfoto 2024-09-13 um 13 33 20"
src="https://github.com/user-attachments/assets/e1a3cfd8-a5a3-4210-a4c3-73648132ef97">
  • Loading branch information
JeangelLF authored Sep 14, 2024
1 parent 5839e77 commit 317e605
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 111 deletions.
2 changes: 1 addition & 1 deletion src/components/Label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/Tailwind';

const labelVariants = cva('text-form leading-4 font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70');
const labelVariants = cva('text-form font-medium leading-2 peer-disabled:cursor-not-allowed peer-disabled:opacity-70');

export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement>, VariantProps<typeof labelVariants> {}

Expand Down
8 changes: 7 additions & 1 deletion src/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@ const ModalContent = React.forwardRef<React.ElementRef<typeof DialogPrimitive.Co
<DialogPrimitive.Content
ref={ref}
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg fill-mode-forwards',
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg fill-mode-forwards',
className
)}
{...props}
onInteractOutside={(e) => {
const { originalEvent } = e.detail;
if (originalEvent.target instanceof Element && originalEvent.target.closest('.group.toast')) {
e.preventDefault();
}
}}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
Expand Down
2 changes: 1 addition & 1 deletion src/components/SideBarMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const SideBarMenu: React.FC<Props> = ({ navItems, unreadMessagesCount }) => {
{t('appFeedback.giveFeedbackButton')}
</Button>
</nav>
<AppFeedbackModal isOpen={isOpen} onClose={() => setIsOpen(false)} />
<AppFeedbackModal isOpen={isOpen} onIsOpenChange={setIsOpen} />
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/SwitchLanguageButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const SwitchLanguageButton: React.FC = () => {
<Button onClick={() => setShowSwitchLanguage(true)} variant="none" size="icon">
<Icon />
</Button>
<SwitchLanguageModal isOpen={showSwitchLanguage} onCloseModal={() => setShowSwitchLanguage(false)} />
<SwitchLanguageModal isOpen={showSwitchLanguage} onIsOpenChange={setShowSwitchLanguage} />
</>
);
};
Expand Down
10 changes: 6 additions & 4 deletions src/components/Toaster.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import { createPortal } from 'react-dom';
import { Toaster as Sonner } from 'sonner';

type ToasterProps = React.ComponentProps<typeof Sonner>;

const Toaster = ({ ...props }: ToasterProps) => {
return (
return createPortal(
<Sonner
richColors
className="toaster group"
className="toaster group pointer-events-auto"
theme="light"
visibleToasts={4}
toastOptions={{
closeButton: true,
duration: 5000,
classNames: {
toast: 'text-base',
toast: 'toast text-base',
description: 'group-[.toast]:text-muted-foreground',
closeButton: 'left-auto right-[-10px] group-[.toast-info]:!bg-white group-[.toast-info]:!text-primary',
actionButton: 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
Expand All @@ -25,7 +26,8 @@ const Toaster = ({ ...props }: ToasterProps) => {
},
}}
{...props}
/>
/>,
document.body
);
};

Expand Down
128 changes: 63 additions & 65 deletions src/modals/AppFeedbackModal.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { Button } from '@/components/Button';
import { Modal, ModalFooter, ModalHeader, ModalTitle } from '@/components/Modal';
import { useMutation } from '@apollo/client';
import { useTheme, Row, Button, Modal, Text, FormControl, TextArea, Heading, Radio, Box, useToast } from 'native-base';
import { ChangeEventHandler, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { gql } from '../gql';
import { toast } from 'sonner';
import { Label } from '@/components/Label';
import { TextArea } from '@/components/TextArea';
import { Input } from '@/components/Input';
import { RadioGroup, RadioGroupItem } from '@/components/RadioGroup';
import { Typography } from '@/components/Typography';

interface AddFeedbackModalProps {
isOpen?: boolean;
onClose: () => any;
isOpen: boolean;
onIsOpenChange: (value: boolean) => any;
}

const AppFeedbackModal = ({ isOpen, onClose }: AddFeedbackModalProps) => {
const { space } = useTheme();
const AppFeedbackModal = ({ isOpen, onIsOpenChange }: AddFeedbackModalProps) => {
const { t } = useTranslation();
const toast = useToast();
const [notes, setNotes] = useState('');
const [isSending, setIsSending] = useState(false);

Expand Down Expand Up @@ -53,7 +58,7 @@ const AppFeedbackModal = ({ isOpen, onClose }: AddFeedbackModalProps) => {
const handleOnSubmit = async () => {
const MAX_FILE_SIZE = 2 * 1024 * 1024;
if (file?.size && file.size > MAX_FILE_SIZE) {
toast.show({ description: t('appFeedback.screenshotShouldNotBeBiggerThan', { size: '2' }) });
toast.error(t('appFeedback.screenshotShouldNotBeBiggerThan', { size: '2' }));
return;
}

Expand All @@ -73,68 +78,61 @@ const AppFeedbackModal = ({ isOpen, onClose }: AddFeedbackModalProps) => {
: undefined,
},
});
toast.show({ description: t('appFeedback.feedbackSuccessfullySent') });
onClose();

toast.success(t('appFeedback.feedbackSuccessfullySent'));
onIsOpenChange(false);
setIsSending(false);
};

return (
<Modal onClose={onClose} isOpen={isOpen} size="lg">
<Modal.Content>
<Modal.CloseButton />
<Modal.Header>
<Heading fontSize="lg">{`💬 ${t('appFeedback.modal.title')}`}</Heading>
</Modal.Header>
<Modal.Body>
<Text fontSize="sm">{t('appFeedback.modal.description')}</Text>
<FormControl>
<Row flexDirection="column" paddingY={space['0.5']}>
<FormControl.Label _text={{ color: 'primary.900' }}>{t('appFeedback.modal.notesLabel')}*</FormControl.Label>
<TextArea
value={notes}
onChangeText={setNotes}
h={20}
placeholder={t('appFeedback.modal.notesPlaceholder')}
autoCompleteType={{}}
/>
</Row>
</FormControl>
<Row flexDirection="column" paddingY={space['0.5']}>
<FormControl.Label _text={{ color: 'primary.900' }}>{t('appFeedback.modal.screenshotLabel')}*</FormControl.Label>
<input type="file" accept="image/*" onChange={handleOnChangeFile} />
</Row>
<Row flexDirection="column" paddingY={space['0.5']}>
<FormControl.Label _text={{ color: 'primary.900' }}>{t('appFeedback.modal.canWeContactPerMailLabel')}*</FormControl.Label>
<Text fontSize="sm" mb="4">
{t('appFeedback.modal.canWeContactPerMailHelperText')}
</Text>
<Radio.Group
name="allowContact"
accessibilityLabel={t('appFeedback.modal.canWeContactPerMailLabel')}
value={allowContact.toString()}
onChange={(nextValue) => {
setAllowContact(nextValue === 'true');
}}
>
<Row>
<Box mr="4">
<Radio value="false">{t('no')}</Radio>
</Box>
<Box>
<Radio value="true">{t('yes')}</Radio>
</Box>
</Row>
</Radio.Group>
</Row>
</Modal.Body>
<Modal.Footer>
<Row space={space['1']}>
<Button isDisabled={!notes} isLoading={isSending} isLoadingText={t('appFeedback.modal.sendFeedback')} onPress={handleOnSubmit}>
{t('appFeedback.modal.sendFeedback')}
</Button>
</Row>
</Modal.Footer>
</Modal.Content>
<Modal onOpenChange={onIsOpenChange} isOpen={isOpen}>
<ModalHeader>
<ModalTitle>{`💬 ${t('appFeedback.modal.title')}`}</ModalTitle>
</ModalHeader>
<div className="flex flex-col gap-y-4">
<Typography className="text-pretty max-w-[95%]">{t('appFeedback.modal.description')}</Typography>
<div className="flex flex-col gap-y-1">
<Label htmlFor="description">{t('appFeedback.modal.notesLabel')}*</Label>
<TextArea
className="resize-none h-20 w-full"
id="description"
value={notes}
onChange={(e) => setNotes(e.target.value)}
placeholder={t('appFeedback.modal.notesPlaceholder')}
/>
</div>
<div className="flex flex-col gap-y-1">
<Label htmlFor="screenshot">{t('appFeedback.modal.screenshotLabel')}</Label>
<Input className="w-full" id="screenshot" type="file" accept="image/*" onChange={handleOnChangeFile} />
</div>
<div className="flex flex-col gap-y-1">
<div className="mb-2">
<Label>{t('appFeedback.modal.canWeContactPerMailLabel')}</Label>
<Typography variant="sm">{t('appFeedback.modal.canWeContactPerMailHelperText')}</Typography>
</div>
<RadioGroup
value={allowContact.toString()}
onValueChange={(nextValue) => {
setAllowContact(nextValue === 'true');
}}
className="flex flex-row gap-x-4"
>
<div className="flex gap-x-2 items-center">
<RadioGroupItem id="no" value="false" />
<Label htmlFor="no">{t('no')}</Label>
</div>
<div className="flex gap-x-2 items-center">
<RadioGroupItem id="yes" value="true" />
<Label htmlFor="yes">{t('yes')}</Label>
</div>
</RadioGroup>
</div>
<ModalFooter>
<Button disabled={!notes.trim()} isLoading={isSending} onClick={handleOnSubmit}>
{t('appFeedback.modal.sendFeedback')}
</Button>
</ModalFooter>
</div>
</Modal>
);
};
Expand Down
65 changes: 28 additions & 37 deletions src/modals/SwitchLanguageModal.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,40 @@
import { VStack, useTheme, Button, Modal, useBreakpointValue } from 'native-base';
import { Modal, ModalHeader, ModalTitle } from '@/components/Modal';
import { Toggle } from '@/components/Toggle';
import { switchLanguage, languageList, languageIcons } from '../I18n';

type Props = {
isOpen: boolean;
onCloseModal: () => any;
onIsOpenChange: (value: boolean) => void;
};

export const SwitchLanguageModal: React.FC<Props> = ({ isOpen, onCloseModal }) => {
const { space } = useTheme();
export const SwitchLanguageModal: React.FC<Props> = ({ isOpen, onIsOpenChange }) => {
const storageLanguage = localStorage.getItem('lernfair-language');

const widthButtonText = useBreakpointValue({
base: '55%',
md: '35%',
});

return (
<Modal isOpen={isOpen} onClose={onCloseModal}>
<Modal.Content>
<VStack>
<Modal.CloseButton />
<Modal.Header>Sprache wechseln / Choose language</Modal.Header>
</VStack>
<VStack padding={space['1']} space={space['1']}>
{languageList.map((button, i) => {
const Icon = languageIcons[button.short as keyof typeof languageIcons];
<Modal onOpenChange={onIsOpenChange} isOpen={isOpen}>
<ModalHeader>
<ModalTitle>Sprache wechseln / Choose language</ModalTitle>
</ModalHeader>
<div className="flex flex-col py-4 gap-y-4">
{languageList.map((button, i) => {
const Icon = languageIcons[button.short as keyof typeof languageIcons];

return (
<Button
isPressed={storageLanguage === button.short}
variant={'outlinemiddle'}
leftIcon={<Icon />}
_stack={{ justifyContent: 'left', width: widthButtonText }}
onPress={() => {
switchLanguage(button.short);
onCloseModal();
}}
key={i}
>
{button.name}
</Button>
);
})}
</VStack>
</Modal.Content>
return (
<Toggle
pressed={storageLanguage === button.short}
variant="outline"
size="lg"
onPressedChange={() => {
switchLanguage(button.short);
onIsOpenChange(false);
}}
key={i}
className="justify-center pl-[5%]"
>
<Icon className="mr-2" /> <span className="min-w-[15%] text-left">{button.name}</span>
</Toggle>
);
})}
</div>
</Modal>
);
};
2 changes: 1 addition & 1 deletion src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const Settings: React.FC = () => {
</VStack>
</WithNavigation>
<DeactivateAccountModal isOpen={showDeactivate} onCloseModal={() => setShowDeactivate(false)} />
<SwitchLanguageModal isOpen={showSwitchLanguage} onCloseModal={() => setShowSwitchLanguage(false)} />
<SwitchLanguageModal isOpen={showSwitchLanguage} onIsOpenChange={setShowSwitchLanguage} />
</>
);
};
Expand Down

0 comments on commit 317e605

Please sign in to comment.