diff --git a/.vscode/settings.json b/.vscode/settings.json
index 99e58e0..eb98407 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -2,12 +2,13 @@
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.format.enable": true,
"eslint.codeActionsOnSave.mode": "all",
+ "css.lint.validProperties": ["composes"],
"css.format.spaceAroundSelectorSeparator": true,
"[css]": {
"editor.suggest.insertMode": "replace",
"gitlens.codeLens.scopes": ["document"],
"editor.formatOnSave": true,
- "editor.defaultFormatter": "vscode.css-language-features"
+ "editor.defaultFormatter": "vscode.css-language-features",
},
"[javascript]": {
"editor.formatOnSave": false,
diff --git a/components/ComponentWrapper.module.css b/components/ComponentWrapper.module.css
new file mode 100644
index 0000000..4fad333
--- /dev/null
+++ b/components/ComponentWrapper.module.css
@@ -0,0 +1,59 @@
+.sideWrapperCommon {
+ background-color: var(--secondary-background);
+ display: flex;
+ padding: calc(40rem / 16) calc(50rem / 16);
+ margin: calc(65rem / 16) 0;
+ width: 90%;
+}
+
+.wrapperRight {
+ composes: sideWrapperCommon;
+ align-self: end;
+ margin-right: 0;
+ padding-right: 0;
+ border-radius: calc(200rem / 16) 0 0 calc(200rem / 16);
+ flex-direction: row-reverse;
+ padding-right: calc(50rem / 16);
+}
+
+.wrapperLeft {
+ composes: sideWrapperCommon;
+ align-self: start;
+ margin-left: 0;
+ padding-left: 0;
+ border-radius: 0 calc(200rem / 16) calc(200rem / 16) 0;
+ flex-direction: row;
+ padding-left: calc(50rem / 16);
+}
+
+.noBackground {
+ background: none;
+ align-self: center;
+}
+
+.wrapperCenter {
+ flex-direction: column;
+ text-align: center;
+ gap: calc(25rem / 16);
+ /* to match the 65px margin with the left/right variant:
+ add 45px bottom margin, since internal wrapper contributes 20px for the center component
+ 0px top margin, since h3 contributes 45px and internal wrapper contributes 20px for the center component
+ */
+ margin: 0 0 calc(45rem / 16) 0;
+ padding: 0 15%;
+}
+
+@media screen and (max-width: 768px) {
+ .sideWrapperCommon {
+ margin: auto;
+ flex-direction: column;
+ text-align: center;
+ padding: 0;
+ border-radius: 0;
+ width: 100%;
+ }
+}
+
+.internalWrapper {
+ padding: calc(20rem / 16);
+}
\ No newline at end of file
diff --git a/components/ComponentWrapper.tsx b/components/ComponentWrapper.tsx
new file mode 100644
index 0000000..8893905
--- /dev/null
+++ b/components/ComponentWrapper.tsx
@@ -0,0 +1,42 @@
+import React from "react";
+
+import styles from "./ComponentWrapper.module.css";
+
+type AlignOption = "left" | "center" | "right";
+
+type ComponentWrapperProps = {
+ children: React.ReactNode;
+ heading: string;
+ bodyText: string;
+ align?: AlignOption;
+ noBackground?: boolean;
+};
+
+export function ComponentWrapper({
+ heading,
+ bodyText,
+ children,
+ align = "left",
+ noBackground = false,
+}: ComponentWrapperProps) {
+ const alignClasses: { [key in AlignOption]: string } = {
+ left: styles.wrapperLeft,
+ center: styles.wrapperCenter,
+ right: styles.wrapperRight,
+ };
+
+ return (
+
+
+
{heading}
+
{bodyText}
+
+
{children}
+
+ );
+}
diff --git a/components/QuotationCarousel.module.css b/components/QuotationCarousel.module.css
new file mode 100644
index 0000000..40241da
--- /dev/null
+++ b/components/QuotationCarousel.module.css
@@ -0,0 +1,124 @@
+.carousel {
+ position: relative;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: calc(8rem / 16);
+}
+
+.circle {
+ position: absolute;
+ top: 30%;
+ right: 52%;
+ z-index: -1;
+
+ background-color: var(--tertiary-background);
+ clip-path: circle();
+}
+
+.right.circle {
+ top: unset;
+ right: unset;
+ bottom: 30%;
+ left: 52%;
+}
+
+.carouselButton {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ padding: calc(16rem / 16);
+ height: min-content;
+
+ background: none;
+ border: none;
+
+ cursor: pointer;
+}
+
+.arrow {
+ position: relative;
+ width: calc(20rem / 16);
+ height: calc(40rem / 16);
+
+ transition: 0.2s;
+}
+
+.previous.arrow {
+ transform: rotate(180deg);
+}
+
+.carouselButton:hover > .arrow {
+ translate: calc(4rem / 16);
+}
+
+.carouselButton:hover > .previous.arrow {
+ translate: calc(-4rem / 16);
+}
+
+.card {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: stretch;
+ gap: calc(16rem / 16);
+
+ min-height: inherit;
+ height: 100%;
+ width: 100%;
+ padding: calc(30rem / 16);
+
+ background-color: var(--translucent-accent);
+ border: calc(2rem / 16) solid var(--primary-text);
+ border-radius: calc(12rem / 16);
+ box-shadow: 0 calc(1rem / 16) calc(10rem / 16) var(--primary-accent);
+}
+
+.card ul {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+
+ position: relative;
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ flex-grow: 1;
+}
+
+.card li {
+ position: absolute;
+ left: 0;
+ right: 0;
+ margin: 0;
+ padding: 0;
+ list-style: none;
+
+ visibility: visible;
+ opacity: 1;
+ transition: 0.1s;
+}
+
+.card li.hidden {
+ visibility: hidden;
+ opacity: 0;
+}
+
+.card p {
+ margin: 0 calc(16rem / 16);
+ font-weight: bold;
+ text-align: center;
+}
+
+.quotationMark {
+ width: calc(20rem / 16);
+ height: calc(20rem / 16);
+}
+
+.right.quotationMark {
+ transform: rotate(180deg);
+ align-self: end;
+}
diff --git a/components/QuotationCarousel.tsx b/components/QuotationCarousel.tsx
new file mode 100644
index 0000000..fed81a7
--- /dev/null
+++ b/components/QuotationCarousel.tsx
@@ -0,0 +1,130 @@
+import React, { useState } from "react";
+import { Color } from "utils/Color";
+
+import styles from "./QuotationCarousel.module.css";
+
+interface QuotationCarouselProps {
+ data: string[];
+ /** Width of the entire carousel including the buttons, in px. */
+ width?: number;
+ /** Minimum height of the carousel, in px. */
+ height?: number;
+ /** Diameter of the background circles, in px. Set to 0 for no circles. */
+ circleDiameter?: number;
+ className?: string;
+}
+
+interface CarouselButtonProps {
+ onClick: () => void;
+ isPrevious?: boolean;
+}
+
+export function QuotationCarousel(props: QuotationCarouselProps) {
+ const {
+ data,
+ width = 600,
+ height = 100,
+ circleDiameter = 120,
+ className,
+ } = props;
+
+ const [activeIdx, setActiveIdx] = useState(0);
+
+ function showNextCard() {
+ setActiveIdx((activeIdx + 1) % data.length);
+ }
+
+ function showPreviousCard() {
+ setActiveIdx((activeIdx - 1 + data.length) % data.length);
+ }
+
+ return (
+
+
+
+
+
+
+
+ {data.map((quote, idx) => (
+ -
+
{quote}
+
+ ))}
+
+
+
+
+
+ );
+}
+
+function Circle({
+ className,
+ diameter,
+}: {
+ className: string;
+ diameter: number;
+}) {
+ return (
+
+ );
+}
+
+function CarouselButton({ isPrevious, onClick }: CarouselButtonProps) {
+ return (
+
+ );
+}
+
+function QuotationMark({ className }: { className: string }) {
+ return (
+
+ );
+}
diff --git a/components/WordCloud.tsx b/components/WordCloud.tsx
index 9591dac..ca295e5 100644
--- a/components/WordCloud.tsx
+++ b/components/WordCloud.tsx
@@ -197,10 +197,12 @@ const shouldNotRerender = (
nextProps: WordCloudWordsProps
) => {
if (
- prevProps.tooltipLeft !== nextProps.tooltipLeft ||
- prevProps.tooltipTop !== nextProps.tooltipTop ||
- nextProps.tooltipLeft === undefined ||
- nextProps.tooltipTop === undefined
+ // if width changes, rerender, else don't rerender for a tooltip change
+ prevProps.width === nextProps.width &&
+ (prevProps.tooltipLeft !== nextProps.tooltipLeft ||
+ prevProps.tooltipTop !== nextProps.tooltipTop ||
+ nextProps.tooltipLeft === undefined ||
+ nextProps.tooltipTop === undefined)
) {
return true; // do not re-render
}
diff --git a/data/mocks.ts b/data/mocks.ts
index 10f49f4..b6024d8 100644
--- a/data/mocks.ts
+++ b/data/mocks.ts
@@ -133,3 +133,15 @@ export const mockStackedBarKeys = [
"geese breeders",
"geese catchers",
];
+
+export const mockQuoteData = [
+ "The quick brown fox jumps over the lazy dog.",
+ "Sphinx of black quartz, judge my vow!",
+ "Pack my box with five dozen liquor jugs.",
+];
+
+export const mockQuoteDataLong = [
+ "Here, have some quotes of varying lengths, and see how they look.",
+ "Hello, world!",
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla in enim neque. Sed sit amet convallis tellus. Integer condimentum a felis id gravida. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam metus libero, sagittis in consectetur in, scelerisque sed sapien. Nullam ut feugiat sapien. Praesent dictum ac ipsum ac lacinia.",
+];
diff --git a/pages/_app.css b/pages/_app.css
index 0c34108..8c5d52f 100644
--- a/pages/_app.css
+++ b/pages/_app.css
@@ -20,6 +20,7 @@ body {
--light--primary-accent-light: var(--orange);
--light--primary-accent-lighter: #FFBC9F;
--light--secondary-accent: #E55F98;
+ --light--translucent-accent: rgba(255, 231, 231, 0.75);
--light--secondary-accent-light: #FEA0C8;
--light--primary-heading: #D02B53;
--light--primary-text: #483B35;
@@ -37,6 +38,7 @@ body {
--dark--primary-accent-lighter: var(--lighter-pink);
--dark--secondary-accent: var(--orange);
--dark--secondary-accent-light: var(--light-orange);
+ --dark--translucent-accent: rgba(239, 131, 157, 0.75);
--dark--primary-heading: #FFC48D;
--dark--secondary-heading: var(--pink);
--dark--link: var(--pink);
@@ -54,6 +56,7 @@ body {
--primary-accent-lighter: var(--dark--primary-accent-lighter);
--secondary-accent: var(--dark--secondary-accent);
--secondary-accent-light: var(--dark--secondary-accent-light);
+ --translucent-accent: var(--dark--translucent-accent);
--primary-heading: var(--dark--primary-heading);
--secondary-heading: var(--dark--secondary-heading);
--link: var(--dark--link);
@@ -73,7 +76,11 @@ h2 {
color: var(--primary-heading);
}
-h3,
+h3 {
+ color: var(--secondary-heading);
+ font-size: calc(45rem / 16);
+}
+
h4,
h5,
h6 {
@@ -89,6 +96,10 @@ a:hover {
color: var(--link-hover);
}
+p {
+ font-size: calc(28rem / 16);
+}
+
@media only screen and (prefers-color-scheme: dark) {
body {
--primary-background: var(--dark--primary-background);
@@ -109,4 +120,4 @@ a:hover {
--card-background: var(--dark--card-background);
--label: var(--dark--label);
}
-}
\ No newline at end of file
+}
diff --git a/pages/_app.tsx b/pages/_app.tsx
index 7803bd0..52d7142 100644
--- a/pages/_app.tsx
+++ b/pages/_app.tsx
@@ -1,9 +1,20 @@
import type { AppProps } from "next/app";
+import Head from "next/head";
import React from "react";
import "./_app.css";
import "./font.css";
export default function App({ Component, pageProps }: AppProps): JSX.Element {
- return ;
+ return (
+ <>
+
+
+
+
+ >
+ );
}
diff --git a/pages/index.tsx b/pages/index.tsx
index 4ff1fb7..58dda94 100644
--- a/pages/index.tsx
+++ b/pages/index.tsx
@@ -5,6 +5,8 @@ export default function Home() {
return (
Click here to visit the playground
+
+ Click here to visit a sample page
);
}
diff --git a/pages/playground.module.css b/pages/playground.module.css
index a7aefd8..97f5eac 100644
--- a/pages/playground.module.css
+++ b/pages/playground.module.css
@@ -5,3 +5,11 @@
.barGraphDemo {
border: calc(1rem / 16) solid black;
}
+
+.quotationCarouselDemo {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: calc(48rem / 16);
+ margin: calc(32rem / 16);
+}
diff --git a/pages/playground.tsx b/pages/playground.tsx
index 1244599..c52d11c 100644
--- a/pages/playground.tsx
+++ b/pages/playground.tsx
@@ -2,12 +2,15 @@ import { BarGraphHorizontal, BarGraphVertical } from "components/BarGraph";
import {
mockCategoricalData,
moreMockCategoricalData,
- mockStackedBarGraphData,
mockStackedBarKeys,
+ mockStackedBarGraphData,
+ mockQuoteDataLong,
+ mockQuoteData,
} from "data/mocks";
import React from "react";
import { Color } from "utils/Color";
+import { QuotationCarousel } from "@/components/QuotationCarousel";
import { StackedBarGraph } from "@/components/StackedBarGraph";
import { ColorPalette } from "../components/ColorPalette";
@@ -86,6 +89,18 @@ export default function Home() {
left: 20,
}}
/>
+
+ {""}
+
+
+
+
+
);
}
diff --git a/pages/samplePage.module.css b/pages/samplePage.module.css
new file mode 100644
index 0000000..b50d07b
--- /dev/null
+++ b/pages/samplePage.module.css
@@ -0,0 +1,5 @@
+.page {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+}
\ No newline at end of file
diff --git a/pages/samplePage.tsx b/pages/samplePage.tsx
new file mode 100644
index 0000000..f067888
--- /dev/null
+++ b/pages/samplePage.tsx
@@ -0,0 +1,127 @@
+import { BarGraphHorizontal, BarGraphVertical } from "components/BarGraph";
+import { mockCategoricalData, moreMockCategoricalData } from "data/mocks";
+import React from "react";
+import { useWindowDimensions } from "utils/getWindowDimensions";
+import { useIsMobile } from "utils/isMobile";
+
+import { ComponentWrapper } from "@/components/ComponentWrapper";
+
+import { WordCloud } from "../components/WordCloud";
+
+import styles from "./samplePage.module.css";
+
+export default function SamplePage() {
+ const { width } = useWindowDimensions();
+ const isMobile = useIsMobile();
+
+ return (
+
+
+
+
+
+
+ ({
+ text: word.key,
+ value: word.value,
+ }))}
+ // For components that we don't want to match the width necessarily we can provide direct values
+ width={isMobile ? width / 1.5 : 800}
+ height={500}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ({
+ text: word.key,
+ value: word.value,
+ }))}
+ width={isMobile ? width / 1.5 : width / 2}
+ height={500}
+ />
+
+
+ );
+}
diff --git a/utils/getWindowDimensions.ts b/utils/getWindowDimensions.ts
new file mode 100644
index 0000000..427568b
--- /dev/null
+++ b/utils/getWindowDimensions.ts
@@ -0,0 +1,35 @@
+// Attribution: https://stackoverflow.com/questions/36862334/get-viewport-window-height-in-reactjs
+import { useState, useEffect } from "react";
+
+type WindowDimensions = {
+ width: number;
+ height: number;
+};
+
+const getWindowDimensions = (): WindowDimensions => {
+ const { innerWidth, innerHeight } = window;
+
+ return {
+ width: innerWidth,
+ height: innerHeight,
+ };
+};
+
+export const useWindowDimensions = (): WindowDimensions => {
+ const [windowDimensions, setWindowDimensions] = useState({
+ width: 0,
+ height: 0,
+ });
+
+ const handleResize = () => {
+ setWindowDimensions(getWindowDimensions());
+ };
+
+ useEffect(() => {
+ handleResize();
+ window.addEventListener("resize", handleResize);
+ return () => window.removeEventListener("resize", handleResize);
+ }, []);
+
+ return windowDimensions;
+};
diff --git a/utils/isMobile.ts b/utils/isMobile.ts
new file mode 100644
index 0000000..4022df1
--- /dev/null
+++ b/utils/isMobile.ts
@@ -0,0 +1,3 @@
+import { useWindowDimensions } from "./getWindowDimensions";
+
+export const useIsMobile = () => useWindowDimensions().width <= 768;