parent
267a4a7be5
commit
d6a28a312a
2 changed files with 275 additions and 20 deletions
@ -0,0 +1,226 @@ |
|||||||
|
import { useEffect, useRef, useState } from 'preact/hooks'; |
||||||
|
import { httpPost } from '../../lib/http'; |
||||||
|
|
||||||
|
type ContributionInputProps = { |
||||||
|
id: number; |
||||||
|
title: string; |
||||||
|
link: string; |
||||||
|
isLast: boolean; |
||||||
|
totalCount: number; |
||||||
|
onAdd: () => void; |
||||||
|
onRemove: () => void; |
||||||
|
onChange: (link: { id: number; title: string; link: string }) => void; |
||||||
|
}; |
||||||
|
|
||||||
|
function ContributionInput(props: ContributionInputProps) { |
||||||
|
const { |
||||||
|
isLast, |
||||||
|
totalCount, |
||||||
|
onAdd, |
||||||
|
onRemove, |
||||||
|
onChange, |
||||||
|
id, |
||||||
|
title: defaultTitle, |
||||||
|
link: defaultLink, |
||||||
|
} = props; |
||||||
|
const titleRef = useRef<HTMLInputElement>(null); |
||||||
|
const [focused, setFocused] = useState(''); |
||||||
|
const [title, setTitle] = useState(defaultTitle); |
||||||
|
const [link, setLink] = useState(defaultLink); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (!titleRef?.current) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
titleRef.current.focus(); |
||||||
|
}, []); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
onChange({ id, title, link }); |
||||||
|
}, [title, link]); |
||||||
|
|
||||||
|
const canAddMore = isLast && totalCount < 5; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className="relative mb-3 rounded-md border p-3"> |
||||||
|
<p |
||||||
|
className={`mb-1 text-xs uppercase ${ |
||||||
|
focused === 'title' ? 'text-black' : 'text-gray-400' |
||||||
|
}`}
|
||||||
|
> |
||||||
|
Resource Title |
||||||
|
</p> |
||||||
|
<input |
||||||
|
ref={titleRef} |
||||||
|
type="text" |
||||||
|
required |
||||||
|
className="block w-full rounded-md border p-2 text-sm focus:border-gray-400 focus:outline-none" |
||||||
|
placeholder="e.g. Introduction to RESTful APIs" |
||||||
|
onFocus={() => setFocused('title')} |
||||||
|
onBlur={() => setFocused('')} |
||||||
|
onChange={(e) => setTitle((e.target as any).value)} |
||||||
|
/> |
||||||
|
<p |
||||||
|
className={`mb-1 mt-3 text-xs uppercase ${ |
||||||
|
focused === 'link' ? 'text-black' : 'text-gray-400' |
||||||
|
}`}
|
||||||
|
> |
||||||
|
Resource Link |
||||||
|
</p> |
||||||
|
<input |
||||||
|
type="url" |
||||||
|
required |
||||||
|
className="block w-full rounded-md border p-2 text-sm focus:border-gray-400 focus:outline-none" |
||||||
|
placeholder="e.g. https://roadmap.sh/guides/some-url" |
||||||
|
onFocus={() => setFocused('link')} |
||||||
|
onBlur={() => setFocused('')} |
||||||
|
onChange={(e) => setLink((e.target as any).value)} |
||||||
|
/> |
||||||
|
|
||||||
|
<div className="mb-0 mt-3 flex gap-3"> |
||||||
|
{totalCount !== 1 && ( |
||||||
|
<button |
||||||
|
onClick={(e) => { |
||||||
|
e.preventDefault(); |
||||||
|
onRemove(); |
||||||
|
}} |
||||||
|
className="rounded-md text-sm font-semibold text-red-500 underline underline-offset-2 hover:text-red-800" |
||||||
|
> |
||||||
|
- Remove Link |
||||||
|
</button> |
||||||
|
)} |
||||||
|
|
||||||
|
{canAddMore && ( |
||||||
|
<button |
||||||
|
onClick={(e) => { |
||||||
|
e.preventDefault(); |
||||||
|
onAdd(); |
||||||
|
}} |
||||||
|
className="rounded-md text-sm font-semibold text-gray-600 underline underline-offset-2 hover:text-black" |
||||||
|
> |
||||||
|
+ Add another Link |
||||||
|
</button> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
type ContributionFormProps = { |
||||||
|
resourceType: string; |
||||||
|
resourceId: string; |
||||||
|
topicId: string; |
||||||
|
onClose: (message?: string) => void; |
||||||
|
}; |
||||||
|
|
||||||
|
export function ContributionForm(props: ContributionFormProps) { |
||||||
|
const { onClose, resourceType, resourceId, topicId } = props; |
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false); |
||||||
|
const [links, setLinks] = useState< |
||||||
|
{ id: number; title: string; link: string }[] |
||||||
|
>([ |
||||||
|
{ |
||||||
|
id: new Date().getTime(), |
||||||
|
title: '', |
||||||
|
link: '', |
||||||
|
}, |
||||||
|
]); |
||||||
|
|
||||||
|
async function onSubmit(e: any) { |
||||||
|
e.preventDefault(); |
||||||
|
setIsSubmitting(true); |
||||||
|
|
||||||
|
const { response, error } = await httpPost( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-contribute-link`, |
||||||
|
{ |
||||||
|
resourceType, |
||||||
|
resourceId, |
||||||
|
topicId, |
||||||
|
links, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
setIsSubmitting(false); |
||||||
|
|
||||||
|
if (!response || error) { |
||||||
|
alert(error?.message || 'Something went wrong. Please try again.'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
onClose('Thanks for your contribution! We will review it shortly.'); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<div className="mb-2 mt-2 rounded-md border bg-gray-100 p-3"> |
||||||
|
<h1 className="mb-2 text-2xl font-bold">Guidelines</h1> |
||||||
|
<ul class="flex flex-col gap-1 text-sm text-gray-700"> |
||||||
|
<li>Content should only be in English</li> |
||||||
|
<li>Do not add things you have not evaluated personally.</li> |
||||||
|
<li>It should strictly be relevant to the topic.</li> |
||||||
|
<li>It should not be paid or behind a signup.</li> |
||||||
|
<li> |
||||||
|
Quality over quantity. Smaller set of quality links is preferred. |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form onSubmit={onSubmit}> |
||||||
|
{links.map((link, counter) => ( |
||||||
|
<ContributionInput |
||||||
|
key={link.id} |
||||||
|
id={link.id} |
||||||
|
title={link.title} |
||||||
|
link={link.link} |
||||||
|
isLast={counter === links.length - 1} |
||||||
|
totalCount={links.length} |
||||||
|
onChange={(newLink) => { |
||||||
|
setLinks( |
||||||
|
links.map((l) => { |
||||||
|
if (l.id === link.id) { |
||||||
|
return newLink; |
||||||
|
} |
||||||
|
|
||||||
|
return l; |
||||||
|
}) |
||||||
|
); |
||||||
|
}} |
||||||
|
onRemove={() => { |
||||||
|
setLinks(links.filter((l) => l.id !== link.id)); |
||||||
|
}} |
||||||
|
onAdd={() => { |
||||||
|
setLinks([ |
||||||
|
...links, |
||||||
|
{ |
||||||
|
id: new Date().getTime(), |
||||||
|
title: '', |
||||||
|
link: '', |
||||||
|
}, |
||||||
|
]); |
||||||
|
}} |
||||||
|
/> |
||||||
|
))} |
||||||
|
|
||||||
|
<div className="flex gap-2"> |
||||||
|
<button |
||||||
|
disabled={isSubmitting} |
||||||
|
type="submit" |
||||||
|
className="block w-full rounded-md bg-gray-800 p-2 text-sm text-white hover:bg-black disabled:cursor-not-allowed disabled:bg-gray-400" |
||||||
|
> |
||||||
|
{isSubmitting ? 'Please wait ...' : 'Submit'} |
||||||
|
</button> |
||||||
|
<button |
||||||
|
className="block w-full rounded-md border border-red-500 p-2 text-sm text-red-600 hover:bg-red-600 hover:text-white" |
||||||
|
onClick={(e) => { |
||||||
|
e.preventDefault(); |
||||||
|
onClose(); |
||||||
|
}} |
||||||
|
> |
||||||
|
Cancel |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue