Add contribution functionality

pull/4006/head
Kamran Ahmed 2 years ago
parent 267a4a7be5
commit d6a28a312a
  1. 226
      src/components/TopicDetail/ContributionForm.tsx
  2. 67
      src/components/TopicDetail/TopicDetail.tsx

@ -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>
);
}

@ -16,10 +16,13 @@ import {
} from '../../lib/resource-progress'; } from '../../lib/resource-progress';
import { pageProgressMessage, sponsorHidden } from '../../stores/page'; import { pageProgressMessage, sponsorHidden } from '../../stores/page';
import { TopicProgressButton } from './TopicProgressButton'; import { TopicProgressButton } from './TopicProgressButton';
import { ContributionForm } from './ContributionForm';
export function TopicDetail() { export function TopicDetail() {
const [contributionAlertMessage, setContributionAlertMessage] = useState('');
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isContributing, setIsContributing] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
const [topicHtml, setTopicHtml] = useState(''); const [topicHtml, setTopicHtml] = useState('');
@ -45,14 +48,15 @@ export function TopicDetail() {
} }
}; };
// Close the topic detail when user clicks outside the topic detail // Close the topic detail when user clicks outside the topic detail
useOutsideClick(topicRef, () => { useOutsideClick(topicRef, () => {
setIsActive(false); setIsActive(false);
setIsContributing(false);
}); });
useKeydown('Escape', () => { useKeydown('Escape', () => {
setIsActive(false); setIsActive(false);
setIsContributing(false);
}); });
// Toggle topic is available even if the component UI is not active // Toggle topic is available even if the component UI is not active
@ -99,6 +103,7 @@ export function TopicDetail() {
setIsActive(true); setIsActive(true);
sponsorHidden.set(true); sponsorHidden.set(true);
setContributionAlertMessage('');
setTopicId(topicId); setTopicId(topicId);
setResourceType(resourceType); setResourceType(resourceType);
setResourceId(resourceId); setResourceId(resourceId);
@ -142,10 +147,6 @@ export function TopicDetail() {
return null; return null;
} }
const contributionDir =
resourceType === 'roadmap' ? 'roadmaps' : 'best-practices';
const contributionUrl = `https://github.com/kamranahmedse/developer-roadmap/tree/master/src/data/${contributionDir}/${resourceId}/content`;
return ( return (
<div> <div>
<div <div
@ -162,7 +163,22 @@ export function TopicDetail() {
</div> </div>
)} )}
{!isLoading && !error && ( {!isLoading && isContributing && (
<ContributionForm
resourceType={resourceType}
resourceId={resourceId}
topicId={topicId}
onClose={(message?: string) => {
if (message) {
setContributionAlertMessage(message);
}
setIsContributing(false);
}}
/>
)}
{!isContributing && !isLoading && !error && (
<> <>
{/* Actions for the topic */} {/* Actions for the topic */}
<div className="mb-2"> <div className="mb-2">
@ -173,6 +189,7 @@ export function TopicDetail() {
onShowLoginPopup={showLoginPopup} onShowLoginPopup={showLoginPopup}
onClose={() => { onClose={() => {
setIsActive(false); setIsActive(false);
setIsContributing(false);
}} }}
/> />
@ -180,7 +197,10 @@ export function TopicDetail() {
type="button" type="button"
id="close-topic" id="close-topic"
className="absolute right-2.5 top-2.5 inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900" className="absolute right-2.5 top-2.5 inline-flex items-center rounded-lg bg-transparent p-1.5 text-sm text-gray-400 hover:bg-gray-200 hover:text-gray-900"
onClick={() => setIsActive(false)} onClick={() => {
setIsActive(false);
setIsContributing(false);
}}
> >
<img alt="Close" class="h-5 w-5" src={CloseIcon} /> <img alt="Close" class="h-5 w-5" src={CloseIcon} />
</button> </button>
@ -193,20 +213,29 @@ export function TopicDetail() {
dangerouslySetInnerHTML={{ __html: topicHtml }} dangerouslySetInnerHTML={{ __html: topicHtml }}
></div> ></div>
<p {/* Contribution */}
id="contrib-meta" <div className="mt-8 flex-1 border-t">
class="mt-10 border-t pt-3 text-sm leading-relaxed text-gray-400" <p class="mb-2 mt-2 text-sm leading-relaxed text-gray-400">
>
Contribute links to learning resources about this topic{' '} Contribute links to learning resources about this topic{' '}
<a
target="_blank"
class="text-blue-700 underline"
href={contributionUrl}
>
on GitHub repository.
</a>
.
</p> </p>
<button
onClick={() => {
if (isGuest) {
setIsActive(false);
showLoginPopup();
return;
}
setIsContributing(true);
}}
disabled={!!contributionAlertMessage}
className="block w-full rounded-md bg-gray-800 p-2 text-sm text-white transition-colors hover:bg-black hover:text-white disabled:bg-green-200 disabled:text-black"
>
{contributionAlertMessage
? contributionAlertMessage
: 'Submit a Link'}
</button>
</div>
</> </>
)} )}
</div> </div>

Loading…
Cancel
Save