Skip to content

Commit

Permalink
Merge token renewal strategy (#3)
Browse files Browse the repository at this point in the history
* Added file upload to add modal

* Add PWA manifest
Added more filter options for data table
Minor padding issues for jobid page

* Added status update to main job table

* Dynamic height for edit modal

* Dynamic height for edit modal

* Dynamic height for edit modal

* Dynamic height for edit modal

* Added stats endpoint

* Added stats page
Fixed extension not scrolling to top when capturing

* Added alert for invalid captures
Added pre-filled url to extension add page

* Added remove fixed classes on screenshot
Added token renewal to extension on visibility change
  • Loading branch information
ncpleslie authored Apr 2, 2024
1 parent a22cd0d commit a26fa15
Show file tree
Hide file tree
Showing 10 changed files with 435 additions and 118 deletions.
136 changes: 91 additions & 45 deletions extension/src/pages/popup/hooks/use-screenshot.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,72 +3,118 @@ import { useRef, useState } from "react";
const useScreenshot = () => {
const canvas = useRef<HTMLCanvasElement | null>(null);
const [capturing, setCapturing] = useState(false);
const [isError, setIsError] = useState(false);

async function captureFullPageScreenshot(
const captureFullPageScreenshot = async (
callback: (dataUrl: string) => void
) {
) => {
if (capturing) {
return;
}

setIsError(false);
setCapturing(true);
const dataUrls: string[] = [];

const activeTab = await new Promise<chrome.tabs.Tab | undefined>(
(resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
resolve(tabs[0]);
});
}
);

if (!activeTab?.id) {
return;
}

await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
function: async () => {
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
await new Promise((resolve) => setTimeout(resolve, 100));
},
});

async function captureScreenshot() {
const screenshot = await new Promise<string>((resolve) => {
chrome.tabs.captureVisibleTab({ format: "png" }, (dataUrl) => {
resolve(dataUrl);
});
});

dataUrls.push(screenshot);
try {
const activeTab = await new Promise<chrome.tabs.Tab | undefined>(
(resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
resolve(tabs[0]);
});
}
);

if (!activeTab?.id) {
return;
}

// Delete "fixed" CSS properties to avoid overlapping elements
await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
function: () => window.scrollBy(0, window.innerHeight),
function: async () => {
const elements = document.querySelectorAll("*");

for (let i = 0; i < elements.length; i++) {
const element = elements[i];
const style = window.getComputedStyle(element);

if (style["position"] === "fixed") {
(element as HTMLElement).setAttribute(
"style",
"position:static !important"
);
(element as HTMLElement).setAttribute(
"job-down-static",
"active"
);
}
}

await new Promise((resolve) => setTimeout(resolve, 200));
},
});

const reachedBottom = await chrome.scripting.executeScript({
await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
function: () =>
window.scrollY >= document.body.scrollHeight - window.innerHeight,
function: async () => {
window.scrollTo({ top: 0, left: 0, behavior: "instant" });
await new Promise((resolve) => setTimeout(resolve, 200));
},
});

if (reachedBottom[0].result) {
await stitchScreenshots(dataUrls, callback);
} else {
await new Promise((resolve) => setTimeout(resolve, 500));
captureScreenshot();
}
}
const captureScreenshot = async () => {
const screenshot = await new Promise<string>((resolve) => {
chrome.tabs.captureVisibleTab({ format: "png" }, (dataUrl) => {
resolve(dataUrl);
});
});

await captureScreenshot();
setCapturing(false);
}
dataUrls.push(screenshot);

if (!activeTab?.id) {
return;
}

await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
function: () => window.scrollBy(0, window.innerHeight),
});

const reachedBottom = await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
function: () =>
window.scrollY >= document.body.scrollHeight - window.innerHeight,
});

if (reachedBottom[0].result) {
await chrome.scripting.executeScript({
target: { tabId: activeTab.id },
function: async () => {
const staticElements = document.querySelectorAll(
'[job-down-static="active"]'
);
staticElements.forEach((element) => {
element.removeAttribute("style");
element.removeAttribute("job-down-static");
});
},
});

await stitchScreenshots(dataUrls, callback);
} else {
await new Promise((resolve) => setTimeout(resolve, 500));
await captureScreenshot();
}
};

await captureScreenshot();
setCapturing(false);
} catch (error) {
setIsError(true);
setCapturing(false);
}
};

async function stitchScreenshots(
dataUrls: string[],
Expand Down Expand Up @@ -114,7 +160,7 @@ const useScreenshot = () => {
);
}

return { captureFullPageScreenshot, canvasRef: canvas };
return { captureFullPageScreenshot, canvasRef: canvas, capturing, isError };
};

export default useScreenshot;
22 changes: 22 additions & 0 deletions extension/src/pages/popup/hooks/use-visible.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useSyncExternalStore } from "react";

export const useVisible = () => {
const visibilitySubscription = (callback: () => void) => {
document.addEventListener("visibilitychange", callback);

return () => {
document.removeEventListener("visibilitychange", callback);
};
};

const getVisibilitySnapshot = () => {
return document.visibilityState;
};

const visibilityState = useSyncExternalStore(
visibilitySubscription,
getVisibilitySnapshot
);

return visibilityState === "visible";
};
13 changes: 13 additions & 0 deletions extension/src/pages/popup/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,14 @@ import {
import { useEffect } from "react";
import { PlusSquare } from "lucide-react";
import { env } from "@src/env";
import { useVisible } from "../hooks/use-visible.hook";

export const Route = createRootRoute({
component: () => {
const visibilityChange = useVisible();
const { callAsync: getTokenAsync } = useMessage({
type: "userToken",
});
const { data: user, isPending, error } = useMessage({ type: "user" });
const { callAsync: signOutAsync } = useMessage(
{ type: "signOut" },
Expand Down Expand Up @@ -43,6 +48,14 @@ export const Route = createRootRoute({
}
}, [user]);

// Grab a new user token when the visibility changes to
// avoid having a stale token each time the popup is opened.
useEffect(() => {
if (visibilityChange) {
getTokenAsync({ type: "userToken" });
}
}, [visibilityChange]);

if (isPending) {
return <LoadingDialog isLoading={true}>Authenticating</LoadingDialog>;
}
Expand Down
71 changes: 64 additions & 7 deletions extension/src/pages/popup/routes/jobs/add.lazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,80 @@ import {
LoadingDialog,
useAddJob,
ScrollArea,
AlertDialog,
AlertDialogContent,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogFooter,
AlertDialogAction,
AlertDialogDescription,
} from "@application-tracker/frontend";
import { createLazyFileRoute } from "@tanstack/react-router";
import useMessage from "@pages/popup/hooks/use-message.hook";
import useScreenshot from "@pages/popup/hooks/use-screenshot.hook";
import { Aperture, Loader2 } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";

export const Route = createLazyFileRoute("/jobs/add")({
component: AddJob,
});

function AddJob() {
const [capturing, setCapturing] = useState(false);
const { captureFullPageScreenshot, canvasRef } = useScreenshot();
const [currentUrl, setCurrentUrl] = useState<string>("");
const { captureFullPageScreenshot, canvasRef, isError, capturing } =
useScreenshot();

const { data: token } = useMessage({ type: "userToken" });
const { onSubmit, onClose, setJobImage, jobImage, isPending } =
useAddJob(token);

const onCapture = () => {
setCapturing(true);
const onCapture = async () => {
captureFullPageScreenshot((dataUrl) => {
setJobImage(dataUrl);
setCapturing(false);
});
};

const retrieveCurrentUrl = async () => {
const activeTab = await new Promise<chrome.tabs.Tab | undefined>(
(resolve) => {
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
resolve(tabs[0]);
});
}
);

if (!activeTab?.url) {
return;
}

setCurrentUrl(activeTab.url);
};

useEffect(() => {
if (currentUrl) {
return;
}
(async () => {
await retrieveCurrentUrl();
})();
}, [retrieveCurrentUrl, currentUrl]);

return (
<>
<div className="flex flex-col items-center justify-center">
<ScrollArea className="h-[455px]" type="always">
<div className="my-2 flex items-center mx-4">
<JobForm.JobForm onSubmit={onSubmit}>
<JobForm.JobForm
key={currentUrl}
onSubmit={onSubmit}
defaultValues={{
position: "",
company: "",
status: "applied",
notes: "",
url: currentUrl,
}}
>
<JobForm.JobFormFooter>
<div className="fixed bottom-0 left-0 z-10 flex w-full justify-between border border-t bg-white px-8 py-4">
<Button type="submit">Add</Button>
Expand All @@ -60,6 +101,22 @@ function AddJob() {
</ScrollArea>
</div>
<LoadingDialog isLoading={isPending}>Adding job</LoadingDialog>
{isError && (
<AlertDialog defaultOpen>
<AlertDialogContent className="max-w-[350px] mx-auto">
<AlertDialogHeader>
<AlertDialogTitle>Capture Error</AlertDialogTitle>
<AlertDialogDescription>
An error occurred while attempting to capture your current
webpage. Ensure you are on a valid webpage and try again.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
<canvas ref={canvasRef} className="hidden" />
</>
);
Expand Down
Loading

0 comments on commit a26fa15

Please sign in to comment.