feat: new dashboards logic, closes #17

This commit is contained in:
Vyacheslav Matyukhin 2022-04-10 01:50:13 +03:00
parent d92f18db3f
commit 986f7ab888
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
12 changed files with 289 additions and 269 deletions

17
src/pages/_middleware.ts Normal file
View File

@ -0,0 +1,17 @@
import { NextRequest, NextResponse } from "next/server";
export async function middleware(req: NextRequest) {
const { pathname, searchParams } = req.nextUrl;
console.log(pathname);
if (pathname === "/dashboards") {
const dashboardId = searchParams.get("dashboardId");
if (dashboardId) {
return NextResponse.redirect(
new URL(`/dashboards/view/${dashboardId}`, req.url)
);
}
}
return NextResponse.next();
}

View File

@ -1,211 +0,0 @@
/* Imports */
import axios from "axios";
import { GetServerSideProps, NextPage } from "next";
import { useRouter } from "next/router"; // https://nextjs.org/docs/api-reference/next/router
import { useState } from "react";
import { DashboardItem } from "../backend/dashboards";
import { getPlatformsConfig, PlatformConfig } from "../backend/platforms";
import { DashboardCreator } from "../web/display/DashboardCreator";
import { DisplayForecasts } from "../web/display/DisplayForecasts";
import { Layout } from "../web/display/Layout";
import { addLabelsToForecasts, FrontendForecast } from "../web/platforms";
import { getDashboardForecastsByDashboardId } from "../web/worker/getDashboardForecasts";
interface Props {
initialDashboardForecasts: FrontendForecast[];
initialDashboardId: string | null;
initialDashboardItem: DashboardItem | null;
platformsConfig: PlatformConfig[];
}
export const getServerSideProps: GetServerSideProps<Props> = async (
context
) => {
const dashboardIdQ = context.query.dashboardId;
const dashboardId: string | undefined =
typeof dashboardIdQ === "object" ? dashboardIdQ[0] : dashboardIdQ;
const platformsConfig = getPlatformsConfig({ withGuesstimate: false });
if (!dashboardId) {
return {
props: {
platformsConfig,
initialDashboardForecasts: [],
initialDashboardId: null,
initialDashboardItem: null,
},
};
}
const { dashboardForecasts, dashboardItem } =
await getDashboardForecastsByDashboardId({
dashboardId,
});
const frontendDashboardForecasts = addLabelsToForecasts(
dashboardForecasts,
platformsConfig
);
return {
props: {
initialDashboardForecasts: frontendDashboardForecasts,
initialDashboardId: dashboardId,
initialDashboardItem: dashboardItem,
platformsConfig,
},
};
};
/* Body */
const DashboardsPage: NextPage<Props> = ({
initialDashboardForecasts,
initialDashboardItem,
platformsConfig,
}) => {
const router = useRouter();
const [dashboardForecasts, setDashboardForecasts] = useState(
initialDashboardForecasts
);
const [dashboardItem, setDashboardItem] = useState(initialDashboardItem);
const handleSubmit = async (data) => {
console.log(data);
// Send to server to create
// Get back the id
let response = await axios({
url: "/api/create-dashboard-from-ids",
method: "POST",
headers: { "Content-Type": "application/json" },
data: JSON.stringify(data),
}).then((res) => res.data);
let dashboardId = response.dashboardId;
if (!!dashboardId) {
console.log("response: ", response);
if (typeof window !== "undefined") {
let urlWithoutDefaultParameters = `/dashboards?dashboardId=${dashboardId}`;
if (!window.location.href.includes(urlWithoutDefaultParameters)) {
window.history.replaceState(
null,
"Metaforecast",
urlWithoutDefaultParameters
);
}
}
// router.push(`?dashboardId=${dashboardId}`)
// display it
let { dashboardForecasts, dashboardItem } =
await getDashboardForecastsByDashboardId({
dashboardId,
});
setDashboardForecasts(
addLabelsToForecasts(dashboardForecasts, platformsConfig)
);
setDashboardItem(dashboardItem);
}
};
let isGraubardEasterEgg = (name: string) =>
name == "Clay Graubard" ? true : false;
return (
<Layout page="dashboard">
{/* Display forecasts */}
<div className="mt-7 mb-7">
<h1
className={
!!dashboardItem && !!dashboardItem.title
? "text-4xl text-center text-gray-600 mt-2 mb-2"
: "hidden"
}
>
{!!dashboardItem ? dashboardItem.title : ""}
</h1>
<p
className={
!!dashboardItem &&
!!dashboardItem.creator &&
!isGraubardEasterEgg(dashboardItem.creator)
? "text-lg text-center text-gray-600 mt-2 mb-2"
: "hidden"
}
>
{!!dashboardItem ? `Created by: ${dashboardItem.creator}` : ""}
</p>
<p
className={
!!dashboardItem &&
!!dashboardItem.creator &&
isGraubardEasterEgg(dashboardItem.creator)
? "text-lg text-center text-gray-600 mt-2 mb-2"
: "hidden"
}
>
{!!dashboardItem ? `Created by: @` : ""}
<a
href={"https://twitter.com/ClayGraubard"}
className="text-blue-600"
>
Clay Graubard
</a>
</p>
<p
className={
!!dashboardItem && !!dashboardItem.description
? "text-lg text-center text-gray-600 mt-2 mb-2"
: "hidden"
}
>
{!!dashboardItem ? `${dashboardItem.description}` : ""}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-6">
<DisplayForecasts
results={dashboardForecasts}
numDisplay={dashboardForecasts.length}
showIdToggle={false}
/>
</div>
{/* */}
<h3 className="flex items-center col-start-2 col-end-2 w-full justify-center mt-8 mb-4">
<span
aria-hidden="true"
className="flex-grow bg-gray-300 rounded h-0.5"
></span>
<span
className={
!!dashboardForecasts && dashboardForecasts.length > 0
? `mx-3 text-md font-medium text-center`
: "hidden"
}
>
Or create your own
</span>
<span
className={
!dashboardForecasts || dashboardForecasts.length == 0
? `mx-3 text-md font-medium text-center`
: "hidden"
}
>
Create a dashboard!
</span>
<span
aria-hidden="true"
className="flex-grow bg-gray-300 rounded h-0.5"
></span>
</h3>
<div className="grid grid-cols-3 justify-center">
<div className="flex col-start-2 col-end-2 items-center justify-center">
<DashboardCreator handleSubmit={handleSubmit} />
</div>
</div>
</Layout>
);
};
export default DashboardsPage;

View File

@ -0,0 +1,88 @@
import axios from "axios";
import { GetServerSideProps, NextPage } from "next";
import { useRouter } from "next/router"; // https://nextjs.org/docs/api-reference/next/router
import { DashboardItem } from "../../backend/dashboards";
import { getPlatformsConfig, PlatformConfig } from "../../backend/platforms";
import { DashboardCreator } from "../../web/display/DashboardCreator";
import { Layout } from "../../web/display/Layout";
import { LineHeader } from "../../web/display/LineHeader";
import { addLabelsToForecasts, FrontendForecast } from "../../web/platforms";
import { getDashboardForecastsByDashboardId } from "../../web/worker/getDashboardForecasts";
interface Props {
initialDashboardForecasts: FrontendForecast[];
initialDashboardId: string | null;
initialDashboardItem: DashboardItem | null;
platformsConfig: PlatformConfig[];
}
export const getServerSideProps: GetServerSideProps<Props> = async (
context
) => {
const dashboardIdQ = context.query.dashboardId;
const dashboardId: string | undefined =
typeof dashboardIdQ === "object" ? dashboardIdQ[0] : dashboardIdQ;
const platformsConfig = getPlatformsConfig({ withGuesstimate: false });
if (!dashboardId) {
return {
props: {
platformsConfig,
initialDashboardForecasts: [],
initialDashboardId: null,
initialDashboardItem: null,
},
};
}
const { dashboardForecasts, dashboardItem } =
await getDashboardForecastsByDashboardId({
dashboardId,
});
const frontendDashboardForecasts = addLabelsToForecasts(
dashboardForecasts,
platformsConfig
);
return {
props: {
initialDashboardForecasts: frontendDashboardForecasts,
initialDashboardId: dashboardId,
initialDashboardItem: dashboardItem,
platformsConfig,
},
};
};
/* Body */
const DashboardsPage: NextPage<Props> = () => {
const router = useRouter();
const handleSubmit = async (data) => {
// Send to server to create
// Get back the id
let response = await axios({
url: "/api/create-dashboard-from-ids",
method: "POST",
headers: { "Content-Type": "application/json" },
data: JSON.stringify(data),
}).then((res) => res.data);
await router.push(`/dashboards/view/${response.dashboardId}`);
};
return (
<Layout page="dashboard">
<div className="flex flex-col my-8 space-y-8">
<LineHeader>Create a dashboard!</LineHeader>
<div className="self-center">
<DashboardCreator handleSubmit={handleSubmit} />
</div>
</div>
</Layout>
);
};
export default DashboardsPage;

View File

@ -0,0 +1,118 @@
import { GetServerSideProps, NextPage } from "next";
import Link from "next/link";
import { DashboardItem } from "../../../backend/dashboards";
import { getPlatformsConfig } from "../../../backend/platforms";
import { DisplayForecasts } from "../../../web/display/DisplayForecasts";
import { InfoBox } from "../../../web/display/InfoBox";
import { Layout } from "../../../web/display/Layout";
import { LineHeader } from "../../../web/display/LineHeader";
import { addLabelsToForecasts, FrontendForecast } from "../../../web/platforms";
import { getDashboardForecastsByDashboardId } from "../../../web/worker/getDashboardForecasts";
interface Props {
dashboardForecasts: FrontendForecast[];
dashboardId: string;
dashboardItem: DashboardItem;
}
export const getServerSideProps: GetServerSideProps<Props> = async (
context
) => {
const dashboardId = context.query.id as string;
const platformsConfig = getPlatformsConfig({ withGuesstimate: false });
const { dashboardForecasts, dashboardItem } =
await getDashboardForecastsByDashboardId({
dashboardId,
});
const frontendDashboardForecasts = addLabelsToForecasts(
dashboardForecasts,
platformsConfig
);
return {
props: {
dashboardForecasts: frontendDashboardForecasts,
dashboardId,
dashboardItem,
},
};
};
const DashboardMetadata: React.FC<{ dashboardItem: DashboardItem }> = ({
dashboardItem,
}) => (
<div>
{dashboardItem?.title ? (
<h1 className="text-4xl text-center text-gray-600 mt-2 mb-2">
{dashboardItem.title}
</h1>
) : null}
{dashboardItem && dashboardItem.creator ? (
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
Created by:{" "}
{dashboardItem.creator === "Clay Graubard" ? (
<>
@
<a
href="https://twitter.com/ClayGraubard"
className="text-blue-600"
>
Clay Graubard
</a>
</>
) : (
dashboardItem.creator
)}
</p>
) : null}
{dashboardItem?.description ? (
<p className="text-lg text-center text-gray-600 mt-2 mb-2">
{dashboardItem.description}
</p>
) : null}
</div>
);
/* Body */
const ViewDashboardPage: NextPage<Props> = ({
dashboardForecasts,
dashboardItem,
dashboardId,
}) => {
return (
<Layout page="view-dashboard">
<div className="flex flex-col my-8 space-y-8">
{dashboardItem ? (
<DashboardMetadata dashboardItem={dashboardItem} />
) : null}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<DisplayForecasts
results={dashboardForecasts}
numDisplay={dashboardForecasts.length}
showIdToggle={false}
/>
</div>
<div className="max-w-xl self-center">
<InfoBox>
Dashboards cannot be changed after they are created.
</InfoBox>
</div>
<LineHeader>
<Link href="/dashboards" passHref>
<a>Create your own dashboard</a>
</Link>
</LineHeader>
</div>
</Layout>
);
};
export default ViewDashboardPage;

View File

@ -0,0 +1,10 @@
interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {}
export const Button: React.FC<Props> = ({ children, ...rest }) => (
<button
{...rest}
className="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded text-center"
>
{children}
</button>
);

View File

@ -0,0 +1,5 @@
export const InfoBox: React.FC = ({ children }) => (
<p className="bg-gray-200 text-gray-700 py-2 px-4 border border-transparent text-center">
{children}
</p>
);

View File

@ -0,0 +1,7 @@
export const LineHeader: React.FC = ({ children }) => (
<h3 className="flex items-center justify-center w-full">
<span aria-hidden="true" className="flex-grow bg-gray-300 rounded h-0.5" />
<span className="mx-3 text-md font-medium text-center">{children}</span>
<span aria-hidden="true" className="flex-grow bg-gray-300 rounded h-0.5" />
</h3>
);

View File

@ -1,4 +1,7 @@
import React, { useState } from "react"; import React, { EventHandler, SyntheticEvent, useState } from "react";
import { Button } from "./Button";
import { InfoBox } from "./InfoBox";
const exampleInput = `{ const exampleInput = `{
"title": "Random example", "title": "Random example",
@ -13,32 +16,27 @@ interface Props {
export const DashboardCreator: React.FC<Props> = ({ handleSubmit }) => { export const DashboardCreator: React.FC<Props> = ({ handleSubmit }) => {
const [value, setValue] = useState(exampleInput); const [value, setValue] = useState(exampleInput);
const [displayingDoneMessage, setDisplayingDoneMessage] = useState(false); const [acting, setActing] = useState(false);
const [displayingDoneMessageTimer, setDisplayingDoneMessageTimer] =
useState(null);
const handleChange = (event) => { const handleChange = (event) => {
setValue(event.target.value); setValue(event.target.value);
}; };
const handleSubmitInner = (event) => { const handleSubmitInner: EventHandler<SyntheticEvent> = async (event) => {
clearTimeout(displayingDoneMessageTimer);
event.preventDefault(); event.preventDefault();
console.log(value);
try { try {
let newData = JSON.parse(value); const newData = JSON.parse(value);
if (!newData || !newData.ids || newData.ids.length == 0) { if (!newData || !newData.ids || newData.ids.length == 0) {
throw Error("Not enough objects"); throw Error("Not enough objects");
} else { } else {
handleSubmit(newData); setActing(true);
setDisplayingDoneMessage(true); await handleSubmit(newData);
const timer = setTimeout(() => setDisplayingDoneMessage(false), 3000); setActing(false);
setDisplayingDoneMessageTimer(timer);
} }
} catch (error) { } catch (error) {
setDisplayingDoneMessage(false); setActing(false);
const substituteText = `Error: ${error.message} const substituteText = `Error: ${error.message}
Try something like: Try something like:
@ -50,36 +48,21 @@ Your old input was: ${value}`;
}; };
return ( return (
<form onSubmit={handleSubmitInner} className="block place-centers"> <form onSubmit={handleSubmitInner}>
<textarea <div className="flex flex-col items-center space-y-5 max-w-2xl">
value={value} <textarea value={value} onChange={handleChange} rows={8} cols={50} />
onChange={handleChange} <Button
rows={8} disabled={acting}
cols={50} onClick={acting ? undefined : handleSubmitInner}
className=""
/>
<br />
<div className="grid grid-cols-3 text-center">
<button
className="block col-start-2 col-end-2 w-full bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 border border-blue-500 hover:border-transparent rounded mt-5 p-10 text-center"
onClick={handleSubmitInner}
> >
Create dashboard {acting ? "Creating..." : "Create dashboard"}
</button> </Button>
<button
className={ <InfoBox>
displayingDoneMessage
? "block col-start-2 col-end-2 bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-2 border border-blue-500 hover:border-transparent rounded mt-2 p-2 text-center "
: "hidden "
}
>
Done!
</button>
<p className="block col-start-1 col-end-4 bg-gray-200 text-gray-700 py-2 px-4 border border-transparent mt-5 p-10 text-center mb-6">
You can find the necessary ids by toggling the advanced options in the You can find the necessary ids by toggling the advanced options in the
search, or by visiting{" "} search, or by visiting{" "}
<a href="/api/all-forecasts">/api/all-forecasts</a> <a href="/api/all-forecasts">/api/all-forecasts</a>
</p> </InfoBox>
</div> </div>
</form> </form>
); );

View File

@ -1,5 +1,5 @@
import { DisplayForecasts } from "./DisplayForecasts"; import { DisplayForecasts } from "./DisplayForecasts";
import displayOneForecast from "./displayOneForecastForCapture"; import { DisplayOneForecastForCapture } from "./DisplayOneForecastForCapture";
export function displayForecastsWrapperForSearch({ export function displayForecastsWrapperForSearch({
results, results,
@ -23,9 +23,9 @@ export function displayForecastsWrapperForCapture({
}) { }) {
return ( return (
<div className="grid grid-cols-1 w-full justify-center"> <div className="grid grid-cols-1 w-full justify-center">
{displayOneForecast({ <DisplayOneForecastForCapture
result: results[whichResultToDisplayAndCapture], result={results[whichResultToDisplayAndCapture]}
})} />
</div> </div>
); );
} }

View File

@ -171,7 +171,7 @@ interface Props {
result: FrontendForecast; result: FrontendForecast;
} }
const DisplayOneForecast: React.FC<Props> = ({ result }) => { export const DisplayOneForecastForCapture: React.FC<Props> = ({ result }) => {
const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false); const [hasDisplayBeenCaptured, setHasDisplayBeenCaptured] = useState(false);
useEffect(() => { useEffect(() => {
@ -248,8 +248,6 @@ const DisplayOneForecast: React.FC<Props> = ({ result }) => {
); );
}; };
export default DisplayOneForecast;
// https://stackoverflow.com/questions/39501289/in-reactjs-how-to-copy-text-to-clipboard // https://stackoverflow.com/questions/39501289/in-reactjs-how-to-copy-text-to-clipboard
// Note: https://stackoverflow.com/questions/66016033/can-no-longer-upload-images-to-imgur-from-localhost // Note: https://stackoverflow.com/questions/66016033/can-no-longer-upload-images-to-imgur-from-localhost
// Use: http://imgurtester:3000/embed for testing. // Use: http://imgurtester:3000/embed for testing.

15
src/web/hooks.ts Normal file
View File

@ -0,0 +1,15 @@
import React, { DependencyList, EffectCallback, useEffect } from "react";
export const useNoInitialEffect = (
effect: EffectCallback,
deps: DependencyList
) => {
const initial = React.useRef(true);
useEffect(() => {
if (initial.current) {
initial.current = false;
return;
}
return effect();
}, deps);
};

View File

@ -1,25 +1,15 @@
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import React, { DependencyList, EffectCallback, Fragment, useEffect, useState } from "react"; import React, { Fragment, useState } from "react";
import { ButtonsForStars } from "../display/ButtonsForStars"; import { ButtonsForStars } from "../display/ButtonsForStars";
import { MultiSelectPlatform } from "../display/MultiSelectPlatform"; import { MultiSelectPlatform } from "../display/MultiSelectPlatform";
import { QueryForm } from "../display/QueryForm"; import { QueryForm } from "../display/QueryForm";
import { SliderElement } from "../display/SliderElement"; import { SliderElement } from "../display/SliderElement";
import { useNoInitialEffect } from "../hooks";
import { FrontendForecast } from "../platforms"; import { FrontendForecast } from "../platforms";
import searchAccordingToQueryData from "../worker/searchAccordingToQueryData"; import searchAccordingToQueryData from "../worker/searchAccordingToQueryData";
import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage"; import { Props as AnySearchPageProps, QueryParameters } from "./anySearchPage";
const useNoInitialEffect = (effect: EffectCallback, deps: DependencyList) => {
const initial = React.useRef(true);
useEffect(() => {
if (initial.current) {
initial.current = false;
return;
}
return effect();
}, deps);
};
interface Props extends AnySearchPageProps { interface Props extends AnySearchPageProps {
hasSearchbar: boolean; hasSearchbar: boolean;
hasCapture: boolean; hasCapture: boolean;