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: UI facelift SwitchLanguageModal & AppFeedbackModal #654

Merged
merged 4 commits into from
Sep 14, 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
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
Loading