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

Profile Settings Modal has no feedback for user data loading state #179

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
85 changes: 72 additions & 13 deletions apps/frontend/src/components/layout/settings.component.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import { useModals } from '@mantine/modals';
import React, { FC, Ref, useCallback, useEffect, useMemo } from 'react';
import React, {
FC,
Ref,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { Input } from '@gitroom/react/form/input';
import { Button } from '@gitroom/react/form/button';
import { Textarea } from '@gitroom/react/form/textarea';
Expand All @@ -16,13 +23,20 @@
import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { LogoutComponent } from '@gitroom/frontend/components/layout/logout.component';
import { useSearchParams } from 'next/navigation';
import ReactLoading from 'react-loading';
import Skeleton from './skeleton';

export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
const { getRef } = props;
const fetch = useFetch();
const toast = useToaster();
const swr = useSWRConfig();
const user = useUser();
// Loading states for better ux
const [loading, setLoading] = useState({
isProfileLoading: false, //When the user details are loading
isSaving: false, // When the user submits the form
});

const resolver = useMemo(() => {
return classValidatorResolver(UserDetailDto);
Expand All @@ -38,10 +52,18 @@
const showLogout = !url.get('onboarding');

const loadProfile = useCallback(async () => {
setLoading((prevState) => ({
...prevState,
isProfileLoading: true,
}));
const personal = await (await fetch('/user/personal')).json();
form.setValue('fullname', personal.name || '');
form.setValue('bio', personal.bio || '');
form.setValue('picture', personal.picture);
setLoading((prevState) => ({
...prevState,
isProfileLoading: false,
}));
}, []);

const openMedia = useCallback(() => {
Expand All @@ -55,6 +77,10 @@
}, []);

const submit = useCallback(async (val: any) => {
setLoading((prevState) => ({
...prevState,
isSaving: true,
}));
await fetch('/user/personal', {
method: 'POST',
body: JSON.stringify(val),
Expand All @@ -64,6 +90,10 @@
return;
}

setLoading((prevState) => ({
...prevState,
isSaving: false,
}));
toast.show('Profile updated');
swr.mutate('/marketplace/account');
close();
Expand Down Expand Up @@ -119,18 +149,29 @@
<div className="rounded-[4px] border border-customColor6 p-[24px] flex flex-col">
<div className="flex justify-between items-center">
<div className="w-[455px]">
<Input label="Full Name" name="fullname" />
{loading.isProfileLoading ? (
<div className="flex flex-col gap-2 mb-4">
<Skeleton width="40%" height="30px" />
<Skeleton width="100%" height="40px" />
</div>
) : (
<Input label="Full Name" name="fullname" />
)}
</div>
<div className="flex gap-[8px] mb-[10px]">
<div className="w-[48px] h-[48px] rounded-full bg-customColor38">
{!!picture?.path && (
<img
src={picture?.path}
alt="profile"
className="w-full h-full rounded-full"
/>
)}
</div>
{loading.isProfileLoading ? (
<Skeleton width="48px" height="48px" type="circle" />
) : (
<div className="w-[48px] h-[48px] rounded-full bg-[#D9D9D9]">
{!!picture?.path && (
<img
src={picture?.path}
alt="profile"
className="w-full h-full rounded-full"
/>
Comment on lines +167 to +171

Check warning

Code scanning / ESLint

Prevent usage of `<img>` element due to slower LCP and higher bandwidth. Warning

Using <img> could result in slower LCP and higher bandwidth. Consider using <Image /> from next/image to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
)}
</div>
)}
<div className="flex flex-col gap-[2px]">
<div className="text-[14px]">Profile Picture</div>
<div className="flex gap-[8px]">
Expand Down Expand Up @@ -183,12 +224,30 @@
</div>
</div>
<div>
<Textarea label="Bio" name="bio" className="resize-none" />
{loading.isProfileLoading ? (
<div className="flex flex-col gap-2 mb-4">
<Skeleton width="30px" height="30px" />
<Skeleton width="100%" height="150px" />
</div>
) : (
<Textarea label="Bio" name="bio" className="resize-none" />
)}
</div>
</div>
{!getRef && (
<div className="justify-end flex">
<Button type="submit">Save</Button>
<Button disabled={loading.isProfileLoading} type="submit">
{loading.isSaving ? (
<ReactLoading
type="spin"
color="#fff"
width={20}
height={20}
/>
) : (
'Save'
)}
</Button>
</div>
)}
{!!user?.tier?.team_members && isGeneral() && <TeamsComponent />}
Expand Down
32 changes: 32 additions & 0 deletions apps/frontend/src/components/layout/skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';

type SkeletonProps = {
width?: string | number;
height?: string | number;
borderRadius?: string | number;
className?: string;
type?: 'circle' | 'rectangle';
};

const Skeleton: React.FC<SkeletonProps> = ({
width = '100%',
height = '100%',
borderRadius = '4px',
className = '',
type = 'rectangle',
}) => {
const styles = {
width: typeof width === 'number' ? `${width}px` : width,
height: typeof height === 'number' ? `${height}px` : height,
borderRadius: type === 'circle' ? '50%' : borderRadius,
};

return (
<div
style={styles}
className={`bg-blue-200 animate-pulse ${className}`}
></div>
);
};

export default Skeleton;