Skip to content

Commit

Permalink
attempt to implement shareable link
Browse files Browse the repository at this point in the history
  • Loading branch information
dabby9734 committed Nov 23, 2024
1 parent 8e2e1bf commit e054807
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 16 deletions.
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"html-to-text": "^9.0.5",
"js-cookie": "^3.0.5",
"next": "^14.2.10",
"qs": "^6.13.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-intersection-observer": "^9.10.3",
Expand Down
8 changes: 7 additions & 1 deletion src/app/mentors/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import ClearFacets from "../../components/ResetButton";
import ResultView from "../../components/ResultView";
import "@elastic/react-search-ui-views/lib/styles/styles.css";
import "../../styles/App.css";
import { useNextRouting } from "../../utils/useNextRouting";

const App = () => {
const WAVE = { waveId: "2024", waveName: "2024 Wave" };
Expand Down Expand Up @@ -75,10 +76,15 @@ const App = () => {
},
};

const configWithNextRouting = useNextRouting(
configurationOptions,
"/mentors",
);

return (
<Canvas>
<div className="results" id="mentors">
<SearchProvider config={configurationOptions}>
<SearchProvider config={configWithNextRouting}>
<div className="App">
<Layout
header={
Expand Down
38 changes: 31 additions & 7 deletions src/components/ResultViewGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,23 +36,47 @@ const ResultViewGrid = ({

const [isModalOpen, setIsModalOpen] = useState(false);

// Inspired by code from https://github.com/AdvisorySG/mentorship-page/pull/731/commits/b234ec556bc6d7de94432bb2f2a87573c20963ba
const handleOpen = () => {
window.history.pushState({}, "");
// Open the modal
const newUrl = `${window.location.origin}${window.location.pathname}?uid=${id}`;
window.history.pushState({ id }, "", newUrl);
setIsModalOpen(true);

// Track interaction
window.umami.track("Click", { id, env: process.env.NODE_ENV });

// Log
console.log(
"ResultViewGrid :: handleOpen : new modal opened with URL",
newUrl,
);
};

const handleClose = () => {
window.history.back();
if (window.history.state && window.history.state.id === id) {
// Case 1: User clicked into the modal -> We should execute `window.history.back()` to return to the previous URL
console.log("ResultViewGrid :: handleClose :: going back");
window.history.back();
} else {
// Case 2: User came from a direct link -> We should clear the query string (since we don't know what the previous URL was)
console.log("ResultViewGrid :: handleClose :: clearing query string");
const newUrl = `${window.location.origin}${window.location.pathname}`;
window.history.replaceState(null, "", newUrl);
}

setIsModalOpen(false);
};

useEffect(() => {
if (isModalOpen) {
window.onpopstate = () => {
setIsModalOpen(false);
};
// We should open this modal if the URL's `uid` query parameter matches this modal's `id`
const searchParams = new URLSearchParams(window.location.search);
const uidFromUrl = searchParams.get("uid");
if (uidFromUrl === id) {
console.log("ResultViewGrid :: useEffect :: open the modal");
setIsModalOpen(true);
}
});
}, [id]);

return (
<Card
Expand Down
24 changes: 21 additions & 3 deletions src/components/ResultViewList.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { Fragment } from "react";

import { Chip } from "@mui/material";
import React, { Fragment, useState } from "react";

import { Chip, Snackbar, IconButton } from "@mui/material";
import ShareIcon from "@mui/icons-material/Share";
import type { DisplayResult } from "./ResultView";

const ResultViewList = ({
Expand All @@ -20,8 +20,22 @@ const ResultViewList = ({
displaySchool,
industryColors,
thumbnailImageUrl,
id,
} = displayResult;

const [snackbarOpen, setSnackbarOpen] = useState(false);

const handleCopyLink = () => {
// Construct link
const link = `${window.location.origin}${window.location.pathname}?uid=${id}&q=${id}`;

// Copy link to clipboard
navigator.clipboard.writeText(link).then(() => {
setSnackbarOpen(true);
setTimeout(() => setSnackbarOpen(false), 1000);
});
};

return (
<div className="sui-result">
<div className="sui-result__image">
Expand All @@ -40,6 +54,10 @@ const ResultViewList = ({
}}
/>
)}
<IconButton aria-label="share" onClick={handleCopyLink}>
<ShareIcon />
</IconButton>
<Snackbar open={snackbarOpen} message="Link copied to clipboard" />
</div>
<ul className="sui-result__details">
<li className="sui-result__industries">
Expand Down
43 changes: 43 additions & 0 deletions src/utils/preserveTypesEncoder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// From https://github.com/elastic/search-ui/blob/main/packages/search-ui/src/preserveTypesEncoder.ts
// License: Apache-2.0

function isTypeNumber(value) {
return value !== undefined && value !== null && typeof value === "number";
}

function isTypeBoolean(value) {
return value && typeof value === "boolean";
}

function toBoolean(value) {
if (value === "true") return true;
if (value === "false") return false;
throw "Invalid type parsed as Boolean value";
}

/* Encoder for qs library which preserve number types on the URL. Numbers
are padded with "n_{number}_n", and booleans with "b_{boolean}_b"*/

export default {
encode(value, encode) {
if (isTypeNumber(value)) {
return `n_${value}_n`;
}
if (isTypeBoolean(value)) {
return `b_${value}_b`;
}
return encode(value);
},
decode(value, decode) {
//eslint-disable-next-line
if (/n_-?[\d\.]*_n/.test(value)) {
const numericValueString = value.substring(2, value.length - 2);
return Number(numericValueString);
}
if (/^b_(true|false)*_b$/.test(value)) {
const booleanValueString = value.substring(2, value.length - 2);
return toBoolean(booleanValueString);
}
return decode(value);
},
};
82 changes: 82 additions & 0 deletions src/utils/useNextRouting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
Filter,
RequestState,
SearchDriverOptions,
SortOption,
} from "@elastic/search-ui";
import { usePathname, useRouter } from "next/navigation";
var qs = require("qs");
import preserveTypesEncoder from "./preserveTypesEncoder";

export const useNextRouting = (
config: SearchDriverOptions,
basePathUrl: string,
) => {
const router = useRouter();
const pathName = usePathname();

// From https://github.com/elastic/search-ui/blob/6583ad0c03056b3df0541d585337dfad12c16272/packages/search-ui/src/URLManager.ts#L9
type QueryParams = {
filters?: Filter[];
current?: number;
q?: string;
size?: number;
"sort-field"?: string;
"sort-direction"?: string;
sort?: SortOption[];
};

// From https://github.com/elastic/search-ui/blob/6583ad0c03056b3df0541d585337dfad12c16272/packages/search-ui/src/URLManager.ts#L84
function stateToParams({
searchTerm,
current,
filters,
resultsPerPage,
sortDirection,
sortField,
sortList,
}: RequestState): QueryParams {
const params: QueryParams = {};
if (current !== undefined && current > 1) params.current = current;
if (searchTerm) params.q = searchTerm;
if (resultsPerPage) params.size = resultsPerPage;
if (filters && filters.length > 0) {
params["filters"] = filters;
}
if (sortList && sortList.length > 0) {
params["sort"] = sortList;
} else if (sortField) {
params["sort-field"] = sortField;
params["sort-direction"] = sortDirection;
}
return params;
}

// From https://github.com/elastic/search-ui/blob/6583ad0c03056b3df0541d585337dfad12c16272/packages/search-ui/src/URLManager.ts#L109
function stateToQueryString(state: RequestState): string {
return qs.stringify(stateToParams(state), {
encoder: preserveTypesEncoder.encode,
});
}

const routingOptions = {
stateToUrl: (state: RequestState) => {
// if URL contains ?uid=... then we are in modal view, and hence we should not update the URL
if (window.location.search.includes("uid=")) {
console.log(
"stateToUrl :: in modal view, not updating URL. URL: ",
window.location.href,
);
return window.location.search.replace("?", ""); // get the query string without the ?, eg. uid=123
} else {
console.log("stateToUrl :: updating URL with state: ", state);
return stateToQueryString(state);
}
},
};

return {
...config,
routingOptions,
};
};

0 comments on commit e054807

Please sign in to comment.