Allow marking roadmaps and best practices as favorites (#4087)
* chore: favorite icon * fix: hero progress mark favorit * chore: mark favorite * fix: mouse overflow * fix: popup redirect * Update favorites on homepage * Refactor favorite logic * Change icon location --------- Co-authored-by: Kamran Ahmed <kamranahmed.se@gmail.com>pull/4095/head
parent
4aca01a98d
commit
afe718ee09
12 changed files with 228 additions and 25 deletions
@ -0,0 +1,45 @@ |
|||||||
|
type FavoriteIconProps = { |
||||||
|
isFavorite?: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
export function FavoriteIcon(props: FavoriteIconProps) { |
||||||
|
const { isFavorite } = props; |
||||||
|
|
||||||
|
if (!isFavorite) { |
||||||
|
return ( |
||||||
|
<svg |
||||||
|
width="8" |
||||||
|
height="10" |
||||||
|
viewBox="0 0 8 10" |
||||||
|
fill="none" |
||||||
|
className="h-3.5 w-3.5" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<path |
||||||
|
fill-rule="evenodd" |
||||||
|
clip-rule="evenodd" |
||||||
|
d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5ZM5.93682 1.25C6.42732 1.25 6.83382 1.632 6.86382 2.122L7.24932 8.506C7.25216 8.55018 7.24229 8.59425 7.22089 8.63301C7.19949 8.67176 7.16745 8.70359 7.12854 8.72472C7.08964 8.74585 7.0455 8.75542 7.00134 8.75228C6.95718 8.74914 6.91484 8.73343 6.87932 8.707L4.27582 6.783C4.19591 6.72397 4.09917 6.69211 3.99982 6.69211C3.90047 6.69211 3.80373 6.72397 3.72382 6.783L1.11982 8.707C1.0843 8.73343 1.04196 8.74914 0.9978 8.75228C0.953639 8.75542 0.909502 8.74585 0.8706 8.72472C0.831697 8.70359 0.799653 8.67176 0.778252 8.63301C0.756851 8.59425 0.746986 8.55018 0.749822 8.506L1.13632 2.122C1.16632 1.632 1.57232 1.25 2.06282 1.25H5.93682Z" |
||||||
|
fill="currentColor" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<svg |
||||||
|
width="8" |
||||||
|
height="10" |
||||||
|
viewBox="0 0 8 10" |
||||||
|
className="h-3.5 w-3.5" |
||||||
|
fill="none" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<path |
||||||
|
fill-rule="evenodd" |
||||||
|
clip-rule="evenodd" |
||||||
|
d="M5.93682 0.5H2.06282C1.63546 0.500094 1.22423 0.663195 0.912987 0.956045C0.601741 1.2489 0.413919 1.64944 0.387822 2.076L0.00182198 8.461C-0.012178 8.6905 0.0548218 8.9185 0.191822 9.104L0.242322 9.1665C0.575322 9.5485 1.15132 9.6165 1.56582 9.31L3.99982 7.5115L6.43382 9.31C6.58413 9.42115 6.76305 9.48708 6.94954 9.50006C7.13603 9.51303 7.32235 9.4725 7.4866 9.38323C7.65085 9.29397 7.78621 9.15967 7.87677 8.99613C7.96733 8.83258 8.00932 8.64659 7.99782 8.46L7.61232 2.0765C7.58622 1.64981 7.39835 1.24914 7.08701 0.956192C6.77567 0.663248 6.36431 0.500094 5.93682 0.5Z" |
||||||
|
fill="currentColor" |
||||||
|
/> |
||||||
|
</svg> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,93 @@ |
|||||||
|
import { useEffect, useState } from 'preact/hooks'; |
||||||
|
import { httpPatch } from '../../lib/http'; |
||||||
|
import type { ResourceType } from '../../lib/resource-progress'; |
||||||
|
import { isLoggedIn } from '../../lib/jwt'; |
||||||
|
import { showLoginPopup } from '../../lib/popup'; |
||||||
|
import { FavoriteIcon } from './FavoriteIcon'; |
||||||
|
import { Spinner } from '../ReactIcons/Spinner'; |
||||||
|
|
||||||
|
type MarkFavoriteType = { |
||||||
|
resourceType: ResourceType; |
||||||
|
resourceId: string; |
||||||
|
favorite?: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
export function MarkFavorite({ resourceId, resourceType, favorite }: MarkFavoriteType) { |
||||||
|
const [isLoading, setIsLoading] = useState(false); |
||||||
|
const [isFavorite, setIsFavorite] = useState(favorite ?? false); |
||||||
|
|
||||||
|
async function toggleFavoriteHandler(e: Event) { |
||||||
|
e.preventDefault(); |
||||||
|
if (!isLoggedIn()) { |
||||||
|
showLoginPopup(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (isLoading) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
setIsLoading(true); |
||||||
|
|
||||||
|
const { error } = await httpPatch<{ status: 'ok' }>( |
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-mark-favorite`, |
||||||
|
{ |
||||||
|
resourceType, |
||||||
|
resourceId, |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
if (error) { |
||||||
|
setIsLoading(false); |
||||||
|
return alert('Failed to update favorite status'); |
||||||
|
} |
||||||
|
|
||||||
|
// Dispatching an event instead of setting the state because
|
||||||
|
// MarkFavorite component is used in the HeroSection as well
|
||||||
|
// as featured items section. We will let the custom event
|
||||||
|
// listener set the update `useEffect`
|
||||||
|
window.dispatchEvent( |
||||||
|
new CustomEvent('mark-favorite', { |
||||||
|
detail: { |
||||||
|
resourceId, |
||||||
|
resourceType, |
||||||
|
isFavorite: !isFavorite, |
||||||
|
}, |
||||||
|
}) |
||||||
|
); |
||||||
|
window.dispatchEvent(new CustomEvent('refresh-favorites', {})); |
||||||
|
|
||||||
|
setIsFavorite(!isFavorite); |
||||||
|
setIsLoading(false); |
||||||
|
} |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const listener = (e: Event) => { |
||||||
|
const { |
||||||
|
resourceId: id, |
||||||
|
resourceType: type, |
||||||
|
isFavorite: fav, |
||||||
|
} = (e as CustomEvent).detail; |
||||||
|
if (id === resourceId && type === resourceType) { |
||||||
|
setIsFavorite(fav); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
window.addEventListener('mark-favorite', listener); |
||||||
|
return () => { |
||||||
|
window.removeEventListener('mark-favorite', listener); |
||||||
|
}; |
||||||
|
}, []); |
||||||
|
|
||||||
|
return ( |
||||||
|
<button |
||||||
|
onClick={toggleFavoriteHandler} |
||||||
|
tabIndex={-1} |
||||||
|
className={`${ |
||||||
|
isFavorite ? '' : 'opacity-30 hover:opacity-100' |
||||||
|
} absolute right-1.5 top-1.5 z-30 focus:outline-0`}
|
||||||
|
> |
||||||
|
{isLoading ? <Spinner /> : <FavoriteIcon isFavorite={isFavorite} />} |
||||||
|
</button> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
export function Spinner() { |
||||||
|
return ( |
||||||
|
<svg |
||||||
|
className="h-3.5 w-3.5 animate-spin" |
||||||
|
viewBox="0 0 93 93" |
||||||
|
fill="none" |
||||||
|
xmlns="http://www.w3.org/2000/svg" |
||||||
|
> |
||||||
|
<path |
||||||
|
fill-rule="evenodd" |
||||||
|
clip-rule="evenodd" |
||||||
|
d="M46.5 93C72.1812 93 93 72.1812 93 46.5C93 20.8188 72.1812 0 46.5 0C20.8188 0 0 20.8188 0 46.5C0 72.1812 20.8188 93 46.5 93ZM46.5 77C63.3447 77 77 63.3447 77 46.5C77 29.6553 63.3447 16 46.5 16C29.6553 16 16 29.6553 16 46.5C16 63.3447 29.6553 77 46.5 77Z" |
||||||
|
style="fill: #404040;" |
||||||
|
></path> |
||||||
|
<path |
||||||
|
d="M84.9746 49.5667C89.3257 49.9135 93.2042 46.6479 92.81 42.3008C92.3588 37.3251 91.1071 32.437 89.0872 27.8298C86.0053 20.7998 81.2311 14.6422 75.1905 9.90623C69.15 5.17027 62.031 2.00329 54.4687 0.687889C49.5126 -0.174203 44.467 -0.223422 39.5274 0.525737C35.2118 1.18024 32.966 5.72596 34.3411 9.86865V9.86865C35.7161 14.0113 40.2118 16.1424 44.5681 15.8677C46.9635 15.7166 49.3773 15.8465 51.7599 16.2609C56.7515 17.1291 61.4505 19.2196 65.4377 22.3456C69.4249 25.4717 72.5762 29.5362 74.6105 34.1764C75.5815 36.3912 76.2835 38.7044 76.7084 41.0666C77.4811 45.3626 80.6234 49.2199 84.9746 49.5667V49.5667Z" |
||||||
|
style="fill: #94a3b8;" |
||||||
|
></path> |
||||||
|
</svg> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue