Skip to content
Merged
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
10 changes: 10 additions & 0 deletions cypress/integration/theme/davids/davidsMain.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
describe("Primary Nav Menu", () => {
it(`should visit 'davids' theme`, () => {
cy.visit("/");
cy.get("[data-testid=NavPrimaryMenuIcon]").click();
cy.get("[data-testid=NavPrimaryMenuDemoThemes]").should("be.visible");
cy.get("[data-testid=NavPrimaryMenuDemoThemes]").click();
cy.get(`a[href='/demo/davids']`).click();
cy.url().should("include", `/demo/davids`);
});
});
22 changes: 22 additions & 0 deletions cypress/integration/theme/davids/davidsSkillFilter.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ThemeDavidsPage } from "../../../pages/themeDavidsPage.cy";

const davidsPage = new ThemeDavidsPage();

describe("Theme Davids", () => {
beforeEach(() => {
davidsPage.getDavidsThemePage();
cy.url().should("include", `/demo/davids`);
});

// https://github.com/mission-minded-llc/ampdresume-theme/issues/40
it.skip("should clear test input when filter change", () => {
const testValue = "Test Skill";

davidsPage.selectFilter("Years of Experience");
davidsPage.getSkillFilter().type(testValue).should("have.value", testValue);

davidsPage.selectFilter("Skill");

davidsPage.getSkillFilter().should("have.value", "");
});
});
10 changes: 10 additions & 0 deletions cypress/integration/theme/default/defaultMain.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
describe("Primary Nav Menu", () => {
it(`should visit 'default' theme`, () => {
cy.visit("/");
cy.get("[data-testid=NavPrimaryMenuIcon]").click();
cy.get("[data-testid=NavPrimaryMenuDemoThemes]").should("be.visible");
cy.get("[data-testid=NavPrimaryMenuDemoThemes]").click();
cy.get(`a[href='/demo/default']`).click();
cy.url().should("include", `/demo/default`);
});
});
14 changes: 14 additions & 0 deletions cypress/pages/navigationMenuPage.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class NavigationMenuPage {
visitNavPrimaryMenu() {
cy.visit("/");
return cy.get("[data-testid=NavPrimaryMenuIcon]");
}

getDavidsThemeItem() {
return cy.get("a[href='/demo/davids']");
}

getCloseView() {
return cy.get("button[data-testid='nav-primary-menu-close-button']");
}
}
22 changes: 22 additions & 0 deletions cypress/pages/themeDavidsPage.cy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export class ThemeDavidsPage {
getDavidsThemePage() {
cy.visit("/demo/davids");
}

getSkillFilter() {
return cy.get('input[type="text"]').scrollIntoView().should("be.visible");
}

getSkillFilterDropDown() {
return cy.get('[data-testid="ArrowDropDownIcon"]').closest("button");
}

getSkillFilterItem(skill) {
return cy.contains("li", skill);
}

selectFilter(skill) {
this.getSkillFilterDropDown().click();
this.getSkillFilterItem(skill).click();
}
}
105 changes: 105 additions & 0 deletions src/app/components/NavPrimary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { useSession } from "next-auth/react";
import React, { useState } from "react";
import CloseIcon from "@mui/icons-material/Close";
import MenuIcon from "@mui/icons-material/Menu";
import ExpandLess from "@mui/icons-material/ExpandLess";
import ExpandMore from "@mui/icons-material/ExpandMore";
import Box from "@mui/material/Box";
import Collapse from "@mui/material/Collapse";
import Drawer from "@mui/material/Drawer";
import IconButton from "@mui/material/IconButton";
import List from "@mui/material/List";
Expand All @@ -15,6 +18,7 @@ import { MuiLink } from "@/components/MuiLink";
import { useIsLoggedIn } from "@/hooks/useIsLoggedIn";
import { getBaseUrl } from "@/util/url";
import { ThemeAppearanceToggle } from "./ThemeAppearanceToggle";
import { themeDefinitions } from "@/theme";

/**
* The primary navigation component for the application. This nav is shared
Expand All @@ -25,6 +29,7 @@ export const NavPrimary = () => {
const isLoggedIn = useIsLoggedIn();

const [isOpen, setIsOpen] = useState(false);
const [demoThemesOpen, setDemoThemesOpen] = useState(false);

const baseUrl = getBaseUrl();

Expand All @@ -39,6 +44,10 @@ export const NavPrimary = () => {
setIsOpen(open);
};

const toggleDemoThemes = () => {
setDemoThemesOpen(!demoThemesOpen);
};

const NavItemTitle = ({ text }: { text: string }) => (
<Box
sx={{
Expand Down Expand Up @@ -94,6 +103,83 @@ export const NavPrimary = () => {
</MuiLink>
);

const SubmenuItem = ({
text,
icon,
href,
target = "_self",
dataTestId = "",
}: {
text: string;
icon: string;
href: string;
target?: "_self" | "_blank";
dataTestId?: string;
}) => (
<MuiLink
href={href}
target={target}
sx={{
textDecoration: "none",
}}
>
<ListItem
component="div"
onClick={() => {
setIsOpen(false);
}}
sx={(theme) => ({
pl: 4,
"&:hover": {
backgroundColor: "black",
color: "white",
borderRight: `4px solid ${theme.palette.secondary.main}`,
},
})}
{...(dataTestId && { "data-testid": dataTestId })}
>
<ListItemIcon>
<Icon icon={icon} width={24} height={24} />
</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
</MuiLink>
);

const SubmenuHeader = ({
text,
icon,
isOpen,
onClick,
dataTestId = "",
}: {
text: string;
icon: string;
isOpen: boolean;
onClick: () => void;
dataTestId?: string;
}) => (
<ListItem
component="div"
onClick={onClick}
sx={(theme) => ({
cursor: "pointer",
"&:hover": {
backgroundColor: "black",
color: "white",
borderRight: `4px solid ${theme.palette.secondary.main}`,
},
})}
{...(dataTestId && { "data-testid": dataTestId })}
>
<ListItemIcon>
<Icon icon={icon} width={36} height={36} />
</ListItemIcon>
<ListItemText primary={text} />
{isOpen ? <ExpandLess /> : <ExpandMore />}
</ListItem>
);

return (
<Box>
<IconButton
Expand Down Expand Up @@ -146,6 +232,25 @@ export const NavPrimary = () => {
href={baseUrl}
dataTestId="NavPrimaryMenuHome"
/>
<SubmenuHeader
text="Demo Themes"
icon="fluent-color:image-48"
isOpen={demoThemesOpen}
onClick={toggleDemoThemes}
dataTestId="NavPrimaryMenuDemoThemes"
/>
<Collapse in={demoThemesOpen} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{Object.entries(themeDefinitions).map(([key, theme]) => (
<SubmenuItem
text={theme.name}
icon={theme.iconifyIcon}
href={`/demo/${key}`}
key={key}
/>
))}
</List>
</Collapse>
{isLoggedIn ? (
<>
{session?.data?.user?.slug ? (
Expand Down
4 changes: 2 additions & 2 deletions src/app/demo/[themeName]/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,13 @@ describe("Theme Page", () => {
it("should generate correct metadata for a theme", async () => {
const metadata = await generateMetadata({ params });

expect(metadata.title).toBe(`Theme: ${themeName} ${titleSuffix}`);
expect(metadata.title).toBe(`Theme: ${themeDefinitions[themeName].name} ${titleSuffix}`);
expect(metadata.description).toBe(themeDefinitions[themeName].description);
expect(Array.isArray(metadata.authors) && metadata.authors[0]?.name).toBe(
themeDefinitions.default.authors[0].name,
);
expect(metadata.openGraph).toEqual({
title: `Theme: ${themeName} ${titleSuffix}`,
title: `Theme: ${themeDefinitions[themeName].name} ${titleSuffix}`,
description: themeDefinitions[themeName].description,
images: [],
});
Expand Down
2 changes: 1 addition & 1 deletion src/app/demo/[themeName]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { themeName } = await params;

const title = `Theme: ${themeName} ${titleSuffix}`;
const title = `Theme: ${themeDefinitions[themeName as ThemeName]?.name} ${titleSuffix}`;

const description =
themeDefinitions[themeName as ThemeName]?.description ||
Expand Down
8 changes: 4 additions & 4 deletions src/app/demo/[themeName]/pdf/page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ describe("PDF Theme Page", () => {
const metadata = await generateMetadata({ params: mockParams });

expect(metadata).toEqual({
title: `PDF Theme: default ${titleSuffix}`,
title: `PDF Theme: Classic ${titleSuffix}`,
description: themeDefinitions.default.description,
authors: themeDefinitions.default.authors.map((author) => ({
name: author.name,
url: author.gitHubUrl || author.linkedInUrl || "",
})),
openGraph: {
title: `PDF Theme: default ${titleSuffix}`,
title: `PDF Theme: Classic ${titleSuffix}`,
description: themeDefinitions.default.description,
images: [],
},
Expand All @@ -48,7 +48,7 @@ describe("PDF Theme Page", () => {
const metadata = await generateMetadata({ params: customThemeParams });

expect(metadata).toEqual({
title: `PDF Theme: davids ${titleSuffix}`,
title: `PDF Theme: David's Theme ${titleSuffix}`,
description: themeDefinitions.davids.description,
authors: [
{
Expand All @@ -57,7 +57,7 @@ describe("PDF Theme Page", () => {
},
],
openGraph: {
title: `PDF Theme: davids ${titleSuffix}`,
title: `PDF Theme: David's Theme ${titleSuffix}`,
description: themeDefinitions.davids.description,
images: [],
},
Expand Down
2 changes: 1 addition & 1 deletion src/app/demo/[themeName]/pdf/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { themeName } = await params;

const title = `PDF Theme: ${themeName} ${titleSuffix}`;
const title = `PDF Theme: ${themeDefinitions[themeName as ThemeName]?.name} ${titleSuffix}`;

const description =
themeDefinitions[themeName as ThemeName]?.description ||
Expand Down
19 changes: 17 additions & 2 deletions src/lib/secureHtmlParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,21 @@ export const ALLOWED_ATTRIBUTES = [
"rowspan",
];

// Check if we're in a browser environment
const isBrowser = typeof window !== "undefined" && typeof document !== "undefined";

/**
* Secure HTML parsing options for html-react-parser
* Only allows safe HTML tags and attributes, blocks everything else
*/
export const secureHtmlParserOptions: HTMLReactParserOptions = {
replace: (domNode) => {
if (domNode instanceof Element) {
// Only run in browser environment
if (!isBrowser) {
return domNode;
}

if (domNode && domNode.type === "tag") {
const tagName = domNode.tagName.toLowerCase();

// Only allow specified tags - everything else is blocked
Expand Down Expand Up @@ -113,7 +121,9 @@ export const secureHtmlParserOptions: HTMLReactParserOptions = {
});

// Copy the content
safeElement.innerHTML = domNode.innerHTML;
if ("innerHTML" in domNode) {
safeElement.innerHTML = String((domNode as Record<string, unknown>).innerHTML);
}
return safeElement;
}
}
Expand All @@ -127,6 +137,11 @@ export const secureHtmlParserOptions: HTMLReactParserOptions = {
* Useful for Lexical editor initialization
*/
export function sanitizeHtmlForEditor(html: string): string {
// Only run in browser environment
if (!isBrowser) {
return html;
}

// Create a temporary div to parse and sanitize HTML
const tempDiv = document.createElement("div");
tempDiv.innerHTML = html;
Expand Down
Loading