fix: download image button

ai/roadmap
Arik Chakma 11 months ago committed by Kamran Ahmed
parent 610aae2a07
commit 377d5d763e
  1. 88
      src/components/GenerateRoadmap/GenerateRoadmap.tsx
  2. 20
      src/components/GenerateRoadmap/RoadmapSearch.tsx
  3. 32
      src/helper/download-image.ts
  4. 2
      src/pages/ai/index.astro

@ -1,19 +1,24 @@
import { useEffect, useRef, useState, type FormEvent } from 'react'; import { useEffect, useRef, useState, type FormEvent } from 'react';
import fp from '@fingerprintjs/fingerprintjs';
import './GenerateRoadmap.css'; import './GenerateRoadmap.css';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator'; import { generateAIRoadmapFromText } from '../../../editor/utils/roadmap-generator';
import { renderFlowJSON } from '../../../editor/renderer/renderer'; import { renderFlowJSON } from '../../../editor/renderer/renderer';
import { replaceChildren } from '../../lib/dom'; import { replaceChildren } from '../../lib/dom';
import { readAIRoadmapStream } from '../../helper/read-stream'; import { readAIRoadmapStream } from '../../helper/read-stream';
import { removeAuthToken } from '../../lib/jwt'; import { isLoggedIn, removeAuthToken } from '../../lib/jwt';
import { RoadmapSearch } from './RoadmapSearch.tsx'; import { RoadmapSearch } from './RoadmapSearch.tsx';
import { Spinner } from '../ReactIcons/Spinner.tsx'; import { Spinner } from '../ReactIcons/Spinner.tsx';
import { Download, PenSquare, Wand } from 'lucide-react'; import { Download, PenSquare, Wand } from 'lucide-react';
import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx'; import { ShareRoadmapButton } from '../ShareRoadmapButton.tsx';
import { httpGet, httpPost } from '../../lib/http.ts'; import { httpGet, httpPost } from '../../lib/http.ts';
import { pageProgressMessage } from '../../stores/page.ts'; import { pageProgressMessage } from '../../stores/page.ts';
import { getUrlParams, setUrlParams } from '../../lib/browser.ts'; import {
deleteUrlParam,
getUrlParams,
setUrlParams,
} from '../../lib/browser.ts';
import { downloadGeneratedRoadmapImage } from '../../helper/download-image.ts';
import { showLoginPopup } from '../../lib/popup.ts';
const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@'); const ROADMAP_ID_REGEX = new RegExp('@ROADMAPID:(\\w+)@');
@ -31,6 +36,14 @@ export function GenerateRoadmap() {
const [roadmapLimit, setRoadmapLimit] = useState(0); const [roadmapLimit, setRoadmapLimit] = useState(0);
const [roadmapLimitUsed, setRoadmapLimitUsed] = useState(0); const [roadmapLimitUsed, setRoadmapLimitUsed] = useState(0);
const renderRoadmap = async (roadmap: string) => {
const { nodes, edges } = generateAIRoadmapFromText(roadmap);
const svg = await renderFlowJSON({ nodes, edges });
if (roadmapContainerRef?.current) {
replaceChildren(roadmapContainerRef?.current, svg);
}
};
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
setIsLoading(true); setIsLoading(true);
@ -48,11 +61,7 @@ export function GenerateRoadmap() {
return; return;
} }
const fingerprintPromise = await fp.load({ deleteUrlParam('id');
debug: import.meta.env.DEV,
});
const fingerprint = await fingerprintPromise.get();
const response = await fetch( const response = await fetch(
`${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`, `${import.meta.env.PUBLIC_API_URL}/v1-generate-ai-roadmap`,
@ -60,7 +69,6 @@ export function GenerateRoadmap() {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
fp: fingerprint.visitorId,
}, },
credentials: 'include', credentials: 'include',
body: JSON.stringify({ topic: roadmapTopic }), body: JSON.stringify({ topic: roadmapTopic }),
@ -99,11 +107,7 @@ export function GenerateRoadmap() {
result = result.replace(ROADMAP_ID_REGEX, ''); result = result.replace(ROADMAP_ID_REGEX, '');
} }
const { nodes, edges } = generateAIRoadmapFromText(result); await renderRoadmap(result);
const svg = await renderFlowJSON({ nodes, edges });
if (roadmapContainerRef?.current) {
replaceChildren(roadmapContainerRef?.current, svg);
}
}, },
onStreamEnd: async (result) => { onStreamEnd: async (result) => {
result = result.replace(ROADMAP_ID_REGEX, ''); result = result.replace(ROADMAP_ID_REGEX, '');
@ -116,6 +120,11 @@ export function GenerateRoadmap() {
}; };
const editGeneratedRoadmap = async () => { const editGeneratedRoadmap = async () => {
if (!isLoggedIn()) {
showLoginPopup();
return;
}
pageProgressMessage.set('Redirecting to Editor'); pageProgressMessage.set('Redirecting to Editor');
const { nodes, edges } = generateAIRoadmapFromText(generatedRoadmap); const { nodes, edges } = generateAIRoadmapFromText(generatedRoadmap);
@ -158,30 +167,8 @@ export function GenerateRoadmap() {
return; return;
} }
// Append a watermark to the bottom right of the image
const watermark = document.createElement('div');
watermark.className = 'flex justify-end absolute bottom-4 right-4 gap-2';
watermark.innerHTML = `
<span
class='rounded-md bg-black py-2 px-2 text-white'
>
roadmap.sh
</span>
`;
node.insertAdjacentElement('afterbegin', watermark);
try { try {
const domtoimage = (await import('dom-to-image')).default; await downloadGeneratedRoadmapImage(roadmapTopic, node);
const dataUrl = await domtoimage.toJpeg(node, {
bgcolor: 'white',
quality: 1,
});
node?.removeChild(watermark);
const link = document.createElement('a');
link.download = `${roadmapTopic}-roadmap.jpg`;
link.href = dataUrl;
link.click();
pageProgressMessage.set(''); pageProgressMessage.set('');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -210,7 +197,6 @@ export function GenerateRoadmap() {
}; };
const loadAIRoadmap = async (roadmapId: string) => { const loadAIRoadmap = async (roadmapId: string) => {
setIsLoading(true);
pageProgressMessage.set('Loading Roadmap'); pageProgressMessage.set('Loading Roadmap');
const { response, error } = await httpGet<{ const { response, error } = await httpGet<{
@ -225,12 +211,7 @@ export function GenerateRoadmap() {
} }
const { topic, data } = response; const { topic, data } = response;
await renderRoadmap(data);
const { nodes, edges } = generateAIRoadmapFromText(data);
const svg = await renderFlowJSON({ nodes, edges });
if (roadmapContainerRef?.current) {
replaceChildren(roadmapContainerRef?.current, svg);
}
setRoadmapTopic(topic); setRoadmapTopic(topic);
setGeneratedRoadmap(data); setGeneratedRoadmap(data);
@ -246,8 +227,7 @@ export function GenerateRoadmap() {
} }
setHasSubmitted(true); setHasSubmitted(true);
loadAIRoadmap(roadmapId).then(() => { loadAIRoadmap(roadmapId).finally(() => {
setIsLoading(false);
pageProgressMessage.set(''); pageProgressMessage.set('');
}); });
}, [roadmapId]); }, [roadmapId]);
@ -279,10 +259,18 @@ export function GenerateRoadmap() {
<div className="flex max-w-[600px] flex-grow flex-col items-center"> <div className="flex max-w-[600px] flex-grow flex-col items-center">
<div className="mt-2 flex w-full items-center justify-between text-sm"> <div className="mt-2 flex w-full items-center justify-between text-sm">
<span className="text-gray-800"> <span className="text-gray-800">
{roadmapLimitUsed} of {roadmapLimit} roadmaps generated{' '} {roadmapLimitUsed} of {roadmapLimit} roadmaps generated
<button className="font-medium text-black underline underline-offset-2"> {!isLoggedIn() && (
Login to increase your limit <>
</button> {' '}
<button
className="font-medium text-black underline underline-offset-2"
onClick={showLoginPopup}
>
Login to increase your limit
</button>
</>
)}
</span> </span>
</div> </div>
<form <form

@ -1,5 +1,7 @@
import { Wand } from 'lucide-react'; import { Wand } from 'lucide-react';
import type { FormEvent } from 'react'; import type { FormEvent } from 'react';
import { isLoggedIn } from '../../lib/jwt';
import { showLoginPopup } from '../../lib/popup';
type RoadmapSearchProps = { type RoadmapSearchProps = {
roadmapTopic: string; roadmapTopic: string;
@ -49,12 +51,20 @@ export function RoadmapSearch(props: RoadmapSearchProps) {
<p className="text-gray-500"> <p className="text-gray-500">
You have generated{' '} You have generated{' '}
<span className="text-gray-800"> <span className="text-gray-800">
{limitUsed} of ${limit} {limitUsed} of {limit}
</span>{' '} </span>{' '}
roadmaps today.{' '} roadmaps today.
<button className="font-semibold text-black underline underline-offset-2"> {!isLoggedIn && (
Log in to increase your limit <>
</button> {' '}
<button
className="font-semibold text-black underline underline-offset-2"
onClick={showLoginPopup}
>
Log in to increase your limit
</button>
</>
)}
</p> </p>
</div> </div>
</div> </div>

@ -34,3 +34,35 @@ export async function downloadImage({
alert('Error downloading image'); alert('Error downloading image');
} }
} }
export async function downloadGeneratedRoadmapImage(
name: string,
node: HTMLElement,
) {
// Append a watermark to the bottom right of the image
const watermark = document.createElement('div');
watermark.className = 'flex justify-end absolute top-4 right-4 gap-2';
watermark.innerHTML = `
<span
class='rounded-md bg-black py-2 px-2 text-white'
>
roadmap.sh
</span>
`;
node.insertAdjacentElement('afterbegin', watermark);
const domtoimage = (await import('dom-to-image')).default;
if (!domtoimage) {
throw new Error('Unable to download image');
}
const dataUrl = await domtoimage.toJpeg(node, {
bgcolor: 'white',
quality: 1,
});
node?.removeChild(watermark);
const link = document.createElement('a');
link.download = `${name}-roadmap.jpg`;
link.href = dataUrl;
link.click();
}

@ -1,8 +1,10 @@
--- ---
import LoginPopup from '../../components/AuthenticationFlow/LoginPopup.astro';
import { GenerateRoadmap } from '../../components/GenerateRoadmap/GenerateRoadmap'; import { GenerateRoadmap } from '../../components/GenerateRoadmap/GenerateRoadmap';
import AccountLayout from '../../layouts/AccountLayout.astro'; import AccountLayout from '../../layouts/AccountLayout.astro';
--- ---
<AccountLayout title='Roadmap AI'> <AccountLayout title='Roadmap AI'>
<GenerateRoadmap client:load /> <GenerateRoadmap client:load />
<LoginPopup />
</AccountLayout> </AccountLayout>

Loading…
Cancel
Save