diff --git a/package.json b/package.json index d9caa3f22..9493f1d24 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "@astrojs/preact": "^2.1.0", "@astrojs/sitemap": "^1.2.2", "@astrojs/tailwind": "^3.1.1", + "@nanostores/preact": "^0.3.1", "astro": "2.2.0", "astro-compress": "^1.1.35", "jose": "^4.13.1", "js-cookie": "^3.0.1", + "nanostores": "^0.7.4", "node-html-parser": "^6.1.5", "npm-check-updates": "^16.10.7", "preact": "^10.13.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 998f9765c..84e9dd314 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,7 @@ specifiers: '@astrojs/preact': ^2.1.0 '@astrojs/sitemap': ^1.2.2 '@astrojs/tailwind': ^3.1.1 + '@nanostores/preact': ^0.3.1 '@playwright/test': ^1.32.2 '@tailwindcss/typography': ^0.5.9 '@types/js-cookie': ^3.0.3 @@ -14,6 +15,7 @@ specifiers: js-cookie: ^3.0.1 js-yaml: ^4.1.0 markdown-it: ^13.0.1 + nanostores: ^0.7.4 node-html-parser: ^6.1.5 npm-check-updates: ^16.10.7 openai: ^3.2.1 @@ -29,10 +31,12 @@ dependencies: '@astrojs/preact': 2.1.0_preact@10.13.2 '@astrojs/sitemap': 1.2.2 '@astrojs/tailwind': 3.1.1_nfbnt7ricougkvppdwsf5maxzu + '@nanostores/preact': 0.3.1_ntvucyavaortwycasiweu74jd4 astro: 2.2.0 astro-compress: 1.1.35 jose: 4.13.1 js-cookie: 3.0.1 + nanostores: 0.7.4 node-html-parser: 6.1.5 npm-check-updates: 16.10.7 preact: 10.13.2 @@ -673,6 +677,17 @@ packages: resolution: {integrity: sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==} dev: false + /@nanostores/preact/0.3.1_ntvucyavaortwycasiweu74jd4: + resolution: {integrity: sha512-D5lC1tNhwlVURCFJUTlnRkQ3LRNt6Sc2XHByBwgHPaKLBUPJxAaNAL7kz2dAEhge1fk9dNXAexCdepwiIFodOQ==} + engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} + peerDependencies: + nanostores: ^0.7.0 + preact: '>=10.0.0' + dependencies: + nanostores: 0.7.4 + preact: 10.13.2 + dev: false + /@nodelib/fs.scandir/2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -3696,6 +3711,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + /nanostores/0.7.4: + resolution: {integrity: sha512-MBeUVt7NBcXqh7AGT+KSr3O0X/995CZsvcP2QEMP+PXFwb07qv3Vjyq+EX0yS8f12Vv3Tn2g/BvK/OZoMhJlOQ==} + engines: {node: ^14.0.0 || ^16.0.0 || >=18.0.0} + dev: false + /napi-build-utils/1.0.2: resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} dev: false diff --git a/src/components/FrameRenderer/FrameRenderer.astro b/src/components/FrameRenderer/FrameRenderer.astro index d050be9c2..153b5efb9 100644 --- a/src/components/FrameRenderer/FrameRenderer.astro +++ b/src/components/FrameRenderer/FrameRenderer.astro @@ -17,7 +17,9 @@ const { resourceId, resourceType, jsonUrl, dimensions = null } = Astro.props;
+ {/* Tailwind based spinner for full page */} +
+
+ Loading +

+ {$pageLoadingMessage} + ... +

+
+
+
+ ); +} diff --git a/src/components/TopicDetail/TopicDetail.tsx b/src/components/TopicDetail/TopicDetail.tsx index 6156a191a..131c8cd88 100644 --- a/src/components/TopicDetail/TopicDetail.tsx +++ b/src/components/TopicDetail/TopicDetail.tsx @@ -15,6 +15,8 @@ import { toggleMarkTopicDone as toggleMarkTopicDoneApi, } from '../../lib/resource-progress'; import { useKeydown } from '../../hooks/use-keydown'; +import { useToggleTopic } from '../../hooks/use-toggle-topic'; +import { pageLoadingMessage } from '../../stores/page'; export function TopicDetail() { const [isActive, setIsActive] = useState(false); @@ -33,6 +35,20 @@ export function TopicDetail() { const [resourceId, setResourceId] = useState(''); const [resourceType, setResourceType] = useState('roadmap'); + const showLoginPopup = () => { + const popupEl = document.querySelector(`#login-popup`); + if (!popupEl) { + return; + } + + popupEl.classList.remove('hidden'); + popupEl.classList.add('flex'); + const focusEl = popupEl.querySelector('[autofocus]'); + if (focusEl) { + focusEl.focus(); + } + }; + const toggleMarkTopicDone = (isDone: boolean) => { setIsUpdatingProgress(true); toggleMarkTopicDoneApi({ topicId, resourceId, resourceType }, isDone) @@ -74,6 +90,39 @@ export function TopicDetail() { setIsActive(false); }); + // Toggle topic is available even if the component UI is not active + // This is used on the best practice screen where we have the checkboxes + // to mark the topic as done/undone. + useToggleTopic(({ topicId, resourceType, resourceId }) => { + if (isGuest) { + showLoginPopup(); + return; + } + + pageLoadingMessage.set('Updating'); + + // Toggle the topic status + isTopicDone({ topicId, resourceId, resourceType }) + .then((oldIsDone) => { + return toggleMarkTopicDoneApi( + { + topicId, + resourceId, + resourceType, + }, + !oldIsDone + ); + }) + .then((newIsDone) => renderTopicProgress(topicId, newIsDone)) + .catch((err) => { + alert(err.message); + console.error(err); + }) + .finally(() => { + pageLoadingMessage.set(''); + }); + }); + // Load the topic detail when the topic detail is active useLoadTopic(({ topicId, resourceType, resourceId }) => { setIsLoading(true); diff --git a/src/hooks/use-load-topic.ts b/src/hooks/use-load-topic.ts index f9603bf53..3b3747e00 100644 --- a/src/hooks/use-load-topic.ts +++ b/src/hooks/use-load-topic.ts @@ -1,5 +1,5 @@ import { useEffect } from 'preact/hooks'; -import type {ResourceType} from "../components/TopicDetail/TopicDetail"; +import type { ResourceType } from '../lib/resource-progress'; type CallbackType = (data: { resourceType: ResourceType; diff --git a/src/hooks/use-toggle-topic.ts b/src/hooks/use-toggle-topic.ts new file mode 100644 index 000000000..70324a277 --- /dev/null +++ b/src/hooks/use-toggle-topic.ts @@ -0,0 +1,30 @@ +import { useEffect } from 'preact/hooks'; +import type { ResourceType } from '../lib/resource-progress'; + +type CallbackType = (data: { + resourceType: ResourceType; + resourceId: string; + topicId: string; +}) => void; + +export function useToggleTopic(callback: CallbackType) { + useEffect(() => { + function handleToggleTopic(e: any) { + const { resourceType, resourceId, topicId } = e.detail; + + callback({ + resourceType, + resourceId, + topicId, + }); + } + + window.addEventListener(`best-practice.topic.toggle`, handleToggleTopic); + return () => { + window.removeEventListener( + `best-practice.topic.toggle`, + handleToggleTopic + ); + }; + }, []); +} diff --git a/src/layouts/BaseLayout.astro b/src/layouts/BaseLayout.astro index 7fd1bd796..9d3c94686 100644 --- a/src/layouts/BaseLayout.astro +++ b/src/layouts/BaseLayout.astro @@ -2,6 +2,7 @@ import Analytics from '../components/Analytics/Analytics.astro'; import Authenticator from '../components/Authenticator/Authenticator.astro'; import Footer from '../components/Footer.astro'; +import { PageProgress } from '../components/PageProgress'; import Navigation from '../components/Navigation/Navigation.astro'; import OpenSourceBanner from '../components/OpenSourceBanner.astro'; import type { SponsorType } from '../components/Sponsor/Sponsor.astro'; @@ -137,9 +138,10 @@ const commitUrl = `https://github.com/kamranahmedse/developer-roadmap/commit/${