feat: change how the 'save changes' look and function in the profile settings

This commit is contained in:
MTG2000
2022-08-10 16:16:27 +03:00
parent 4c50bfbdad
commit 118dd3622a
7 changed files with 365 additions and 243 deletions

View File

@@ -93,7 +93,7 @@ export default function EditProfilePage() {
</div>
}
</aside>
<main className="md:col-span-2">
<main className="md:col-span-3">
<Routes>
<Route index element={<Navigate to='my-profile' />} />
<Route path='my-profile' element={<UpdateMyProfileCard data={profileQuery.data.profile} />} />

View File

@@ -0,0 +1,63 @@
import React from 'react'
import { Link } from 'react-router-dom'
import Button from 'src/Components/Button/Button'
import Avatar from 'src/features/Profiles/Components/Avatar/Avatar'
import { useProfileQuery } from 'src/graphql'
import { trimText } from 'src/utils/helperFunctions'
import { useAppSelector } from 'src/utils/hooks'
import { createRoute } from 'src/utils/routing'
interface Props {
isLoading?: boolean;
isDirty?: boolean;
onSubmit?: () => void
onCancel?: () => void;
}
export default function SaveChangesCard(props: Props) {
const userId = useAppSelector(state => state.user.me?.id!)
const profileQuery = useProfileQuery({
variables: {
profileId: userId,
},
})
if (!profileQuery.data?.profile)
return <></>
return (
<div className="md:p-24 rounded-16 bg-white md:border-2 border-gray-200 flex flex-col gap-24">
<div className='hidden md:flex gap-8'>
<Link to={createRoute({ type: 'profile', id: profileQuery.data.profile.id, username: profileQuery.data.profile.name })}>
<Avatar width={48} src={profileQuery.data.profile.avatar!} />
</Link>
<div className='overflow-hidden'>
<p className={`text-body4 text-black font-medium overflow-hidden text-ellipsis`}>{profileQuery.data.profile ? trimText(profileQuery.data.profile.name, 30) : "Anonymouse"}</p>
{profileQuery.data.profile.jobTitle && <p className={`text-body6 text-gray-600`}>{profileQuery.data.profile.jobTitle}</p>}
</div>
{/* {showTimeAgo && <p className={`${nameSize[size]} text-gray-500 ml-auto `}>
{dayjs().diff(props.date, 'hour') < 24 ? `${dayjs().diff(props.date, 'hour')}h ago` : undefined}
</p>} */}
</div>
<p className="hidden md:block text-body5">{trimText(profileQuery.data.profile.bio, 120)}</p>
<div className="flex md:flex-col gap-16 justify-end">
<Button
color="primary"
onClick={props.onSubmit}
isLoading={props.isLoading}
disabled={!props.isDirty || props.isLoading}
>
Save Changes
</Button>
<Button
color="gray"
onClick={props.onCancel}
disabled={!props.isDirty || props.isLoading}
>
Cancel
</Button>
</div>
</div>
)
}

View File

@@ -5,8 +5,8 @@ import { NotificationsService } from "src/services/notifications.service";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import Avatar from "src/features/Profiles/Components/Avatar/Avatar";
import { usePrompt } from "src/utils/hooks";
import SaveChangesCard from "../SaveChangesCard/SaveChangesCard";
interface Props {
data: Pick<User,
@@ -63,7 +63,7 @@ const schema: yup.SchemaOf<IFormInputs> = yup.object({
export default function UpdateMyProfileCard({ data, onClose }: Props) {
const { register, formState: { errors }, handleSubmit } = useForm<IFormInputs>({
const { register, formState: { errors, isDirty, }, handleSubmit, reset } = useForm<IFormInputs>({
defaultValues: data,
resolver: yupResolver(schema),
mode: 'onBlur',
@@ -77,6 +77,8 @@ export default function UpdateMyProfileCard({ data, onClose }: Props) {
usePrompt('You may have some unsaved changes. You still want to leave?', isDirty)
const onSubmit: SubmitHandler<IFormInputs> = data => {
mutate({
@@ -94,6 +96,9 @@ export default function UpdateMyProfileCard({ data, onClose }: Props) {
twitter: data.twitter,
website: data.website,
}
},
onCompleted: () => {
reset(data);
}
}).catch(() => {
NotificationsService.error('A network error happened');
@@ -102,186 +107,179 @@ export default function UpdateMyProfileCard({ data, onClose }: Props) {
};
return (
<div className="rounded-16 bg-white border-2 border-gray-200">
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2">
<Avatar src={data.avatar} width={120} />
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<div className="col-span-2 rounded-16 bg-white border-2 border-gray-200">
<div className="bg-gray-600 relative h-[160px] rounded-t-16">
<div className="absolute left-24 bottom-0 translate-y-1/2">
<Avatar src={data.avatar} width={120} />
</div>
</div>
<div className="p-16 md:p-24 mt-64">
<form onSubmit={handleSubmit(onSubmit)}>
<p className="text-body5 font-medium">
Name
</p>
<div className="input-wrapper mt-8 relative">
<input
autoFocus
type='text'
className="input-text"
placeholder='John Doe'
{...register("name")}
/>
</div>
{errors.name && <p className="input-error">
{errors.name.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Avatar
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://images.com/my-avatar.jpg'
{...register("avatar")}
/>
</div>
{errors.avatar && <p className="input-error">
{errors.avatar.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Bio
</p>
<div className="input-wrapper mt-8 relative">
<textarea
rows={3}
className="input-text !p-20"
placeholder='Tell others a little bit about yourself'
{...register("bio")}
/>
</div>
{errors.bio && <p className="input-error">
{errors.bio.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Job Title
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="Back-end Developer"
{...register("jobTitle")}
/>
</div>
{errors.jobTitle && <p className="input-error">
{errors.jobTitle.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Location
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="UK, London"
{...register("location")}
/>
</div>
{errors.location && <p className="input-error">
{errors.location.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Website
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.website.io"
{...register("website")}
/>
</div>
{errors.website && <p className="input-error">
{errors.website.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Twitter
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="@johndoe"
{...register("twitter")}
/>
</div>
{errors.twitter && <p className="input-error">
{errors.twitter.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Github
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe"
{...register("github")}
/>
</div>
{errors.github && <p className="input-error">
{errors.github.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Linkedin
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.linkedin.com/in/john-doe"
{...register("linkedin")}
/>
</div>
{errors.linkedin && <p className="input-error">
{errors.linkedin.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Lightning address
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe@lnd.com"
{...register("lightning_address")}
/>
</div>
{errors.lightning_address && <p className="input-error">
{errors.lightning_address.message}
</p>}
<p className="text-body6 text-gray-400 mt-8 max-w-[70ch]">
Your lightning address is used to send the votes you get on your posts, comments, apps...etc, directly to you.
</p>
</form>
</div>
</div>
<div className="p-16 md:p-24 mt-64">
<form onSubmit={handleSubmit(onSubmit)}>
<p className="text-body5 font-medium">
Name
</p>
<div className="input-wrapper mt-8 relative">
<input
autoFocus
type='text'
className="input-text"
placeholder='John Doe'
{...register("name")}
/>
</div>
{errors.name && <p className="input-error">
{errors.name.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Avatar
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder='https://images.com/my-avatar.jpg'
{...register("avatar")}
/>
</div>
{errors.avatar && <p className="input-error">
{errors.avatar.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Bio
</p>
<div className="input-wrapper mt-8 relative">
<textarea
rows={3}
className="input-text !p-20"
placeholder='Tell others a little bit about yourself'
{...register("bio")}
/>
</div>
{errors.bio && <p className="input-error">
{errors.bio.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Job Title
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="Back-end Developer"
{...register("jobTitle")}
/>
</div>
{errors.jobTitle && <p className="input-error">
{errors.jobTitle.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Location
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="UK, London"
{...register("location")}
/>
</div>
{errors.location && <p className="input-error">
{errors.location.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Website
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.website.io"
{...register("website")}
/>
</div>
{errors.website && <p className="input-error">
{errors.website.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Twitter
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="@johndoe"
{...register("twitter")}
/>
</div>
{errors.twitter && <p className="input-error">
{errors.twitter.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Github
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe"
{...register("github")}
/>
</div>
{errors.github && <p className="input-error">
{errors.github.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Linkedin
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="www.linkedin.com/in/john-doe"
{...register("linkedin")}
/>
</div>
{errors.linkedin && <p className="input-error">
{errors.linkedin.message}
</p>}
<p className="text-body5 mt-16 font-medium">
Lightning address
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
placeholder="johndoe@lnd.com"
{...register("lightning_address")}
/>
</div>
{errors.lightning_address && <p className="input-error">
{errors.lightning_address.message}
</p>}
<p className="text-body6 text-gray-400 mt-8 max-w-[70ch]">
Your lightning address is used to send the votes you get on your posts, comments, apps...etc, directly to you.
</p>
<div className="mt-24 flex gap-16 justify-end">
<Button
color='gray'
disabled={mutationStatus.loading}
onClick={onClose}
>
Cancel
</Button>
<Button
type='submit'
color='primary'
isLoading={mutationStatus.loading}
disabled={mutationStatus.loading}
>
Save changes
</Button>
</div>
</form>
<div className="self-start sticky-side-element">
<SaveChangesCard
isLoading={mutationStatus.loading}
isDirty={isDirty}
onSubmit={handleSubmit(onSubmit)}
onCancel={() => reset()}
/>
</div>
</div>
)

View File

@@ -21,48 +21,49 @@ export default function CommentsSettingsCard({ nostr_prv_key, nostr_pub_key, isO
return (
<div className="rounded-16 bg-white border-2 border-gray-200 p-24">
<p className="text-body2 font-bold">💬 Nostr comments <span className="bg-green-50 text-green-500 text-body5 font-medium py-4 px-12 rounded-48 ml-8">Experimental</span></p>
<p className="mt-8 text-body4 text-gray-600">
Our commenting system is experimental and uses Nostr to store and relay your messages and replies to our own relay, as well as relays ran by other people in the community.
We generate Nostr keys for you since there are no popular wallets which support it.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-24">
<div className="col-span-2 rounded-16 bg-white border-2 border-gray-200 p-24">
<p className="text-body2 font-bold">💬 Nostr comments <span className="bg-green-50 text-green-500 text-body5 font-medium py-4 px-12 rounded-48 ml-8">Experimental</span></p>
<p className="mt-8 text-body4 text-gray-600">
Our commenting system is experimental and uses Nostr to store and relay your messages and replies to our own relay, as well as relays ran by other people in the community.
We generate Nostr keys for you since there are no popular wallets which support it.
</p>
<div className='mt-24 flex flex-col gap-16'>
<p className="text-body3 font-bold">Nostr keys</p>
{nostr_prv_key && <div>
<p className="text-body5 font-bold">
Your Nostr Private Key
</p>
<div className="input-wrapper mt-8 relative">
<input
type={'password'}
className="input-text"
defaultValue={nostr_prv_key}
readOnly
/>
<div className='mt-24 flex flex-col gap-16'>
<p className="text-body3 font-bold">Nostr keys</p>
{nostr_prv_key && <div>
<p className="text-body5 font-bold">
Your Nostr Private Key
</p>
<div className="input-wrapper mt-8 relative">
<input
type={'password'}
className="input-text"
defaultValue={nostr_prv_key}
readOnly
/>
<CopyToClipboard text={nostr_prv_key} />
</div>
</div>}
<div>
<p className="text-body5 font-bold">
Your Nostr Public Key
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
defaultValue={nostr_pub_key!}
readOnly
/>
<CopyToClipboard text={nostr_pub_key ?? ''} />
<CopyToClipboard text={nostr_prv_key} />
</div>
</div>}
<div>
<p className="text-body5 font-bold">
Your Nostr Public Key
</p>
<div className="input-wrapper mt-8 relative">
<input
type='text'
className="input-text"
defaultValue={nostr_pub_key!}
readOnly
/>
<CopyToClipboard text={nostr_pub_key ?? ''} />
</div>
</div>
</div>
</div>
{/* <p className="text-body4 font-bold mt-24">
{/* <p className="text-body4 font-bold mt-24">
Connect your Nostr identity
</p>
<div className="mt-8 py-12 relative">
@@ -70,31 +71,32 @@ export default function CommentsSettingsCard({ nostr_prv_key, nostr_pub_key, isO
🚧 Coming Soon 🚧
</p>
</div> */}
<div className='mt-24'>
<div className="flex justify-between">
<p className="text-body4 font-bold">
Nostr relays
</p>
<IconButton onClick={() => toggleRelaysDropdownOpen()}>
<motion.div
animate={{ rotate: relaysDropdownOpen ? 180 : 0 }}
>
<FaChevronDown />
</motion.div>
</IconButton>
<div className='mt-24'>
<div className="flex justify-between">
<p className="text-body4 font-bold">
Nostr relays
</p>
<IconButton onClick={() => toggleRelaysDropdownOpen()}>
<motion.div
animate={{ rotate: relaysDropdownOpen ? 180 : 0 }}
>
<FaChevronDown />
</motion.div>
</IconButton>
</div>
{relaysDropdownOpen &&
<motion.ul
initial={{ y: '-50%', opacity: 0 }}
animate={{ y: '0', opacity: 1 }}
className="mt-8 relative flex flex-col gap-8">
{CONSTS.DEFAULT_RELAYS.map((url, idx) => <li key={idx} className="text-body4 border-b py-12 px-16 border border-gray-200 rounded-16">{url}</li>)}
</motion.ul>}
</div>
{relaysDropdownOpen &&
<motion.ul
initial={{ y: '-50%', opacity: 0 }}
animate={{ y: '0', opacity: 1 }}
className="mt-8 relative flex flex-col gap-8">
{CONSTS.DEFAULT_RELAYS.map((url, idx) => <li key={idx} className="text-body4 border-b py-12 px-16 border border-gray-200 rounded-16">{url}</li>)}
</motion.ul>}
</div>
<Button color='gray' fullWidth disabled className='mt-24'>
Connect your Nostr ID (coming soon)
</Button>
<Button color='gray' fullWidth disabled className='mt-24'>
Connect your Nostr ID (coming soon)
</Button>
</div>
</div>
)
}

View File

@@ -61,7 +61,8 @@ export function lazyModal<T extends ComponentType<any>>
return { LazyComponent, preload };
}
export function trimText(text: string, length: number) {
export function trimText(text: string | undefined | null, length: number) {
if (!text) return '';
return text.slice(0, length) + (text.length > length ? "..." : "")
}

View File

@@ -11,3 +11,5 @@ export * from './useMediaQuery'
export * from './useCurrentSection'
export * from './usePreload'
export * from './useCarousel'
export * from './usePrompt'

View File

@@ -0,0 +1,56 @@
/**
* These hooks re-implement the now removed useBlocker and usePrompt hooks in 'react-router-dom'.
* Thanks for the idea @piecyk https://github.com/remix-run/react-router/issues/8139#issuecomment-953816315
* Source: https://github.com/remix-run/react-router/commit/256cad70d3fd4500b1abcfea66f3ee622fb90874#diff-b60f1a2d4276b2a605c05e19816634111de2e8a4186fe9dd7de8e344b65ed4d3L344-L381
*/
import { useContext, useEffect, useCallback } from 'react';
import { UNSAFE_NavigationContext as NavigationContext } from 'react-router-dom';
/**
* Blocks all navigation attempts. This is useful for preventing the page from
* changing until some condition is met, like saving form data.
*
* @param blocker
* @param when
* @see https://reactrouter.com/api/useBlocker
*/
export function useBlocker(blocker: any, when = true) {
const { navigator } = useContext(NavigationContext);
useEffect(() => {
if (!when) return;
const unblock = (navigator as any).block((tx: any) => {
const autoUnblockingTx = {
...tx,
retry() {
// Automatically unblock the transition so it can play all the way
// through before retrying it. TODO: Figure out how to re-enable
// this block if the transition is cancelled for some reason.
unblock();
tx.retry();
},
};
blocker(autoUnblockingTx);
});
return unblock;
}, [navigator, blocker, when]);
}
/**
* Prompts the user with an Alert before they leave the current screen.
*
* @param message
* @param when
*/
export function usePrompt(message: string, when = true) {
const blocker = useCallback(
(tx: any) => {
// eslint-disable-next-line no-alert
if (window.confirm(message)) tx.retry();
},
[message]
);
useBlocker(blocker, when);
}