More app directory refactoring

This commit is contained in:
Darren Clarke 2023-06-28 09:09:45 +00:00 committed by GitHub
parent b312a8c862
commit 8bbeaa25cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 903 additions and 899 deletions

View file

@ -8,7 +8,7 @@ import {
useState,
PropsWithChildren,
} from "react";
import { colors, typography } from "styles/theme";
import { colors, typography } from "@/app/_styles/theme";
const basePath = process.env.GITLAB_CI
? "/link/link-stack/apps/leafcutter"

View file

@ -3,7 +3,7 @@
import { FC, useState } from "react";
import { Dialog, Box, Grid, Checkbox, IconButton } from "@mui/material";
import { Close as CloseIcon } from "@mui/icons-material";
import { useRouter } from "next/router";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider";
@ -62,9 +62,11 @@ export const GettingStartedDialog: FC = () => {
typography: { h4 },
} = useAppContext();
const t = useTranslate();
const [completedItems, setCompletedItems] = useState([] as any[]);
const router = useRouter();
const open = router.query.tooltip?.toString() === "checklist";
const [completedItems, setCompletedItems] = useState([] as any[]);
const searchParams = useSearchParams();
const pathname = usePathname();
const open = searchParams.get("tooltip")?.toString() === "checklist";
const toggleCompletedItem = (item: any) => {
if (completedItems.includes(item)) {
setCompletedItems(completedItems.filter((i) => i !== item));
@ -92,7 +94,7 @@ export const GettingStartedDialog: FC = () => {
<Box sx={{ ...h4, mb: 3 }}>{t("getStartedChecklist")}</Box>
</Grid>
<Grid item>
<IconButton onClick={() => router.push(router.pathname)}>
<IconButton onClick={() => router.push(pathname)}>
<CloseIcon sx={{ color: almostBlack, fontSize: "18px" }} />
</IconButton>
</Grid>

View file

@ -1,20 +1,21 @@
"use client";
import { FC, useState } from "react";
import { useRouter } from "next/router";
import { useRouter, usePathname } from "next/navigation";
import { Button } from "@mui/material";
import { QuestionMark as QuestionMarkIcon } from "@mui/icons-material";
import { useAppContext } from "./AppProvider";
export const HelpButton: FC = () => {
const router = useRouter();
const pathname = usePathname();
const [helpActive, setHelpActive] = useState(false);
const {
colors: { leafcutterElectricBlue },
} = useAppContext();
const onClick = () => {
if (helpActive) {
router.push(router.pathname);
router.push(pathname);
} else {
router.push("/?tooltip=welcome");
}

View file

@ -1,7 +1,7 @@
"use client";
import { useEffect, FC } from "react";
import { useRouter } from "next/router";
import { useRouter, usePathname } from "next/navigation";
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import { Grid, Button } from "@mui/material";
@ -14,11 +14,11 @@ import { useAppContext } from "@/app/_components/AppProvider";
type HomeProps = {
visualizations: any;
embedded: boolean;
};
export const Home: FC<HomeProps> = ({ visualizations, embedded }) => {
export const Home: FC<HomeProps> = ({ visualizations }) => {
const router = useRouter();
const pathname = usePathname();
const cookieName = "homeIntroComplete";
const [cookies, setCookie] = useCookies([cookieName]);
const t = useTranslate();
@ -31,7 +31,7 @@ export const Home: FC<HomeProps> = ({ visualizations, embedded }) => {
useEffect(() => {
if (homeIntroComplete === 0) {
setCookie(cookieName, `${1}`, { path: "/" });
router.push(`${router.pathname}?tooltip=welcome`);
router.push(`${pathname}?tooltip=welcome`);
}
}, [homeIntroComplete, router, setCookie]);

View file

@ -1,6 +1,6 @@
"use client";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { IconButton, Menu, MenuItem, Box } from "@mui/material";
import { KeyboardArrowDown as KeyboardArrowDownIcon } from "@mui/icons-material";
import {
@ -17,6 +17,7 @@ export const LanguageSelect = () => {
} = useAppContext();
const router = useRouter();
const locales: any = { en: "English", fr: "Français" };
const locale = "en";
const popupState = usePopupState({ variant: "popover", popupId: "language" });
return (
@ -38,7 +39,7 @@ export const LanguageSelect = () => {
},
}}
>
{locales[router.locale as any] ?? locales.en}
{locales[locale as any] ?? locales.en}
<KeyboardArrowDownIcon />
</IconButton>
<Menu {...bindMenu(popupState)}>
@ -46,7 +47,7 @@ export const LanguageSelect = () => {
<MenuItem
key={locale}
onClick={() => {
router.push(router.route, router.route, { locale });
// router.push(router.route, router.route, { locale });
popupState.close();
}}
>

View file

@ -1,3 +1,5 @@
"use client";
/* eslint-disable react/jsx-props-no-spreading */
import { FC, PropsWithChildren } from "react";
import { SessionProvider } from "next-auth/react";
@ -5,19 +7,17 @@ import { CssBaseline } from "@mui/material";
import { CookiesProvider } from "react-cookie";
import { I18n } from "react-polyglot";
import { AdapterDateFns } from "@mui/x-date-pickers-pro/AdapterDateFns";
("use client");
import { LocalizationProvider } from "@mui/x-date-pickers-pro";
import { AppProvider } from "@/app/_components/AppProvider";
import { NextAppDirEmotionCacheProvider } from "tss-react/next/appDir";
import en from "locales/en.json";
import fr from "locales/fr.json";
import en from "app/_locales/en.json";
import fr from "app/_locales/fr.json";
import "@fontsource/poppins/400.css";
import "@fontsource/poppins/700.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css";
import "styles/global.css";
import "app/_styles/global.css";
import { LicenseInfo } from "@mui/x-data-grid-pro";
LicenseInfo.setLicenseKey(process.env.MUI_LICENSE_KEY ?? "");

View file

@ -4,7 +4,7 @@ import { FC, useState, useEffect } from "react";
import { Box, Grid } from "@mui/material";
import { useTranslate } from "react-polyglot";
import taxonomy from "app/_config/taxonomy.json";
import { colors } from "styles/theme";
import { colors } from "@/app/_styles/theme";
import { useAppContext } from "./AppProvider";
export const QueryText: FC = () => {

View file

@ -18,7 +18,7 @@ import {
Drawer,
} from "@mui/material";
import Link from "next/link";
import { useRouter } from "next/router";
import { usePathname } from "next/navigation";
import { useTranslate } from "react-polyglot";
import { useAppContext } from "@/app/_components/AppProvider";
import { Tooltip } from "@/app/_components/Tooltip";
@ -101,8 +101,8 @@ interface SidebarProps {
export const Sidebar: FC<SidebarProps> = ({ open }) => {
const t = useTranslate();
const router = useRouter();
const section = router.pathname.split("/")[1];
const pathname = usePathname();
const section = pathname.split("/")[1];
const {
colors: { white }, // leafcutterElectricBlue, leafcutterLightBlue,
} = useAppContext();

View file

@ -2,7 +2,7 @@
/* eslint-disable react/require-default-props */
import { FC } from "react";
import { useRouter } from "next/router";
import { useRouter, usePathname, useSearchParams } from "next/navigation";
import {
Box,
Grid,
@ -40,7 +40,9 @@ export const Tooltip: FC<TooltipProps> = ({
colors: { white, leafcutterElectricBlue, almostBlack },
} = useAppContext();
const router = useRouter();
const activeTooltip = router.query.tooltip?.toString();
const pathname = usePathname();
const searchParams = useSearchParams();
const activeTooltip = searchParams.get('tooltip')?.toString();
const open = activeTooltip === tooltipID;
const showNavigation = true;
@ -51,7 +53,7 @@ export const Tooltip: FC<TooltipProps> = ({
<Grid container direction="column">
<Grid item container direction="row-reverse">
<Grid item>
<IconButton onClick={() => router.push(router.pathname)}>
<IconButton onClick={() => router.push(pathname)}>
<CloseIcon
sx={{
color: leafcutterElectricBlue,
@ -125,7 +127,7 @@ export const Tooltip: FC<TooltipProps> = ({
color: leafcutterElectricBlue,
textTransform: "none",
}}
onClick={() => router.push(router.pathname)}
onClick={() => router.push(pathname)}
>
{t("done")}
</Button>

View file

@ -8,9 +8,12 @@ import { useAppContext } from "./AppProvider";
export const Welcome = () => {
const t = useTranslate();
const { data: session } = useSession();
/*
const {
user: { name },
} = session as any;
*/
const name = "Test User";
const {
colors: { white, leafcutterElectricBlue },
typography: { h1, h4, p },

View file

@ -1,7 +1,7 @@
"use client";
import { Box, Grid, Dialog, Button } from "@mui/material";
import { useRouter } from "next/router";
import { useRouter, useSearchParams } from "next/navigation";
// import { useSession } from "next-auth/react";
// import { useTranslate } from "react-polyglot";
import { useAppContext } from "./AppProvider";
@ -9,13 +9,14 @@ import { useAppContext } from "./AppProvider";
export const WelcomeDialog = () => {
// const t = useTranslate();
const router = useRouter();
const searchParams = useSearchParams();
// const { data: session } = useSession();
// const { user } = session;
const {
colors: { white, leafcutterElectricBlue },
typography: { h1, h6, p },
} = useAppContext();
const activeTooltip = router.query.tooltip?.toString();
const activeTooltip = searchParams.get('tooltip')?.toString();
const open = activeTooltip === "welcome";
return (

View file

@ -0,0 +1,17 @@
import type { NextAuthOptions } from "next-auth";
import Google from "next-auth/providers/google";
import Apple from "next-auth/providers/apple";
export const authOptions: NextAuthOptions = {
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
}),
Apple({
clientId: process.env.APPLE_CLIENT_ID ?? "",
clientSecret: process.env.APPLE_CLIENT_SECRET ?? "",
}),
],
secret: process.env.NEXTAUTH_SECRET,
};

View file

@ -1,5 +0,0 @@
import createCache from "@emotion/cache";
export default function createEmotionCache() {
return createCache({ key: "css" });
}

View file

@ -8,10 +8,16 @@ const globalIndex = ".kibana_1";
const dataIndexName = "sample_tagged_tickets";
const userMetadataIndexName = "user_metadata";
const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
// const baseURL = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
const baseURL = `https://localhost:9200`;
const createClient = () => new Client({
node: baseURL,
auth: {
username: process.env.OPENSEARCH_USERNAME!,
password: process.env.OPENSEARCH_PASSWORD!,
},
ssl: {
rejectUnauthorized: false,
},
@ -532,6 +538,7 @@ export const getTrends = async (limit: number) => {
export const getTemplates = async (limit: number) => {
const client = createClient();
const query = {
query: {
bool: {
@ -546,11 +553,14 @@ export const getTemplates = async (limit: number) => {
},
},
};
const rawResponse = await client.search({
index: globalIndex,
size: limit,
body: query,
});
const response = rawResponse.body;
const {
hits: { hits },

View file

@ -1,4 +0,0 @@
import { GetServerSidePropsContext } from "next";
export const getEmbedded = (context: GetServerSidePropsContext) =>
context.req.headers["x-leafcutter-embedded"] === "true";

View file

@ -0,0 +1,186 @@
{
"leafcutterDashboard": "Leafcutter Dashboard",
"welcomeToLeafcutter": "Welcome to Leafcutter",
"welcomeToLeafcutterDescription": "A Digital Security Threat Analysis Platform from CDR",
"signInWith": "Sign In With",
"emailMagicLink": "Email a Magic Link",
"dontHaveAccount": "Don't have an account?",
"requestAccessHere": "Request access here",
"goHome": "Go Home",
"trendsTitle": "Trends",
"trendsSubtitle": "Discover whats happening globally",
"trendsDescription": "Here you will view what digital security threats and trends are facing civil society right now. On a regular basis a CDR team member reviews the aggregated Leafcutter data and creates visualizations. You can also find third party datasets like OONI, and MISP. See all or filter your view using the drop down menu.",
"frequentlyAskedQuestionsTitle": "Frequently Asked Questions",
"frequentlyAskedQuestionsSubtitle": "Get answers to your questions",
"frequentlyAskedQuestionsDescription": "Find out what you want to know about the Leafcutter Project.",
"dashboardTitle": "My Dashboard",
"dashboardSubtitle": "Welcome",
"dashboardDescription": "This is your personal dashboard where all your saved items are stored. All your created visualizations or items you save from the Trends page are stored here.",
"searchAndCreateTitle": "Search and Create",
"searchAndCreateSubtitle": "Search datasets and create your own visualizations",
"searchAndCreateDescription": "Find out how you can use data to create visuals that can be shared externally.",
"aboutLeafcutterTitle": "About Leafcutter",
"aboutLeafcutterDescription": "A digital threat security analysis dashboard from CDR",
"whatIsLeafcutterTitle": "What Is Leafcutter?",
"whatIsLeafcutterDescription": "Leafcutter is a secure platform for aggregating, displaying, and sharing data on digital security threats and attacks facing global civil society.",
"whatIsItForTitle": "What Is It For?",
"whatIsItForDescription": "Leafcutter helps civil society incident responders view and analyze regional and global threat data to get ahead of digital attacks.",
"whoCanUseItTitle": "Who Can Use It?",
"whoCanUseItDescription": "Leafcutter is available to civil society incident responders and analysts around the world.",
"createVisualization": "Create Visualization",
"whereDataComesFromTitle": "Where the Data Comes From",
"whereDataComesFromDescription": "All data originates from CDR partner communities using the CDR Link helpdesk or from third-party partners gathering data on network filtering, including OONI. All personally identifiable information (PII) is removed before being added to the Leafcutter database, where it is aggregated with data from other CDR Link instances to provide a comprehensive view of digital threats facing civil society in regions around the world.",
"projectSupportTitle": "Project Support",
"projectSupportDescription": "Leafcutter is a project of Center for Digital Resilience. It is a free and open source tool, built on Open Search and Label Studio. Leafcutter was designed and built by Center for Digital Resilience in collaboration with Julie Kioli (design) and Guardian Project (platform development). Thank you to all who make this project possible.",
"interestedInLeafcutterTitle": "Interested in using Leafcutter for your community?",
"interestedInLeafcutterDescription": "Leafcutter is part of the CDR Link ecosystem. helping partner communities around the world safely collect data about digital security threats and attacks. Please contact us if you are interested in using Leafcutter.",
"dashboardMenuItem": "Dashboard",
"dashboardTooltipTitle": "Dashboard",
"dashboardTooltipDescription": "Dashboard",
"aboutMenuItem": "About",
"aboutTooltipTitle": "About",
"aboutTooltipDescription": "About",
"trendsMenuItem": "Trends",
"trendsTooltipTitle": "Trends",
"trendsTooltipDescription": "Trends",
"searchAndCreateMenuItem": "Search and Create",
"searchAndCreateTooltipTitle": "Search and Create",
"searchAndCreateTooltipDescription": "Search and Create",
"faqMenuItem": "FAQ",
"faqTooltipTitle": "FAQ",
"faqTooltipDescription": "FAQ",
"recentUpdatesTitle": "Recent Updates",
"language": "Language",
"languageTooltipTitle": "Language select",
"languageTooltipDescription": "Select language",
"copyright": "Copyright",
"projectOf": "A project of",
"privacyPolicy": "Privacy Policy",
"codeOfPractice": "Code of Practice",
"contactUs": "Contact Us",
"whatIsLeafcutterQuestion": "What is Leafcutter?",
"whatIsLeafcutterAnswer": "Leafcutter is a secure platform for aggregating, displaying, and sharing data on digital security threats and attacks facing global civil society.",
"whoBuiltLeafcutterQuestion": "Who built Leafcutter?",
"whoBuiltLeafcutterAnswer": "Leafcutter was built and is maintained by Center for Digital Resilience [https://digiresilience.org](https://digiresilience.org/).",
"whoCanUseLeafcutterQuestion": "Who can use Leafcutter?",
"whoCanUseLeafcutterAnswer": "Incident responders, threat analysts, security trainers, and security service providers, in general, can make use of Leafcutter to contextualize threats, draw insights and provide qualitative, data-driven support to the communities they serve.",
"whatCanYouDoWithLeafcutterQuestion": "What can you do with Leafcutter?",
"whatCanYouDoWithLeafcutterAnswer": "1. Analyze current and previously curated threats and attacks facing the civil society community\n2. Keep track of threats and attacks in other geographies\n3. Make accurate threat predictions with live data and visualizations\n4. Leverage threat data to provide tailored and preventative supports to your communities\n5. Create personalized visualizations to interpret the data in the way that works for you\n6. Share your visualizations with your community or colleagues",
"whereIsTheDataComingFromQuestion": "Where is the data coming from?",
"whereIsTheDataComingFromAnswer": "Data aggregated into Leafcutter is currently pooled from two sources:\n1. Individual CDR Link instances (CDR's rapid response helpdesk built on Zammad)\n2. Open Observatory of Network Interference (OONI).\nData from OONI is pulled from their public API, and is displayed to give users a sense of global trends in network filtering and interference.\n\nLeafcutter data is also shared with CDRs MISP instance, to enable sharing of data to other community MISP instances.",
"whereIsTheDataStoredQuestion": "Where is the data stored?",
"whereIsTheDataStoredAnswer": "Leafcutter data is securely stored in an Amazon Web Services (AWS) database.",
"howDoWeKeepTheDataSafeQuestion": "How do we keep the data safe?",
"howDoWeKeepTheDataSafeAnswer": "We keep data protected in a number of ways\n1. Tickets and data from CDR Link helpdesks are de-identified and scrubbed of personal/sensitive information before they are aggregated into the Leafcutter database to enable querying via Amazons [OpenSearch](https://aws.amazon.com/opensearch-service/the-elk-stack/what-is-opensearch/) service.\n2. Our domain is deployed on a Virtual Private Cloud, eliminating public access to the cluster and data\n3. We employ Amazon's [server side encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/serv-side-encryption.html) with [Key Management Service](https://aws.amazon.com/kms/) to encrypt data. Only CDR holds the decryption keys.\n4. Within OpenSearch, we employ security best practices to protect data, including: - Using the latest version of OpenSearch - Employing a least-privilege, restrictive access-control.\n5. For more on CDRs approach to privacy and security, see our [Privacy Policy](https://digiresilience.org/about/privacy/).",
"howLongDoYouKeepTheDataQuestion": "How long do you keep the data?",
"howLongDoYouKeepTheDataAnswer": "Ticket data from CDR Link helpdesks are kept only for as long as necessary (detailed [here in our privacy policy](https://digiresilience.org/about/privacy/)). Other non-identifiable data are preserved indefinitely to support historical search and analysis for our community.",
"whatOrganizationsAreParticipatingQuestion": "What organizations are participating?",
"whatOrganizationsAreParticipatingAnswer": "Leafcutter was built with the civil society security community in mind and features strong participation from organizations and professionals within the [CiviCERT](https://www.civicert.org/) network and the [Threat Intel Coalition](https://www.first.org/global/sigs/tic/), among other rapid response and service provider communities.",
"howDidYouGetMyProfileInformationQuestion": "How did you get my profile information?",
"howDidYouGetMyProfileInformationAnswer": "We use Google sign-in to authenticate.",
"howCanILearnMoreAboutLeafcutterQuestion": "How can I learn more about Leafcutter?",
"howCanILearnMoreAboutLeafcutterAnswer": "Contact us at [info@cdr.link](mailto:info@cdr.link) (public PGP key [here](https://digiresilience.org/keys/info.cdr.link-public.txt)).",
"of": "of",
"previous": "Previous",
"next": "Next",
"done": "Done",
"dashboardNavigationCardTitle": "Dashboard Navigation",
"dashboardNavigationCardDescription": "Move between different pages to search data, discover recent trends, and create your own data visualizations.",
"recentUpdatesCardTitle": "Recent Updates",
"recentUpdatesCardDescription": "Your quick look at recent digital security trends in your community and around the world.",
"languageOptionsCardTitle": "Language Options",
"languageOptionsCardDescription": "You can change your language preference here and view your dashboard in your preferred language.",
"profileSettingsCardTitle": "Profile Settings",
"profileSettingsCardDescription": "This is your profile menu, where you can turn on/off your notifications and logout.",
"categoriesCardTitle": "Categories",
"categoriesCardDescription": "Search global data by choosing one or multiple categories.",
"subcategoriesCardTitle": "Subcategories",
"subcategoriesCardDescription": "Narrow your search by selecting one or multiple subcategories.",
"optionsCardTitle": "Options",
"optionsCardDescription": "Choose to Include or Exclude fields from your search.",
"incidentTypeCardTitle": "Incident Type",
"incidentTypeCardDescription": "What kind of attack or attempted attack was it?",
"dateRangeCardTitle": "Date Range",
"dateRangeCardDescription": "Choose a beginning and end date for your search or search within a relative timeframe.",
"targetedGroupCardTitle": "Targeted Group",
"targetedGroupCardDescription": "What individual, organization, or community was the target of the attack?",
"impactedTechnologyCardTitle": "Impacted Technology",
"impactedTechnologyCardDescription": "What devices, platforms, or services were affected by the incident?",
"regionCardTitle": "Region",
"regionCardDescription": "Choose geographical regions by country, continent or subregion. When nothing is checked, by default all regions are selected.",
"advancedOptionsCardTitle": "+ Advanced Options",
"advancedOptionsCardDescription": "Clicking this option will redirect you to the Kibana interface. Kibana is the data visualization platform that powers the Leafcutter dashboard. It provides more advanced search fields and options.",
"queryResultsCardTitle": "Your Query & Results",
"queryResultsCardDescription": "Your Query displays your chosen search criteria. Blue is what youve included and Pink is what youve excluded. Your Results display all available search results in a list view based on your search critiera.",
"viewResultsCardTitle": "View Results As:",
"viewResultsCardDescription": "Below are visualization options for displaying your search results. Highlighted boxes are the available options based on your specific search. Click to expand the visualization, save, or edit.",
"technology": "Technology",
"actor": "Actor",
"geography": "Geography",
"channel": "Channel",
"status": "Status",
"ticketFrequency": "Ticket Frequency",
"incidentType": "Incident Type",
"type": "Type",
"date": "Date",
"targetedGroup": "Targeted Group",
"group": "Group",
"impactedTechnology": "Impacted Technology",
"platform": "Platform",
"device": "Device",
"service": "Service",
"maker": "Maker",
"region": "Region",
"continent": "Continent",
"country": "Country",
"subregion": "Subregion",
"advancedOptions": "Advanced Options",
"fullInterfaceWillOpen": "The full OpenSearch Dashboards interface will open in a new window.",
"cancel": "Cancel",
"save": "Save",
"open": "Open",
"include": "Include",
"exclude": "Exclude",
"startDate": "Start Date",
"endDate": "End Date",
"relativeDate": "Relative Date",
"last7Days": "Last 7 days",
"last30Days": "Last 30 days",
"last3Months": "Last 3 months",
"last6Months": "Last 6 months",
"lastYear": "Last year",
"last2Years": "Last 2 years",
"where": "where",
"is": "is",
"isNot": "is not",
"or": "or",
"onOrAfter": "on or after",
"onOrBefore": "on or before",
"findAllIncidents": "Find all incidents",
"title": "Title",
"description": "Description",
"welcome": "Welcome",
"signOut": "Sign Out",
"incident": "Incident",
"results": "Results",
"query": "Query",
"viewAs": "View As",
"selectVisualization": "Select a Visualization",
"selectFieldVisualize": "Select a Field to Visualize",
"noSavedVisualizations": "You dont have any saved visualizations. Go to [Search and Create](/create) or [Trends](/trends) to get started.",
"getStartedChecklist": "Get Started Checklist",
"searchTitle": "Search",
"searchDescription": "This can be trends, new/saved visualizations",
"createVisualizationTitle": "Create Visualization",
"createVisualizationDescription": "This can be from data in trends",
"saveTitle": "Save",
"saveDescription": "As a .png, .pdf or .csv",
"exportTitle": "Export",
"exportDescription": "",
"shareTitle": "Share",
"shareDescription": "Externally or internally",
"savedSearch": "Saved Search",
"saveCurrentSearch": "Save Current Search",
"clear": "Clear",
"delete": "Delete"
}

View file

@ -0,0 +1,44 @@
{
"aboutLeafcutterTitle": "FRENCH!",
"aboutLeafcutterDescription": "FRENCH!",
"whatIsLeafcutterTitle": "FRENCH!",
"whatIsLeafcutterDescription": "FRENCH!",
"whatIsItForTitle": "FRENCH!",
"whatIsItForDescription": "FRENCH!",
"whoCanUseItTitle": "FRENCH!",
"whoCanUseItDescription": "FRENCH!",
"createVisualization": "FRENCH!",
"whereDataComesFromTitle": "FRENCH!",
"whereDataComesFromDescription": "FRENCH!",
"projectSupportTitle": "FRENCH!",
"projectSupportDescription": "FRENCH!",
"interestedInLeafcutterTitle": "FRENCH!",
"interestedInLeafcutterDescription": "FRENCH!\nFRENCH!",
"myVisualizationsMenuItem": "FRENCH!",
"myVisualizationsTooltipTitle": "FRENCH!",
"myVisualizationsTooltipDescription": "FRENCH!",
"aboutMenuItem": "FRENCH!",
"aboutTooltipTitle": "FRENCH!",
"aboutTooltipDescription": "FRENCH!",
"trendsMenuItem": "FRENCH!",
"trendsTooltipTitle": "FRENCH!",
"trendsTooltipDescription": "FRENCH!",
"searchDataMenuItem": "FRENCH!",
"searchDataTooltipTitle": "FRENCH!",
"searchDataTooltipDescription": "FRENCH!",
"createVisualizationsMenuItem": "FRENCH!",
"createVisualizationsTooltipTitle": "FRENCH!",
"createVisualizationsTooltipDescription": "FRENCH!",
"faqMenuItem": "FRENCH!",
"faqTooltipTitle": "FRENCH!",
"faqTooltipDescription": "FRENCH!",
"recentUpdatesTitle": "FRENCH!",
"language": "FRENCH!",
"languageTooltipTitle": "FRENCH!",
"languageTooltipDescription": "FRENCH!",
"copyright": "FRENCH!",
"projectOf": "FRENCH!",
"privacyPolicy": "FRENCH!",
"codeOfPractice": "FRENCH!",
"contactUs": "FRENCH!"
}

View file

@ -0,0 +1,5 @@
body {
overscroll-behavior-x: none;
overscroll-behavior-y: none;
text-size-adjust: none;
}

View file

@ -0,0 +1,86 @@
export const colors: any = {
lightGray: "#ededf0",
mediumGray: "#e3e5e5",
darkGray: "#33302f",
mediumBlue: "#4285f4",
green: "#349d7b",
lavender: "#a5a6f6",
darkLavender: "#5d5fef",
pink: "#fcddec",
cdrLinkOrange: "#ff7115",
coreYellow: "#fac942",
helpYellow: "#fff4d5",
dwcDarkBlue: "#191847",
hazyMint: "#ecf7f8",
leafcutterElectricBlue: "#4d6aff",
leafcutterLightBlue: "#fafbfd",
waterbearElectricPurple: "#332c83",
waterbearLightSmokePurple: "#eff3f8",
bumpedPurple: "#212058",
mutedPurple: "#373669",
warningPink: "#ef5da8",
lightPink: "#fff0f7",
lightGreen: "#f0fff3",
lightOrange: "#fff5f0",
beige: "#f6f2f1",
almostBlack: "#33302f",
white: "#ffffff",
};
export const typography: any = {
h1: {
fontFamily: "Playfair, serif",
fontSize: 45,
fontWeight: 700,
lineHeight: 1.1,
margin: 0,
},
h2: {
fontFamily: "Poppins, sans-serif",
fontSize: 35,
fontWeight: 700,
lineHeight: 1.1,
margin: 0,
},
h3: {
fontFamily: "Poppins, sans-serif",
fontWeight: 400,
fontSize: 27,
lineHeight: 1.1,
margin: 0,
},
h4: {
fontFamily: "Poppins, sans-serif",
fontWeight: 700,
fontSize: 18,
},
h5: {
fontFamily: "Roboto, sans-serif",
fontWeight: 700,
fontSize: 16,
lineHeight: "24px",
textTransform: "uppercase",
textAlign: "center",
margin: 1,
},
h6: {
fontFamily: "Roboto, sans-serif",
fontWeight: 400,
fontSize: 14,
textAlign: "center",
},
p: {
fontFamily: "Roboto, sans-serif",
fontSize: 17,
lineHeight: "26.35px",
fontWeight: 400,
margin: 0,
},
small: {
fontFamily: "Roboto, sans-serif",
fontSize: 13,
lineHeight: "18px",
fontWeight: 400,
margin: 0,
},
};

View file

@ -0,0 +1,164 @@
"use client";
import { FC } from "react";
import { useTranslate } from "react-polyglot";
import Image from "next/legacy/image";
import Link from "next/link";
import { Grid, Container, Box, Button } from "@mui/material";
import { useAppContext } from "@/app/_components/AppProvider";
import { PageHeader } from "@/app/_components/PageHeader";
import { AboutFeature } from "@/app/_components/AboutFeature";
import { AboutBox } from "@/app/_components/AboutBox";
import AbstractDiagram from "images/abstract-diagram.png";
import AboutHeader from "images/about-header.png";
import Globe from "images/globe.png";
import Controls from "images/controls.png";
import CommunityBackground from "images/community-background.png";
import Bicycle from "images/bicycle.png";
export const About: FC = () => {
const t = useTranslate();
const {
colors: { white, leafcutterElectricBlue, cdrLinkOrange },
typography: { h1, h4, p },
} = useAppContext();
return (
<>
<PageHeader
backgroundColor={leafcutterElectricBlue}
sx={{
backgroundImage: `url(${AboutHeader.src})`,
backgroundSize: "200px",
backgroundPosition: "bottom right",
backgroundRepeat: "no-repeat",
}}
>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item xs={9}>
<Box component="h1" sx={h1}>
{t("aboutLeafcutterTitle")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("aboutLeafcutterDescription")}
</Box>
</Grid>
</Grid>
</PageHeader>
<Container maxWidth="lg">
<AboutFeature
title={t("whatIsLeafcutterTitle")}
description={t("whatIsLeafcutterDescription")}
direction="row"
image={AbstractDiagram}
showBackground={false}
textColumns={8}
/>
<AboutFeature
title={t("whatIsItForTitle")}
description={t("whatIsItForDescription")}
direction="row-reverse"
image={Controls}
showBackground
textColumns={8}
/>
<AboutFeature
title={t("whoCanUseItTitle")}
description={t("whoCanUseItDescription")}
direction="row"
image={Globe}
showBackground
textColumns={6}
/>
</Container>
<AboutBox backgroundColor={cdrLinkOrange}>
<Box component="h4" sx={{ ...h4, mt: 0 }}>
{t("whereDataComesFromTitle")}
</Box>
{t("whereDataComesFromDescription")
.split("\n")
.map((line: string, i: number) => (
<Box component="p" key={i} sx={p}>
{line}
</Box>
))}
</AboutBox>
<AboutBox backgroundColor={leafcutterElectricBlue}>
<Box component="h4" sx={{ ...h4, mt: 0 }}>
{t("projectSupportTitle")}
</Box>
{t("projectSupportDescription")
.split("\n")
.map((line: string, i: number) => (
<Box component="p" key={i} sx={p}>
{line}
</Box>
))}
</AboutBox>
<Box
sx={{
backgroundImage: `url(${CommunityBackground.src})`,
backgroundSize: "90%",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
position: "relative",
height: "700px",
}}
>
<Box sx={{ position: "absolute", left: 0, bottom: -20, width: 300 }}>
<Image src={Bicycle} alt="" />
</Box>
<Container
maxWidth="md"
sx={{ textAlign: "center", paddingTop: "280px" }}
>
<Box
component="h4"
sx={{ ...h4, maxWidth: 500, margin: "0 auto", mt: 3 }}
>
{t("interestedInLeafcutterTitle")}
</Box>
{t("interestedInLeafcutterDescription")
.split("\n")
.map((line: string, i: number) => (
<Box
component="p"
key={i}
sx={{ ...p, maxWidth: 500, margin: "0 auto" }}
>
{line}
</Box>
))}
<Link href="mailto:info@digiresilience.org" passHref>
<Button
sx={{
fontSize: 14,
borderRadius: 500,
color: white,
backgroundColor: cdrLinkOrange,
fontWeight: "bold",
textTransform: "uppercase",
pl: 6,
pr: 5,
mt: 4,
":hover": {
backgroundColor: leafcutterElectricBlue,
color: white,
opacity: 0.8,
},
}}
>
{t("contactUs")}
</Button>
</Link>
</Container>
</Box>
</>
);
};

View file

@ -1,173 +1,5 @@
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next";
import { useTranslate } from "react-polyglot";
import Head from "next/head";
import Image from "next/legacy/image";
import Link from "next/link";
import { Grid, Container, Box, Button } from "@mui/material";
import { useAppContext } from "@/app/_components/AppProvider";
import { PageHeader } from "@/app/_components/PageHeader";
import { AboutFeature } from "@/app/_components/AboutFeature";
import { AboutBox } from "@/app/_components/AboutBox";
import AbstractDiagram from "images/abstract-diagram.png";
import AboutHeader from "images/about-header.png";
import Globe from "images/globe.png";
import Controls from "images/controls.png";
import CommunityBackground from "images/community-background.png";
import Bicycle from "images/bicycle.png";
import { getEmbedded } from "@/app/_lib/utils";
import { About } from './_components/About';
type AboutProps = {
embedded: boolean;
};
const About: NextPage<AboutProps> = ({ embedded }) => {
const t = useTranslate();
const {
colors: { white, leafcutterElectricBlue, cdrLinkOrange },
typography: { h1, h4, p },
} = useAppContext();
return (
<>
<PageHeader
backgroundColor={leafcutterElectricBlue}
sx={{
backgroundImage: `url(${AboutHeader.src})`,
backgroundSize: "200px",
backgroundPosition: "bottom right",
backgroundRepeat: "no-repeat",
}}
>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item xs={9}>
<Box component="h1" sx={h1}>
{t("aboutLeafcutterTitle")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("aboutLeafcutterDescription")}
</Box>
</Grid>
</Grid>
</PageHeader>
<Container maxWidth="lg">
<AboutFeature
title={t("whatIsLeafcutterTitle")}
description={t("whatIsLeafcutterDescription")}
direction="row"
image={AbstractDiagram}
showBackground={false}
textColumns={8}
/>
<AboutFeature
title={t("whatIsItForTitle")}
description={t("whatIsItForDescription")}
direction="row-reverse"
image={Controls}
showBackground
textColumns={8}
/>
<AboutFeature
title={t("whoCanUseItTitle")}
description={t("whoCanUseItDescription")}
direction="row"
image={Globe}
showBackground
textColumns={6}
/>
</Container>
<AboutBox backgroundColor={cdrLinkOrange}>
<Box component="h4" sx={{ ...h4, mt: 0 }}>
{t("whereDataComesFromTitle")}
</Box>
{t("whereDataComesFromDescription")
.split("\n")
.map((line: string, i: number) => (
<Box component="p" key={i} sx={p}>
{line}
</Box>
))}
</AboutBox>
<AboutBox backgroundColor={leafcutterElectricBlue}>
<Box component="h4" sx={{ ...h4, mt: 0 }}>
{t("projectSupportTitle")}
</Box>
{t("projectSupportDescription")
.split("\n")
.map((line: string, i: number) => (
<Box component="p" key={i} sx={p}>
{line}
</Box>
))}
</AboutBox>
<Box
sx={{
backgroundImage: `url(${CommunityBackground.src})`,
backgroundSize: "90%",
backgroundRepeat: "no-repeat",
backgroundPosition: "center",
position: "relative",
height: "700px",
}}
>
<Box sx={{ position: "absolute", left: 0, bottom: -20, width: 300 }}>
<Image src={Bicycle} alt="" />
</Box>
<Container
maxWidth="md"
sx={{ textAlign: "center", paddingTop: "280px" }}
>
<Box
component="h4"
sx={{ ...h4, maxWidth: 500, margin: "0 auto", mt: 3 }}
>
{t("interestedInLeafcutterTitle")}
</Box>
{t("interestedInLeafcutterDescription")
.split("\n")
.map((line: string, i: number) => (
<Box
component="p"
key={i}
sx={{ ...p, maxWidth: 500, margin: "0 auto" }}
>
{line}
</Box>
))}
<Link href="mailto:info@digiresilience.org" passHref>
<Button
sx={{
fontSize: 14,
borderRadius: 500,
color: white,
backgroundColor: cdrLinkOrange,
fontWeight: "bold",
textTransform: "uppercase",
pl: 6,
pr: 5,
mt: 4,
":hover": {
backgroundColor: leafcutterElectricBlue,
color: white,
opacity: 0.8,
},
}}
>
{t("contactUs")}
</Button>
</Link>
</Container>
</Box>
</>
);
};
export default About;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => ({ props: { embedded: getEmbedded(context) } });
export default function Page() {
return <About />;
}

View file

@ -1,19 +1,6 @@
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
import Apple from "next-auth/providers/apple";
import { authOptions } from "@/app/_lib/auth";
const handler = NextAuth({
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID ?? "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? "",
}),
Apple({
clientId: process.env.APPLE_CLIENT_ID ?? "",
clientSecret: process.env.APPLE_CLIENT_SECRET ?? "",
}),
],
secret: process.env.NEXTAUTH_SECRET,
});
const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };

View file

@ -1,32 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { getUserMetadata, saveUserMetadata } from "@/app/_lib/opensearch";
export const POST = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { email }: any = session;
const { name, query } = JSON.parse(req.body);
const result = await getUserMetadata(email);
const { savedSearches } = result;
await saveUserMetadata(email, {
savedSearches: [...savedSearches, { name, query }]
});
const { savedSearches: updatedSavedSearches } = await getUserMetadata(email);
return res.json(updatedSavedSearches);
};

View file

@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/_lib/auth";
import { getUserMetadata, saveUserMetadata } from "@/app/_lib/opensearch";
export const POST = async (req: NextRequest) => {
const session = await getServerSession(authOptions);
const { user: { email } }: any = session;
const { name, query } = await req.json();
const result = await getUserMetadata(email);
const { savedSearches } = result;
await saveUserMetadata(email, {
savedSearches: [...savedSearches, { name, query }]
});
const { savedSearches: updatedSavedSearches } = await getUserMetadata(email);
return NextResponse.json(updatedSavedSearches);
};

View file

@ -1,31 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { getUserMetadata, saveUserMetadata } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { email }: any = session;
const { name } = JSON.parse(req.body);
const { savedSearches } = await getUserMetadata(email);
const updatedSavedSearches = savedSearches.filter((search: any) => search.name !== name);
const result = await saveUserMetadata(email, { savedSearches: updatedSavedSearches });
return res.json({ result });
};
export default handler;

View file

@ -0,0 +1,19 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/_lib/auth";
import { getUserMetadata, saveUserMetadata } from "@/app/_lib/opensearch";
export const POST = async (req: NextRequest) => {
const session = await getServerSession(authOptions);
const { user: { email } }: any = session;
const { name } = await req.json();
const { savedSearches } = await getUserMetadata(email);
const updatedSavedSearches = savedSearches.filter((search: any) => search.name !== name);
const result = await saveUserMetadata(email, { savedSearches: updatedSavedSearches });
return NextResponse.json({ result });
};

View file

@ -1,25 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { getUserMetadata } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "GET") {
return res.status(500).json({ message: "Only GET requests are allowed" });
}
const { email }: any = session;
const { savedSearches } = await getUserMetadata(email);
return res.json(savedSearches);
};
export default handler;

View file

@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/_lib/auth";
import { getUserMetadata } from "@/app/_lib/opensearch";
export const GET = async () => {
const session = await getServerSession(authOptions);
const { user: { email } }: any = session;
const { savedSearches } = await getUserMetadata(email);
return NextResponse.json(savedSearches);
};

View file

@ -1,20 +0,0 @@
import { NextResponse } from "next/server";
// import { getToken } from "next-auth/jwt";
import { getTrends } from "@/app/_lib/opensearch";
export const GET = async () => {
/*
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
*/
const results = await getTrends(5);
NextResponse.json(results);
};

View file

@ -0,0 +1,9 @@
import { NextResponse } from "next/server";
import { getTrends } from "@/app/_lib/opensearch";
export const GET = async () => {
const results = await getTrends(5);
NextResponse.json(results);
};

View file

@ -1,12 +1,13 @@
/* eslint-disable no-restricted-syntax */
import { NextApiRequest, NextApiResponse } from "next";
import { NextRequest, NextResponse } from "next/server";
import { Client } from "@opensearch-project/opensearch";
import { v4 as uuid } from "uuid";
import taxonomy from "app/_config/taxonomy.json";
import unRegions from "app/_config/unRegions.json";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { headers: { authorization }, body: { tickets } } = req;
export const POST = async (req: NextRequest) => {
const { tickets } = await req.json();
const authorization = req.headers.get("authorization");
const baseURL = `https://${process.env.OPENSEARCH_URL}`;
const client = new Client({
node: baseURL,
@ -48,7 +49,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}
const results = { succeeded, failed };
return res.json(results);
return NextResponse.json(results);
};
export default handler;

View file

@ -1,32 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { createUserVisualization } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { visualizationID, title, description, query } = req.body;
const id = await createUserVisualization({
email: session.email as string,
visualizationID,
title,
description,
query
});
return res.json({ id });
};
export default handler;

View file

@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/_lib/auth";
import { createUserVisualization } from "@/app/_lib/opensearch";
export const POSt = async (req: NextRequest) => {
const session = await getServerSession(authOptions);
const { user: { email } }: any = session;
const { visualizationID, title, description, query } = await req.json();
const id = await createUserVisualization({
email,
visualizationID,
title,
description,
query
});
return NextResponse.json({ id });
};

View file

@ -1,26 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { deleteUserVisualization } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { id } = req.body;
await deleteUserVisualization(session.email as string, id);
return res.json({ id });
};
export default handler;

View file

@ -0,0 +1,15 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/_lib/auth";
import { deleteUserVisualization } from "@/app/_lib/opensearch";
export const POST = async (req: NextRequest, res: NextResponse) => {
const session = await getServerSession(authOptions);
const { user: { email } }: any = session;
const { id } = await req.json();
await deleteUserVisualization(email as string, id);
return NextResponse.json({ id });
};

View file

@ -1,23 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { performQuery } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
const { searchQuery } = req.query;
const rawQuery = await JSON.parse(decodeURI(searchQuery as string));
const results = await performQuery(rawQuery, 1000);
return res.json(results);
};
export default handler;

View file

@ -0,0 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { performQuery } from "@/app/_lib/opensearch";
export const GET = async (req: NextRequest) => {
const searchQuery = req.nextUrl.searchParams.get("searchQuery");
const rawQuery = await JSON.parse(decodeURI(searchQuery as string));
const results = await performQuery(rawQuery, 1000);
return NextResponse.json(results);
};

View file

@ -1,32 +0,0 @@
import { NextApiRequest, NextApiResponse } from "next";
import { getToken } from "next-auth/jwt";
import { updateUserVisualization } from "@/app/_lib/opensearch";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getToken({
req,
secret: process.env.NEXTAUTH_SECRET,
});
if (!session) {
return res.redirect("/login");
}
if (req.method !== "POST") {
return res.status(500).json({ message: "Only POST requests are allowed" });
}
const { id, title, description, query } = req.body;
await updateUserVisualization({
email: session.email as string,
id,
title,
description,
query
});
return res.json({ id });
};
export default handler;

View file

@ -0,0 +1,22 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/app/_lib/auth";
import { updateUserVisualization } from "@/app/_lib/opensearch";
const handler = async (req: NextRequest) => {
const session = await getServerSession(authOptions);
const { user: { email } }: any = session;
const { id, title, description, query } = await req.json();
await updateUserVisualization({
email,
id,
title,
description,
query
});
return NextResponse.json({ id });
};
export default handler;

View file

@ -0,0 +1,66 @@
"use client";
import { FC, useEffect } from "react";
import { useTranslate } from "react-polyglot";
import { useRouter, usePathname } from "next/navigation";
import { Box, Grid } from "@mui/material";
import { useCookies } from "react-cookie";
import { useAppContext } from "@/app/_components/AppProvider";
import { PageHeader } from "@/app/_components/PageHeader";
import { VisualizationBuilder } from "@/app/_components/VisualizationBuilder";
type CreateProps = {
templates: any;
embedded: boolean;
};
export const Create: FC<CreateProps> = ({ templates, embedded }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4 },
} = useAppContext();
const router = useRouter();
const pathname = usePathname();
const cookieName = "searchIntroComplete";
const [cookies, setCookie] = useCookies([cookieName]);
const searchIntroComplete = parseInt(cookies[cookieName], 10) || 0;
useEffect(() => {
if (searchIntroComplete === 0) {
setCookie(cookieName, `${1}`, { path: "/" });
router.push(`${pathname}?group=search&tooltip=1&checklist=1`);
}
}, [searchIntroComplete, router, setCookie]);
return (
<>
<PageHeader backgroundColor={cdrLinkOrange}>
<Grid container direction="row" spacing={2} alignItems="center">
{/* <Grid item xs={2} sx={{ textAlign: "center" }}>
<Image src={SearchCreateHeader} width={100} height={100} alt="" />
</Grid> */}
<Grid container direction="column" item xs={10}>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("searchAndCreateTitle")}
</Box>
</Grid>
<Grid item>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("searchAndCreateSubtitle")}
</Box>
</Grid>
{/* <Grid>
<Box component="p" sx={{ ...p }}>
{t("searchAndCreateDescription")}
</Box>
</Grid> */}
</Grid>
</Grid>
</PageHeader>
<VisualizationBuilder templates={templates} />
</>
);
};

View file

@ -1,76 +1,9 @@
import { FC, useEffect } from "react";
import { GetServerSideProps, GetServerSidePropsContext } from "next";
import { useTranslate } from "react-polyglot";
import { useRouter } from "next/router";
import { Box, Grid } from "@mui/material";
import { useCookies } from "react-cookie";
import { getTemplates } from "@/app/_lib/opensearch";
import { useAppContext } from "@/app/_components/AppProvider";
import { PageHeader } from "@/app/_components/PageHeader";
import { VisualizationBuilder } from "@/app/_components/VisualizationBuilder";
import { getEmbedded } from "@/app/_lib/utils";
import { Create } from "./_components/Create";
type CreateProps = {
templates: any;
embedded: boolean;
};
const Create: FC<CreateProps> = ({ templates, embedded }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4 },
} = useAppContext();
const router = useRouter();
const cookieName = "searchIntroComplete";
const [cookies, setCookie] = useCookies([cookieName]);
const searchIntroComplete = parseInt(cookies[cookieName], 10) || 0;
useEffect(() => {
if (searchIntroComplete === 0) {
setCookie(cookieName, `${1}`, { path: "/" });
router.push(`${router.pathname}?group=search&tooltip=1&checklist=1`);
}
}, [searchIntroComplete, router, setCookie]);
return (
<>
<PageHeader backgroundColor={cdrLinkOrange}>
<Grid container direction="row" spacing={2} alignItems="center">
{/* <Grid item xs={2} sx={{ textAlign: "center" }}>
<Image src={SearchCreateHeader} width={100} height={100} alt="" />
</Grid> */}
<Grid container direction="column" item xs={10}>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("searchAndCreateTitle")}
</Box>
</Grid>
<Grid item>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("searchAndCreateSubtitle")}
</Box>
</Grid>
{/* <Grid>
<Box component="p" sx={{ ...p }}>
{t("searchAndCreateDescription")}
</Box>
</Grid> */}
</Grid>
</Grid>
</PageHeader>
<VisualizationBuilder templates={templates} />
</>
);
};
export default Create;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
export default async function Page() {
const templates = await getTemplates(100);
console.log({templates});
return { props: { templates, embedded: getEmbedded(context) } };
return <Create templates={templates} embedded={false}/>;
};

View file

@ -0,0 +1,102 @@
"use client";
import { FC } from "react";
import { useTranslate } from "react-polyglot";
import { Box, Grid } from "@mui/material";
import { PageHeader } from "@/app/_components/PageHeader";
import { Question } from "@/app/_components/Question";
import { useAppContext } from "@/app/_components/AppProvider";
import FaqHeader from "images/faq-header.svg";
export const FAQ: FC = () => {
const t = useTranslate();
const {
colors: { lavender },
typography: { h1, h4, p },
} = useAppContext();
const questions = [
{
question: t("whatIsLeafcutterQuestion"),
answer: t("whatIsLeafcutterAnswer"),
},
{
question: t("whoBuiltLeafcutterQuestion"),
answer: t("whoBuiltLeafcutterAnswer"),
},
{
question: t("whoCanUseLeafcutterQuestion"),
answer: t("whoCanUseLeafcutterAnswer"),
},
{
question: t("whatCanYouDoWithLeafcutterQuestion"),
answer: t("whatCanYouDoWithLeafcutterAnswer"),
},
{
question: t("whereIsTheDataComingFromQuestion"),
answer: t("whereIsTheDataComingFromAnswer"),
},
{
question: t("whereIsTheDataStoredQuestion"),
answer: t("whereIsTheDataStoredAnswer"),
},
{
question: t("howDoWeKeepTheDataSafeQuestion"),
answer: t("howDoWeKeepTheDataSafeAnswer"),
},
{
question: t("howLongDoYouKeepTheDataQuestion"),
answer: t("howLongDoYouKeepTheDataAnswer"),
},
{
question: t("whatOrganizationsAreParticipatingQuestion"),
answer: t("whatOrganizationsAreParticipatingAnswer"),
},
{
question: t("howDidYouGetMyProfileInformationQuestion"),
answer: t("howDidYouGetMyProfileInformationAnswer"),
},
{
question: t("howCanILearnMoreAboutLeafcutterQuestion"),
answer: t("howCanILearnMoreAboutLeafcutterAnswer"),
},
];
return (
<>
<PageHeader
backgroundColor={lavender}
sx={{
backgroundImage: `url(${FaqHeader.src})`,
backgroundSize: "150px",
backgroundPosition: "bottom right",
backgroundRepeat: "no-repeat",
}}
>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("frequentlyAskedQuestionsTitle")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("frequentlyAskedQuestionsSubtitle")}
</Box>
<Box component="p" sx={{ ...p }}>
{t("frequentlyAskedQuestionsDescription")}
</Box>
</Grid>
</Grid>
</PageHeader>
{questions.map((q: any, index: number) => (
<Question key={index} question={q.question} answer={q.answer} />
))}
</>
);
};

View file

@ -1,111 +1,5 @@
import { useTranslate } from "react-polyglot";
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next";
import { Box, Grid } from "@mui/material";
import { PageHeader } from "@/app/_components/PageHeader";
import { Question } from "@/app/_components/Question";
import { useAppContext } from "@/app/_components/AppProvider";
import FaqHeader from "images/faq-header.svg";
import { getEmbedded } from "@/app/_lib/utils";
import { FAQ } from "./_components/FAQ";
type FAQProps = {
embedded: boolean;
};
const FAQ: NextPage<FAQProps> = ({ embedded }) => {
const t = useTranslate();
const {
colors: { lavender },
typography: { h1, h4, p },
} = useAppContext();
const questions = [
{
question: t("whatIsLeafcutterQuestion"),
answer: t("whatIsLeafcutterAnswer"),
},
{
question: t("whoBuiltLeafcutterQuestion"),
answer: t("whoBuiltLeafcutterAnswer"),
},
{
question: t("whoCanUseLeafcutterQuestion"),
answer: t("whoCanUseLeafcutterAnswer"),
},
{
question: t("whatCanYouDoWithLeafcutterQuestion"),
answer: t("whatCanYouDoWithLeafcutterAnswer"),
},
{
question: t("whereIsTheDataComingFromQuestion"),
answer: t("whereIsTheDataComingFromAnswer"),
},
{
question: t("whereIsTheDataStoredQuestion"),
answer: t("whereIsTheDataStoredAnswer"),
},
{
question: t("howDoWeKeepTheDataSafeQuestion"),
answer: t("howDoWeKeepTheDataSafeAnswer"),
},
{
question: t("howLongDoYouKeepTheDataQuestion"),
answer: t("howLongDoYouKeepTheDataAnswer"),
},
{
question: t("whatOrganizationsAreParticipatingQuestion"),
answer: t("whatOrganizationsAreParticipatingAnswer"),
},
{
question: t("howDidYouGetMyProfileInformationQuestion"),
answer: t("howDidYouGetMyProfileInformationAnswer"),
},
{
question: t("howCanILearnMoreAboutLeafcutterQuestion"),
answer: t("howCanILearnMoreAboutLeafcutterAnswer"),
},
];
return (
<>
<PageHeader
backgroundColor={lavender}
sx={{
backgroundImage: `url(${FaqHeader.src})`,
backgroundSize: "150px",
backgroundPosition: "bottom right",
backgroundRepeat: "no-repeat",
}}
>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("frequentlyAskedQuestionsTitle")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("frequentlyAskedQuestionsSubtitle")}
</Box>
<Box component="p" sx={{ ...p }}>
{t("frequentlyAskedQuestionsDescription")}
</Box>
</Grid>
</Grid>
</PageHeader>
{questions.map((q: any, index: number) => (
<Question key={index} question={q.question} answer={q.answer} />
))}
</>
);
};
export default FAQ;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => ({ props: { embedded: getEmbedded(context) } });
export default function Page() {
return <FAQ />;
}

View file

@ -1,16 +1,16 @@
import { ReactNode } from "react";
import { Metadata } from "next";
import "./_styles/global.css";
import "app/_styles/global.css";
import "@fontsource/poppins/400.css";
import "@fontsource/poppins/700.css";
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/700.css";
import "@fontsource/playfair-display/900.css";
import "styles/global.css";
// import getConfig from "next/config";
// import { LicenseInfo } from "@mui/x-data-grid-pro";
import { MultiProvider } from "app/_components/MultiProvider";
import { InternalLayout } from "app/_components/InternalLayout";
import { headers } from 'next/headers'
export const metadata: Metadata = {
title: "Leafcutter",
@ -21,6 +21,8 @@ type LayoutProps = {
};
export default function Layout({ children }: LayoutProps) {
const allHeaders = headers();
const embedded = Boolean(allHeaders.get('x-leafcutter-embedded'));
// const { publicRuntimeConfig } = getConfig();
// LicenseInfo.setLicenseKey(publicRuntimeConfig.muiLicenseKey);
@ -28,7 +30,7 @@ export default function Layout({ children }: LayoutProps) {
<html lang="en">
<body>
<MultiProvider>
<InternalLayout>{children}</InternalLayout>
<InternalLayout embedded={embedded}>{children}</InternalLayout>
</MultiProvider>
</body>
</html>

View file

@ -0,0 +1,114 @@
"use client";
import { FC } from "react";
import Link from "next/link";
import Image from "next/legacy/image";
import { Box, Grid, Container, IconButton } from "@mui/material";
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
import { useTranslate } from "react-polyglot";
import { LanguageSelect } from "@/app/_components/LanguageSelect";
import LeafcutterLogoLarge from "images/leafcutter-logo-large.png";
import { signIn } from "next-auth/react";
import { useAppContext } from "@/app/_components/AppProvider";
type LoginProps = {
session: any;
};
export const Login: FC<LoginProps> = ({ session }) => {
const t = useTranslate();
const {
colors: { leafcutterElectricBlue, lightGray },
typography: { h1, h4 },
} = useAppContext();
const buttonStyles = {
backgroundColor: lightGray,
borderRadius: 500,
width: "100%",
fontSize: "16px",
fontWeight: "bold",
};
return (
<>
<Grid container direction="row-reverse" sx={{ p: 3 }}>
<Grid item>
<LanguageSelect />
</Grid>
</Grid>
<Container maxWidth="md" sx={{ mt: 3, mb: 20 }}>
<Grid container spacing={2} direction="column" alignItems="center">
<Grid item>
<Box sx={{ maxWidth: 200 }}>
<Image src={LeafcutterLogoLarge} alt="" objectFit="fill" />
</Box>
</Grid>
<Grid item sx={{ textAlign: "center" }}>
<Box component="h1" sx={{ ...h1, color: leafcutterElectricBlue }}>
{t("welcomeToLeafcutter")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1 }}>
{t("welcomeToLeafcutterDescription")}
</Box>
</Grid>
<Grid item>
{!session ? (
<Grid
container
spacing={3}
direction="column"
alignItems="center"
sx={{ width: 450, mt: 1 }}
>
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("google", {
callbackUrl: `${window.location.origin}/setup`,
})
}
>
<GoogleIcon sx={{ mr: 1 }} />
{`${t("signInWith")} Google`}
</IconButton>
</Grid>
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("apple", {
callbackUrl: `${window.location.origin}/setup`,
})
}
>
<AppleIcon sx={{ mr: 1 }} />
{`${t("signInWith")} Apple`}
</IconButton>
</Grid>
<Grid item sx={{ mt: 2 }}>
<Box>
{t("dontHaveAccount")}{" "}
<Link href="mailto:info@digiresilience.org">
{t("requestAccessHere")}
</Link>
</Box>
</Grid>
</Grid>
) : null}
{session ? (
<>
<Box component="h4" sx={h4}>
{`${t("welcome")}, ${
session.user.name ?? session.user.email
}.`}
</Box>
<Link href="/">{t("goHome")}</Link>
</>
) : null}
</Grid>
</Grid>
</Container>
</>
);
};

View file

@ -1,126 +1,16 @@
import Head from "next/head";
import { NextPage } from "next";
import Link from "next/link";
import Image from "next/legacy/image";
import { Box, Grid, Container, IconButton } from "@mui/material";
import { Apple as AppleIcon, Google as GoogleIcon } from "@mui/icons-material";
import { useTranslate } from "react-polyglot";
import { LanguageSelect } from "@/app/_components/LanguageSelect";
import LeafcutterLogoLarge from "images/leafcutter-logo-large.png";
import { signIn, getSession } from "next-auth/react";
import { useAppContext } from "@/app/_components/AppProvider";
import { Metadata } from "next";
import { getServerSession } from "next-auth";
import { authOptions } from "app/_lib/auth";
import { Login } from "./_components/Login";
type LoginProps = {
session: any;
export const metadata: Metadata = {
title: "Login",
};
const Login: NextPage<LoginProps> = ({ session }) => {
const t = useTranslate();
const {
colors: { leafcutterElectricBlue, lightGray },
typography: { h1, h4 },
} = useAppContext();
const buttonStyles = {
backgroundColor: lightGray,
borderRadius: 500,
width: "100%",
fontSize: "16px",
fontWeight: "bold",
};
export default async function Page() {
const session = await getServerSession(authOptions);
return (
<>
<Head>
<title>Leafcutter: Login</title>
</Head>
<Grid container direction="row-reverse" sx={{ p: 3 }}>
<Grid item>
<LanguageSelect />
</Grid>
</Grid>
<Container maxWidth="md" sx={{ mt: 3, mb: 20 }}>
<Grid container spacing={2} direction="column" alignItems="center">
<Grid item>
<Box sx={{ maxWidth: 200 }}>
<Image src={LeafcutterLogoLarge} alt="" objectFit="fill" />
</Box>
</Grid>
<Grid item sx={{ textAlign: "center" }}>
<Box component="h1" sx={{ ...h1, color: leafcutterElectricBlue }}>
{t("welcomeToLeafcutter")}
</Box>
<Box component="h4" sx={{ ...h4, mt: 1 }}>
{t("welcomeToLeafcutterDescription")}
</Box>
</Grid>
<Grid item>
{!session ? (
<Grid
container
spacing={3}
direction="column"
alignItems="center"
sx={{ width: 450, mt: 1 }}
>
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("google", {
callbackUrl: `${window.location.origin}/setup`,
})
}
>
<GoogleIcon sx={{ mr: 1 }} />
{`${t("signInWith")} Google`}
</IconButton>
</Grid>
<Grid item sx={{ width: "100%" }}>
<IconButton
sx={buttonStyles}
onClick={() =>
signIn("apple", {
callbackUrl: `${window.location.origin}/setup`,
})
}
>
<AppleIcon sx={{ mr: 1 }} />
{`${t("signInWith")} Apple`}
</IconButton>
</Grid>
<Grid item sx={{ mt: 2 }}>
<Box>
{t("dontHaveAccount")}{" "}
<Link href="mailto:info@digiresilience.org">
{t("requestAccessHere")}
</Link>
</Box>
</Grid>
</Grid>
) : null}
{session ? (
<>
<Box component="h4" sx={h4}>
{`${t("welcome")}, ${
session.user.name ?? session.user.email
}.`}
</Box>
<Link href="/">{t("goHome")}</Link>
</>
) : null}
</Grid>
</Grid>
</Container>
</>
);
};
export default Login;
export async function getServerSideProps(context: any) {
const session = (await getSession(context)) ?? null;
return {
props: { session },
};
return <Login session={session} />;
}

View file

@ -1,6 +1,5 @@
import { getSession } from "next-auth/react";
import { getUserVisualizations } from "@/app/_lib/opensearch";
import { getEmbedded } from "@/app/_lib/utils";
import { Home } from "@/app/_components/Home";
export default async function Page() {
@ -10,7 +9,6 @@ export default async function Page() {
session?.user?.email ?? "none",
20
);
const embedded = false; // getEmbedded(context);
return <Home visualizations={visualizations} embedded={embedded} />;
return <Home visualizations={visualizations} />;
}

View file

@ -1,6 +1,6 @@
import { useLayoutEffect } from "react";
import { NextPage } from "next";
import { useRouter } from "next/router";
import { useRouter } from "next/navigation";
import { Grid, CircularProgress } from "@mui/material";
import Iframe from "react-iframe";
import { useAppContext } from "@/app/_components/AppProvider";

View file

@ -0,0 +1,72 @@
"use client";
import { FC, } from "react";
import { Grid, Box } from "@mui/material";
import { useTranslate } from "react-polyglot";
import { PageHeader } from "@/app/_components/PageHeader";
import { VisualizationCard } from "@/app/_components/VisualizationCard";
import { useAppContext } from "@/app/_components/AppProvider";
type TrendsProps = {
visualizations: any;
};
export const Trends: FC<TrendsProps> = ({ visualizations }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4, p },
} = useAppContext();
return (
<>
<PageHeader backgroundColor={cdrLinkOrange}>
<Grid
container
direction="row"
spacing={2}
justifyContent="space-between"
alignItems="center"
>
{/* <Grid item xs={3} sx={{ textAlign: "center" }}>
<Image src={SearchCreateHeader} width={200} height={200} alt="" />
</Grid> */}
<Grid item container direction="column" xs={12}>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("trendsTitle")}
</Box>
</Grid>
<Grid item>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("trendsSubtitle")}
</Box>
</Grid>
<Grid>
<Box component="p" sx={{ ...p }}>
{t("trendsDescription")}
</Box>
</Grid>
</Grid>
</Grid>
</PageHeader>
<Grid
container
direction="row"
wrap="wrap"
spacing={3}
justifyContent="space-between"
>
{visualizations.map((visualization: any, index: number) => (
<VisualizationCard
key={index}
id={visualization.id}
title={visualization.title}
description={visualization.description}
url={visualization.url}
/>
))}
</Grid>
</>
);
};

View file

@ -1,84 +1,8 @@
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next";
import Head from "next/head";
import { Grid, Box } from "@mui/material";
import { useTranslate } from "react-polyglot";
import { getTrends } from "@/app/_lib/opensearch";
import { PageHeader } from "@/app/_components/PageHeader";
import { VisualizationCard } from "@/app/_components/VisualizationCard";
import { useAppContext } from "@/app/_components/AppProvider";
import { getEmbedded } from "@/app/_lib/utils";
import { Trends } from "./_components/Trends";
type TrendsProps = {
visualizations: any;
embedded: boolean;
};
const Trends: NextPage<TrendsProps> = ({ visualizations, embedded }) => {
const t = useTranslate();
const {
colors: { cdrLinkOrange },
typography: { h1, h4, p },
} = useAppContext();
return (
<>
<PageHeader backgroundColor={cdrLinkOrange}>
<Grid
container
direction="row"
spacing={2}
justifyContent="space-between"
alignItems="center"
>
{/* <Grid item xs={3} sx={{ textAlign: "center" }}>
<Image src={SearchCreateHeader} width={200} height={200} alt="" />
</Grid> */}
<Grid item container direction="column" xs={12}>
<Grid item>
<Box component="h1" sx={{ ...h1 }}>
{t("trendsTitle")}
</Box>
</Grid>
<Grid item>
<Box component="h4" sx={{ ...h4, mt: 1, mb: 1 }}>
{t("trendsSubtitle")}
</Box>
</Grid>
<Grid>
<Box component="p" sx={{ ...p }}>
{t("trendsDescription")}
</Box>
</Grid>
</Grid>
</Grid>
</PageHeader>
<Grid
container
direction="row"
wrap="wrap"
spacing={3}
justifyContent="space-between"
>
{visualizations.map((visualization: any, index: number) => (
<VisualizationCard
key={index}
id={visualization.id}
title={visualization.title}
description={visualization.description}
url={visualization.url}
/>
))}
</Grid>
</>
);
};
export default Trends;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
export default async function Page() {
const visualizations = await getTrends(25);
return { props: { visualizations, embedded: getEmbedded(context) } };
};
return <Trends visualizations={visualizations} />;
}

View file

@ -1,26 +1,8 @@
/* eslint-disable no-underscore-dangle */
import { NextPage, GetServerSideProps, GetServerSidePropsContext } from "next";
import { Client } from "@opensearch-project/opensearch";
import { VisualizationDetail } from "@/app/_components/VisualizationDetail";
import { getEmbedded } from "@/app/_lib/utils";
type VisualizationProps = {
visualization: any;
embedded: boolean;
};
const Visualization: NextPage<VisualizationProps> = ({
visualization,
embedded,
}) => <VisualizationDetail {...visualization} />;
export default Visualization;
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const { visualizationID } = context.query;
const getVisualization = async (visualizationID: string) => {
const node = `https://${process.env.OPENSEARCH_USERNAME}:${process.env.OPENSEARCH_PASSWORD}@${process.env.OPENSEARCH_URL}`;
const client = new Client({
node,
@ -36,7 +18,6 @@ export const getServerSideProps: GetServerSideProps = async (
const response = rawResponse.body;
const hits = response.hits.hits.filter(
// @ts-expect-error
(hit: any) => hit._id.split(":")[1] === visualizationID[0]
);
const hit = hits[0];
@ -49,5 +30,19 @@ export const getServerSideProps: GetServerSideProps = async (
}?embed=true`,
};
return { props: { visualization, embedded: getEmbedded(context) } };
};
return visualization;
}
type PageProps = {
params: {
visualizationID: string;
};
}
export default async function Page({ params: { visualizationID } }: PageProps) {
const visualization = await getVisualization(visualizationID);
return <VisualizationDetail {...visualization} editing={false}/>;
}