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 */}
+
+
+
+
+ {$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/${
{sponsor && }
-
+
+