mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-30 12:40:03 +02:00
Add contribution functionality
This commit is contained in:
226
src/components/TopicDetail/ContributionForm.tsx
Normal file
226
src/components/TopicDetail/ContributionForm.tsx
Normal file
@@ -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';
|
||||
import { pageProgressMessage, sponsorHidden } from '../../stores/page';
|
||||
import { TopicProgressButton } from './TopicProgressButton';
|
||||
import { ContributionForm } from './ContributionForm';
|
||||
|
||||
export function TopicDetail() {
|
||||
const [contributionAlertMessage, setContributionAlertMessage] = useState('');
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isContributing, setIsContributing] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [topicHtml, setTopicHtml] = useState('');
|
||||
|
||||
@@ -45,14 +48,15 @@ export function TopicDetail() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Close the topic detail when user clicks outside the topic detail
|
||||
useOutsideClick(topicRef, () => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
});
|
||||
|
||||
useKeydown('Escape', () => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
});
|
||||
|
||||
// Toggle topic is available even if the component UI is not active
|
||||
@@ -99,6 +103,7 @@ export function TopicDetail() {
|
||||
setIsActive(true);
|
||||
sponsorHidden.set(true);
|
||||
|
||||
setContributionAlertMessage('');
|
||||
setTopicId(topicId);
|
||||
setResourceType(resourceType);
|
||||
setResourceId(resourceId);
|
||||
@@ -142,10 +147,6 @@ export function TopicDetail() {
|
||||
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 (
|
||||
<div>
|
||||
<div
|
||||
@@ -162,7 +163,22 @@ export function TopicDetail() {
|
||||
</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 */}
|
||||
<div className="mb-2">
|
||||
@@ -173,6 +189,7 @@ export function TopicDetail() {
|
||||
onShowLoginPopup={showLoginPopup}
|
||||
onClose={() => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -180,7 +197,10 @@ export function TopicDetail() {
|
||||
type="button"
|
||||
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"
|
||||
onClick={() => setIsActive(false)}
|
||||
onClick={() => {
|
||||
setIsActive(false);
|
||||
setIsContributing(false);
|
||||
}}
|
||||
>
|
||||
<img alt="Close" class="h-5 w-5" src={CloseIcon} />
|
||||
</button>
|
||||
@@ -193,20 +213,29 @@ export function TopicDetail() {
|
||||
dangerouslySetInnerHTML={{ __html: topicHtml }}
|
||||
></div>
|
||||
|
||||
<p
|
||||
id="contrib-meta"
|
||||
class="mt-10 border-t pt-3 text-sm leading-relaxed text-gray-400"
|
||||
>
|
||||
{/* Contribution */}
|
||||
<div className="mt-8 flex-1 border-t">
|
||||
<p class="mb-2 mt-2 text-sm leading-relaxed text-gray-400">
|
||||
Contribute links to learning resources about this topic{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
class="text-blue-700 underline"
|
||||
href={contributionUrl}
|
||||
>
|
||||
on GitHub repository.
|
||||
</a>
|
||||
.
|
||||
</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>
|
||||
|
Reference in New Issue
Block a user