Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MPT-125] [MPT-65] attempt to implement shareable link #864

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
};
};
Loading