Add page wide spinner

pull/3813/head
Kamran Ahmed 2 years ago
parent 3825968106
commit ae72680a5b
  1. 2
      package.json
  2. 20
      pnpm-lock.yaml
  3. 4
      src/components/FrameRenderer/FrameRenderer.astro
  4. 29
      src/components/PageProgress.tsx
  5. 49
      src/components/TopicDetail/TopicDetail.tsx
  6. 2
      src/hooks/use-load-topic.ts
  7. 30
      src/hooks/use-toggle-topic.ts
  8. 2
      src/layouts/BaseLayout.astro
  9. 4
      src/lib/resource-progress.ts
  10. 1
      src/pages/best-practices/[bestPracticeId]/index.astro
  11. 3
      src/stores/page.ts

@ -23,10 +23,12 @@
"@astrojs/preact": "^2.1.0", "@astrojs/preact": "^2.1.0",
"@astrojs/sitemap": "^1.2.2", "@astrojs/sitemap": "^1.2.2",
"@astrojs/tailwind": "^3.1.1", "@astrojs/tailwind": "^3.1.1",
"@nanostores/preact": "^0.3.1",
"astro": "2.2.0", "astro": "2.2.0",
"astro-compress": "^1.1.35", "astro-compress": "^1.1.35",
"jose": "^4.13.1", "jose": "^4.13.1",
"js-cookie": "^3.0.1", "js-cookie": "^3.0.1",
"nanostores": "^0.7.4",
"node-html-parser": "^6.1.5", "node-html-parser": "^6.1.5",
"npm-check-updates": "^16.10.7", "npm-check-updates": "^16.10.7",
"preact": "^10.13.2", "preact": "^10.13.2",

@ -4,6 +4,7 @@ specifiers:
'@astrojs/preact': ^2.1.0 '@astrojs/preact': ^2.1.0
'@astrojs/sitemap': ^1.2.2 '@astrojs/sitemap': ^1.2.2
'@astrojs/tailwind': ^3.1.1 '@astrojs/tailwind': ^3.1.1
'@nanostores/preact': ^0.3.1
'@playwright/test': ^1.32.2 '@playwright/test': ^1.32.2
'@tailwindcss/typography': ^0.5.9 '@tailwindcss/typography': ^0.5.9
'@types/js-cookie': ^3.0.3 '@types/js-cookie': ^3.0.3
@ -14,6 +15,7 @@ specifiers:
js-cookie: ^3.0.1 js-cookie: ^3.0.1
js-yaml: ^4.1.0 js-yaml: ^4.1.0
markdown-it: ^13.0.1 markdown-it: ^13.0.1
nanostores: ^0.7.4
node-html-parser: ^6.1.5 node-html-parser: ^6.1.5
npm-check-updates: ^16.10.7 npm-check-updates: ^16.10.7
openai: ^3.2.1 openai: ^3.2.1
@ -29,10 +31,12 @@ dependencies:
'@astrojs/preact': 2.1.0_preact@10.13.2 '@astrojs/preact': 2.1.0_preact@10.13.2
'@astrojs/sitemap': 1.2.2 '@astrojs/sitemap': 1.2.2
'@astrojs/tailwind': 3.1.1_nfbnt7ricougkvppdwsf5maxzu '@astrojs/tailwind': 3.1.1_nfbnt7ricougkvppdwsf5maxzu
'@nanostores/preact': 0.3.1_ntvucyavaortwycasiweu74jd4
astro: 2.2.0 astro: 2.2.0
astro-compress: 1.1.35 astro-compress: 1.1.35
jose: 4.13.1 jose: 4.13.1
js-cookie: 3.0.1 js-cookie: 3.0.1
nanostores: 0.7.4
node-html-parser: 6.1.5 node-html-parser: 6.1.5
npm-check-updates: 16.10.7 npm-check-updates: 16.10.7
preact: 10.13.2 preact: 10.13.2
@ -673,6 +677,17 @@ packages:
resolution: {integrity: sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==} resolution: {integrity: sha512-4/RWEeXDO6bocPONheFe6gX/oQdP/bEpv0oL4HqjPP5DCenBSt0mHgahppY49N0CpsaqffdwPq+TlX9CYOq2Dw==}
dev: false 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: /@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@ -3696,6 +3711,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true 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: /napi-build-utils/1.0.2:
resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==}
dev: false dev: false

@ -17,7 +17,9 @@ const { resourceId, resourceType, jsonUrl, dimensions = null } = Astro.props;
<div <div
id='resource-svg-wrap' id='resource-svg-wrap'
style={dimensions ? `--aspect-ratio:${dimensions.width}/${dimensions.height}` : null} style={dimensions
? `--aspect-ratio:${dimensions.width}/${dimensions.height}`
: null}
data-resource-type={resourceType} data-resource-type={resourceType}
data-resource-id={resourceId} data-resource-id={resourceId}
data-json-url={jsonUrl} data-json-url={jsonUrl}

@ -0,0 +1,29 @@
import { useStore } from '@nanostores/preact';
import { pageLoadingMessage } from '../stores/page';
import SpinnerIcon from '../icons/spinner.svg';
export function PageProgress() {
const $pageLoadingMessage = useStore(pageLoadingMessage);
if (!$pageLoadingMessage) {
return null;
}
return (
<div>
{/* Tailwind based spinner for full page */}
<div className="fixed left-0 top-0 z-50 flex h-full w-full items-center justify-center bg-white bg-opacity-75">
<div class="flex items-center justify-center rounded-md border bg-white px-4 py-2 ">
<img
src={SpinnerIcon}
alt="Loading"
className="h-4 w-4 animate-spin fill-blue-600 text-gray-200 sm:h-4 sm:w-4"
/>
<h1 className="ml-2">
{$pageLoadingMessage}
<span className="animate-pulse">...</span>
</h1>
</div>
</div>
</div>
);
}

@ -15,6 +15,8 @@ import {
toggleMarkTopicDone as toggleMarkTopicDoneApi, toggleMarkTopicDone as toggleMarkTopicDoneApi,
} from '../../lib/resource-progress'; } from '../../lib/resource-progress';
import { useKeydown } from '../../hooks/use-keydown'; import { useKeydown } from '../../hooks/use-keydown';
import { useToggleTopic } from '../../hooks/use-toggle-topic';
import { pageLoadingMessage } from '../../stores/page';
export function TopicDetail() { export function TopicDetail() {
const [isActive, setIsActive] = useState(false); const [isActive, setIsActive] = useState(false);
@ -33,6 +35,20 @@ export function TopicDetail() {
const [resourceId, setResourceId] = useState(''); const [resourceId, setResourceId] = useState('');
const [resourceType, setResourceType] = useState<ResourceType>('roadmap'); const [resourceType, setResourceType] = useState<ResourceType>('roadmap');
const showLoginPopup = () => {
const popupEl = document.querySelector(`#login-popup`);
if (!popupEl) {
return;
}
popupEl.classList.remove('hidden');
popupEl.classList.add('flex');
const focusEl = popupEl.querySelector<HTMLElement>('[autofocus]');
if (focusEl) {
focusEl.focus();
}
};
const toggleMarkTopicDone = (isDone: boolean) => { const toggleMarkTopicDone = (isDone: boolean) => {
setIsUpdatingProgress(true); setIsUpdatingProgress(true);
toggleMarkTopicDoneApi({ topicId, resourceId, resourceType }, isDone) toggleMarkTopicDoneApi({ topicId, resourceId, resourceType }, isDone)
@ -74,6 +90,39 @@ export function TopicDetail() {
setIsActive(false); 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 // Load the topic detail when the topic detail is active
useLoadTopic(({ topicId, resourceType, resourceId }) => { useLoadTopic(({ topicId, resourceType, resourceId }) => {
setIsLoading(true); setIsLoading(true);

@ -1,5 +1,5 @@
import { useEffect } from 'preact/hooks'; import { useEffect } from 'preact/hooks';
import type {ResourceType} from "../components/TopicDetail/TopicDetail"; import type { ResourceType } from '../lib/resource-progress';
type CallbackType = (data: { type CallbackType = (data: {
resourceType: ResourceType; resourceType: ResourceType;

@ -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
);
};
}, []);
}

@ -2,6 +2,7 @@
import Analytics from '../components/Analytics/Analytics.astro'; import Analytics from '../components/Analytics/Analytics.astro';
import Authenticator from '../components/Authenticator/Authenticator.astro'; import Authenticator from '../components/Authenticator/Authenticator.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import { PageProgress } from '../components/PageProgress';
import Navigation from '../components/Navigation/Navigation.astro'; import Navigation from '../components/Navigation/Navigation.astro';
import OpenSourceBanner from '../components/OpenSourceBanner.astro'; import OpenSourceBanner from '../components/OpenSourceBanner.astro';
import type { SponsorType } from '../components/Sponsor/Sponsor.astro'; import type { SponsorType } from '../components/Sponsor/Sponsor.astro';
@ -140,6 +141,7 @@ const commitUrl = `https://github.com/kamranahmedse/developer-roadmap/commit/${
<Analytics /> <Analytics />
<Authenticator /> <Authenticator />
<PageProgress client:load />
<slot name='after-footer' /> <slot name='after-footer' />
</body> </body>

@ -25,7 +25,7 @@ export async function isTopicDone(topic: TopicMeta): Promise<boolean> {
export async function toggleMarkTopicDone( export async function toggleMarkTopicDone(
topic: TopicMeta, topic: TopicMeta,
isDone: boolean isDone: boolean
): Promise<void> { ): Promise<boolean> {
const { topicId, resourceType, resourceId } = topic; const { topicId, resourceType, resourceId } = topic;
const { response, error } = await httpPatch<{ done: string[] }>( const { response, error } = await httpPatch<{ done: string[] }>(
@ -43,6 +43,8 @@ export async function toggleMarkTopicDone(
} }
setResourceProgress(resourceType, resourceId, response.done); setResourceProgress(resourceType, resourceId, response.done);
return isDone;
} }
export async function getResourceProgress( export async function getResourceProgress(

@ -84,6 +84,7 @@ const contentContributionLink = `https://github.com/kamranahmedse/developer-road
description={bestPracticeData.briefDescription} description={bestPracticeData.briefDescription}
pageUrl={`https://roadmap.sh/best-practices/${bestPracticeId}`} pageUrl={`https://roadmap.sh/best-practices/${bestPracticeId}`}
/> />
<TopicDetail client:load /> <TopicDetail client:load />
<FrameRenderer <FrameRenderer

@ -0,0 +1,3 @@
import { atom } from 'nanostores';
export const pageLoadingMessage = atom('');
Loading…
Cancel
Save