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;