Skip to content

Commit c720888

Browse files
Add functionality to share progress (kamranahmedse#4279)
* wip: user progress modal * wip: modal loading state * wip: share progress * chore: best practices share * chore: prettier * fix: classname * Progress button design * Progress modal * Update * Update * Progress modal refactoring * Remove event binding for progress * Update * UI changes on progress --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>
1 parent 2018b9b commit c720888

File tree

11 files changed

+547
-94
lines changed

11 files changed

+547
-94
lines changed

src/components/Activity/ResourceProgress.tsx

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState } from 'preact/hooks';
22
import { httpPost } from '../../lib/http';
33
import { getRelativeTimeString } from '../../lib/date';
44
import { useToast } from '../../hooks/use-toast';
5+
import { ProgressShareButton } from '../UserProgress/ProgressShareButton';
56

67
type ResourceProgressType = {
78
resourceType: 'roadmap' | 'best-practice';
@@ -88,7 +89,7 @@ export function ResourceProgress(props: ResourceProgressType) {
8889
{getRelativeTimeString(updatedAt)}
8990
</span>
9091
</a>
91-
<p className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
92+
<div className="sm:space-between flex flex-row items-start rounded-b-md border border-t-0 px-2 py-2 text-xs text-gray-500">
9293
<span className="hidden flex-1 gap-1 sm:flex">
9394
{doneCount > 0 && (
9495
<>
@@ -107,44 +108,55 @@ export function ResourceProgress(props: ResourceProgressType) {
107108
)}
108109
<span>{totalCount} total</span>
109110
</span>
110-
{showClearButton && (
111-
<>
112-
{!isConfirming && (
113-
<button
114-
className="text-red-500 hover:text-red-800"
115-
onClick={() => setIsConfirming(true)}
116-
disabled={isClearing}
117-
>
118-
{!isClearing && (
119-
<>
120-
Clear Progress <span>&times;</span>
121-
</>
122-
)}
111+
<div className="flex w-full items-center justify-between gap-2 sm:w-auto sm:justify-start">
112+
<ProgressShareButton
113+
resourceType={resourceType}
114+
resourceId={resourceId}
115+
className="text-xs font-normal"
116+
shareIconClassName="w-2.5 h-2.5 stroke-2"
117+
checkIconClassName="w-2.5 h-2.5"
118+
/>
119+
<span className={'hidden sm:block'}>&bull;</span>
123120

124-
{isClearing && 'Processing...'}
125-
</button>
126-
)}
127-
128-
{isConfirming && (
129-
<span>
130-
Are you sure?{' '}
131-
<button
132-
onClick={clearProgress}
133-
className="ml-1 mr-1 text-red-500 underline hover:text-red-800"
134-
>
135-
Yes
136-
</button>{' '}
121+
{showClearButton && (
122+
<>
123+
{!isConfirming && (
137124
<button
138-
onClick={() => setIsConfirming(false)}
139-
className="text-red-500 underline hover:text-red-800"
125+
className="text-red-500 hover:text-red-800"
126+
onClick={() => setIsConfirming(true)}
127+
disabled={isClearing}
140128
>
141-
No
129+
{!isClearing && (
130+
<>
131+
Clear Progress <span>&times;</span>
132+
</>
133+
)}
134+
135+
{isClearing && 'Processing...'}
142136
</button>
143-
</span>
144-
)}
145-
</>
146-
)}
147-
</p>
137+
)}
138+
139+
{isConfirming && (
140+
<span>
141+
Are you sure?{' '}
142+
<button
143+
onClick={clearProgress}
144+
className="ml-1 mr-1 text-red-500 underline hover:text-red-800"
145+
>
146+
Yes
147+
</button>{' '}
148+
<button
149+
onClick={() => setIsConfirming(false)}
150+
className="text-red-500 underline hover:text-red-800"
151+
>
152+
No
153+
</button>
154+
</span>
155+
)}
156+
</>
157+
)}
158+
</div>
159+
</div>
148160
</div>
149161
);
150162
}

src/components/BestPracticeHint.astro

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import ResourceProgressStats from './ResourceProgressStats.astro';
33
export interface Props {
44
bestPracticeId: string;
55
}
6+
const { bestPracticeId } = Astro.props;
67
---
78

89
<div class='mt-4 sm:mt-7 border-0 sm:border rounded-md mb-0 sm:-mb-[65px]'>
9-
<ResourceProgressStats />
10+
<ResourceProgressStats resourceId={bestPracticeId} resourceType='best-practice' />
1011
</div>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { JSX } from "preact/jsx-runtime";
2+
3+
type ShareIconProps = JSX.SVGAttributes<SVGSVGElement>
4+
5+
export function ShareIcon(props: ShareIconProps) {
6+
return (
7+
<svg
8+
xmlns="http://www.w3.org/2000/svg"
9+
width="24"
10+
height="24"
11+
viewBox="0 0 24 24"
12+
fill="none"
13+
stroke="currentColor"
14+
stroke-width="2"
15+
stroke-linecap="round"
16+
stroke-linejoin="round"
17+
{...props}
18+
>
19+
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
20+
<polyline points="16 6 12 2 8 6" />
21+
<line x1="12" x2="12" y1="2" y2="15" />
22+
</svg>
23+
);
24+
}
Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
---
2+
import type { ResourceType } from '../lib/resource-progress';
23
import AstroIcon from './AstroIcon.astro';
4+
import { ProgressShareButton } from './UserProgress/ProgressShareButton';
35
export interface Props {
6+
resourceId: string;
7+
resourceType: ResourceType;
48
isSecondaryBanner?: boolean;
59
}
610
7-
const { isSecondaryBanner = false } = Astro.props;
11+
const { isSecondaryBanner = false, resourceId, resourceType } = Astro.props;
812
---
913

1014
<div
@@ -27,42 +31,62 @@ const { isSecondaryBanner = false } = Astro.props;
2731
<span data-progress-percentage>0</span>% Done
2832
</span>
2933

30-
<span><span data-progress-done>0</span> completed</span><span
31-
class='mx-1.5 text-gray-400'>&middot;</span
32-
>
33-
<span><span data-progress-learning>0</span> in progress</span><span
34-
class='mx-1.5 text-gray-400'>&middot;</span
35-
>
36-
<span><span data-progress-skipped>0</span> skipped</span><span
37-
class='mx-1.5 text-gray-400'>&middot;</span
38-
>
39-
<span><span data-progress-total>0</span> Total</span>
34+
<span class='itesm-center hidden md:flex'>
35+
<span><span data-progress-done>0</span> completed</span><span
36+
class='mx-1.5 text-gray-400'>&middot;</span
37+
>
38+
<span><span data-progress-learning>0</span> in progress</span><span
39+
class='mx-1.5 text-gray-400'>&middot;</span
40+
>
41+
<span><span data-progress-skipped>0</span> skipped</span><span
42+
class='mx-1.5 text-gray-400'>&middot;</span
43+
>
44+
<span><span data-progress-total>0</span> Total</span>
45+
</span>
46+
<span class='md:hidden'>
47+
<span data-progress-done>0</span> of <span data-progress-total>0</span> Done
48+
</span>
4049
</p>
4150

42-
<button
43-
data-popup='progress-help'
44-
class='flex items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black'
51+
<div
52+
class='flex items-center gap-3 opacity-0 transition-opacity duration-300'
4553
data-progress-nums
4654
>
47-
<AstroIcon icon='question' />
48-
Track Progress
49-
</button>
55+
<ProgressShareButton
56+
resourceId={resourceId}
57+
resourceType={resourceType}
58+
client:load
59+
/>
60+
<button
61+
data-popup='progress-help'
62+
class='flex items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black'
63+
data-progress-nums
64+
>
65+
<AstroIcon icon='question' />
66+
Track Progress
67+
</button>
68+
</div>
5069
</div>
5170

52-
<p
71+
<div
5372
data-progress-nums-container
54-
class='striped-loader relative -mb-2 flex items-center justify-between rounded-md border bg-white bg-white px-2 py-1.5 text-sm text-sm text-gray-700 sm:hidden'
73+
class='striped-loader relative -mb-2 flex items-center justify-between rounded-md border bg-white px-2 py-1.5 text-sm text-gray-700 sm:hidden'
5574
>
56-
<span data-progress-nums class='opacity-0 transition-opacity duration-300 text-gray-500'>
75+
<span
76+
data-progress-nums
77+
class='text-gray-500 opacity-0 transition-opacity duration-300'
78+
>
5779
<span data-progress-done>0</span> of <span data-progress-total>0</span> Done
5880
</span>
5981

60-
<button
61-
data-popup='progress-help'
62-
class='flex items-center gap-1 text-sm font-medium text-gray-500 opacity-0 transition-opacity hover:text-black'
82+
<div
83+
class='flex items-center gap-2 opacity-0 transition-opacity duration-300'
6384
data-progress-nums
6485
>
65-
<AstroIcon icon='question' />
66-
Track Progress
67-
</button>
68-
</p>
86+
<ProgressShareButton
87+
resourceId={resourceId}
88+
resourceType={resourceType}
89+
client:load
90+
/>
91+
</div>
92+
</div>

src/components/RoadmapHint.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,5 @@ const roadmapTitle =
4343
)
4444
}
4545

46-
<ResourceProgressStats isSecondaryBanner={hasTNSBanner} />
46+
<ResourceProgressStats isSecondaryBanner={hasTNSBanner} resourceId={roadmapId} resourceType='roadmap' />
4747
</div>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { useAuth } from '../../hooks/use-auth';
2+
import { useCopyText } from '../../hooks/use-copy-text';
3+
import type { ResourceType } from '../../lib/resource-progress';
4+
import { CheckIcon } from '../ReactIcons/CheckIcon';
5+
import { ShareIcon } from '../ReactIcons/ShareIcon';
6+
7+
type ProgressShareButtonProps = {
8+
resourceId: string;
9+
resourceType: ResourceType;
10+
className?: string;
11+
shareIconClassName?: string;
12+
checkIconClassName?: string;
13+
};
14+
export function ProgressShareButton(props: ProgressShareButtonProps) {
15+
const {
16+
resourceId,
17+
resourceType,
18+
className,
19+
shareIconClassName,
20+
checkIconClassName,
21+
} = props;
22+
23+
const user = useAuth();
24+
const { copyText, isCopied } = useCopyText();
25+
26+
function handleCopyLink() {
27+
const isDev = import.meta.env.DEV;
28+
const newUrl = new URL(
29+
isDev ? 'http://localhost:3000' : 'https://roadmap.sh'
30+
);
31+
32+
if (resourceType === 'roadmap') {
33+
newUrl.pathname = `/${resourceId}`;
34+
} else {
35+
newUrl.pathname = `/best-practices/${resourceId}`;
36+
}
37+
38+
newUrl.searchParams.set('s', user?.id || '');
39+
copyText(newUrl.toString());
40+
}
41+
42+
if (!user) {
43+
return null;
44+
}
45+
46+
return (
47+
<button
48+
className={`flex items-center gap-1 text-sm font-medium ${
49+
isCopied ? 'text-green-500' : 'text-gray-500 hover:text-black'
50+
} ${className}`}
51+
onClick={handleCopyLink}
52+
>
53+
{isCopied ? (
54+
<>
55+
<CheckIcon additionalClasses={`h-3.5 w-3.5 ${checkIconClassName}`} />{' '}
56+
Link Copied
57+
</>
58+
) : (
59+
<>
60+
<ShareIcon
61+
className={`h-3.5 w-3.5 stroke-[2.5px] ${shareIconClassName}`}
62+
/>{' '}
63+
Share Progress
64+
</>
65+
)}
66+
</button>
67+
);
68+
}

0 commit comments

Comments
 (0)