diff --git a/.env b/.env index ad08936..85c4681 100644 --- a/.env +++ b/.env @@ -4,4 +4,5 @@ PUBLIC_IMPRESSO_API_HOST=http://localhost PUBLIC_IMPRESSO_API_PATH=/public-api PUBLIC_IMPRESSO_WS_API_HOST=http://localhost PUBLIC_IMPRESSO_WS_API_PATH=/socket.io -PUBLIC_IMPRESSO_SQ_LS_KEY=impressoLatestSearchQuery \ No newline at end of file +PUBLIC_IMPRESSO_SQ_LS_KEY=impressoLatestSearchQuery +GITHUB_TOKEN=yourgithubtoken \ No newline at end of file diff --git a/.github/workflows/docker-build-latest.yml b/.github/workflows/docker-build-latest.yml index 2a106bc..fd5b045 100644 --- a/.github/workflows/docker-build-latest.yml +++ b/.github/workflows/docker-build-latest.yml @@ -76,3 +76,4 @@ jobs: PUBLIC_IMPRESSO_DATALAB_BASE=${{ secrets.PUBLIC_IMPRESSO_DATALAB_BASE }} PUBLIC_IMPRESSO_API_PATH=/public-api/v1 PUBLIC_IMPRESSO_WS_API_PATH=/api/socket.io + GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 545bfba..7d7a8dd 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ src/styles/fonts.css impresso-datalab.code-workspace .env.development .env.local +*.local diff --git a/Dockerfile b/Dockerfile index dc40093..bf9c5ca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ ARG PUBLIC_IMPRESSO_DATALAB_SITE ARG PUBLIC_IMPRESSO_DATALAB_BASE ARG PUBLIC_IMPRESSO_API_PATH ARG PUBLIC_IMPRESSO_WS_API_PATH - +ARG GITHUB_TOKEN # Set the working directory inside the container WORKDIR /app @@ -39,7 +39,7 @@ ENV PUBLIC_IMPRESSO_DATALAB_SITE=$PUBLIC_IMPRESSO_DATALAB_SITE ENV PUBLIC_IMPRESSO_DATALAB_BASE=$PUBLIC_IMPRESSO_DATALAB_BASE ENV PUBLIC_IMPRESSO_API_PATH=$PUBLIC_IMPRESSO_API_PATH ENV PUBLIC_IMPRESSO_WS_API_PATH=${PUBLIC_IMPRESSO_WS_API_PATH} - +ENV GITHUB_TOKEN=${GITHUB_TOKEN} # Build the Astro site for production RUN npm run build diff --git a/astro.config.mjs b/astro.config.mjs index 5894572..c921986 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -39,9 +39,9 @@ if (process.env.NODE_ENV === "development") { // https://astro.build/config export default defineConfig({ integrations: [react(), mdx()], - legacy: { - collections: true, - }, + // legacy: { + // collections: true, + // }, site: process.env.PUBLIC_IMPRESSO_DATALAB_SITE || "http://localhost:4321", base: process.env.PUBLIC_IMPRESSO_DATALAB_BASE || "/", ssr: { diff --git a/package-lock.json b/package-lock.json index 6bde5d8..62c0e16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6855,10 +6855,11 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -6879,7 +6880,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -6894,6 +6895,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/cookie": { @@ -6921,10 +6926,11 @@ "dev": true }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", - "dev": true + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" }, "node_modules/extend": { "version": "3.0.2", diff --git a/src/components/App.astro b/src/components/App.astro index 06b8df2..8c0f3b1 100644 --- a/src/components/App.astro +++ b/src/components/App.astro @@ -22,8 +22,8 @@ const seriesDataIndex = {} as any const numberOfSeries = series.length for(const seriesEntry of series){ - seriesDataIndex[seriesEntry.slug] = await getRecursivelyEntryData(seriesEntry) - seriesDataIndex[seriesEntry.slug]['body'] = seriesEntry.body + seriesDataIndex[seriesEntry.id] = await getRecursivelyEntryData(seriesEntry) + seriesDataIndex[seriesEntry.id]['body'] = seriesEntry.body } const seriesValues:Collection[] = Object.values(seriesDataIndex) diff --git a/src/components/AuthorCard.tsx b/src/components/AuthorCard.tsx index a8fc682..47abc07 100644 --- a/src/components/AuthorCard.tsx +++ b/src/components/AuthorCard.tsx @@ -1,5 +1,5 @@ export interface Author { - slug: string + id: string name: string fullName?: string } diff --git a/src/components/CorpusAccess.tsx b/src/components/CorpusAccess.tsx new file mode 100644 index 0000000..43b99cc --- /dev/null +++ b/src/components/CorpusAccess.tsx @@ -0,0 +1,9 @@ +export type CorpusAccessProps = { + className?: string +} + +const CorpusAccess: React.FC = ({ className = "" }) => { + return
test
+} + +export default CorpusAccess diff --git a/src/components/DatasetCard.tsx b/src/components/DatasetCard.tsx new file mode 100644 index 0000000..8758156 --- /dev/null +++ b/src/components/DatasetCard.tsx @@ -0,0 +1,98 @@ +import type { FC } from "react" +import type { Dataset } from "../types" +import { Col, OverlayTrigger, Row, Tooltip } from "react-bootstrap" +import { CheckCircleSolid, Xmark, XmarkCircleSolid } from "iconoir-react" +import { + PlanAvailabilityLabels, + PlanEducational, + PlanGuest, + PlanImpressoUser, + PlanNone, + PlanResearcher, +} from "../constants" + +export type DatasetCardProps = { + dataset: Dataset + userPlan?: string + className?: string +} + +const compareDatasetPlanWithUserPlan = ( + userPlan: string, + datasetPlan: string +) => { + if (userPlan === datasetPlan || datasetPlan === PlanGuest) { + return + } + if ( + [PlanGuest, PlanImpressoUser].includes(datasetPlan) && + [PlanImpressoUser, PlanResearcher, PlanEducational].includes(userPlan) + ) { + return + } + if (datasetPlan === PlanNone) { + return ( + + This feature is not yet avalable to any plan + + } + > + + + ) + } + + return ( + + {PlanAvailabilityLabels[datasetPlan] ?? datasetPlan} + + } + > + + + ) +} + +const DatasetCard: FC = ({ + dataset, + userPlan = PlanGuest, + className = "", +}) => { + // translate + return ( + + {dataset.startYear} + {dataset.endYear} + {dataset.medium} + +

+ {dataset.mediaTitle} — {dataset.associatedPartner} +

+
+
{dataset.media}
+
+ {dataset.permittedUse} + + + {[ + dataset.minimumUserPlanRequiredToExploreInWebapp, + dataset.minimumUserPlanRequiredToExportTranscripts, + dataset.minimumUserPlanRequiredToExportIllustration, + ].map((plan, i) => ( + + {compareDatasetPlanWithUserPlan(userPlan, plan)} + + ))} + + +
+ +
+ ) +} + +export default DatasetCard diff --git a/src/components/DatasetsModal.tsx b/src/components/DatasetsModal.tsx new file mode 100644 index 0000000..0ce73c0 --- /dev/null +++ b/src/components/DatasetsModal.tsx @@ -0,0 +1,195 @@ +import { useRef, useState } from "react" +import type { Dataset } from "./../types" +import Page from "./Page" +import { Col, Container, Form, Row } from "react-bootstrap" +import DatasetCard from "./DatasetCard" +import { useBrowserStore, usePersistentStore } from "../store" +import { BrowserViewLogin } from "../constants" +import MarkdownSnippet from "./MarkdownSnippet" + +export type DatasetModalProps = { + title?: string + modalTitle?: string + content: string + displayFeatures?: boolean + datasets: Dataset[] +} + +const SortByPropAsString = (_prop: string, direction: "asc" | "desc" = "asc") => + function (a: Dataset, b: Dataset) { + if (direction === "asc") { + return a.id.localeCompare(b.id) + } else { + return b.id.localeCompare(a.id) + } + } + +// const AvailableSortings = { +// idAsc: SortByPropAsString("id", "asc"), +// idDesc: SortByPropAsString("id", "desc"), +// } + +// const FilterByMediaTitle = (identity: string) => (dataset: Dataset) => +// dataset.mediaTitle === identity + +// const FilterByNone = () => (_dataset: Dataset) => true + +// const AvailableFilterBy = { +// mediaTitle: FilterByMediaTitle, +// none: FilterByNone, +// } +type filterValues = { + search: string +} + +interface DatasetFilter { + key: "search" | keyof Dataset + accessor: (dataset: Dataset) => boolean +} + +const DatasetModal: React.FC = ({ + modalTitle = "Datasets", + content, + title, + datasets = [], +}) => { + const [user, userPlan] = usePersistentStore((state) => [ + state.user, + state.userPlan, + ]) + + const setView = useBrowserStore((state) => state.setView) + const delaySetStateTimerRef = useRef(null) + const [filterBy, setFilterBy] = useState([]) + + const filterValuesRef = useRef({ + search: "", + }) + + const sortedDatasets: Dataset[] = datasets + .filter((d: Dataset) => { + if (filterBy.length === 0) return true + return filterBy.every((f) => f.accessor(d)) + }) + .sort(SortByPropAsString("id", "asc")) + + const updateFilterBy = (key: keyof filterValues, value: string) => { + filterValuesRef.current[key] = value + console.info("[DatasetModal] @updateFilterBy", key, value) + // handpick the fields to preview + if (delaySetStateTimerRef.current) { + clearTimeout(delaySetStateTimerRef.current) + } + delaySetStateTimerRef.current = setTimeout(() => { + if (filterValuesRef.current.search === "") { + setFilterBy([]) + return + } + setFilterBy([ + { + key: "search", + accessor: (d: Dataset) => { + const q = filterValuesRef.current.search.toLocaleLowerCase() + if (d.mediaTitle.toLocaleLowerCase().indexOf(q) > -1) { + return true + } + if (d.id.toLocaleLowerCase().indexOf(q) > -1) { + return true + } + return false + }, + }, + ]) + }, 250) + } + + return ( + + + + +

{title}

+ + +
+ + +
+ + + Period + + + Medium + + + + + Title{" "} + {filterBy.length ? ( + + {sortedDatasets.length} of {datasets.length} + + ) : null} + + updateFilterBy("search", e.target.value)} + placeholder="..." + /> + + + + Data Availability +
+
+ {user ? userPlan : "(guest)"} +
+
+ {user ? ( + "Discover Available Datasets according to your plan" + ) : ( + + Please{" "} + setView(BrowserViewLogin)} + > + login + {" "} + to check data availability according to your plan + + )} +
+ + +
+ +
+ {sortedDatasets.map((dataset) => ( + + ))} +
+
+ ) +} + +export default DatasetModal diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 4cfe161..81598cd 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -213,6 +213,11 @@ const Footer: React.FC<{ Plans +
  • + + Datasets + +
  • Terms of Use diff --git a/src/components/NotebookCard.tsx b/src/components/NotebookCard.tsx index 102b0ce..54cd859 100644 --- a/src/components/NotebookCard.tsx +++ b/src/components/NotebookCard.tsx @@ -7,7 +7,7 @@ import { DateTime } from "luxon" import { OverlayTrigger, Tooltip } from "react-bootstrap" export interface Notebook { - slug: string + id: string href: string title: string langModel?: string @@ -36,7 +36,7 @@ const NotebookCard: React.FC<{ "- title:", notebook?.title, "notebook.langModel", - notebook.langModel, + notebook.langModel ) return (
    diff --git a/src/components/NotebookViewer.tsx b/src/components/NotebookViewer.tsx index 74c5a83..95cda95 100644 --- a/src/components/NotebookViewer.tsx +++ b/src/components/NotebookViewer.tsx @@ -67,7 +67,7 @@ const splitTextWithCellInfo = (text: string): Array => { return cells } const getGithubIssuesUrl = ( - githubUrl: string, + githubUrl: string ): { url: string; account: string; repository: string } => { const repoPattern = /github\.com\/([^/]+)\/([^/]+)/ const match = repoPattern.exec(githubUrl) @@ -125,7 +125,7 @@ const NotebookViewer: React.FC = ({
    By{" "} {notebook.authors.map((author) => ( - + ))}
    {notebook.langModel && ( @@ -240,7 +240,7 @@ const NotebookViewer: React.FC = ({ }} > {notebook.seealso.map((notebook) => ( -
  • +
  • ))} diff --git a/src/components/NotebooksModal.tsx b/src/components/NotebooksModal.tsx index 3ee760b..55b8ddd 100644 --- a/src/components/NotebooksModal.tsx +++ b/src/components/NotebooksModal.tsx @@ -18,7 +18,7 @@ const NotebooksModal: React.FC = ({ notebooks = [] }) => { {notebooks.map((notebook) => ( diff --git a/src/components/ProfileModal.tsx b/src/components/ProfileModal.tsx index b869093..e2c720f 100644 --- a/src/components/ProfileModal.tsx +++ b/src/components/ProfileModal.tsx @@ -4,7 +4,6 @@ import { BrowserViewProfile } from "../constants" // import { useState } from "react" // import { type User } from "./UserCard" import RegisterForm, { type RegisterFormPreview } from "./RegisterForm" -import Link from "./Link" import { useEffect, useState } from "react" import type { FeathersError } from "@feathersjs/errors" import { userService } from "../services" diff --git a/src/components/RegisterForm.tsx b/src/components/RegisterForm.tsx index f128fd3..ef2b409 100644 --- a/src/components/RegisterForm.tsx +++ b/src/components/RegisterForm.tsx @@ -2,9 +2,9 @@ import { useEffect, useRef, useState } from "react" import { Col, Form, Row } from "react-bootstrap" import UserCard from "./UserCard" import { - PlanAcademicUser, + PlanResearcher, PlanImpressoUser, - PlanStudentUser, + PlanEducational, PlanLabels, BrowserViewTermsOfUse, } from "../constants" @@ -12,6 +12,7 @@ import { useBrowserStore, usePersistentStore } from "../store" import { DateTime } from "luxon" import { BadRequest, type FeathersError } from "@feathersjs/errors" import ErrorManager, { type BadRequestData } from "./ErrorManager" +import type { Group } from "../types" const Colors: string[] = [ "#96ceb4", @@ -49,7 +50,7 @@ const generatePattern = (): string[] => { return colors } -const Plans = [PlanImpressoUser, PlanStudentUser, PlanAcademicUser] +const Plans = [PlanImpressoUser, PlanEducational, PlanResearcher] export interface RegisterFormPayload { email: string @@ -72,7 +73,7 @@ export interface RegisterFormPreview { pattern: string isStaff: boolean agreedToTerms: boolean - groups: string[] + groups: Group[] } export interface RegisterFormProps { @@ -212,7 +213,12 @@ const RegisterForm: React.FC = ({ firstname: formPayload.current.firstname, lastname: formPayload.current.lastname, username: formPayload.current.username, - groups: [formPayload.current.plan], + groups: [ + { + id: 1, + name: formPayload.current.plan, + }, + ], })) }, 100) } diff --git a/src/components/UserArea.tsx b/src/components/UserArea.tsx index 3a85203..01b1ef8 100644 --- a/src/components/UserArea.tsx +++ b/src/components/UserArea.tsx @@ -42,7 +42,11 @@ const UserArea = () => { const setView = useBrowserStore((state) => state.setView) const wsStatus = useBrowserStore((state) => state.wsStatus) - const [token, user] = usePersistentStore((state) => [state.token, state.user]) + const [token, user, userPlan] = usePersistentStore((state) => [ + state.token, + state.user, + state.userPlan, + ]) useEffect(() => { if (wsStatus !== "connected") { @@ -81,12 +85,13 @@ const UserArea = () => { <> - + setView(BrowserViewProfile)}> Profile + Datasets setView(BrowserViewTermsOfUse)}> Terms Of Use diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx index 037ce39..84ffa80 100644 --- a/src/components/UserCard.tsx +++ b/src/components/UserCard.tsx @@ -1,35 +1,27 @@ import Avatar from "boring-avatars" -import { PlanAcademicUser, PlanStudentUser } from "../constants" - -export interface User { - username: string - isStaff: boolean - firstname?: string - lastname?: string - pattern?: string - email?: string - profile?: { - pattern: string[] - } - bitmap?: string - groups?: string[] - agreedToTerms?: boolean -} +import { + PlanResearcher, + PlanEducational, + Plans, + PlanImpressoUser, +} from "../constants" +import type { User } from "../types" const UserCard = ({ className = "", user, + userPlan = PlanImpressoUser, }: { className?: string user: User + userPlan?: (typeof Plans)[number] | null }) => { console.debug("[UserCard] rendering:", user) let role = user.isStaff ? "Staff - as Basic User" : "Basic User" - - if (user.groups) { - if (user.groups.includes(PlanStudentUser)) { + if (userPlan) { + if (userPlan === PlanEducational) { role = user.isStaff ? "Staff - as Student" : "Student" - } else if (user.groups.includes(PlanAcademicUser)) { + } else if (userPlan === PlanResearcher) { role = user.isStaff ? "Staff - as Academic" : "Academic" } } diff --git a/src/constants.ts b/src/constants.ts index 91e3789..3dcc6a0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -143,15 +143,36 @@ export const Features: string[] = [ export const PlanGuest = "guest" export const PlanImpressoUser = "plan-basic" -export const PlanStudentUser = "plan-educational" -export const PlanAcademicUser = "plan-academic" -export const PlanAcademicUserPlus = "academic-user-plus" +// IMPRESSO_GROUP_USER_PLAN_EDUCATIONAL = "plan-educational" +// IMPRESSO_GROUP_USER_PLAN_RESEARCHER = "plan-researcher" +export const PlanEducational = "plan-educational" +export const PlanResearcher = "plan-researcher" +export const PlanResearcherPlus = "academic-user-plus" +export const PlanNone = "no-plan" export const PlanLabels: Record = { [PlanGuest]: "Guest", [PlanImpressoUser]: "No academic affiliation", - [PlanStudentUser]: "Student", - [PlanAcademicUser]: "Academic", - [PlanAcademicUserPlus]: "Academic User Plus", + [PlanEducational]: "Student", + [PlanResearcher]: "Academic", + [PlanResearcherPlus]: "Academic User Plus", + [PlanNone]: "No Plan", +} +export const Plans: string[] = [ + PlanGuest, + PlanImpressoUser, + PlanEducational, + PlanResearcher, + PlanResearcherPlus, + PlanNone, +] + +export const PlanAvailabilityLabels: Record = { + [PlanGuest]: "Public Domain, always accessible", + [PlanImpressoUser]: "Feature accessible with a basic account", + [PlanEducational]: "Feature accessible with a student account", + [PlanResearcher]: "Feature accessible with an academic account", + [PlanResearcherPlus]: "Feature accessible with an academic account", + [PlanNone]: "Feature not accessible", } export const PlanIconRestrictedAccessNoDownload = diff --git a/src/content/authors.yaml b/src/content/authors.yaml new file mode 100644 index 0000000..526db2f --- /dev/null +++ b/src/content/authors.yaml @@ -0,0 +1,6 @@ +- id: RomanKalyakin + name: Roman Kalyakin +- id: EmanuelaBoros + name: Emanuela Boros +- id: impresso-team + name: Impresso team diff --git a/src/content/authors/EmanuelaBoros.yaml b/src/content/authors/EmanuelaBoros.yaml deleted file mode 100644 index be3fea1..0000000 --- a/src/content/authors/EmanuelaBoros.yaml +++ /dev/null @@ -1 +0,0 @@ -name: Emanuela Boros diff --git a/src/content/authors/RomanKalyakin.yaml b/src/content/authors/RomanKalyakin.yaml deleted file mode 100644 index ec8d61c..0000000 --- a/src/content/authors/RomanKalyakin.yaml +++ /dev/null @@ -1 +0,0 @@ -name: Roman Kalyakin diff --git a/src/content/authors/at--littlecolumns.yaml b/src/content/authors/at--littlecolumns.yaml deleted file mode 100644 index ad8aab6..0000000 --- a/src/content/authors/at--littlecolumns.yaml +++ /dev/null @@ -1 +0,0 @@ -name: "@littlecolumns" diff --git a/src/content/authors/danieleguido.yaml b/src/content/authors/danieleguido.yaml deleted file mode 100644 index be9064d..0000000 --- a/src/content/authors/danieleguido.yaml +++ /dev/null @@ -1 +0,0 @@ -name: Daniele Guido diff --git a/src/content/authors/impresso-team.yaml b/src/content/authors/impresso-team.yaml deleted file mode 100644 index c19fb17..0000000 --- a/src/content/authors/impresso-team.yaml +++ /dev/null @@ -1 +0,0 @@ -name: Impresso team diff --git a/src/content/config.ts b/src/content/config.ts index d6f6fce..6ad6c8f 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -1,14 +1,148 @@ import { z, defineCollection, reference } from "astro:content" +import { glob, file } from "astro/loaders" +import axios from "axios" import { Requirements, Features, SeriesCategories, SeriesPositions, PlanIcons, + PlanGuest, + PlanResearcher, + PlanImpressoUser, + PlanNone, + PlanEducational, } from "../constants" +const CorpusAccessUserPlansToPlan: Record = { + "Guest User Plan": PlanGuest, + "Basic User Plan": PlanImpressoUser, + "Student User Plan": PlanEducational, + "Academic User Plan": PlanResearcher, + "Not Possible": PlanNone, +} + +const CorpusAccessToDatasetMapper = (dataset: any) => { + return { + id: [dataset.data_partner_institution, dataset.media_alias].join("-"), + associatedPartner: dataset.data_partner_institution, + mediaId: dataset.media_alias, + mediaTitle: dataset.media_title, + timePeriod: dataset.time_period, + startYear: parseInt(dataset.time_period.split("-").shift(), 10), + endYear: parseInt(dataset.time_period.split("-").pop(), 10), + media: dataset.media, // e.g. Newspaper + medium: dataset.medium, // eg Print + copyright: dataset.copyright_or_copyright_status, + permittedUse: dataset.permitted_use, + minimumUserPlanRequiredToExploreInWebapp: + CorpusAccessUserPlansToPlan[ + dataset.minimum_user_plan_required_to_explore_in_the_webapp + ], + minimumUserPlanRequiredToExportTranscripts: + CorpusAccessUserPlansToPlan[ + dataset.minimum_user_plan_required_to_export_transcripts + ], + minimumUserPlanRequiredToExportIllustration: + CorpusAccessUserPlansToPlan[ + dataset.minimum_user_plan_required_to_export_illustration + ], + partnerBitmapIndex: dataset.partner_bitmap_index, + } +} + +const datasets = defineCollection({ + loader: () => + axios + .get( + "https://raw.githubusercontent.com/impresso/impresso-corpus-metadata/refs/heads/master/data/access_rights_masterfiles/corpus_access_catalogue.json", + { + headers: { + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + }, + } + ) + .then((res) => { + const response = res.data + + return response.map(CorpusAccessToDatasetMapper) + }) + .catch((err) => { + console.error(err.mssage, process.env.GITHUB_TOKEN) + return [ + CorpusAccessToDatasetMapper({ + data_partner_institution: "SNL", + media_alias: "BLB", + media_title: "B\u00fcndner Landbote", + time_period: "1846-1847", + media: "Newspaper", + medium: "print", + copyright_or_copyright_status: "Public Domain", + permitted_use: "Personal, Research and Educational", + minimum_user_plan_required_to_explore_in_the_webapp: + "Guest User Plan", + minimum_user_plan_required_to_export_transcripts: "Basic User Plan", + minimum_user_plan_required_to_export_illustration: + "Basic User Plan", + partner_bitmap_index: 5, + }), + CorpusAccessToDatasetMapper({ + data_partner_institution: "BCUF", + media_alias: "FZG", + media_title: "Freiburger Nachrichten", + time_period: "1865-2018", + media: "Newspaper", + medium: "print", + copyright_or_copyright_status: "Protected Domain: In copyright", + permitted_use: "Research and Educational", + minimum_user_plan_required_to_explore_in_the_webapp: + "Basic User Plan", + minimum_user_plan_required_to_export_transcripts: + "Student User Plan", + minimum_user_plan_required_to_export_illustration: + "Student User Plan", + partner_bitmap_index: 23, + }), + CorpusAccessToDatasetMapper({ + data_partner_institution: "BCUL", + media_alias: "RN", + media_title: "Bulletins du Grand Conseil", + time_period: "1829-2020", + media: "Newspaper", + medium: "print", + copyright_or_copyright_status: "Protected Domain: In copyright", + permitted_use: "Research", + minimum_user_plan_required_to_explore_in_the_webapp: + "Academic User Plan", + minimum_user_plan_required_to_export_transcripts: + "Academic User Plan", + minimum_user_plan_required_to_export_illustration: + "Academic User Plan", + partner_bitmap_index: 22, + }), + ] + }), + schema: z.object({ + id: z.string(), + associatedPartner: z.string(), + mediaId: z.string(), + mediaTitle: z.string(), + timePeriod: z.string(), + startYear: z.number(), + endYear: z.number(), + media: z.string(), + medium: z.string(), + copyright: z.string(), + permittedUse: z.string(), + minimumUserPlanRequiredToExploreInWebapp: z.string(), + minimumUserPlanRequiredToExportTranscripts: z.string(), + minimumUserPlanRequiredToExportIllustration: z.string(), + partnerBitmapIndex: z.number(), + }), +}) + const notebooks = defineCollection({ - type: "content", // v2.5.0 and later + loader: glob({ pattern: "*.mdx", base: "./src/content/notebooks" }), schema: z.object({ title: z.string().optional(), url: z.string().url().optional(), @@ -31,7 +165,7 @@ const notebooks = defineCollection({ }) const plans = defineCollection({ - type: "content", + loader: glob({ pattern: "*.mdx", base: "./src/content/plans" }), schema: z.object({ id: z.string().optional(), title: z.string(), @@ -44,7 +178,7 @@ const plans = defineCollection({ status: z.string().optional(), iconColor: z.string().optional(), icon: z.enum(PlanIcons as any).optional(), - }), + }) ) .optional(), requirements: z.array(z.enum(Requirements as any)), @@ -52,7 +186,7 @@ const plans = defineCollection({ }) const authors = defineCollection({ - type: "data", + loader: file("./src/content/authors.yaml"), schema: z.object({ name: z.string(), url: z.string().url().optional(), @@ -60,7 +194,7 @@ const authors = defineCollection({ }) const associatedPartners = defineCollection({ - type: "data", + loader: glob({ pattern: "*.yaml", base: "./src/content/associatedPartners" }), schema: z.object({ name: z.string(), url: z.string().url(), @@ -68,7 +202,7 @@ const associatedPartners = defineCollection({ }) const series = defineCollection({ - type: "content", + loader: glob({ pattern: "*.mdx", base: "./src/content/series" }), schema: z.object({ title: z.string(), excerpt: z.string(), @@ -79,7 +213,7 @@ const series = defineCollection({ }) const pagesContents = defineCollection({ - type: "content", + loader: glob({ pattern: "*.md", base: "./src/content/pagesContents" }), schema: z.object({ title: z.string(), modalTitle: z.string().optional(), @@ -95,4 +229,5 @@ export const collections = { associatedPartners, plans, pagesContents, + datasets, } diff --git a/src/content/pagesContents/datasets.md b/src/content/pagesContents/datasets.md new file mode 100644 index 0000000..cb2af0c --- /dev/null +++ b/src/content/pagesContents/datasets.md @@ -0,0 +1,8 @@ +--- +title: Dataset Availability by Plan +modalTitle: Datasets +excerpt: Overview of the different user plans available for the Impresso Project +--- + +Explore a wide range of datasets featuring newspapers and radio content, each associated with its permitted use. This table allows you to find the resources you need for your research. +You can view which datasets are available under your [current subscription plan](/datalab/profile) and identify those that can be unlocked with an upgrade. If you’re not logged in, please [log in or register](/datalab/login) to see your available datasets. To learn more about the various plans available through your university enrollment, visit the [Plans](/datalab/plans) page. diff --git a/src/logic.ts b/src/logic.ts index ac7910a..14a6dc2 100644 --- a/src/logic.ts +++ b/src/logic.ts @@ -3,11 +3,11 @@ import { getEntry } from "astro:content" export async function getRecursivelyEntryData(entry: any) { const result: any = {} - if (entry.collection && entry.slug) { + if (entry.collection && entry.id) { result.collection = entry.collection - result.href = `${entry.collection}/${entry.slug}` - } else if (entry.slug) { - result.href = `${entry.slug}` + result.href = `${entry.collection}/${entry.id}` + } else if (entry.id) { + result.href = `${entry.id}` } for (const k of Object.keys(entry.data)) { diff --git a/src/pages/datasets.astro b/src/pages/datasets.astro new file mode 100644 index 0000000..2ce0cdd --- /dev/null +++ b/src/pages/datasets.astro @@ -0,0 +1,27 @@ +--- +import { getEntry } from "astro:content"; +import App from "../components/App.astro"; +import Layout from "../layouts/Layout.astro"; +import { CanonicalUrl } from "../constants"; +import { AstroSeo } from "@astrolib/seo"; +import DatasetModal from "../components/DatasetsModal"; +import { getCollection } from "astro:content"; +import { getRecursivelyEntryData } from "../logic"; + +const pageContent = await getEntry("pagesContents", "datasets") as any; + +// load all notebooks, order by date +const datasets = await getCollection('datasets') +const datasetsProps = await Promise.all(datasets.map((datasets) => getRecursivelyEntryData(datasets))) + +--- + + + + + + \ No newline at end of file diff --git a/src/pages/notebooks/[...slug].astro b/src/pages/notebooks/[...slug].astro index 0d67c42..781202c 100644 --- a/src/pages/notebooks/[...slug].astro +++ b/src/pages/notebooks/[...slug].astro @@ -11,7 +11,7 @@ import { AstroSeo } from '@astrolib/seo'; export async function getStaticPaths() { const notebooks = await getCollection('notebooks'); return notebooks.map(notebook => ({ - params: { slug: notebook.slug }, props: { notebook }, + params: { slug: notebook.id }, props: { notebook }, })); } // 2. For your template, you can get the entry directly from the prop diff --git a/src/store.tsx b/src/store.tsx index e40bedd..4b1bc03 100644 --- a/src/store.tsx +++ b/src/store.tsx @@ -1,15 +1,22 @@ import { create } from "zustand" import { createJSONStorage, persist } from "zustand/middleware" -import type { User } from "./components/UserCard" + import { AccessTokenKey, BrowserWsStatuses, BrowserWsStatusIdle, BrowserViews, + PlanGuest, + PlanEducational, + PlanResearcher, + Plans, + PlanImpressoUser, } from "./constants" +import type { User } from "./types" interface PersistentStoreState { user: User | null + userPlan: (typeof Plans)[number] token: string | null acceptTermsDate: string | null setAuthenticatedUser: (user: User, token: string) => void @@ -38,6 +45,7 @@ export const usePersistentStore = create< persist( (set, get) => ({ user: null, + userPlan: PlanGuest, token: null, rememberCredentials: false, acceptTermsDate: null, @@ -51,7 +59,19 @@ export const usePersistentStore = create< set({ user, token }) }, setUser(user) { - set({ user }) + let userPlan = user !== null ? PlanImpressoUser : PlanGuest + if (user !== null && user.groups) { + for (const group of user.groups) { + if ( + group.name === PlanEducational || + group.name === PlanResearcher + ) { + userPlan = group.name + break + } + } + } + set({ user, userPlan }) }, setToken(token) { set({ token }) @@ -74,8 +94,8 @@ export const usePersistentStore = create< { name: "impresso-datalab", storage: createJSONStorage(() => localStorage), // (optional) by default, 'localStorage' is used - }, - ), + } + ) ) // get fresh data from the localstorage diff --git a/src/stories/components/CorpusAccess.stories.tsx b/src/stories/components/CorpusAccess.stories.tsx new file mode 100644 index 0000000..4d9a410 --- /dev/null +++ b/src/stories/components/CorpusAccess.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from "@storybook/react" +import CorpusAccess, { + type CorpusAccessProps, +} from "../../components/CorpusAccess" +// import { fn } from "@storybook/test" + +const meta: Meta = { + component: CorpusAccess, + render: (args) => ( +
    + +
    + ), +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + name: "Default", + args: {} as CorpusAccessProps, +} diff --git a/src/stories/components/DatasetCard.stories.tsx b/src/stories/components/DatasetCard.stories.tsx new file mode 100644 index 0000000..aca95d8 --- /dev/null +++ b/src/stories/components/DatasetCard.stories.tsx @@ -0,0 +1,25 @@ +import type { Meta, StoryObj } from "@storybook/react" +// import { fn } from "@storybook/test" +import type { DatasetCardProps } from "../../components/DatasetCard" +import DatasetCard from "../../components/DatasetCard" +import type { Dataset } from "../../types" + +const meta: Meta = { + component: DatasetCard, + render: (args) => ( +
    + +
    + ), +} + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + dataset: { + id: "123", + } as Dataset, + } as DatasetCardProps, +} diff --git a/src/stories/components/NotebookCard.stories.tsx b/src/stories/components/NotebookCard.stories.tsx index 0717b08..d9f4857 100644 --- a/src/stories/components/NotebookCard.stories.tsx +++ b/src/stories/components/NotebookCard.stories.tsx @@ -18,7 +18,7 @@ type Story = StoryObj export const Default: Story = { args: { notebook: { - slug: "notebook-1", + id: "notebook-1", href: "/notebook-1", title: "Notebook 1", authors: [ diff --git a/src/stories/components/NotebookViewer.stories.tsx b/src/stories/components/NotebookViewer.stories.tsx index 5b5ace6..37d1b40 100644 --- a/src/stories/components/NotebookViewer.stories.tsx +++ b/src/stories/components/NotebookViewer.stories.tsx @@ -31,12 +31,12 @@ type Story = StoryObj export const Default: Story = { args: { notebook: { - slug: "notebook-1", + id: "notebook-1", href: "/notebook-1", title: "Notebook 1", authors: [ { - slug: "author-1", + id: "author-1", name: "Author 1", }, ], diff --git a/src/styles/global.css b/src/styles/global.css index adc9f69..208e760 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -320,6 +320,14 @@ button.btn-secodary:hover { font-weight: var(--impresso-wght-bold); font-variation-settings: "wght" var(--impresso-wght-bold); } +.font-weight-normal { + font-weight: var(--impresso-wght-normal); + font-variation-settings: "wght" var(--impresso-wght-normal); +} +.font-weight-medium { + font-weight: var(--impresso-wght-medium); + font-variation-settings: "wght" var(--impresso-wght-medium); +} .font-weight-bold { font-weight: var(--impresso-wght-bold); font-variation-settings: "wght" var(--impresso-wght-bold); @@ -343,6 +351,10 @@ button.btn-secodary:hover { .border-dotted { --bs-border-style: dotted; } +/* colored text */ +.text-paper { + color: var(--impresso-color-paper); +} /* ... */ h1 { font-variation-settings: "wght" var(--impresso-wght-bold); @@ -504,7 +516,7 @@ hr { } a.dropdown-item { position: relative; - display: inline-block; + display: block; width: auto; } a.dropdown-item::after { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..e2e01aa --- /dev/null +++ b/src/types.ts @@ -0,0 +1,45 @@ +export type Group = { + name: string + id: number +} + +export type User = { + username: string + isStaff: boolean + firstname?: string + lastname?: string + pattern?: string + email?: string + profile?: { + pattern: string[] + } + bitmap?: string + groups?: Group[] + agreedToTerms?: boolean +} + +export type Dataset = { + id: string + associatedPartner: string + mediaId: string + mediaTitle: string + timePeriod: string + startYear: number + endYear: number + media: string + medium: string + copyright: string + permittedUse: string + minimumUserPlanRequiredToExploreInWebapp: string + minimumUserPlanRequiredToExportTranscripts: string + minimumUserPlanRequiredToExportIllustration: string + partnerBitmapIndex: number +} + +// see values in constants +export enum Plans { + PlanGuest = "guest", + PlanImpressoUser = "plan-basic", + PlanEducational = "plan-student", + PlanResearcher = "plan-academic", +}