mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-17 21:34:23 +01:00
feat: change how the 'save changes' look and function in the profile settings
This commit is contained in:
@@ -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} />} />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 ? "..." : "")
|
||||
}
|
||||
|
||||
|
||||
@@ -11,3 +11,5 @@ export * from './useMediaQuery'
|
||||
export * from './useCurrentSection'
|
||||
export * from './usePreload'
|
||||
export * from './useCarousel'
|
||||
export * from './usePrompt'
|
||||
|
||||
|
||||
56
src/utils/hooks/usePrompt.tsx
Normal file
56
src/utils/hooks/usePrompt.tsx
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user