From d5f0a8e9d184931390fefb33fa953f0d1903237b Mon Sep 17 00:00:00 2001 From: Thomas O'Neill Date: Thu, 1 Feb 2024 15:47:40 +0000 Subject: [PATCH 1/6] framework upgrade Next.js 12 -> 14 --- .env.example | 5 - .eslintrc.json | 2 +- .gitignore | 2 +- README.md | 10 +- app/(with-map)/entry/[slug]/EntryDetails.tsx | 54 + app/(with-map)/entry/[slug]/EntryHeader.tsx | 26 + app/(with-map)/entry/[slug]/EntryItem.tsx | 19 + .../entry/[slug]/EntryPageChart.tsx | 113 + .../entry/[slug]/EntryPageChartImpl.tsx | 200 + .../entry/[slug]/EntryPortableText.tsx | 40 + .../entry/[slug]/EntryReferences.tsx | 18 + app/(with-map)/entry/[slug]/page.tsx | 78 + app/(with-map)/layout.tsx | 25 + app/(with-map)/page.tsx | 7 + .../(without-map)/about/page.tsx | 15 +- app/(without-map)/accessibility/page.tsx | 22 + app/(without-map)/layout.tsx | 17 + app/(without-map)/licence/page.tsx | 10 + .../(without-map)/patterns/PatternsImpl.tsx | 22 +- app/(without-map)/patterns/page.tsx | 27 + app/(without-map)/terms-of-use/page.tsx | 22 + {public => app}/favicon.ico | Bin {styles => app}/globals.css | 1 + next-seo.config.ts => app/layout.tsx | 21 +- {components => app/ui}/BetaBanner.tsx | 2 + {components => app/ui}/ContentPage.tsx | 11 +- app/ui/EntryPopout.tsx | 33 + {components => app/ui}/PageNavbar.tsx | 0 app/ui/PatternIcon.tsx | 46 + {components/layout => app}/ui/Tag.tsx | 0 .../ui/accordion}/Accordion.module.css | 0 .../ui/accordion}/Accordion.tsx | 14 +- .../ui/accordion}/AccordionItem.module.css | 0 .../ui/accordion}/AccordionItem.tsx | 0 .../ui}/carousel/Carousel.module.css | 0 {components => app/ui}/carousel/Carousel.tsx | 25 +- app/ui/footer/Contributors.tsx | 18 + app/ui/footer/Disclaimer.tsx | 10 + .../ui/footer}/Footer.module.css | 0 app/ui/footer/Footer.tsx | 19 + app/ui/footer/FooterLinks.tsx | 41 + app/ui/footer/FooterLogos.tsx | 30 + app/ui/footer/SocialIcons.tsx | 48 + .../ui/header}/Header.module.css | 0 {components => app/ui/header}/Header.tsx | 17 +- .../ui/header}/Search.module.css | 0 app/ui/header/Search.tsx | 11 + .../ui/header/SearchClientComponent.tsx | 37 +- .../ui/layout/SubmitButton.module.css | 0 app/ui/layout/SubmitButton.tsx | 17 + {components => app/ui}/map/MapboxGlobe.tsx | 134 +- app/ui/map/MapboxGlobeServerSide.tsx | 19 + app/ui/map/markers/DataRow.tsx | 55 + app/ui/map/markers/Marker.tsx | 144 + app/ui/map/markers/MarkerChart.tsx | 41 + app/ui/map/markers/Markers.tsx | 56 + app/ui/map/markers/styles.ts | 38 + .../ui}/sidebar/Sidebar.module.css | 0 app/ui/sidebar/Sidebar.tsx | 19 + .../ui/sidebar/SidebarClientComponent.tsx | 108 +- {components => app/ui}/sidebar/selection.ts | 23 +- lib/utils.ts => app/utils/dom.ts | 0 {lib => app/utils}/fp.ts | 3 +- app/utils/sanity/client.ts | 13 + app/utils/sanity/entry.ts | 15 + {lib => app/utils/sanity}/queries.ts | 79 +- {lib => app/utils/sanity}/types.ts | 9 +- {lib => app/utils}/store.ts | 0 components/Back.tsx | 16 - components/Button.tsx | 12 - components/Chart.tsx | 297 - components/Footer.tsx | 143 - components/layout/EntryLayout.tsx | 188 - components/layout/GlobeLayout.tsx | 71 - components/layout/NoopLayout.tsx | 24 - components/layout/TheRealNoopLayout.tsx | 5 - components/layout/ui/PatternIcon.tsx | 34 - components/map/Markers.tsx | 126 - lib/config.ts | 17 - lib/constants.ts | 1 - lib/entry.ts | 10 - lib/mock.ts | 8 - lib/sanity.server.ts | 17 - lib/sanity.ts | 21 - lib/trpc.ts | 49 - lib/valtio.ts | 24 - next.config.js | 2 - package-lock.json | 17858 ---------------- package.json | 76 +- pages/_app.tsx | 53 - pages/_document.tsx | 16 - pages/accessibility.tsx | 15 - pages/api/trpc/[trpc].ts | 9 - pages/entry/[slug].tsx | 40 - pages/index.tsx | 5 - pages/licence.tsx | 15 - pages/patterns.tsx | 54 - pages/terms-of-use.tsx | 15 - pnpm-lock.yaml | 6966 ++++++ public/logos/arts-council.png | Bin 17263 -> 0 bytes public/logos/lottery.png | Bin 44715 -> 0 bytes public/logos/osl.png | Bin 289 -> 0 bytes public/vercel.svg | 4 - server/routers/_app.ts | 77 - server/trpc.ts | 11 - tailwind.config.js | 24 - tailwind.config.ts | 35 + tsconfig.json | 19 +- yarn.lock | 4485 ---- 109 files changed, 8719 insertions(+), 24014 deletions(-) delete mode 100644 .env.example create mode 100644 app/(with-map)/entry/[slug]/EntryDetails.tsx create mode 100644 app/(with-map)/entry/[slug]/EntryHeader.tsx create mode 100644 app/(with-map)/entry/[slug]/EntryItem.tsx create mode 100644 app/(with-map)/entry/[slug]/EntryPageChart.tsx create mode 100644 app/(with-map)/entry/[slug]/EntryPageChartImpl.tsx create mode 100644 app/(with-map)/entry/[slug]/EntryPortableText.tsx create mode 100644 app/(with-map)/entry/[slug]/EntryReferences.tsx create mode 100644 app/(with-map)/entry/[slug]/page.tsx create mode 100644 app/(with-map)/layout.tsx create mode 100644 app/(with-map)/page.tsx rename pages/about.tsx => app/(without-map)/about/page.tsx (58%) create mode 100644 app/(without-map)/accessibility/page.tsx create mode 100644 app/(without-map)/layout.tsx create mode 100644 app/(without-map)/licence/page.tsx rename components/layout/PatternsLayout.tsx => app/(without-map)/patterns/PatternsImpl.tsx (94%) create mode 100644 app/(without-map)/patterns/page.tsx create mode 100644 app/(without-map)/terms-of-use/page.tsx rename {public => app}/favicon.ico (100%) rename {styles => app}/globals.css (98%) rename next-seo.config.ts => app/layout.tsx (62%) rename {components => app/ui}/BetaBanner.tsx (97%) rename {components => app/ui}/ContentPage.tsx (79%) create mode 100644 app/ui/EntryPopout.tsx rename {components => app/ui}/PageNavbar.tsx (100%) create mode 100644 app/ui/PatternIcon.tsx rename {components/layout => app}/ui/Tag.tsx (100%) rename {components/sidebar => app/ui/accordion}/Accordion.module.css (100%) rename {components/sidebar => app/ui/accordion}/Accordion.tsx (89%) rename {components/sidebar => app/ui/accordion}/AccordionItem.module.css (100%) rename {components/sidebar => app/ui/accordion}/AccordionItem.tsx (100%) rename {components => app/ui}/carousel/Carousel.module.css (100%) rename {components => app/ui}/carousel/Carousel.tsx (67%) create mode 100644 app/ui/footer/Contributors.tsx create mode 100644 app/ui/footer/Disclaimer.tsx rename {components => app/ui/footer}/Footer.module.css (100%) create mode 100644 app/ui/footer/Footer.tsx create mode 100644 app/ui/footer/FooterLinks.tsx create mode 100644 app/ui/footer/FooterLogos.tsx create mode 100644 app/ui/footer/SocialIcons.tsx rename {components => app/ui/header}/Header.module.css (100%) rename {components => app/ui/header}/Header.tsx (69%) rename {components => app/ui/header}/Search.module.css (100%) create mode 100644 app/ui/header/Search.tsx rename components/Search.tsx => app/ui/header/SearchClientComponent.tsx (83%) rename components/layout/GlobeLayout.module.css => app/ui/layout/SubmitButton.module.css (100%) create mode 100644 app/ui/layout/SubmitButton.tsx rename {components => app/ui}/map/MapboxGlobe.tsx (82%) create mode 100644 app/ui/map/MapboxGlobeServerSide.tsx create mode 100644 app/ui/map/markers/DataRow.tsx create mode 100644 app/ui/map/markers/Marker.tsx create mode 100644 app/ui/map/markers/MarkerChart.tsx create mode 100644 app/ui/map/markers/Markers.tsx create mode 100644 app/ui/map/markers/styles.ts rename {components => app/ui}/sidebar/Sidebar.module.css (100%) create mode 100644 app/ui/sidebar/Sidebar.tsx rename components/sidebar/Sidebar.tsx => app/ui/sidebar/SidebarClientComponent.tsx (71%) rename {components => app/ui}/sidebar/selection.ts (70%) rename lib/utils.ts => app/utils/dom.ts (100%) rename {lib => app/utils}/fp.ts (89%) create mode 100644 app/utils/sanity/client.ts create mode 100644 app/utils/sanity/entry.ts rename {lib => app/utils/sanity}/queries.ts (51%) rename {lib => app/utils/sanity}/types.ts (87%) rename {lib => app/utils}/store.ts (100%) delete mode 100644 components/Back.tsx delete mode 100644 components/Button.tsx delete mode 100644 components/Chart.tsx delete mode 100644 components/Footer.tsx delete mode 100644 components/layout/EntryLayout.tsx delete mode 100644 components/layout/GlobeLayout.tsx delete mode 100644 components/layout/NoopLayout.tsx delete mode 100644 components/layout/TheRealNoopLayout.tsx delete mode 100644 components/layout/ui/PatternIcon.tsx delete mode 100644 components/map/Markers.tsx delete mode 100644 lib/config.ts delete mode 100644 lib/constants.ts delete mode 100644 lib/entry.ts delete mode 100644 lib/mock.ts delete mode 100644 lib/sanity.server.ts delete mode 100644 lib/sanity.ts delete mode 100644 lib/trpc.ts delete mode 100644 lib/valtio.ts delete mode 100644 package-lock.json delete mode 100644 pages/_app.tsx delete mode 100644 pages/_document.tsx delete mode 100644 pages/accessibility.tsx delete mode 100644 pages/api/trpc/[trpc].ts delete mode 100644 pages/entry/[slug].tsx delete mode 100644 pages/index.tsx delete mode 100644 pages/licence.tsx delete mode 100644 pages/patterns.tsx delete mode 100644 pages/terms-of-use.tsx create mode 100644 pnpm-lock.yaml delete mode 100644 public/logos/arts-council.png delete mode 100644 public/logos/lottery.png delete mode 100644 public/logos/osl.png delete mode 100644 public/vercel.svg delete mode 100644 server/routers/_app.ts delete mode 100644 server/trpc.ts delete mode 100644 tailwind.config.js create mode 100644 tailwind.config.ts delete mode 100644 yarn.lock diff --git a/.env.example b/.env.example deleted file mode 100644 index c510f0e..0000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN= -NEXT_PUBLIC_MAPBOX_STYLE_URL= -NEXT_PUBLIC_SANITY_DATASET= -NEXT_PUBLIC_SANITY_PROJECT_ID= -NEXT_PUBLIC_FATHOM_TRACKING_CODE= diff --git a/.eslintrc.json b/.eslintrc.json index eb82587..24e1c6c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": "next/core-web-vitals", "rules": { - "react/display-name": "off" + "react/jsx-key": "off" } } diff --git a/.gitignore b/.gitignore index c87c9b3..fd3dbb5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ /node_modules /.pnp .pnp.js +.yarn/install-state.gz # testing /coverage @@ -23,7 +24,6 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* # local env files .env*.local diff --git a/README.md b/README.md index c87e042..c403366 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,17 @@ First, run the development server: npm run dev # or yarn dev +# or +pnpm dev +# or +bun dev ``` Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. - -[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. ## Learn More diff --git a/app/(with-map)/entry/[slug]/EntryDetails.tsx b/app/(with-map)/entry/[slug]/EntryDetails.tsx new file mode 100644 index 0000000..26253e5 --- /dev/null +++ b/app/(with-map)/entry/[slug]/EntryDetails.tsx @@ -0,0 +1,54 @@ +import { + getFormattedTenureTypes, + getFormattedEntryDates, +} from "@/app/utils/sanity/entry" +import { Entry } from "@/app/utils/sanity/types" +import { Tag } from "@carbon/icons-react" +import EntryItem from "./EntryItem" +import EntryReferences from "./EntryReferences" +import EntryPortableText from "./EntryPortableText" + +const EntryDetails = (entry?: Entry) => ( +
+ + + + + + + {entry?.references?.length && ( + + + + )} + + <> +

+ This entry is{" "} + + {entry?.entryRating?.grade || "a draft"} + +

+
+ {entry?.tags && + entry.tags.map((entryTag) => ( + + {entryTag.label} + + ))} +
+ +
+
+) + +export default EntryDetails diff --git a/app/(with-map)/entry/[slug]/EntryHeader.tsx b/app/(with-map)/entry/[slug]/EntryHeader.tsx new file mode 100644 index 0000000..6fc0854 --- /dev/null +++ b/app/(with-map)/entry/[slug]/EntryHeader.tsx @@ -0,0 +1,26 @@ +import { Tag } from "@/app/ui/Tag" +import { Entry } from "@/app/utils/sanity/types" +import { Close } from "@carbon/icons-react" +import Link from "next/link" + +const EntryHeader = (entry?: Entry) => ( +
+ +
+

{entry?.name}

+ {entry?.type} +
+
+) + +export default EntryHeader diff --git a/app/(with-map)/entry/[slug]/EntryItem.tsx b/app/(with-map)/entry/[slug]/EntryItem.tsx new file mode 100644 index 0000000..d6699b5 --- /dev/null +++ b/app/(with-map)/entry/[slug]/EntryItem.tsx @@ -0,0 +1,19 @@ +import { HTMLProps, PropsWithChildren } from "react" + +type Props = HTMLProps & { + heading: string +} + +const EntryItem = ({ heading, children, ...rest }: Props) => ( +
+
+ {heading} +
+ {children} +
+) + +export default EntryItem diff --git a/app/(with-map)/entry/[slug]/EntryPageChart.tsx b/app/(with-map)/entry/[slug]/EntryPageChart.tsx new file mode 100644 index 0000000..62d78eb --- /dev/null +++ b/app/(with-map)/entry/[slug]/EntryPageChart.tsx @@ -0,0 +1,113 @@ +import { A } from "@/app/utils/fp" +import { + getPatternClasses, + getPatterns, + getRelatedEntriesByPattern, + getRelatedEntriesByTenureQuery, +} from "@/app/utils/sanity/queries" +import { Entry, Term } from "@/app/utils/sanity/types" +import { pipe } from "fp-ts/lib/function" +import _ from "lodash" +import EntryPageChartImpl from "./EntryPageChartImpl" + +type Props = { + entry: Entry +} + +const EntryPageChart = async (props: Props) => { + const { entry } = props + + const terms: Term[] = entry.terms ?? [] + + const patterns = await getPatterns() + const patternClasses = await getPatternClasses() + + const relatedEntriesByTenure = await getRelatedEntriesByTenureQuery( + entry.tenureType, + entry._id + ) + + const augmentedTerms = await Promise.all( + pipe( + terms, + A.map(async (term) => { + const patternId = term.pattern._ref + const entryId = entry._id + + const relatedEntriesByPattern = await getRelatedEntriesByPattern( + patternId, + entryId + ) + + const pattern = _.find(patterns, ["_id", term.pattern?._ref]) + + const patternName = _.find(patterns, ["_id", term.pattern?._ref])?.name + + const type = _.capitalize( + _.find(patterns, ["_id", term.pattern?._ref])?.type + ) + + // strength: term.strength, // 1-5 + // description: term.description, + const legalMechanisms = term.termLegalMechanisms?.map( + (mechanism: Record): string => mechanism.name + ) + + return { + meta: pattern, + name: patternName, + patternClassName: _.find(patternClasses, [ + "_id", + pattern?.class?._ref, + ])?.name, + patternClassOrder: _.find(patternClasses, [ + "_id", + pattern?.class?._ref, + ])?.order, + patternIconUrl: pattern?.iconUrl, + type: type === "Limitation" ? "Obligation" : type, + strength: term.strength, + description: term.description, + legalMechanisms, + relatedEntriesByPattern, + } + }) + ) + ) + + // Format the list of individual terms that apply to this entry + // let formattedTerms = _(entry.terms) + // .map((term: any) => ({ + // pattern: _.find(patterns, ["_id", term.pattern?._ref]), + // patternName: _.find(patterns, ["_id", term.pattern?._ref])?.name, + // type: _.capitalize(_.find(patterns, ["_id", term.pattern?._ref])?.type), + // strength: term.strength, // 1-5 + // description: term.description, + // legalMechanisms: term.termLegalMechanisms?.map( + // (mechanism: Record) => mechanism.name + // ), + // })) + // .map((term: any) => ({ + // meta: term.pattern, + // name: term.patternName, + // patternClassName: _.find(patternClasses, [ + // "_id", + // term.pattern?.class?._ref, + // ])?.name, + // patternClassOrder: _.find(patternClasses, [ + // "_id", + // term.pattern?.class?._ref, + // ])?.order, + // patternIconUrl: term.pattern?.iconUrl, + // type: term.type === "Limitation" ? "Obligation" : term.type, + // strength: term.strength, + // description: term.description, + // legalMechanisms: term.legalMechanisms, + // })) + // .sortBy("patternClassOrder", "name") + // .value() + + return +} + +export default EntryPageChart diff --git a/app/(with-map)/entry/[slug]/EntryPageChartImpl.tsx b/app/(with-map)/entry/[slug]/EntryPageChartImpl.tsx new file mode 100644 index 0000000..4a320cc --- /dev/null +++ b/app/(with-map)/entry/[slug]/EntryPageChartImpl.tsx @@ -0,0 +1,200 @@ +"use client" +import { useWindowDimensions } from "@/app/utils/dom" +import { ChevronUp } from "@carbon/icons-react" +import "client-only" +import clsx from "clsx" +import { useState } from "react" +import { PatternIcon } from "../../../ui/PatternIcon" +import { Tag } from "../../../ui/Tag" +import { Carousel } from "../../../ui/carousel/Carousel" +import { + backgroundColorClasses, + descriptionBackgroundColorClasses, + hoverColorClasses, +} from "../../../ui/map/markers/styles" + +type Props = { + augmentedTerms: any[] +} + +const EntryPageChartImpl = (props: Props) => { + const { augmentedTerms } = props + + const { width } = useWindowDimensions() + const showLabels = width && width > 450 ? true : false + const gridCols = showLabels ? 8 : 5 + + const [openIndex, setOpenIndex] = useState(undefined) + + const handleClick = (i: number) => { + i === openIndex ? setOpenIndex(undefined) : setOpenIndex(i) + } + + return ( +
+
+
+ Obligations +
+
+ Rights +
+
+ {augmentedTerms.map((term: any, i: number) => ( +
+
+
+
0 && + backgroundColorClasses[term.patternClassName!] + } ${ + term.type === "Obligation" && + hoverColorClasses[term.patternClassName!] + } h-10 cursor-pointer flex justify-end items-center`} + style={{ gridColumn: `span ${term.strength || 1}` }} + onClick={() => handleClick(i)} + > + {term.type === "Obligation" && ( + + )} +
+ {showLabels && ( +
+ {term.type === "Obligation" && term.name} +
+ )} +
+
+
0 && + backgroundColorClasses[term.patternClassName!] + } ${ + term.type === "Right" && + hoverColorClasses[term.patternClassName!] + } h-10 cursor-pointer flex justify-end items-center`} + style={{ gridColumn: `span ${term.strength || 1}` }} + onClick={() => handleClick(i)} + > + {term.type === "Right" && ( + + )} +
+ {showLabels && ( +
+ {term.type === "Right" && term.name} +
+ )} +
+
+ {openIndex === i && ( +
handleClick(i)} + > +
+
+ +
+ +

+ {term.patternClassName} {term.type.toLowerCase()} +

+
+
+

{term.name}

+

+ {term.meta?.description} +

+ {term?.description && ( +
+

How it applies here

+

{term?.description}

+
+ )} + {term?.legalMechanisms && ( +
+ {term.legalMechanisms.map((mechanism: string) => ( + + {mechanism} + + ))} +
+ )} +
+ {augmentedTerms[i].relatedEntriesByPattern.length > 0 && ( + + )} +
+ )} +
+ ))} +
+ ) +} + +export default EntryPageChartImpl diff --git a/app/(with-map)/entry/[slug]/EntryPortableText.tsx b/app/(with-map)/entry/[slug]/EntryPortableText.tsx new file mode 100644 index 0000000..1ede7f0 --- /dev/null +++ b/app/(with-map)/entry/[slug]/EntryPortableText.tsx @@ -0,0 +1,40 @@ +import { ArrowUpRight } from "@carbon/icons-react" +import { + PortableText, + PortableTextComponents, + PortableTextProps, +} from "@portabletext/react" + +export const components: PortableTextComponents = { + block: { + normal: ({ children }) =>

{children}

, + blockquote: ({ children }) => ( +
{children}
+ ), + h2: ({ children }) => ( +

{children}

+ ), + }, + marks: { + link: ({ children, value }) => { + return ( + + {children} + + + + ) + }, + }, +} + +const EntryPortableText = ({ value }: PortableTextProps) => { + return +} + +export default EntryPortableText diff --git a/app/(with-map)/entry/[slug]/EntryReferences.tsx b/app/(with-map)/entry/[slug]/EntryReferences.tsx new file mode 100644 index 0000000..2061764 --- /dev/null +++ b/app/(with-map)/entry/[slug]/EntryReferences.tsx @@ -0,0 +1,18 @@ +import { Entry } from "@/app/utils/sanity/types" +import { ArrowUpRight } from "@carbon/icons-react" + +const EntryReferences = (entry?: Entry) => ( + <> + {entry?.references?.map((reference) => ( + + {reference.name} + + ))} + +) + +export default EntryReferences diff --git a/app/(with-map)/entry/[slug]/page.tsx b/app/(with-map)/entry/[slug]/page.tsx new file mode 100644 index 0000000..abefaa3 --- /dev/null +++ b/app/(with-map)/entry/[slug]/page.tsx @@ -0,0 +1,78 @@ +import { Carousel } from "@/app/ui/carousel/Carousel" +import { getFormattedTenureTypes } from "@/app/utils/sanity/entry" +import { + getEntry, + getRelatedEntriesByTenureQuery, +} from "@/app/utils/sanity/queries" +import { ArrowRight } from "@carbon/icons-react" +import { Metadata, ResolvingMetadata } from "next" +import EntryDetails from "./EntryDetails" +import EntryHeader from "./EntryHeader" +import EntryPageChart from "./EntryPageChart" + +type Props = { + params: { slug: string } +} + +export async function generateMetadata( + { params }: Props, + parent: ResolvingMetadata +): Promise { + // read route params + const slug = params.slug + + // fetch data + const entry = await getEntry(slug) + + const { name: title, mainImage } = entry + + return { + title, + openGraph: { + images: [ + { + url: mainImage?.file?.asset?.url ?? "", + alt: title, + }, + ], + }, + } +} + +const EntryPage = async ({ params: { slug } }: Props) => { + const entry = await getEntry(slug) + const relatedEntriesByTenure = await getRelatedEntriesByTenureQuery( + entry.tenureType, + entry._id + ) + + return ( +
+ + + + {entry.tenureType && ( +
+ +
+ )} + + Suggest an improvement to this entry + + +
+ ) +} + +export default EntryPage diff --git a/app/(with-map)/layout.tsx b/app/(with-map)/layout.tsx new file mode 100644 index 0000000..4551eb5 --- /dev/null +++ b/app/(with-map)/layout.tsx @@ -0,0 +1,25 @@ +import BetaBanner from "../ui/BetaBanner" +import EntryPopout from "../ui/EntryPopout" +import Footer from "../ui/footer/Footer" +import Header from "../ui/header/Header" +import SubmitButton from "../ui/layout/SubmitButton" +import MapboxGlobeServerSide from "../ui/map/MapboxGlobeServerSide" +import Sidebar from "../ui/sidebar/Sidebar" + +const MapLayout = ({ children }: { children: React.ReactNode }) => ( +
+ + {children} +
+ +
+
+ + +
+
+
+
+) + +export default MapLayout diff --git a/app/(with-map)/page.tsx b/app/(with-map)/page.tsx new file mode 100644 index 0000000..99225e6 --- /dev/null +++ b/app/(with-map)/page.tsx @@ -0,0 +1,7 @@ +import { Fragment } from "react" + +const AppIndexPage = () => { + return +} + +export default AppIndexPage diff --git a/pages/about.tsx b/app/(without-map)/about/page.tsx similarity index 58% rename from pages/about.tsx rename to app/(without-map)/about/page.tsx index 2621318..ab06a1e 100644 --- a/pages/about.tsx +++ b/app/(without-map)/about/page.tsx @@ -1,14 +1,13 @@ -import { ContentPage } from "@/components/ContentPage" -import { trpc } from "@/lib/trpc" -import Image from "next/future/image" -import NoopLayout from "../components/layout/NoopLayout" +import { ContentPage } from "@/app/ui/ContentPage" +import { getPage } from "@/app/utils/sanity/queries" +import Image from "next/image" -const AboutPage = () => { - const { data: aboutPage } = trpc.page.useQuery({ pageSlug: "about" }) +const AboutPage = async () => { + const aboutPage = await getPage("about") return (
- +
{ ) } -AboutPage.getLayout = NoopLayout - export default AboutPage diff --git a/app/(without-map)/accessibility/page.tsx b/app/(without-map)/accessibility/page.tsx new file mode 100644 index 0000000..fca5930 --- /dev/null +++ b/app/(without-map)/accessibility/page.tsx @@ -0,0 +1,22 @@ +import { Metadata } from "next" +import { ContentPage } from "../../ui/ContentPage" +import { getPage } from "../../utils/sanity/queries" + +const AccessibilityPage = async () => { + const accessibilityPage = await getPage("accessibility") + return +} + +export async function generateMetadata(): Promise { + // fetch data + const accessibilityPage = await getPage("accessibility") + + return { + title: accessibilityPage.title, + // openGraph: { + // images: ['/some-specific-page-image.jpg', ...previousImages], + // }, + } +} + +export default AccessibilityPage diff --git a/app/(without-map)/layout.tsx b/app/(without-map)/layout.tsx new file mode 100644 index 0000000..e401f9b --- /dev/null +++ b/app/(without-map)/layout.tsx @@ -0,0 +1,17 @@ +import { PropsWithChildren } from "react" +import { PageNavbar } from "../ui/PageNavbar" + +type Props = PropsWithChildren<{}> + +const NoMapLayout = (props: Props) => { + const { children } = props + + return ( +
+ +
{children}
+
+ ) +} + +export default NoMapLayout diff --git a/app/(without-map)/licence/page.tsx b/app/(without-map)/licence/page.tsx new file mode 100644 index 0000000..00d083c --- /dev/null +++ b/app/(without-map)/licence/page.tsx @@ -0,0 +1,10 @@ +import { ContentPage } from "@/app/ui/ContentPage" +import { getPage } from "@/app/utils/sanity/queries" + +const LicencePage = async () => { + const licencePage = await getPage("licence") + + return +} + +export default LicencePage diff --git a/components/layout/PatternsLayout.tsx b/app/(without-map)/patterns/PatternsImpl.tsx similarity index 94% rename from components/layout/PatternsLayout.tsx rename to app/(without-map)/patterns/PatternsImpl.tsx index 8e04cd4..b0f6433 100644 --- a/components/layout/PatternsLayout.tsx +++ b/app/(without-map)/patterns/PatternsImpl.tsx @@ -1,13 +1,19 @@ -import { Page, Pattern, PatternClass, PatternInfo } from "@/lib/types" +"use client" +import "client-only" +import { pageComponents } from "@/app/ui/ContentPage" +import { PageNavbar } from "@/app/ui/PageNavbar" +import { PatternIcon } from "@/app/ui/PatternIcon" +import { + Page, + Pattern, + PatternClass, + PatternInfo, +} from "@/app/utils/sanity/types" import { PortableText } from "@portabletext/react" import clsx from "clsx" import { Dispatch, SetStateAction, useEffect, useState } from "react" -import { trpc } from "../../lib/trpc" -import { pageComponents } from "../ContentPage" -import { PageNavbar } from "../PageNavbar" -import { PatternIcon } from "./ui/PatternIcon" -interface PatternsLayoutProps { +type Props = { patternClasses?: PatternClass[] patternsPageData?: Page patternInfoList?: PatternInfo[] @@ -192,7 +198,7 @@ const PatternItem = (props: { ) } -export const PatternsLayout = (props: PatternsLayoutProps) => { +const PatternsImpl = (props: Props) => { const { patternClasses, patternsPageData, patternInfoList } = props const [selectedPatternClass, setSelectedPatternClass] = useState< PatternClass | undefined @@ -218,3 +224,5 @@ export const PatternsLayout = (props: PatternsLayoutProps) => {
) } + +export default PatternsImpl diff --git a/app/(without-map)/patterns/page.tsx b/app/(without-map)/patterns/page.tsx new file mode 100644 index 0000000..02bc720 --- /dev/null +++ b/app/(without-map)/patterns/page.tsx @@ -0,0 +1,27 @@ +import { + getPage, + getPatternClasses, + getPatternInfo, +} from "@/app/utils/sanity/queries" +import { Metadata } from "next" +import PatternsImpl from "./PatternsImpl" + +const PatternsPage = async () => { + const patternClasses = await getPatternClasses() + const patternsPageData = await getPage("patterns") + const patternInfoList = await getPatternInfo() + + return ( + + ) +} + +export const metadata: Metadata = { + title: "Explore the patterns - The Atlas of Ownership", +} + +export default PatternsPage diff --git a/app/(without-map)/terms-of-use/page.tsx b/app/(without-map)/terms-of-use/page.tsx new file mode 100644 index 0000000..4ba588b --- /dev/null +++ b/app/(without-map)/terms-of-use/page.tsx @@ -0,0 +1,22 @@ +import { ContentPage } from "@/app/ui/ContentPage" +import { getPage } from "@/app/utils/sanity/queries" +import { Metadata } from "next" + +const TermsOfUsePage = async () => { + const termsOfUsePage = await getPage("terms-of-use") + + return +} + +export async function generateMetadata(): Promise { + // fetch data + const termsOfUse = await getPage("terms-of-use") + + return { + title: termsOfUse.title, + // openGraph: { + // images: ['/some-specific-page-image.jpg', ...previousImages], + // }, + } +} +export default TermsOfUsePage diff --git a/public/favicon.ico b/app/favicon.ico similarity index 100% rename from public/favicon.ico rename to app/favicon.ico diff --git a/styles/globals.css b/app/globals.css similarity index 98% rename from styles/globals.css rename to app/globals.css index 2645066..7bf8863 100644 --- a/styles/globals.css +++ b/app/globals.css @@ -20,6 +20,7 @@ html, body { padding: 0; margin: 0; + @apply tracking-normal; } a { diff --git a/next-seo.config.ts b/app/layout.tsx similarity index 62% rename from next-seo.config.ts rename to app/layout.tsx index 1718305..805f3da 100644 --- a/next-seo.config.ts +++ b/app/layout.tsx @@ -1,6 +1,20 @@ -import { DefaultSeoProps } from "next-seo" +import { Inter } from "next/font/google" +import React from "react" +import "./globals.css" -const SEO: DefaultSeoProps = { +const inter = Inter({ + subsets: ["latin"], + display: "swap", +}) + +const RootLayout = ({ children }: { children: React.ReactNode }) => ( + + {children} + +) + +export const metadata = { + metadataBase: new URL("https://atlasofownership.org"), title: "The Atlas of Ownership", additionalLinkTags: [{ rel: "icon", href: "/favicon.ico" }], openGraph: { @@ -27,5 +41,4 @@ const SEO: DefaultSeoProps = { cardType: "summary_large_image", }, } - -export default SEO +export default RootLayout diff --git a/components/BetaBanner.tsx b/app/ui/BetaBanner.tsx similarity index 97% rename from components/BetaBanner.tsx rename to app/ui/BetaBanner.tsx index 23c5210..db094dd 100644 --- a/components/BetaBanner.tsx +++ b/app/ui/BetaBanner.tsx @@ -1,3 +1,5 @@ +"use client" +import "client-only" import { ArrowUpRight, Close } from "@carbon/icons-react" import { useState } from "react" diff --git a/components/ContentPage.tsx b/app/ui/ContentPage.tsx similarity index 79% rename from components/ContentPage.tsx rename to app/ui/ContentPage.tsx index bca13bc..e387fa9 100644 --- a/components/ContentPage.tsx +++ b/app/ui/ContentPage.tsx @@ -1,8 +1,6 @@ -import { Page } from "@/lib/types" import { ArrowUpRight } from "@carbon/icons-react" import { PortableText, PortableTextComponents } from "@portabletext/react" -import Head from "next/head" -import React from "react" +import { Page } from "../utils/sanity/types" export const pageComponents: PortableTextComponents = { block: { @@ -37,12 +35,9 @@ interface ContentPageProps { title?: string } -export const ContentPage = ({ page, title }: ContentPageProps) => { +export const ContentPage = ({ page }: ContentPageProps) => { return ( -
- - {title || page?.title} - The Atlas of Ownership - +

{page?.title}

diff --git a/app/ui/EntryPopout.tsx b/app/ui/EntryPopout.tsx new file mode 100644 index 0000000..4f3cc04 --- /dev/null +++ b/app/ui/EntryPopout.tsx @@ -0,0 +1,33 @@ +"use client" +import React, { PropsWithChildren } from "react" +import { motion } from "framer-motion" +import "client-only" +import { usePathname } from "next/navigation" + +const EntryPopout = ({ children }: PropsWithChildren<{}>) => { + const pathname = usePathname() + const entryOpen = pathname.startsWith(`/entry/`) + + return ( + + {children} + + ) +} + +export default EntryPopout diff --git a/components/PageNavbar.tsx b/app/ui/PageNavbar.tsx similarity index 100% rename from components/PageNavbar.tsx rename to app/ui/PageNavbar.tsx diff --git a/app/ui/PatternIcon.tsx b/app/ui/PatternIcon.tsx new file mode 100644 index 0000000..a386e07 --- /dev/null +++ b/app/ui/PatternIcon.tsx @@ -0,0 +1,46 @@ +import { CarbonIconProps } from "@carbon/icons-react/lib/CarbonIcon" +import Image from "next/image" +import { Pattern } from "../utils/sanity/types" + +interface PatternIconProps { + pattern: Partial + className?: string + size: CarbonIconProps["size"] +} + +const FallbackIcon = (props: PatternIconProps) => ( +
+ + + +
+) + +export const PatternIcon = (props: PatternIconProps) => { + const { pattern, size, className } = props + + return pattern.iconUrl ? ( +
+ {`${pattern.name} +
+ ) : ( + + ); +} diff --git a/components/layout/ui/Tag.tsx b/app/ui/Tag.tsx similarity index 100% rename from components/layout/ui/Tag.tsx rename to app/ui/Tag.tsx diff --git a/components/sidebar/Accordion.module.css b/app/ui/accordion/Accordion.module.css similarity index 100% rename from components/sidebar/Accordion.module.css rename to app/ui/accordion/Accordion.module.css diff --git a/components/sidebar/Accordion.tsx b/app/ui/accordion/Accordion.tsx similarity index 89% rename from components/sidebar/Accordion.tsx rename to app/ui/accordion/Accordion.tsx index a757d9c..c310efb 100644 --- a/components/sidebar/Accordion.tsx +++ b/app/ui/accordion/Accordion.tsx @@ -1,11 +1,13 @@ +"use client" +import "client-only" import css from "./Accordion.module.css" import { pipe } from "fp-ts/lib/function" import { AnimatePresence, motion } from "framer-motion" import { ChangeEvent, useState } from "react" -import { A } from "../../lib/fp" import AccordionItem from "./AccordionItem" import { ChevronDown, ChevronUp } from "@carbon/icons-react" -import { EntryType, Pattern, TenureType } from "@/lib/types" +import { EntryType, Pattern, TenureType } from "@/app/utils/sanity/types" +import { A } from "@/app/utils/fp" interface AccordionGroup { color: string @@ -31,11 +33,7 @@ type Props = { const Accordion = (props: Props) => { const { - group: { - color, - name, - description - }, + group: { color, name, description }, items, itemChange, } = props @@ -64,7 +62,7 @@ const Accordion = (props: Props) => { }} transition={{ duration: 0.8, ease: [0.04, 0.62, 0.23, 0.98] }} > - {description &&

{description}

} + {description &&

{description}

} {pipe( items, A.map((item) => ( diff --git a/components/sidebar/AccordionItem.module.css b/app/ui/accordion/AccordionItem.module.css similarity index 100% rename from components/sidebar/AccordionItem.module.css rename to app/ui/accordion/AccordionItem.module.css diff --git a/components/sidebar/AccordionItem.tsx b/app/ui/accordion/AccordionItem.tsx similarity index 100% rename from components/sidebar/AccordionItem.tsx rename to app/ui/accordion/AccordionItem.tsx diff --git a/components/carousel/Carousel.module.css b/app/ui/carousel/Carousel.module.css similarity index 100% rename from components/carousel/Carousel.module.css rename to app/ui/carousel/Carousel.module.css diff --git a/components/carousel/Carousel.tsx b/app/ui/carousel/Carousel.tsx similarity index 67% rename from components/carousel/Carousel.tsx rename to app/ui/carousel/Carousel.tsx index f75b42b..df82e11 100644 --- a/components/carousel/Carousel.tsx +++ b/app/ui/carousel/Carousel.tsx @@ -1,19 +1,22 @@ -import css from "./Carousel.module.css" +import { getFormattedEntryDates } from "@/app/utils/sanity/entry" +import { CarouselEntry } from "@/app/utils/sanity/types" import { ArrowRight } from "@carbon/icons-react" -import { CarouselItem } from "@/lib/types" -import { getFormattedEntryDates } from "@/lib/entry" -import Link from "next/link" import clsx from "clsx" +import Link from "next/link" +import css from "./Carousel.module.css" export interface CarouselProps { - data?: CarouselItem[] + data?: CarouselEntry[] title: string cardClassNames?: string } -const CarouselItem = (props: { item: CarouselItem, cardClassNames: string | undefined }) => { +const CarouselItem = (props: { + item: CarouselEntry + cardClassNames: string | undefined +}) => { return ( - +
@@ -34,9 +37,13 @@ export const Carousel = (props: CarouselProps) => (
{props.data?.map((item, i) => ( - + ))}
-) \ No newline at end of file +) diff --git a/app/ui/footer/Contributors.tsx b/app/ui/footer/Contributors.tsx new file mode 100644 index 0000000..a24bef9 --- /dev/null +++ b/app/ui/footer/Contributors.tsx @@ -0,0 +1,18 @@ +import { getContributors } from "@/app/utils/sanity/queries" +import css from "./Footer.module.css" + +const Contributors = async () => { + const contributors = await getContributors() + return ( +
+ Contributors +
+ {contributors?.map((contributor) => ( +

{contributor}

+ ))} +
+
+ ) +} + +export default Contributors diff --git a/app/ui/footer/Disclaimer.tsx b/app/ui/footer/Disclaimer.tsx new file mode 100644 index 0000000..663c7ba --- /dev/null +++ b/app/ui/footer/Disclaimer.tsx @@ -0,0 +1,10 @@ +import css from "./Footer.module.css" + +const Disclaimer = () => ( +

+ The Atlas of Ownership is maintained by Open Systems Lab, non profit company + 9152368 registered in England and Wales +

+) + +export default Disclaimer diff --git a/components/Footer.module.css b/app/ui/footer/Footer.module.css similarity index 100% rename from components/Footer.module.css rename to app/ui/footer/Footer.module.css diff --git a/app/ui/footer/Footer.tsx b/app/ui/footer/Footer.tsx new file mode 100644 index 0000000..70ea934 --- /dev/null +++ b/app/ui/footer/Footer.tsx @@ -0,0 +1,19 @@ +import Contributors from "./Contributors" +import Disclaimer from "./Disclaimer" +import css from "./Footer.module.css" +import FooterLinks from "./FooterLinks" +import FooterLogos from "./FooterLogos" +import SocialIcons from "./SocialIcons" + +const Footer = () => { + return ( +
+ + + + + +
+ ) +} +export default Footer diff --git a/app/ui/footer/FooterLinks.tsx b/app/ui/footer/FooterLinks.tsx new file mode 100644 index 0000000..f3867a6 --- /dev/null +++ b/app/ui/footer/FooterLinks.tsx @@ -0,0 +1,41 @@ +import Link from "next/link" +import css from "./Footer.module.css" + +const FooterLinks = () => { + const pageLinks = [ + { title: "About", path: "/about" }, + { title: "Explore the patterns", path: "/patterns" }, + { title: "Licence", path: "/licence" }, + { title: "Accessibility", path: "/accessibility" }, + { title: "Terms of use", path: "/terms-of-use" }, + ] + + return ( +
+ ) +} + +export default FooterLinks diff --git a/app/ui/footer/FooterLogos.tsx b/app/ui/footer/FooterLogos.tsx new file mode 100644 index 0000000..4b58835 --- /dev/null +++ b/app/ui/footer/FooterLogos.tsx @@ -0,0 +1,30 @@ +import { getFooterLogos } from "@/app/utils/sanity/queries" +import Image from "next/image" + +const FooterLogos = async () => { + const footerLogos = await getFooterLogos() + + return ( +
+ Thanks to +
+ {footerLogos && + footerLogos.map((footerLogo) => ( + {footerLogo.description} + ))} +
+
+ ); +} + +export default FooterLogos diff --git a/app/ui/footer/SocialIcons.tsx b/app/ui/footer/SocialIcons.tsx new file mode 100644 index 0000000..eb44d62 --- /dev/null +++ b/app/ui/footer/SocialIcons.tsx @@ -0,0 +1,48 @@ +import { LogoGithub, LogoTwitter } from "@carbon/icons-react" +import css from "./Footer.module.css" + +const OSLLogo = () => ( +
+ + + +
+) + +const SocialIcons = () => { + const socialLinks = [ + { + url: "https://twitter.com/OpenSystemsLab", + component: , + }, + { + url: "https://github.com/theopensystemslab", + component: , + }, + { + url: "https://www.opensystemslab.io/", + component: , + }, + ] + + return ( +
+ {socialLinks.map((link, i) => ( + + {link.component} + + ))} +
+ ) +} + +export default SocialIcons diff --git a/components/Header.module.css b/app/ui/header/Header.module.css similarity index 100% rename from components/Header.module.css rename to app/ui/header/Header.module.css diff --git a/components/Header.tsx b/app/ui/header/Header.tsx similarity index 69% rename from components/Header.tsx rename to app/ui/header/Header.tsx index 135145e..003c56e 100644 --- a/components/Header.tsx +++ b/app/ui/header/Header.tsx @@ -9,18 +9,19 @@ const Header = () => (

A map of property rights and obligations across time and space

- - - About - + + About
- - Patterns + + Patterns - - About + + About
diff --git a/components/Search.module.css b/app/ui/header/Search.module.css similarity index 100% rename from components/Search.module.css rename to app/ui/header/Search.module.css diff --git a/app/ui/header/Search.tsx b/app/ui/header/Search.tsx new file mode 100644 index 0000000..74ef453 --- /dev/null +++ b/app/ui/header/Search.tsx @@ -0,0 +1,11 @@ +import { getEntries } from "@/app/utils/sanity/queries" +import React from "react" +import SearchClientComponent from "./SearchClientComponent" + +const Search = async () => { + const entries = await getEntries() + + return +} + +export default Search diff --git a/components/Search.tsx b/app/ui/header/SearchClientComponent.tsx similarity index 83% rename from components/Search.tsx rename to app/ui/header/SearchClientComponent.tsx index 5a7e8bb..34b8eb8 100644 --- a/components/Search.tsx +++ b/app/ui/header/SearchClientComponent.tsx @@ -1,3 +1,5 @@ +"use client" +import "client-only" import { Search as SearchIcon } from "@carbon/icons-react" import clsx from "clsx" import { pipe } from "fp-ts/lib/function" @@ -8,12 +10,11 @@ import { ChangeEvent, useEffect, useRef, useState } from "react" import usePortal from "react-cool-portal" import { useKey } from "react-use" import { useDebouncedCallback } from "use-debounce" -import { A } from "../lib/fp" -import { trpc } from "../lib/trpc" -import { Entry } from "../lib/types" -import { useClickAway } from "../lib/utils" import css from "./Search.module.css" import { motion } from "framer-motion" +import { Entry } from "../../utils/sanity/types" +import { useClickAway } from "../../utils/dom" +import { A } from "../../utils/fp" const SearchResult = ({ entry, @@ -24,23 +25,22 @@ const SearchResult = ({ }) => { return (
- +

{entry.name}

-

{truncate(entry.description, { length: 280, separator: " " })}

+

+ {truncate(entry?.content?.[0].children[0].text ?? "", { + length: 280, + separator: " ", + })} +

) } -const Search = () => { - const { data: entries = [] } = trpc.entries.useQuery() - +const SearchClientComponent = ({ entries }: { entries: Entry[] }) => { const [results, setResults] = useState([]) const [searchQuery, setSearchQuery] = useState("") const [isOpen, setIsOpen] = useState(false) @@ -100,7 +100,11 @@ const Search = () => {
{ onChange={handler} placeholder="Search the atlas" className={css.searchInput} - > - + >
{searchQuery.length > 0 && ( @@ -151,4 +154,4 @@ const Search = () => { ) } -export default Search +export default SearchClientComponent diff --git a/components/layout/GlobeLayout.module.css b/app/ui/layout/SubmitButton.module.css similarity index 100% rename from components/layout/GlobeLayout.module.css rename to app/ui/layout/SubmitButton.module.css diff --git a/app/ui/layout/SubmitButton.tsx b/app/ui/layout/SubmitButton.tsx new file mode 100644 index 0000000..d3c0c21 --- /dev/null +++ b/app/ui/layout/SubmitButton.tsx @@ -0,0 +1,17 @@ +import { ArrowRight } from "@carbon/icons-react" +import css from "./SubmitButton.module.css" + +const SubmitButton = () => ( + +) + +export default SubmitButton diff --git a/components/map/MapboxGlobe.tsx b/app/ui/map/MapboxGlobe.tsx similarity index 82% rename from components/map/MapboxGlobe.tsx rename to app/ui/map/MapboxGlobe.tsx index 2daac6b..d1043ba 100644 --- a/components/map/MapboxGlobe.tsx +++ b/app/ui/map/MapboxGlobe.tsx @@ -1,4 +1,4 @@ -import { trpc } from "@/lib/trpc" +"use client" import { pipe } from "fp-ts/lib/function" import { Feature, @@ -6,9 +6,8 @@ import { GeoJsonProperties, Geometry, } from "geojson" -import "mapbox-gl/dist/mapbox-gl.css" -import { useRouter } from "next/router" -import React, { useCallback, useEffect, useMemo } from "react" +import { usePathname } from "next/navigation" +import { useEffect, useMemo } from "react" import Map, { AttributionControl, GeoJSONSource, @@ -18,31 +17,63 @@ import Map, { Source, } from "react-map-gl" import { ref } from "valtio" -import { A, O, S } from "../../lib/fp" -import store from "../../lib/store" -import { Entry, TenureType } from "../../lib/types" +import { A, O, S } from "../../utils/fp" +import { + Entry, + Pattern, + PatternClass, + TenureType, +} from "../../utils/sanity/types" +import store from "../../utils/store" import { useSelection } from "../sidebar/selection" -import Markers from "./Markers" - -const MapboxGlobe = () => { - const { data: entries = [] } = trpc.entries.useQuery() - - const router = useRouter() +import Markers from "./markers/Markers" + +const MapboxGlobe = ({ + entries, + patterns, + patternClasses, +}: { + entries: Entry[] + patterns: Pattern[] + patternClasses: PatternClass[] +}) => { + const sourceId = "entries" - const handleRouteChange = useCallback(() => { - if (router.asPath === "/") store.lastBirdseyeViewState = store.viewState - }, [router.asPath]) + const clusterLayer: LayerProps = { + id: "entryClusters", + type: "circle", + source: sourceId, + filter: ["has", "point_count"], + paint: { + "circle-color": "#ffffff", + "circle-radius": 18, + }, + } - useEffect(() => { - router.events.on("beforeHistoryChange", handleRouteChange) - router.beforePopState((popstate) => { - if (popstate.as === "/") store.map?.flyTo(store.lastBirdseyeViewState) - return true - }) - return () => - void router.events.off("beforeHistoryChange", handleRouteChange) - }, [handleRouteChange, router, router.events]) + const clusterCountLayer: LayerProps = { + id: "cluster-count", + type: "symbol", + source: sourceId, + filter: ["has", "point_count"], + layout: { + "text-field": "{point_count_abbreviated}", + // "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], + "text-size": 12, + }, + } + const unclusteredPointLayer: LayerProps = { + id: "unclustered-point", + type: "circle", + source: sourceId, + filter: ["!", ["has", "point_count"]], + paint: { + "circle-color": "#fff", + "circle-radius": 0, + // "circle-stroke-width": 1, + // "circle-stroke-color": "#fff", + }, + } const { patternNames: selectedPatternNames, entryType: selectedEntryTypes, @@ -65,7 +96,6 @@ const MapboxGlobe = () => { slug: entry.slug?.current ?? null, }, }) - const data = useMemo>(() => { const patternNameFilter = (entry: Entry) => { if (selectedPatternNames.length === 0) return true @@ -109,44 +139,6 @@ const MapboxGlobe = () => { } }, [entries, selectedPatternNames, selectedEntryTypes, selectedTenureTypes]) - const sourceId = "entries" - - const clusterLayer: LayerProps = { - id: "entryClusters", - type: "circle", - source: sourceId, - filter: ["has", "point_count"], - paint: { - "circle-color": "#ffffff", - "circle-radius": 18, - }, - } - - const clusterCountLayer: LayerProps = { - id: "cluster-count", - type: "symbol", - source: sourceId, - filter: ["has", "point_count"], - layout: { - "text-field": "{point_count_abbreviated}", - // "text-font": ["DIN Offc Pro Medium", "Arial Unicode MS Bold"], - "text-size": 12, - }, - } - - const unclusteredPointLayer: LayerProps = { - id: "unclustered-point", - type: "circle", - source: sourceId, - filter: ["!", ["has", "point_count"]], - paint: { - "circle-color": "#fff", - "circle-radius": 0, - // "circle-stroke-width": 1, - // "circle-stroke-color": "#fff", - }, - } - const onClick = (event: MapLayerMouseEvent) => { const feature: any = event?.features?.[0] if (!feature) return @@ -184,6 +176,13 @@ const MapboxGlobe = () => { ) } + const pathname = usePathname() + + useEffect(() => { + if (pathname === "/") store.map?.flyTo(store.lastBirdseyeViewState) + // TODO: could set last viewState per zoom threshold? + }, [pathname]) + return ( { @@ -209,16 +208,15 @@ const MapboxGlobe = () => { cluster={true} clusterMaxZoom={13} clusterRadius={150} - > - - + + ) } -export default React.memo(MapboxGlobe) +export default MapboxGlobe diff --git a/app/ui/map/MapboxGlobeServerSide.tsx b/app/ui/map/MapboxGlobeServerSide.tsx new file mode 100644 index 0000000..5639637 --- /dev/null +++ b/app/ui/map/MapboxGlobeServerSide.tsx @@ -0,0 +1,19 @@ +import "server-only" +import React from "react" +import MapboxGlobe from "./MapboxGlobe" +import { + getEntries, + getPatternClasses, + getPatterns, +} from "@/app/utils/sanity/queries" +import "mapbox-gl/dist/mapbox-gl.css" + +const MapboxGlobeServerSide = async () => { + const entries = await getEntries() + const patterns = await getPatterns() + const patternClasses = await getPatternClasses() + + return +} + +export default MapboxGlobeServerSide diff --git a/app/ui/map/markers/DataRow.tsx b/app/ui/map/markers/DataRow.tsx new file mode 100644 index 0000000..96f7de5 --- /dev/null +++ b/app/ui/map/markers/DataRow.tsx @@ -0,0 +1,55 @@ +import { backgroundColorClasses } from "./styles" +import { PatternClassTotal } from "./MarkerChart" + +type Props = { + patternClassTotal: PatternClassTotal + showLabels: boolean +} + +const DataRow = (props: Props) => { + const { patternClassTotal: patternClass, showLabels } = props + + return ( +
+ {showLabels ? ( +
+ {patternClass.name} +
+ ) : ( + `` + )} +
+
0 && + backgroundColorClasses[patternClass.name!] + } h-10`} + style={{ gridColumn: `span ${patternClass.avgObligations}` }} + >
+
+
+
0 && + backgroundColorClasses[patternClass.name!] + } h-10`} + style={{ gridColumn: `span ${patternClass.avgRights}` }} + >
+
+
+ ) +} + +export default DataRow diff --git a/app/ui/map/markers/Marker.tsx b/app/ui/map/markers/Marker.tsx new file mode 100644 index 0000000..3d22385 --- /dev/null +++ b/app/ui/map/markers/Marker.tsx @@ -0,0 +1,144 @@ +"use client" +import "client-only" +import { Entry, Pattern, PatternClass } from "@/app/utils/sanity/types" +import store from "@/app/utils/store" +import Link from "next/link" +// import Chart from "../Chart" +import { getFormattedTenureTypes } from "@/app/utils/sanity/entry" +import { ArrowRight } from "@carbon/icons-react" +import { useState } from "react" +import { Marker as MapboxMarker, Popup } from "react-map-gl" +import MarkerChart from "./MarkerChart" +import _ from "lodash" +// import { useGetEntryFromSlug } from "@/lib/queries" +// import _ from "lodash" +// import { getFormattedTenureTypes } from "@/lib/entry" + +type Props = { + // slug: string + entry: Entry + patterns: Pattern[] + patternClasses: PatternClass[] + lat: number + lng: number +} + +const Marker = (props: Props) => { + const { lat, lng, entry, patterns, patternClasses } = props + + const slug = entry.slug?.current ?? "" + + const [showPopup, setShowPopup] = useState(false) + + const terms = entry.terms + + // Format the list of individual terms that apply to this entry + let formattedTerms = _(terms) + .map((term: any) => ({ + pattern: _.find(patterns, ["_id", term.pattern?._ref]), + patternName: _.find(patterns, ["_id", term.pattern?._ref])?.name, + type: _.capitalize(_.find(patterns, ["_id", term.pattern?._ref])?.type), + strength: term.strength, // 1-5 + description: term.description, + legalMechanisms: term.termLegalMechanisms?.map( + (mechanism: Record) => mechanism.name + ), + })) + .map((term: any) => ({ + meta: term.pattern, + name: term.patternName, + patternClassName: _.find(patternClasses, [ + "_id", + term.pattern?.class?._ref, + ])?.name, + patternClassOrder: _.find(patternClasses, [ + "_id", + term.pattern?.class?._ref, + ])?.order, + patternIconUrl: term.pattern?.iconUrl, + type: term.type === "Limitation" ? "Obligation" : term.type, + strength: term.strength, + description: term.description, + legalMechanisms: term.legalMechanisms, + })) + .sortBy("patternClassOrder", "name") + .value() + // Rollup the individual terms by their pattern class + let totalsByPatternClass = _(formattedTerms) + .groupBy("patternClassName") + .map((terms: any) => ({ + terms: terms, + meta: _.find(patternClasses, ["_id", terms[0].meta?.class._ref]), + name: terms[0].patternClassName, + avgRights: _(terms).filter({ type: "Right" }).meanBy("strength"), + avgObligations: _(terms) + .filter({ type: "Obligation" }) + .meanBy("strength"), + })) + .sortBy("meta.order") + .value() + + const PopupContent = () => ( +
+

{entry?.name}

+ + {getFormattedTenureTypes(entry?.tenureType)} + + {entry.terms?.length && ( + + )} + + +
setShowPopup(false)} + > + Find out more + +
+
+ +
+ ) + + return ( + <> + { + store.map?.flyTo({ + center: { lat, lng }, + padding: { top: 500, bottom: 0, left: 0, right: 0 }, + zoom: 18, + }) + e.originalEvent.stopPropagation() + setShowPopup(!showPopup) + }} + > +
+ 1 +
+
+ {showPopup && ( + setShowPopup(false)} + > + + + )} + + ) +} + +export default Marker diff --git a/app/ui/map/markers/MarkerChart.tsx b/app/ui/map/markers/MarkerChart.tsx new file mode 100644 index 0000000..9fabdeb --- /dev/null +++ b/app/ui/map/markers/MarkerChart.tsx @@ -0,0 +1,41 @@ +import DataRow from "./DataRow" +import { PatternClass } from "@/app/utils/sanity/types" + +export type PatternClassTotal = { + name: string | undefined + meta: PatternClass | undefined + avgObligations: number + avgRights: number +} + +type Props = { + data: PatternClassTotal[] + showLabels: boolean +} + +const MarkerChart = (props: Props) => { + const { data: totalsByPatternClass, showLabels } = props + + return ( +
+
+ {showLabels ?
: ``} +
+ Obligations +
+
+ Rights +
+
+ {totalsByPatternClass.map((patternClass) => ( + + ))} +
+ ) +} + +export default MarkerChart diff --git a/app/ui/map/markers/Markers.tsx b/app/ui/map/markers/Markers.tsx new file mode 100644 index 0000000..5189dc4 --- /dev/null +++ b/app/ui/map/markers/Markers.tsx @@ -0,0 +1,56 @@ +"use client" +import { A, O } from "@/app/utils/fp" +import { Entry, Pattern, PatternClass } from "@/app/utils/sanity/types" +import { useStore } from "@/app/utils/store" +import { pipe } from "fp-ts/lib/function" +import { Fragment } from "react" +import Marker from "./Marker" + +type Props = { + entries: Entry[] + patterns: Pattern[] + patternClasses: PatternClass[] +} + +const Markers = (props: Props) => { + const { entries, patterns, patternClasses } = props + + const { unclusteredSlugs } = useStore() + + return ( + + {pipe( + unclusteredSlugs, + A.filterMap((slug) => { + return pipe( + entries, + A.findFirstMap((entry) => + entry.slug?.current === slug + ? O.some( + pipe(entry, (entry) => { + const { + geopoint: { lat, lng }, + } = entry.location ?? { geopoint: { lat: 0, lng: 0 } } + + return ( + + ) + }) + ) + : O.none + ) + ) + }) + )} + + ) +} + +export default Markers diff --git a/app/ui/map/markers/styles.ts b/app/ui/map/markers/styles.ts new file mode 100644 index 0000000..63fb75c --- /dev/null +++ b/app/ui/map/markers/styles.ts @@ -0,0 +1,38 @@ +// TODO: Move to style utils +// maps patternClass.name to custom color keys defined in tailwind.config.js +// tailwind doesn't support templated class names, hence we need to use this lookup +export const backgroundColorClasses: any = { + Rent: "bg-rent", + Transfer: "bg-transfer", + Administration: "bg-administration", + Eligibility: "bg-eligibility", + "Security of tenure": "bg-security", + Develop: "bg-develop", + Stewardship: "bg-stewardship", + Use: "bg-use", + Access: "bg-access", +} + +export const hoverColorClasses: any = { + Rent: "hover:bg-rent/70", + Transfer: "hover:bg-transfer/70", + Administration: "hover:bg-administration/70", + Eligibility: "hover:bg-eligibility/70", + "Security of tenure": "hover:bg-security/70", + Develop: "hover:bg-develop/70", + Stewardship: "hover:bg-stewardship/70", + Use: "hover:bg-use/70", + Access: "hover:bg-access/70", +} + +export const descriptionBackgroundColorClasses: any = { + Rent: "bg-rent/20", + Transfer: "bg-transfer/20", + Administration: "bg-administration/20", + Eligibility: "bg-eligibility/20", + "Security of tenure": "bg-security/20", + Develop: "bg-develop/20", + Stewardship: "bg-stewardship/20", + Use: "bg-use/20", + Access: "bg-access/20", +} diff --git a/components/sidebar/Sidebar.module.css b/app/ui/sidebar/Sidebar.module.css similarity index 100% rename from components/sidebar/Sidebar.module.css rename to app/ui/sidebar/Sidebar.module.css diff --git a/app/ui/sidebar/Sidebar.tsx b/app/ui/sidebar/Sidebar.tsx new file mode 100644 index 0000000..c0c2b6d --- /dev/null +++ b/app/ui/sidebar/Sidebar.tsx @@ -0,0 +1,19 @@ +import { + getPatternClasses, + getPatternsWithClass, +} from "@/app/utils/sanity/queries" +import SidebarClientComponent from "./SidebarClientComponent" + +const Sidebar = async () => { + const patterns = await getPatternsWithClass() + const patternClasses = await getPatternClasses() + + return ( + + ) +} + +export default Sidebar diff --git a/components/sidebar/Sidebar.tsx b/app/ui/sidebar/SidebarClientComponent.tsx similarity index 71% rename from components/sidebar/Sidebar.tsx rename to app/ui/sidebar/SidebarClientComponent.tsx index 38f7def..3c83e12 100644 --- a/components/sidebar/Sidebar.tsx +++ b/app/ui/sidebar/SidebarClientComponent.tsx @@ -1,14 +1,20 @@ +"use client" +import "client-only" import css from "./Sidebar.module.css" import { pipe } from "fp-ts/lib/function" import { motion } from "framer-motion" import theme from "tailwindcss/defaultTheme" -import { A } from "../../lib/fp" -import { trpc } from "../../lib/trpc" -import Accordion, { EntryFilterData, AccordionItemData } from "./Accordion" -import { ChevronLeft, ChevronRight, RadioButton, RadioButtonChecked } from "@carbon/icons-react" -import { toggleSidebar, useStore } from "lib/store" +import Accordion, { + EntryFilterData, + AccordionItemData, +} from "../accordion/Accordion" +import { + ChevronLeft, + ChevronRight, + RadioButton, + RadioButtonChecked, +} from "@carbon/icons-react" import { ChangeEvent, useEffect, useState } from "react" -import { EntryType, Pattern, TenureType } from "@/lib/types" import { deselectAll, deselectEntryType, @@ -21,7 +27,15 @@ import { useSelection, } from "./selection" import clsx from "clsx" -import { PatternIcon } from "../layout/ui/PatternIcon" +import { + EntryType, + Pattern, + PatternClass, + TenureType, +} from "@/app/utils/sanity/types" +import { A } from "@/app/utils/fp" +import { PatternIcon } from "../PatternIcon" +import { toggleSidebar, useStore } from "@/app/utils/store" const Chevvy = (props: any) => (
@@ -36,7 +50,11 @@ const Chevvy = (props: any) => ( }, }} > - {props?.open ? : } + {props?.open ? ( + + ) : ( + + )}
) @@ -51,14 +69,28 @@ const ToggleAllEntries = () => { return ( <> -
) From 002ca7fb97c1b9e89da2d9eccdf9441c9a2eff52 Mon Sep 17 00:00:00 2001 From: Thomas O'Neill Date: Mon, 5 Feb 2024 06:47:47 +0000 Subject: [PATCH 4/6] npmrc for pnpm public hoisting --- .npmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .npmrc diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..9d774d2 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=* \ No newline at end of file From a2d3561e35e3e206d8b75eac75d1f5b9d6d8b448 Mon Sep 17 00:00:00 2001 From: Thomas O'Neill Date: Mon, 5 Feb 2024 08:12:54 +0000 Subject: [PATCH 5/6] "Present" if no end date --- app/utils/sanity/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/utils/sanity/entry.ts b/app/utils/sanity/entry.ts index bfab949..a4fd3e4 100644 --- a/app/utils/sanity/entry.ts +++ b/app/utils/sanity/entry.ts @@ -4,7 +4,7 @@ export const getFormattedEntryDates = (dates?: Entry["dates"]): string => dates?.start ? new Date(Date.parse(dates.start)).getFullYear() + " - " + - (dates.end ? new Date(Date.parse(dates.end)).getFullYear() : "") + (dates.end ? new Date(Date.parse(dates.end)).getFullYear() : "Present") : "Unknown dates" export const getFormattedTenureTypes = ( From 04ec0b742450f6b2a683304956b9fd0779bc10b1 Mon Sep 17 00:00:00 2001 From: Thomas O'Neill Date: Mon, 5 Feb 2024 08:57:49 +0000 Subject: [PATCH 6/6] upgrade dates to support BCE --- app/utils/sanity/entry.ts | 24 ++++++++++++++++++------ app/utils/sanity/types.ts | 4 ++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/utils/sanity/entry.ts b/app/utils/sanity/entry.ts index a4fd3e4..fe5bbba 100644 --- a/app/utils/sanity/entry.ts +++ b/app/utils/sanity/entry.ts @@ -1,11 +1,23 @@ import { Entry, TenureType } from "./types" -export const getFormattedEntryDates = (dates?: Entry["dates"]): string => - dates?.start - ? new Date(Date.parse(dates.start)).getFullYear() + - " - " + - (dates.end ? new Date(Date.parse(dates.end)).getFullYear() : "Present") - : "Unknown dates" +const formatYear = (year: number): string => + year < 0 ? `${Math.abs(year)} BCE` : year.toString() + +export const getFormattedEntryDates = (dates?: { + startYear?: number + endYear?: number +}): string => { + if (dates?.startYear !== null && typeof dates?.startYear !== "undefined") { + const startYearFormatted = formatYear(dates.startYear) + const endYearFormatted = + dates.endYear !== null && typeof dates.endYear !== "undefined" + ? formatYear(dates.endYear) + : "Present" + return `${startYearFormatted} - ${endYearFormatted}` + } else { + return "Unknown dates" + } +} export const getFormattedTenureTypes = ( tenureType?: Entry["tenureType"] diff --git a/app/utils/sanity/types.ts b/app/utils/sanity/types.ts index c41ab40..cbd0dd9 100644 --- a/app/utils/sanity/types.ts +++ b/app/utils/sanity/types.ts @@ -66,8 +66,8 @@ export type Entry = { content?: PortableTextBlock[] location?: Location dates?: { - start?: string - end?: string + startYear?: number + endYear?: number } mainImage?: { source?: string