diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 6966d9e3..5f05c67c 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/components/Analysis/Analysis.jsx b/frontend/components/Analysis/Analysis.tsx similarity index 86% rename from frontend/components/Analysis/Analysis.jsx rename to frontend/components/Analysis/Analysis.tsx index 960cd112..a74dec30 100644 --- a/frontend/components/Analysis/Analysis.jsx +++ b/frontend/components/Analysis/Analysis.tsx @@ -1,12 +1,18 @@ import styles from "./Analysis.module.css"; -import { useState } from "react"; +import { useState, ReactNode } from "react"; import FolderIcon from "@/public/static/folder.svg"; import FilterIcon from "@/public/static/filter.svg"; import { fontLight } from "@fonts"; -const Analysis = ({ text = "Analysis", icon = "folder", children }) => { +type AnalysisProps = { + text?: string; + icon?: "folder" | "filter"; + children?: ReactNode; +}; + +const Analysis = ({ text = "Analysis", icon = "folder", children }: AnalysisProps) => { const [open, setOpen] = useState(false); return (
{ ); }; -export default Analysis; +export default Analysis; \ No newline at end of file diff --git a/frontend/components/Bar/Bar.jsx b/frontend/components/Bar/Bar.tsx similarity index 76% rename from frontend/components/Bar/Bar.jsx rename to frontend/components/Bar/Bar.tsx index 0be9b04e..1d54f0ac 100644 --- a/frontend/components/Bar/Bar.jsx +++ b/frontend/components/Bar/Bar.tsx @@ -8,7 +8,32 @@ import Router from "next/router"; import * as NProgress from "nprogress"; import * as React from "react"; -const NextNProgress = ({ +type NProgressOptions = { + minimum?: number; + easing?: string; + speed?: number; + trickle?: boolean; + trickleSpeed?: number; + showSpinner?: boolean; + barSelector?: string; + spinnerSelector?: string; + parent?: string; + template?: string; +}; + +type NextNProgressProps = { + color?: string; + displaySpinner?: boolean; + startPosition?: number; + stopDelayMs?: number; + height?: number; + showOnShallow?: boolean; + options?: NProgressOptions; + nonce?: string; + transformCSS?: (css: string) => JSX.Element; +}; + +const NextNProgress: React.FC = ({ color = "var(--primary-dark)", displaySpinner = false, startPosition = 0.3, @@ -19,7 +44,7 @@ const NextNProgress = ({ nonce, transformCSS = (css) => , }) => { - let timer = null; + let timer: NodeJS.Timeout | null = null; React.useEffect(() => { if (options) { @@ -35,14 +60,14 @@ const NextNProgress = ({ }; }, []); - const routeChangeStart = (_, { shallow }) => { + const routeChangeStart = (_: string, { shallow }: { shallow: boolean }) => { if (!shallow || showOnShallow) { NProgress.set(startPosition); NProgress.start(); } }; - const routeChangeEnd = (_, { shallow }) => { + const routeChangeEnd = (_: string, { shallow }: { shallow: boolean }) => { if (!shallow || showOnShallow) { if (timer) clearTimeout(timer); timer = setTimeout(() => { @@ -51,7 +76,7 @@ const NextNProgress = ({ } }; - const routeChangeError = (_err, _url, { shallow }) => { + const routeChangeError = (_err: Error, _url: string, { shallow }: { shallow: boolean }) => { if (!shallow || showOnShallow) { if (timer) clearTimeout(timer); timer = setTimeout(() => { @@ -130,4 +155,4 @@ const NextNProgress = ({ `); }; -export default React.memo(NextNProgress); +export default React.memo(NextNProgress); \ No newline at end of file diff --git a/frontend/components/Charts/Allocation/Allocation.jsx b/frontend/components/Charts/Allocation/Allocation.jsx deleted file mode 100644 index e898b923..00000000 --- a/frontend/components/Charts/Allocation/Allocation.jsx +++ /dev/null @@ -1,208 +0,0 @@ -import React from "react"; -import { BarStack } from "@visx/shape"; -import { Group } from "@visx/group"; -import { Grid } from "@visx/grid"; -import { AxisBottom } from "@visx/axis"; -import cityTemperature from "@visx/mock-data/lib/mocks/cityTemperature"; -import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale"; -import { timeParse, timeFormat } from "@visx/vendor/d3-time-format"; -import { useTooltip, useTooltipInPortal, defaultStyles } from "@visx/tooltip"; -import { LegendOrdinal } from "@visx/legend"; -import { localPoint } from "@visx/event"; - -const purple1 = "#6c5efb"; -const purple2 = "#c998ff"; -export const purple3 = "#a44afe"; -export const background = "#eaedff"; -const defaultMargin = { top: 40, right: 0, bottom: 0, left: 0 }; -const tooltipStyles = { - ...defaultStyles, - minWidth: 60, - backgroundColor: "rgba(0,0,0,0.9)", - color: "white", -}; - -const data = cityTemperature.slice(0, 12); -const keys = Object.keys(data[0]).filter((d) => d !== "date"); - -const temperatureTotals = data.reduce((allTotals, currentDate) => { - const totalTemperature = keys.reduce((dailyTotal, k) => { - dailyTotal += Number(currentDate[k]); - return dailyTotal; - }, 0); - allTotals.push(totalTemperature); - return allTotals; -}, []); - -const parseDate = timeParse("%Y-%m-%d"); -const format = timeFormat("%b %d"); -const formatDate = (date) => format(parseDate(date)); - -// accessors -const getDate = (d) => d.date; - -// scales -const dateScale = scaleBand({ - domain: data.map(getDate), - padding: 0.2, -}); -const temperatureScale = scaleLinear({ - domain: [0, Math.max(...temperatureTotals)], - nice: true, -}); -const colorScale = scaleOrdinal({ - domain: keys, - range: [purple1, purple2, purple3], -}); - -let tooltipTimeout; - -const Allocation = ({ - width, - height, - events = false, - margin = defaultMargin, -}) => { - const { - tooltipOpen, - tooltipLeft, - tooltipTop, - tooltipData, - hideTooltip, - showTooltip, - } = useTooltip(); - - const { containerRef, TooltipInPortal } = useTooltipInPortal({ - // TooltipInPortal is rendered in a separate child of and positioned - // with page coordinates which should be updated on scroll. consider using - // Tooltip or TooltipWithBounds if you don't need to render inside a Portal - scroll: true, - }); - - if (width < 10) return null; - // bounds - const xMax = width; - const yMax = height - margin.top - 100; - - dateScale.rangeRound([0, xMax]); - temperatureScale.range([yMax, 0]); - console.log(cityTemperature); - - return width < 10 ? null : ( -
- - - - - - {(barStacks) => - barStacks.map((barStack) => - barStack.bars.map((bar) => ( - { - if (events) alert(`clicked: ${JSON.stringify(bar)}`); - }} - onMouseLeave={() => { - tooltipTimeout = window.setTimeout(() => { - hideTooltip(); - }, 300); - }} - onMouseMove={(event) => { - if (tooltipTimeout) clearTimeout(tooltipTimeout); - // TooltipInPortal expects coordinates to be relative to containerRef - // localPoint returns coordinates relative to the nearest SVG, which - // is what containerRef is set to in this example. - const eventSvgCoords = localPoint(event); - const left = bar.x + bar.width / 2; - showTooltip({ - tooltipData: bar, - tooltipTop: eventSvgCoords?.y, - tooltipLeft: left, - }); - }} - /> - )) - ) - } - - - - -
- -
- - {tooltipOpen && tooltipData && ( - -
- {tooltipData.key} -
-
{tooltipData.bar.data[tooltipData.key]}℉
-
- {formatDate(getDate(tooltipData.bar.data))} -
-
- )} -
- ); -}; - -export default Allocation; diff --git a/frontend/components/Charts/Allocation/Allocation.tsx b/frontend/components/Charts/Allocation/Allocation.tsx new file mode 100644 index 00000000..70483d8e --- /dev/null +++ b/frontend/components/Charts/Allocation/Allocation.tsx @@ -0,0 +1,219 @@ +import React from "react"; +import { BarStack } from "@visx/shape"; +import { Group } from "@visx/group"; +import { Grid } from "@visx/grid"; +import { AxisBottom } from "@visx/axis"; +import cityTemperature from "@visx/mock-data/lib/mocks/cityTemperature"; +import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale"; +import { timeParse, timeFormat } from "@visx/vendor/d3-time-format"; +import { useTooltip, useTooltipInPortal, defaultStyles } from "@visx/tooltip"; +import { LegendOrdinal } from "@visx/legend"; +import { localPoint } from "@visx/event"; + +const purple1 = "#6c5efb"; +const purple2 = "#c998ff"; +export const purple3 = "#a44afe"; +export const background = "#eaedff"; +const defaultMargin = { top: 40, right: 0, bottom: 0, left: 0 }; +const tooltipStyles = { + ...defaultStyles, + minWidth: 60, + backgroundColor: "rgba(0,0,0,0.9)", + color: "white", +}; + +const data = cityTemperature.slice(0, 12); +const keys = Object.keys(data[0]).filter((d) => d !== "date"); + +const temperatureTotals = data.reduce((allTotals: number[], currentDate: any) => { + const totalTemperature = keys.reduce((dailyTotal, k) => { + dailyTotal += Number(currentDate[k]); + return dailyTotal; + }, 0); + allTotals.push(totalTemperature); + return allTotals; +}, []); + +const parseDate = timeParse("%Y-%m-%d"); +const format = timeFormat("%b %d"); +const formatDate = (date: string) => format(parseDate(date) as Date); + +// accessors +const getDate = (d: any) => d.date; + +// scales +const dateScale = scaleBand({ + domain: data.map(getDate), + padding: 0.2, +}); +const temperatureScale = scaleLinear({ + domain: [0, Math.max(...temperatureTotals)], + nice: true, +}); +const colorScale = scaleOrdinal({ + domain: keys, + range: [purple1, purple2, purple3], +}); + +let tooltipTimeout: number; + +interface AllocationProps { + width: number; + height: number; + events?: boolean; + margin?: { top: number; right: number; bottom: number; left: number }; +} + +const Allocation: React.FC = ({ + width, + height, + events = false, + margin = defaultMargin, +}) => { + + return <> + + // const { + // tooltipOpen, + // tooltipLeft, + // tooltipTop, + // tooltipData, + // hideTooltip, + // showTooltip, + // } = useTooltip(); + + // const { containerRef, TooltipInPortal } = useTooltipInPortal({ + // // TooltipInPortal is rendered in a separate child of and positioned + // // with page coordinates which should be updated on scroll. consider using + // // Tooltip or TooltipWithBounds if you don't need to render inside a Portal + // scroll: true, + // }); + + // if (width < 10) return null; + // // bounds + // const xMax = width; + // const yMax = height - margin.top - 100; + + // dateScale.rangeRound([0, xMax]); + // temperatureScale.range([yMax, 0]); + // console.log(cityTemperature); + + // return width < 10 ? null : ( + //
+ // + // + // + // + // + // {(barStacks) => + // barStacks.map((barStack) => + // barStack.bars.map((bar) => ( + // { + // if (events) alert(`clicked: ${JSON.stringify(bar)}`); + // }} + // onMouseLeave={() => { + // tooltipTimeout = window.setTimeout(() => { + // hideTooltip(); + // }, 300); + // }} + // onMouseMove={(event) => { + // if (tooltipTimeout) clearTimeout(tooltipTimeout); + // // TooltipInPortal expects coordinates to be relative to containerRef + // // localPoint returns coordinates relative to the nearest SVG, which + // // is what containerRef is set to in this example. + // const eventSvgCoords = localPoint(event); + // const left = bar.x + bar.width / 2; + // showTooltip({ + // tooltipData: bar, + // tooltipTop: eventSvgCoords?.y, + // tooltipLeft: left, + // }); + // }} + // /> + // )) + // ) + // } + // + // + // + // + //
+ // + //
+ + // {tooltipOpen && tooltipData && ( + // + //
+ // {tooltipData.key} + //
+ //
{tooltipData.bar.data[tooltipData.key]}℉
+ //
+ // {formatDate(getDate(tooltipData.bar.data))} + //
+ //
+ // )} + //
+ // ); + +}; + +export default Allocation; \ No newline at end of file diff --git a/frontend/components/Charts/Charts.jsx b/frontend/components/Charts/Charts.tsx similarity index 81% rename from frontend/components/Charts/Charts.jsx rename to frontend/components/Charts/Charts.tsx index 07df67c5..d2b9ee6a 100644 --- a/frontend/components/Charts/Charts.jsx +++ b/frontend/components/Charts/Charts.tsx @@ -2,7 +2,7 @@ import styles from "./Charts.module.css"; // import Allocation from "./Allocation/Allocation"; -const Charts = () => { +const Charts: React.FC = () => { return ( <>
@@ -12,4 +12,4 @@ const Charts = () => { ); }; -export default Charts; +export default Charts; \ No newline at end of file diff --git a/frontend/components/Expand/Expand.jsx b/frontend/components/Expand/Expand.tsx similarity index 62% rename from frontend/components/Expand/Expand.jsx rename to frontend/components/Expand/Expand.tsx index 87c93275..f9887088 100644 --- a/frontend/components/Expand/Expand.jsx +++ b/frontend/components/Expand/Expand.tsx @@ -1,23 +1,28 @@ -import { useState } from "react"; -import styles from "./Expand.module.css"; - -import ExpandSVG from "@/public/static/expand.svg"; - -const Expand = ({ onClick, expandState }) => { - const [clickState, setClick] = useState(false); - const click = expandState ? expandState : clickState; - - return ( - - ); -}; - -export default Expand; +import { useState } from "react"; +import styles from "./Expand.module.css"; +import ExpandSVG from "@/public/static/expand.svg"; +import { FC } from "react"; + +interface ExpandProps { + onClick: () => void; + expandState?: boolean; +} + +const Expand: FC = ({ onClick, expandState }) => { + const [clickState, setClick] = useState(false); + const click = expandState ? expandState : clickState; + + return ( + + ); +}; + +export default Expand; \ No newline at end of file diff --git a/frontend/components/Explorer/Explorer.jsx b/frontend/components/Explorer/Explorer.tsx similarity index 83% rename from frontend/components/Explorer/Explorer.jsx rename to frontend/components/Explorer/Explorer.tsx index 39223e0c..fc872cc2 100644 --- a/frontend/components/Explorer/Explorer.jsx +++ b/frontend/components/Explorer/Explorer.tsx @@ -1,6 +1,6 @@ import styles from "./Explorer.module.css"; -import Error from "next/error"; +import NextError from "next/error"; import axios from "axios"; import useSWR from "swr"; @@ -23,23 +23,28 @@ import Table from "components/Table/Table"; import Unavailable from "components/Unavailable/Unavailable"; import Timeline from "./Timeline/Timeline"; -const server = process.env.NEXT_PUBLIC_SERVER; +const server: string | undefined = process.env.NEXT_PUBLIC_SERVER; -// Most janky code I've ever written. Really, just the worst. -// I made some mistakes in the infastructure making the stocks table, -// and now as I repeat the code here, the same mistakes are amplifed -// greatly. Way too much repitition, partly my own fault, but -// (I think) mostly due to React's at times terrible data fetching -// system(s). Libraries help at first, then make it worse later. -// TLDR: Fix later. +interface Filing { + access_number: string; +} -const Explorer = () => { +interface FilingData { + filings: Filing[]; +} + +interface Comparison { + type: "primary" | "secondary"; + access: string; +} + +const Explorer: React.FC = () => { const dispatch = useDispatch(); const cik = useSelector(selectCik); const primary = useSelector(selectPrimary); const secondary = useSelector(selectSecondary); - const filingFetcher = (url, cik) => + const filingFetcher = (url: string, cik: string) => axios .get(url, { params: { @@ -47,12 +52,12 @@ const Explorer = () => { }, }) .then((r) => r.data) - .then((data) => { + .then((data: FilingData) => { if (data) { const filings = data.filings; dispatch(setFilings(filings)); - if (primary.access == "") { + if (primary.access === "") { dispatch( setComparison({ type: "primary", @@ -60,7 +65,7 @@ const Explorer = () => { }) ); } - if (secondary.access == "") { + if (secondary.access === "") { dispatch( setComparison({ type: "secondary", @@ -76,7 +81,7 @@ const Explorer = () => { .catch((e) => console.error(e)); const { isLoading: loading, error } = useSWR( cik ? [server + "/filers/filings", cik] : null, - ([url, cik]) => filingFetcher(url, cik), + ([url, cik]: [string, string]) => filingFetcher(url, cik), { revalidateOnFocus: false, revalidateOnReconnect: false, @@ -147,7 +152,7 @@ const Explorer = () => {
{loading ? : null}
- {primaryError ? : null} + {primaryError ? : null} { />
- {secondaryError ? : null} + {secondaryError ? : null}
{ ); }; -export default Explorer; +export default Explorer; \ No newline at end of file diff --git a/frontend/components/Explorer/Timeline/Difference/Difference.jsx b/frontend/components/Explorer/Timeline/Difference/Difference.tsx similarity index 75% rename from frontend/components/Explorer/Timeline/Difference/Difference.jsx rename to frontend/components/Explorer/Timeline/Difference/Difference.tsx index c7d95937..bce9bd8a 100644 --- a/frontend/components/Explorer/Timeline/Difference/Difference.jsx +++ b/frontend/components/Explorer/Timeline/Difference/Difference.tsx @@ -15,14 +15,33 @@ import { font, fontLight } from "components/fonts"; import Headers from "components/Headers/Headers"; import Record from "../Select/Record/Record"; -const Difference = (props) => { +interface DifferenceProps { + setDescription: (description: string) => void; +} + +interface Header { + accessor: string; + active: boolean; +} + +interface Sort { + sold: boolean; + na: boolean; +} + +interface DifferenceState { + headers: Header[]; + sort: Sort; +} + +const Difference: React.FC = (props) => { const dispatch = useDispatch(); - const difference = useSelector(selectDifference); + const difference: DifferenceState = useSelector(selectDifference); const headers = difference.headers; - const updateHeaders = (h) => dispatch(editDifference({ headers: h })); - const updateDescription = (d) => props.setDescription(d); - const updateActivation = (a) => + const updateHeaders = (h: Header[]) => dispatch(editDifference({ headers: h })); + const updateDescription = (d: string) => props.setDescription(d); + const updateActivation = (a: string) => dispatch( editDifference({ headers: headers.map((h) => @@ -73,4 +92,4 @@ const Difference = (props) => { ); }; -export default Difference; +export default Difference; \ No newline at end of file diff --git a/frontend/components/Explorer/Timeline/Select/Picker/Picker.jsx b/frontend/components/Explorer/Timeline/Select/Picker/Picker.tsx similarity index 89% rename from frontend/components/Explorer/Timeline/Select/Picker/Picker.jsx rename to frontend/components/Explorer/Timeline/Select/Picker/Picker.tsx index 666028ef..325b4396 100644 --- a/frontend/components/Explorer/Timeline/Select/Picker/Picker.jsx +++ b/frontend/components/Explorer/Timeline/Select/Picker/Picker.tsx @@ -1,9 +1,8 @@ + import styles from "./Picker.module.css"; import selectStyles from "../Select.module.css"; import { useEffect, useState } from "react"; - import axios from "axios"; - import { useDispatch, useSelector } from "react-redux"; import { selectCik, @@ -11,16 +10,26 @@ import { setComparison, setOpen, } from "@/redux/filerSlice"; - import { font, fontLight } from "components/fonts"; const server = process.env.NEXT_PUBLIC_SERVER; -const Picker = (props) => { - const selected = props.selected; - const attributes = props.attributes; - const picking = props.picking; - const setPicking = () => props.setPicking(); +interface PickerProps { + selected: { type: string }; + attributes: { text: string; hint: string }[]; + picking: boolean; + setPicking: (picking: boolean) => void; +} + +interface Filing { + access_number: string; + report_date: number; + filing_date: number; + market_value?: number; +} + +const Picker: React.FC = (props) => { + const { selected, attributes, picking, setPicking } = props; const cik = useSelector(selectCik); const filings = useSelector(selectFilings); const dispatch = useDispatch(); @@ -33,7 +42,7 @@ const Picker = (props) => { ].join(" ")} >
- {filings.map((filing) => { + {filings.map((filing: Filing) => { const accessNumber = filing.access_number; const reportDate = new Date( filing.report_date * 1000 diff --git a/frontend/components/Explorer/Timeline/Select/Record/Record.jsx b/frontend/components/Explorer/Timeline/Select/Record/Record.tsx similarity index 83% rename from frontend/components/Explorer/Timeline/Select/Record/Record.jsx rename to frontend/components/Explorer/Timeline/Select/Record/Record.tsx index cb053d39..da704968 100644 --- a/frontend/components/Explorer/Timeline/Select/Record/Record.jsx +++ b/frontend/components/Explorer/Timeline/Select/Record/Record.tsx @@ -9,7 +9,14 @@ import DataIcon from "@/public/static/data.svg"; import TableIcon from "@/public/static/csv.svg"; const server = process.env.NEXT_PUBLIC_SERVER; -const Record = (props) => { + +type Props = { + variant?: "json" | "csv"; + selected: { access: string }; + headers?: { tooltip: string; [key: string]: any }[]; +}; + +const Record: React.FC = (props) => { const cik = useSelector(selectCik); const variant = props.variant || "json"; const selected = props.selected; @@ -23,6 +30,7 @@ const Record = (props) => { "_blank" ); }; + const handleCSVDownload = () => { window.open( server + @@ -30,11 +38,12 @@ const Record = (props) => { new URLSearchParams({ cik, access_number: selected.access, - headers: JSON.stringify(headers.map(({ tooltip, ...rest }) => rest)), + headers: JSON.stringify(headers?.map(({ tooltip, ...rest }) => rest)), }), "_blank" ); }; + return ( - ); -}; - -export default Header; +import styles from "./Header.module.css"; + +import { font } from "@fonts"; + +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +import React from "react"; + +interface HeaderProps { + header: HeaderType; + activate: () => void; + fixed?: boolean; + count?: number; + onMouseEnter?: () => void; + onMouseLeave?: () => void; +} + +interface HeaderType { + accessor: string; + display: string; + active: boolean; +} + +const Header: React.FC = (props) => { + const header = props.header; + const activate = props.activate; + const fixed = props.fixed || false; + + const count = props.count; + const onMouseEnter = props.onMouseEnter; + const onMouseLeave = props.onMouseLeave; + + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: header.accessor }); + const style = fixed + ? {} + : { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( + + ); +}; + +export default Header; \ No newline at end of file diff --git a/frontend/components/Headers/Headers.jsx b/frontend/components/Headers/Headers.tsx similarity index 77% rename from frontend/components/Headers/Headers.jsx rename to frontend/components/Headers/Headers.tsx index c2da8acb..d25bc043 100644 --- a/frontend/components/Headers/Headers.jsx +++ b/frontend/components/Headers/Headers.tsx @@ -18,12 +18,30 @@ import { import Header from "./Header/Header"; -const Headers = (props) => { +interface HeaderProps { + accessor: string; + display: string; + tooltip: string; + active: boolean; +} + +interface HeadersProps { + headers: HeaderProps[]; + sold: boolean; + na: boolean; + updateDescription: (description: { title: string; text: string }) => void; + updateHeaders: (newHeaders: HeaderProps[]) => void; + updateActivation: (accessor: string) => void; + updateSold: () => void; + updateNa: () => void; +} + +const Headers: React.FC = (props) => { const headers = props.headers; const sold = props.sold; const na = props.na; - const [event, setEvent] = useReducer((prev, next) => { + const [event, setEvent] = useReducer((prev: any, next: any) => { return { ...prev, ...next }; }, {}); const sensors = useSensors( @@ -40,21 +58,21 @@ const Headers = (props) => { }) ); - const [delayHandler, setDelayHandler] = useState(null); + const [delayHandler, setDelayHandler] = useState(null); const count = headers.length; - const updateDescription = (description) => + const updateDescription = (description: { title: string; text: string }) => props.updateDescription(description); - const updateHeaders = (newHeaders) => props.updateHeaders(newHeaders); - const updateActivation = (accessor) => props.updateActivation(accessor); + const updateHeaders = (newHeaders: HeaderProps[]) => props.updateHeaders(newHeaders); + const updateActivation = (accessor: string) => props.updateActivation(accessor); const updateSold = () => props.updateSold(); const updateNa = () => props.updateNa(); - const handleDragStart = (e) => { + const handleDragStart = (e: any) => { const header = headers.find((h) => h.accessor === e.active.id); setEvent({ ...e, dragging: true, header }); }; - const handleDragEnd = (e) => { + const handleDragEnd = (e: any) => { const active = e.active; const over = e.over; @@ -73,11 +91,11 @@ const Headers = (props) => { updateHeaders(updatedHeaders); }; - const handleMouseEnter = (event, title, text) => { + const handleMouseEnter = (event: React.MouseEvent, title: string, text: string) => { setDelayHandler(setTimeout(() => updateDescription({ title, text }), 300)); }; const handleMouseLeave = () => { - clearTimeout(delayHandler); + clearTimeout(delayHandler!); }; return (
@@ -150,4 +168,4 @@ const Headers = (props) => { ); }; -export default Headers; +export default Headers; \ No newline at end of file diff --git a/frontend/components/Health/Health.jsx b/frontend/components/Health/Health.tsx similarity index 80% rename from frontend/components/Health/Health.jsx rename to frontend/components/Health/Health.tsx index ef2c98d7..ba1e211a 100644 --- a/frontend/components/Health/Health.jsx +++ b/frontend/components/Health/Health.tsx @@ -1,10 +1,11 @@ import { Toaster, toast } from "sonner"; - import { fontLight } from "@fonts"; - -const Health = (props) => { +import React from "react"; +type HealthProps = { + health?: boolean; +}; +const Health: React.FC = (props) => { const health = props.health || false; - if (health === false) { setTimeout(() => { toast.warning( @@ -12,7 +13,6 @@ const Health = (props) => { ); }, 1000); } - return ( { /> ); }; - -export default Health; +export default Health; \ No newline at end of file diff --git a/frontend/components/Hooks/useEllipsis.jsx b/frontend/components/Hooks/useEllipsis.jsx deleted file mode 100644 index 34326375..00000000 --- a/frontend/components/Hooks/useEllipsis.jsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useState } from "react"; - -import useInterval from "./useInterval"; - -const useEllipsis = (interval = 500, pause = false) => { - const [ellipsis, setEllipsis] = useState("."); - useInterval(() => { - if (!pause) - setEllipsis(ellipsis === "..." ? "." : ellipsis + "."); - }, interval); - if (pause) { - return {}; - } - return { ellipsis }; -}; - -export default useEllipsis; diff --git a/frontend/components/Hooks/useEllipsis.tsx b/frontend/components/Hooks/useEllipsis.tsx new file mode 100644 index 00000000..543cfab0 --- /dev/null +++ b/frontend/components/Hooks/useEllipsis.tsx @@ -0,0 +1,20 @@ +import { useState } from "react"; +import useInterval from "./useInterval"; + +type UseEllipsisReturn = { + ellipsis?: string; +}; + +const useEllipsis = (interval: number = 500, pause: boolean = false): UseEllipsisReturn => { + const [ellipsis, setEllipsis] = useState("."); + useInterval(() => { + if (!pause) + setEllipsis(ellipsis === "..." ? "." : ellipsis + "."); + }, interval); + if (pause) { + return {}; + } + return { ellipsis }; +}; + +export default useEllipsis; \ No newline at end of file diff --git a/frontend/components/Hooks/useFilingStocks.jsx b/frontend/components/Hooks/useFilingStocks.tsx similarity index 67% rename from frontend/components/Hooks/useFilingStocks.jsx rename to frontend/components/Hooks/useFilingStocks.tsx index 2ce30b1e..f591318c 100644 --- a/frontend/components/Hooks/useFilingStocks.jsx +++ b/frontend/components/Hooks/useFilingStocks.tsx @@ -1,15 +1,44 @@ import axios from "axios"; import useSWR from "swr"; +import Header from "@/redux/filerSlice" + const server = process.env.NEXT_PUBLIC_SERVER; + +interface Selected { + sort: Sort; + stocks: Stock[]; + access: string; + headers: Header[]; +} + +interface Sort { + sort: string; + reverse: boolean; +} + +interface Stock { + cusip: string; + [key: string]: any; +} + +interface StockFetcherParams { + pagination: number; + sort: string; + offset: number; + reverse: boolean; + sold: boolean; + na: boolean; +} + const useFilingStocks = ( - cik, - selected, - setCount, - setStocks, - activate, - skip, - paginate + cik: string, + selected: Selected, + setCount: (count: number) => void, + setStocks: (stocks: Stock[]) => void, + activate: boolean, + skip: boolean, + paginate: boolean ) => { const sort = selected.sort; const stocks = selected.stocks; @@ -18,10 +47,10 @@ const useFilingStocks = ( const pagination = selected.sort; const stockFetcher = ( - url, - cik, - access, - { pagination, sort, offset, reverse, sold, na } + url: string, + cik: string, + access: string, + { pagination, sort, offset, reverse, sold, na }: StockFetcherParams ) => axios .get(url, { @@ -50,6 +79,7 @@ const useFilingStocks = ( } }) .catch((e) => console.error(e)); + const { isLoading: loading, error } = useSWR( cik && access ? [server + "/stocks/filing", cik, access, sort] : null, ([url, cik, access, sort]) => stockFetcher(url, cik, access, sort), @@ -81,4 +111,4 @@ const useFilingStocks = ( }; }; -export default useFilingStocks; +export default useFilingStocks; \ No newline at end of file diff --git a/frontend/components/Hooks/useInterval.jsx b/frontend/components/Hooks/useInterval.tsx similarity index 59% rename from frontend/components/Hooks/useInterval.jsx rename to frontend/components/Hooks/useInterval.tsx index d325346c..f5f5ef16 100644 --- a/frontend/components/Hooks/useInterval.jsx +++ b/frontend/components/Hooks/useInterval.tsx @@ -1,21 +1,25 @@ -import { useEffect, useRef } from "react"; - -export default function useInterval(callback, delay) { - const savedCallback = useRef(); - - // Remember the latest callback. - useEffect(() => { - savedCallback.current = callback; - }, [callback]); - - // Set up the interval. - useEffect(() => { - function tick() { - savedCallback.current(); - } - if (delay !== null) { - let id = setInterval(tick, delay); - return () => clearInterval(id); - } - }, [delay]); -} +import { useEffect, useRef } from "react"; + +type Callback = () => void; + +export default function useInterval(callback: Callback, delay: number | null): void { + const savedCallback = useRef(); + + // Remember the latest callback. + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + // Set up the interval. + useEffect(() => { + function tick() { + if (savedCallback.current) { + savedCallback.current(); + } + } + if (delay !== null) { + let id = setInterval(tick, delay); + return () => clearInterval(id); + } + }, [delay]); +} \ No newline at end of file diff --git a/frontend/components/Hooks/useStocks.jsx b/frontend/components/Hooks/useStocks.tsx similarity index 65% rename from frontend/components/Hooks/useStocks.jsx rename to frontend/components/Hooks/useStocks.tsx index d1cc6fc8..c3e344d0 100644 --- a/frontend/components/Hooks/useStocks.jsx +++ b/frontend/components/Hooks/useStocks.tsx @@ -2,22 +2,47 @@ import axios from "axios"; import useSWR from "swr"; const server = process.env.NEXT_PUBLIC_SERVER; + +type Stock = { + cusip: string; + [key: string]: any; +}; + +type Sort = { + sort: string; + reverse: boolean; +}; + +type Pagination = { + limit: number; + offset: number; +}; + +type StockFetcherParams = { + pagination: number; + sort: string; + offset: number; + reverse: boolean; + sold: boolean; + na: boolean; +}; + const useStocks = ( - cik, - headers, - pagination, - sort, - stocks, - setCount, - setStocks, - activate, - skip, - paginate + cik: string, + headers: any, + pagination: Pagination, + sort: Sort, + stocks: Stock[], + setCount: (count: number) => void, + setStocks: (stocks: Stock[]) => void, + activate: boolean, + skip: boolean, + paginate: boolean ) => { const stockFetcher = ( - url, - cik, - { pagination, sort, offset, reverse, sold, na } + url: string, + cik: string, + { pagination, sort, offset, reverse, sold, na }: StockFetcherParams ) => axios .get(url, { @@ -74,4 +99,4 @@ const useStocks = ( }; }; -export default useStocks; +export default useStocks; \ No newline at end of file diff --git a/frontend/components/Index/Index.jsx b/frontend/components/Index/Index.tsx similarity index 78% rename from frontend/components/Index/Index.jsx rename to frontend/components/Index/Index.tsx index 8126aa93..5cdf0772 100644 --- a/frontend/components/Index/Index.jsx +++ b/frontend/components/Index/Index.tsx @@ -1,102 +1,106 @@ -import styles from "./Index.module.css"; -import { useEffect, useState } from "react"; - -import axios from "axios"; - -import Error from "next/error"; - -import { useDispatch, useSelector } from "react-redux"; -import { - setStocks, - setCount, - setOffset, - setPagination, - sortHeader, - selectCik, - selectPagination, - selectStocks, - selectSort, - selectHeaders, -} from "@/redux/filerSlice"; - -import useStocks from "components/Hooks/useStocks"; -import Analysis from "components/Analysis/Analysis"; -import Table from "components/Table/Table"; -import Sort from "./Sort/Sort"; - -const server = process.env.NEXT_PUBLIC_SERVER; -const Index = () => { - const dispatch = useDispatch(); - const cik = useSelector(selectCik); - const stocks = useSelector(selectStocks); - - const { - items, - loading, - error, - headers, - pagination, - select, - reverse, - activate, - skip, - paginate, - } = useStocks( - cik, - useSelector(selectHeaders), - useSelector(selectPagination), - useSelector(selectSort), - useSelector(selectStocks), - (s) => dispatch(setCount(s)), - (c) => dispatch(setStocks(c)), - (a, d) => - dispatch( - sortHeader({ - sort: a, - reverse: d, - }) - ), - (o) => { - dispatch(setOffset(o)); - }, - (p) => { - dispatch(setPagination(p)); - } - ); - - const [queryStocks, setQueryStocks] = useState(true); - useEffect(() => { - if (queryStocks && stocks.length) { - axios - .get(server + "/stocks/query", { params: { cik } }) - .catch((e) => console.error(e)); - setQueryStocks(false); - } - }, [stocks]); - - console.log(items); - if (error) return ; - - return ( - <> -
- - - -
- - - ); -}; - -export default Index; +import styles from "./Index.module.css"; +import { useEffect, useState } from "react"; +import axios from "axios"; +import Error from "next/error"; +import { useDispatch, useSelector } from "react-redux"; +import { + setStocks, + setCount, + setOffset, + setPagination, + sortHeader, + selectCik, + selectPagination, + selectStocks, + selectSort, + selectHeaders, +} from "@/redux/filerSlice"; +import useStocks from "components/Hooks/useStocks"; +import Analysis from "components/Analysis/Analysis"; +import Table from "components/Table/Table"; +import Sort from "./Sort/Sort"; + +const server: string | undefined = process.env.NEXT_PUBLIC_SERVER; + +interface Stock { + // Define the properties of a stock item here +} + +interface Pagination { + // Define the properties of pagination here +} + +const Index: React.FC = () => { + const dispatch = useDispatch(); + const cik: string = useSelector(selectCik); + const stocks: Stock[] = useSelector(selectStocks); + + const { + items, + loading, + error, + headers, + pagination, + select, + reverse, + activate, + skip, + paginate, + } = useStocks( + cik, + useSelector(selectHeaders), + useSelector(selectPagination), + useSelector(selectSort), + useSelector(selectStocks), + (s: number) => dispatch(setCount(s)), + (c: Stock[]) => dispatch(setStocks(c)), + (a: string, d: boolean) => + dispatch( + sortHeader({ + sort: a, + reverse: d, + }) + ), + (o: number) => { + dispatch(setOffset(o)); + }, + (p: Pagination) => { + dispatch(setPagination(p)); + } + ); + + const [queryStocks, setQueryStocks] = useState(true); + useEffect(() => { + if (queryStocks && stocks.length) { + axios + .get(server + "/stocks/query", { params: { cik } }) + .catch((e) => console.error(e)); + setQueryStocks(false); + } + }, [stocks]); + + if (error) return ; + + return ( + <> +
+ + + +
+ + + ); +}; + +export default Index; \ No newline at end of file diff --git a/frontend/components/Index/Sort/Filter/Filter.jsx b/frontend/components/Index/Sort/Filter/Filter.tsx similarity index 76% rename from frontend/components/Index/Sort/Filter/Filter.jsx rename to frontend/components/Index/Sort/Filter/Filter.tsx index 4cb5983c..985fdc2b 100644 --- a/frontend/components/Index/Sort/Filter/Filter.jsx +++ b/frontend/components/Index/Sort/Filter/Filter.tsx @@ -1,67 +1,77 @@ -import styles from "./Filter.module.css"; -import { useState } from "react"; - -import { useDispatch, useSelector } from "react-redux"; -import { - selectHeaders, - selectSold, - selectNa, - sortSold, - sortNa, - activateHeader, - setHeaders, -} from "@/redux/filerSlice"; - -import { font, fontLight } from "@fonts"; - -import Tip from "components/Tip/Tip"; -import Headers from "components/Headers/Headers"; - -const Filter = () => { - const headers = useSelector(selectHeaders); - const sold = useSelector(selectSold); - const na = useSelector(selectNa); - const dispatch = useDispatch(); - - const initialHeader = headers[0]; - const [description, setDescription] = useState({ - title: initialHeader.display, - text: initialHeader.tooltip, - }); - const updateDescription = (d) => setDescription(d); - const updateHeaders = (h) => dispatch(setHeaders(h)); - const updateActivation = (a) => dispatch(activateHeader(a)); - const updateSold = () => dispatch(sortSold()); - const updateNa = () => dispatch(sortNa()); - - return ( -
-
- - {description.title} - - - {description.text} - -
- - -
- ); -}; - -export default Filter; +import styles from "./Filter.module.css"; +import { useState } from "react"; + +import { useDispatch, useSelector } from "react-redux"; +import { + selectHeaders, + selectSold, + selectNa, + sortSold, + sortNa, + activateHeader, + setHeaders, +} from "@/redux/filerSlice"; + +import { font, fontLight } from "@fonts"; + +import Tip from "components/Tip/Tip"; +import Headers from "components/Headers/Headers"; + +interface Description { + title: string; + text: string; +} + +interface Header { + display: string; + tooltip: string; +} + +const Filter: React.FC = () => { + const headers = useSelector(selectHeaders) as Header[]; + const sold = useSelector(selectSold); + const na = useSelector(selectNa); + const dispatch = useDispatch(); + + const initialHeader = headers[0]; + const [description, setDescription] = useState({ + title: initialHeader.display, + text: initialHeader.tooltip, + }); + const updateDescription = (d: Description) => setDescription(d); + const updateHeaders = (h: Header[]) => dispatch(setHeaders(h)); + const updateActivation = (a: string) => dispatch(activateHeader(a)); + const updateSold = () => dispatch(sortSold()); + const updateNa = () => dispatch(sortNa()); + + return ( +
+
+ + {description.title} + + + {description.text} + +
+ + +
+ ); +}; + +export default Filter; \ No newline at end of file diff --git a/frontend/components/Index/Sort/Gain/Droppable/Plus.jsx b/frontend/components/Index/Sort/Gain/Droppable/Plus.tsx similarity index 79% rename from frontend/components/Index/Sort/Gain/Droppable/Plus.jsx rename to frontend/components/Index/Sort/Gain/Droppable/Plus.tsx index 4b523e18..726fb9f9 100644 --- a/frontend/components/Index/Sort/Gain/Droppable/Plus.jsx +++ b/frontend/components/Index/Sort/Gain/Droppable/Plus.tsx @@ -1,36 +1,45 @@ -import styles from "../Gain.module.css"; - -import { useDispatch } from "react-redux"; -import { newDate } from "@/redux/filerSlice"; - -import { useDroppable } from "@dnd-kit/core"; - -import PlusSVG from "./plus.svg"; - -const Plus = (props) => { - const event = props.event; - - const dispatch = useDispatch(); - - const id = "plus"; - const { setNodeRef } = useDroppable({ id: id }); - - return ( -
- -
- ); -}; - -export default Plus; +import styles from "../Gain.module.css"; + +import { useDispatch } from "react-redux"; +import { newDate } from "@/redux/filerSlice"; + +import { useDroppable } from "@dnd-kit/core"; + +import PlusSVG from "./plus.svg"; + +interface PlusProps { + event: { + dragging: boolean; + over: { + id: string; + }; + }; +} + +const Plus: React.FC = (props) => { + const { event } = props; + + const dispatch = useDispatch(); + + const id = "plus"; + const { setNodeRef } = useDroppable({ id: id }); + + return ( +
+ +
+ ); +}; + +export default Plus; \ No newline at end of file diff --git a/frontend/components/Index/Sort/Gain/Droppable/Trash.jsx b/frontend/components/Index/Sort/Gain/Droppable/Trash.tsx similarity index 72% rename from frontend/components/Index/Sort/Gain/Droppable/Trash.jsx rename to frontend/components/Index/Sort/Gain/Droppable/Trash.tsx index 22d17eba..e25745c5 100644 --- a/frontend/components/Index/Sort/Gain/Droppable/Trash.jsx +++ b/frontend/components/Index/Sort/Gain/Droppable/Trash.tsx @@ -1,34 +1,43 @@ -import styles from "../Gain.module.css"; - -import { useDroppable } from "@dnd-kit/core"; - -import { useDispatch } from "react-redux"; -import { removeDate } from "@/redux/filerSlice"; - -import TrashSVG from "./trash.svg"; - -const Trash = (props) => { - const event = props.event; - const dispatch = useDispatch(); - - const id = "trash"; - const { setNodeRef } = useDroppable({ id: id }); - - return ( -
- -
- ); -}; - -export default Trash; +import styles from "../Gain.module.css"; + +import { useDroppable } from "@dnd-kit/core"; + +import { useDispatch } from "react-redux"; +import { removeDate } from "@/redux/filerSlice"; + +import TrashSVG from "./trash.svg"; + +interface TrashProps { + event: { + dragging: boolean; + over?: { + id: string; + }; + }; +} + +const Trash: React.FC = (props) => { + const { event } = props; + const dispatch = useDispatch(); + + const id = "trash"; + const { setNodeRef } = useDroppable({ id: id }); + + return ( +
+ +
+ ); +}; + +export default Trash; \ No newline at end of file diff --git a/frontend/components/Index/Sort/Gain/Gain.jsx b/frontend/components/Index/Sort/Gain/Gain.tsx similarity index 84% rename from frontend/components/Index/Sort/Gain/Gain.jsx rename to frontend/components/Index/Sort/Gain/Gain.tsx index 960f9508..f775c5a2 100644 --- a/frontend/components/Index/Sort/Gain/Gain.jsx +++ b/frontend/components/Index/Sort/Gain/Gain.tsx @@ -1,134 +1,151 @@ -import styles from "./Gain.module.css"; -import { useReducer } from "react"; - -import { - selectDates, - updateDates, - newDate, - openDate, - removeDate, -} from "@/redux/filerSlice"; -import { useDispatch, useSelector } from "react-redux"; - -import { - DndContext, - DragOverlay, - closestCenter, - MouseSensor, - TouchSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { - SortableContext, - horizontalListSortingStrategy, - arrayMove, -} from "@dnd-kit/sortable"; - -import Select from "./Select/Select"; -import Plus from "./Droppable/Plus"; -import Trash from "./Droppable/Trash"; -import Tip from "components/Tip/Tip"; - -const Gain = () => { - const dates = useSelector(selectDates); - const dispatch = useDispatch(); - - const [event, setEvent] = useReducer((prev, next) => { - return { ...prev, ...next }; - }, {}); - const sensors = useSensors( - useSensor(MouseSensor, { - activationConstraint: { - distance: 8, - }, - }), - useSensor(TouchSensor, { - activationConstraint: { - delay: 200, - tolerance: 6, - }, - }) - ); - - const handleDragStart = (e) => { - const date = dates.find((date) => date.id === e.active.id); - - setEvent({ ...e, dragging: true, date: date }); - dispatch(openDate({ accessor: e.active.id, open: false })); - }; - const handleDragEnd = (e) => { - const active = e.active; - const over = e.over; - - setEvent({ - ...e, - dragging: false, - date: null, - }); - - if (!over) return; - - switch (over.id) { - case "plus": - dispatch(newDate()); - return; - case "trash": - dispatch(removeDate(active.id)); - return; - } - - const activeIndex = dates.findIndex(({ id }) => id === active.id); - const overIndex = dates.findIndex(({ id }) => id === over.id); - const updatedDates = arrayMove(dates, activeIndex, overIndex); - dispatch(updateDates(updatedDates)); - }; - - return ( -
-
- -
- date.accessor)} - strategy={horizontalListSortingStrategy} - > - {dates.map((date, index) => ( - - ) : null} - -
- -
- dispatch(newDate())} event={event} /> - -
-
-
- - - -
- ); -}; -export default Gain; + +import styles from "./Gain.module.css"; +import { useReducer } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { + selectDates, + updateDates, + newDate, + openDate, + removeDate, +} from "@/redux/filerSlice"; +import { + DndContext, + DragOverlay, + closestCenter, + MouseSensor, + TouchSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + SortableContext, + horizontalListSortingStrategy, + arrayMove, +} from "@dnd-kit/sortable"; +import Select from "./Select/Select"; +import Plus from "./Droppable/Plus"; +import Trash from "./Droppable/Trash"; +import Tip from "components/Tip/Tip"; + +interface DateType { + id: string; + accessor: string; + // Add other properties as needed +} + +interface EventType { + active: { id: string }; + over?: { id: string }; + dragging?: boolean; + date?: DateType | null; +} + +const Gain: React.FC = () => { + const dates = useSelector(selectDates) as DateType[]; + const dispatch = useDispatch(); + + const [event, setEvent] = useReducer( + (prev: EventType, next: Partial) => { + return { ...prev, ...next }; + }, + {} as EventType + ); + + const sensors = useSensors( + useSensor(MouseSensor, { + activationConstraint: { + distance: 8, + }, + }), + useSensor(TouchSensor, { + activationConstraint: { + delay: 200, + tolerance: 6, + }, + }) + ); + + const handleDragStart = (e: EventType) => { + const date = dates.find((date) => date.id === e.active.id); + + setEvent({ ...e, dragging: true, date: date || null }); + dispatch(openDate({ accessor: e.active.id, open: false })); + }; + + const handleDragEnd = (e: EventType) => { + const active = e.active; + const over = e.over; + + setEvent({ + ...e, + dragging: false, + date: null, + }); + + if (!over) return; + + switch (over.id) { + case "plus": + dispatch(newDate()); + return; + case "trash": + dispatch(removeDate(active.id)); + return; + } + + const activeIndex = dates.findIndex(({ id }) => id === active.id); + const overIndex = dates.findIndex(({ id }) => id === over.id); + const updatedDates = arrayMove(dates, activeIndex, overIndex); + dispatch(updateDates(updatedDates)); + }; + + return ( +
+
+ +
+ date.accessor)} + strategy={horizontalListSortingStrategy} + > + {dates.map((date, index) => ( + + ) : null} + +
+ +
+ dispatch(newDate())} event={event} /> + +
+
+
+ + + +
+ ); +}; + +export default Gain; diff --git a/frontend/components/Index/Sort/Gain/Select/Picker/Picker.jsx b/frontend/components/Index/Sort/Gain/Select/Picker/Picker.tsx similarity index 79% rename from frontend/components/Index/Sort/Gain/Select/Picker/Picker.jsx rename to frontend/components/Index/Sort/Gain/Select/Picker/Picker.tsx index 5a23cced..0cec4553 100644 --- a/frontend/components/Index/Sort/Gain/Select/Picker/Picker.jsx +++ b/frontend/components/Index/Sort/Gain/Select/Picker/Picker.tsx @@ -1,174 +1,183 @@ -import styles from "./Picker.module.css"; -import { useState, useEffect } from "react"; - -import { font } from "@fonts"; - -import { editDate, openDate } from "@/redux/filerSlice"; -import { useDispatch } from "react-redux"; - -import CalendarSVG from "./calendar.svg"; -import RightSVG from "./right.svg"; -import LeftSVG from "./left.svg"; - -const months = [ - { name: "Jan", value: 0 }, - { name: "Feb", value: 1 }, - { name: "Mar", value: 2 }, - { name: "Apr", value: 3 }, - { name: "May", value: 4 }, - { name: "Jun", value: 5 }, - { name: "Jul", value: 6 }, - { name: "Aug", value: 7 }, - { name: "Sep", value: 8 }, - { name: "Oct", value: 9 }, - { name: "Nov", value: 10 }, - { name: "Dec", value: 11 }, -]; - -const yearRegex = /^-?\d+$/; - -const Picker = (props) => { - const date = props.date || {}; - const year = date.year; - const open = date.open; - const dispatch = useDispatch(); - - const handleDateChange = (e) => { - const value = e.slice(0, -1); - const character = e.slice(-1); - - let newValue = value; - if (value.length >= 4) { - newValue = value.slice(1); - } - - if (yearRegex.test(character)) { - newValue = newValue + character; - } - - setDateDisplay(newValue); - }; - const handleBlur = () => { - setFocus(false); - if ( - dateDisplay.length === 4 && - yearRegex.test(dateDisplay) && - dateDisplay > 1899 && - dateDisplay < 2100 - ) { - dispatch( - editDate({ - type: "year", - accessor: date.accessor, - value: dateDisplay, - }) - ); - } - }; - - const [dateDisplay, setDateDisplay] = useState(year); - const [focus, setFocus] = useState(false); - useEffect(() => { - setDateDisplay(year); - }, [year]); - - const month = months[date.month] || months[0]; - const display = `${month.name} ${year}`; - - return ( -
- -
-
- - handleDateChange(e.target.value)} - onFocus={() => setFocus(true)} - onBlur={() => handleBlur()} - onKeyDown={(e) => (e.key === "Enter" ? e.target.blur() : null)} - /> - -
-
- {months.map((month) => ( - - ))} -
-
-
- ); -}; - -export default Picker; +import styles from "./Picker.module.css"; +import { useState, useEffect, ChangeEvent, FocusEvent, KeyboardEvent } from "react"; +import { font } from "@fonts"; +import { editDate, openDate } from "@/redux/filerSlice"; +import { useDispatch } from "react-redux"; +import CalendarSVG from "./calendar.svg"; +import RightSVG from "./right.svg"; +import LeftSVG from "./left.svg"; + +interface DateProps { + year: number; + month: number; + open: boolean; + accessor: string; +} + +interface PickerProps { + date: DateProps; +} + +const months = [ + { name: "Jan", value: 0 }, + { name: "Feb", value: 1 }, + { name: "Mar", value: 2 }, + { name: "Apr", value: 3 }, + { name: "May", value: 4 }, + { name: "Jun", value: 5 }, + { name: "Jul", value: 6 }, + { name: "Aug", value: 7 }, + { name: "Sep", value: 8 }, + { name: "Oct", value: 9 }, + { name: "Nov", value: 10 }, + { name: "Dec", value: 11 }, +]; + +const yearRegex = /^-?\d+$/; + +const Picker: React.FC = (props) => { + const date = props.date || {} as DateProps; + const year = date.year; + const open = date.open; + const dispatch = useDispatch(); + + const handleDateChange = (e: string) => { + const value = e.slice(0, -1); + const character = e.slice(-1); + + let newValue = value; + if (value.length >= 4) { + newValue = value.slice(1); + } + + if (yearRegex.test(character)) { + newValue = newValue + character; + } + + setDateDisplay(newValue); + }; + + const handleBlur = () => { + setFocus(false); + if ( + dateDisplay.length === 4 && + yearRegex.test(dateDisplay) && + parseInt(dateDisplay) > 1899 && + parseInt(dateDisplay) < 2100 + ) { + dispatch( + editDate({ + type: "year", + accessor: date.accessor, + value: parseInt(dateDisplay), + }) + ); + } + }; + + const [dateDisplay, setDateDisplay] = useState(year.toString()); + const [focus, setFocus] = useState(false); + useEffect(() => { + setDateDisplay(year.toString()); + }, [year]); + + const month = months[date.month] || months[0]; + const display = `${month.name} ${year}`; + + return ( +
+ +
+
+ + ) => handleDateChange(e.target.value)} + onFocus={() => setFocus(true)} + onBlur={(e: FocusEvent) => handleBlur()} + onKeyDown={(e: KeyboardEvent) => (e.key === "Enter" ? e.currentTarget.blur() : null)} + /> + +
+
+ {months.map((month) => ( + + ))} +
+
+
+ ); +}; + +export default Picker; \ No newline at end of file diff --git a/frontend/components/Index/Sort/Gain/Select/Select.jsx b/frontend/components/Index/Sort/Gain/Select/Select.tsx similarity index 81% rename from frontend/components/Index/Sort/Gain/Select/Select.jsx rename to frontend/components/Index/Sort/Gain/Select/Select.tsx index fb23c654..2d2488e7 100644 --- a/frontend/components/Index/Sort/Gain/Select/Select.jsx +++ b/frontend/components/Index/Sort/Gain/Select/Select.tsx @@ -1,172 +1,157 @@ -import styles from "./Select.module.css"; -import { useState, useEffect } from "react"; - -import useSWR from "swr"; -import axios from "axios"; - -import { useDispatch, useSelector } from "react-redux"; -import { - addHeader, - updateStocks, - selectCik, - selectHeaders, - removeHeader, - editHeader, -} from "@/redux/filerSlice"; - -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; - -import { font } from "@fonts"; - -import Picker from "./Picker/Picker"; -import Loading from "components/Loading/Loading"; - -// const animateLayoutChanges = (args) => { -// const { isSorting, wasSorting } = args; - -// if (isSorting || wasSorting) { -// return defaultAnimateLayoutChanges(args); -// } - -// return true; -// }; - -const server = process.env.NEXT_PUBLIC_SERVER; -const getFetcher = (url, cik, time) => - axios - .get(url, { params: { cik: cik, time: time } }) - .then((r) => r.data) - .catch((e) => console.error(e)); - -const Select = (props) => { - const propDate = props.date; - const accessor = propDate.accessor; - const index = props.index; - const dispatch = useDispatch(); - const cik = useSelector(selectCik) || false; - const headers = useSelector(selectHeaders); - - const date = - useSelector((state) => - state.filer.dates.find((d) => d.accessor == propDate.accessor) - ) || propDate; - const open = date.open; - const time = date.timestamp; - const { - data, - error, - isLoading: loading, - } = useSWR( - open || cik === false ? null : [server + "/stocks/timeseries", cik, time], - ([url, cik, time]) => getFetcher(url, cik, time), - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - } - ); - useEffect(() => { - if (open) return; - - if (data) { - const timeseries = {}; - data.stocks.forEach((price) => { - const close = price.close_str; - const cusip = price.cusip; - timeseries[cusip] = close; - }); - - dispatch( - updateStocks({ - field: accessor, - values: timeseries, - }) - ); - const display = `${date.month}/${date.day}/${date.year}`; - dispatch(editHeader({ accessor: accessor, display: display })); - } - }, [open, data, accessor, dispatch, date]); - - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging, - } = useSortable({ id: accessor }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.5 : 1, - }; - - const active = headers.find((h) => h.accessor == accessor) ? false : true; - const handleTable = () => { - const header = headers.find((h) => h.accessor == accessor); - - const display = `${date.month + 1}/${date.day}/${date.year}`; - const tooltip = `The prices of the stocks at ${date.month + 1}/${ - date.day - }/${date.year}.`; - if (header) { - dispatch(removeHeader(accessor)); - } else { - dispatch( - addHeader({ - display, - sort: accessor, - tooltip, - accessor: accessor, - active: true, - }) - ); - } - }; - - const handleDownload = () => { - window.open( - server + - "/filers/record/timeseries/?" + - new URLSearchParams({ cik, time }), - "_blank" - ); - }; - - if (error) { - console.error(e); - } - - return ( -
- {loading ? : null} - - - -
- ); -}; - -export default Select; + +import styles from "./Select.module.css"; +import { useState, useEffect } from "react"; +import useSWR from "swr"; +import axios from "axios"; +import { useDispatch, useSelector } from "react-redux"; +import { + addHeader, + updateStocks, + selectCik, + selectHeaders, + removeHeader, + editHeader, +} from "@/redux/filerSlice"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { font } from "@fonts"; +import Picker from "./Picker/Picker"; +import Loading from "components/Loading/Loading"; +const server = process.env.NEXT_PUBLIC_SERVER; +const getFetcher = (url: string, cik: string, time: number) => + axios + .get(url, { params: { cik: cik, time: time } }) + .then((r) => r.data) + .catch((e) => console.error(e)); +interface SelectProps { + date: { + accessor: string; + open: boolean; + timestamp: number; + month: number; + day: number; + year: number; + }; + index: number; +} +const Select: React.FC = (props) => { + const propDate = props.date; + const accessor = propDate.accessor; + const index = props.index; + const dispatch = useDispatch(); + const cik = useSelector(selectCik) || false; + const headers = useSelector(selectHeaders); + const date = + useSelector((state: any) => + state.filer.dates.find((d: any) => d.accessor == propDate.accessor) + ) || propDate; + const open = date.open; + const time = date.timestamp; + const { + data, + error, + isLoading: loading, + } = useSWR( + open || cik === false ? null : [server + "/stocks/timeseries", cik, time], + ([url, cik, time]) => getFetcher(url, cik, time), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); + useEffect(() => { + if (open) return; + if (data) { + const timeseries: { [key: string]: string } = {}; + data.stocks.forEach((price: { close_str: string; cusip: string }) => { + const close = price.close_str; + const cusip = price.cusip; + timeseries[cusip] = close; + }); + dispatch( + updateStocks({ + field: accessor, + values: timeseries, + }) + ); + const display = `${date.month}/${date.day}/${date.year}`; + dispatch(editHeader({ accessor: accessor, display: display })); + } + }, [open, data, accessor, dispatch, date]); + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ id: accessor }); + const style = { + transform: CSS.Transform.toString(transform), + transition, + opacity: isDragging ? 0.5 : 1, + }; + const active = headers.find((h: any) => h.accessor == accessor) ? false : true; + const handleTable = () => { + const header = headers.find((h: any) => h.accessor == accessor); + const display = `${date.month + 1}/${date.day}/${date.year}`; + const tooltip = `The prices of the stocks at ${date.month + 1}/${ + date.day + }/${date.year}.`; + if (header) { + dispatch(removeHeader(accessor)); + } else { + dispatch( + addHeader({ + display, + sort: accessor, + tooltip, + accessor: accessor, + active: true, + }) + ); + } + }; + const handleDownload = () => { + window.open( + server + + "/filers/record/timeseries/?" + + new URLSearchParams({ cik, time }), + "_blank" + ); + }; + if (error) { + console.error(error); + } + return ( +
+ {loading ? : null} + + + +
+ ); +}; +export default Select; diff --git a/frontend/components/Index/Sort/Record/Record.jsx b/frontend/components/Index/Sort/Record/Record.tsx similarity index 69% rename from frontend/components/Index/Sort/Record/Record.jsx rename to frontend/components/Index/Sort/Record/Record.tsx index 29a3874b..affec2d7 100644 --- a/frontend/components/Index/Sort/Record/Record.jsx +++ b/frontend/components/Index/Sort/Record/Record.tsx @@ -1,40 +1,50 @@ -import styles from "./Record.module.css"; - -import { useSelector } from "react-redux"; -import { selectCik, selectHeaders } from "@/redux/filerSlice"; - -import Link from "next/link"; - -import { font } from "@fonts"; - -import DataIcon from "@/public/static/data.svg"; -import TableIcon from "@/public/static/csv.svg"; - -const server = process.env.NEXT_PUBLIC_SERVER; -const Record = (props) => { - const cik = useSelector(selectCik); - const headers = useSelector(selectHeaders); - const variant = props.variant === "csv" ? "csv" : ""; - - const headerString = JSON.stringify(headers); - const url = new URL("/filers/record" + variant, server); - url.searchParams.append("cik", cik); - url.searchParams.append("headers", headerString); - - return ( - - - - ); -}; - -export default Record; +import styles from "./Record.module.css"; + +import { useSelector } from "react-redux"; +import { selectCik, selectHeaders } from "@/redux/filerSlice"; + +import Link from "next/link"; + +import { font } from "@fonts"; + +import DataIcon from "@/public/static/data.svg"; +import TableIcon from "@/public/static/csv.svg"; + +const server = process.env.NEXT_PUBLIC_SERVER; + +enum Variant { + CSV = "csv", + DEFAULT = "" +} + +interface RecordProps { + variant: Variant; +} + +const Record: React.FC = (props) => { + const cik = useSelector(selectCik); + const headers = useSelector(selectHeaders); + const variant = props.variant === Variant.CSV ? Variant.CSV : Variant.DEFAULT; + + const headerString = JSON.stringify(headers); + const url = new URL("/filers/record" + variant, server); + url.searchParams.append("cik", cik); + url.searchParams.append("headers", headerString); + + return ( + + + + ); +}; + +export default Record; \ No newline at end of file diff --git a/frontend/components/Index/Sort/Sort.jsx b/frontend/components/Index/Sort/Sort.tsx similarity index 64% rename from frontend/components/Index/Sort/Sort.jsx rename to frontend/components/Index/Sort/Sort.tsx index 64b322c1..145eb602 100644 --- a/frontend/components/Index/Sort/Sort.jsx +++ b/frontend/components/Index/Sort/Sort.tsx @@ -1,25 +1,34 @@ -import { useState } from "react"; -import styles from "./Sort.module.css"; - -import Filter from "./Filter/Filter"; -import Gain from "./Gain/Gain"; -import Record from "./Record/Record"; -import Tip from "components/Tip/Tip"; - -const Sort = () => { - return ( - <> - - -
-
- - -
- -
- - ); -}; - -export default Sort; +import { useState } from "react"; +import styles from "./Sort.module.css"; + +import Filter from "./Filter/Filter"; +import Gain from "./Gain/Gain"; +import Record from "./Record/Record"; +import Tip from "components/Tip/Tip"; + +interface RecordProps { + variant: RecordVariant; +} + +enum RecordVariant { + JSON = "json", + CSV = "csv", +} + +const Sort: React.FC = () => { + return ( + <> + + +
+
+ + +
+ +
+ + ); +}; + +export default Sort; \ No newline at end of file diff --git a/frontend/components/Layouts/Home.jsx b/frontend/components/Layouts/Home.tsx similarity index 67% rename from frontend/components/Layouts/Home.jsx rename to frontend/components/Layouts/Home.tsx index 01167685..812b9323 100644 --- a/frontend/components/Layouts/Home.jsx +++ b/frontend/components/Layouts/Home.tsx @@ -1,15 +1,17 @@ -import Navigation from "components/Navigation/Navigation"; -import Footer from "components/Footer/Footer"; - -import { Analytics } from "@vercel/analytics/react"; - -export default function Layout({ children }) { - return ( - <> - -
{children}
- -
- - ); -} +import Navigation from "components/Navigation/Navigation"; +import Footer from "components/Footer/Footer"; +import { Analytics } from "@vercel/analytics/react"; +import { ReactNode } from "react"; +interface LayoutProps { + children: ReactNode; +} +export default function Layout({ children }: LayoutProps) { + return ( + <> + +
{children}
+ +
+ + ); +} \ No newline at end of file diff --git a/frontend/components/Layouts/Layout.jsx b/frontend/components/Layouts/Layout.tsx similarity index 64% rename from frontend/components/Layouts/Layout.jsx rename to frontend/components/Layouts/Layout.tsx index 804af25e..5b282d9e 100644 --- a/frontend/components/Layouts/Layout.jsx +++ b/frontend/components/Layouts/Layout.tsx @@ -1,15 +1,17 @@ -import Navigation from "components/Navigation/Navigation"; -import Footer from "components/Footer/Footer"; - -import { Analytics } from "@vercel/analytics/react"; - -export default function Layout({ children }) { - return ( - <> - -
{children}
- -
- - ); -} +import Navigation from "components/Navigation/Navigation"; +import Footer from "components/Footer/Footer"; +import { Analytics } from "@vercel/analytics/react"; +import React, { ReactNode } from "react"; +interface LayoutProps { + children: ReactNode; +} +export default function Layout({ children }: LayoutProps) { + return ( + <> + +
{children}
+ +
+ + ); +} \ No newline at end of file diff --git a/frontend/components/Layouts/Mobile.jsx b/frontend/components/Layouts/Mobile.tsx similarity index 75% rename from frontend/components/Layouts/Mobile.jsx rename to frontend/components/Layouts/Mobile.tsx index 849c4d7b..067e20c4 100644 --- a/frontend/components/Layouts/Mobile.jsx +++ b/frontend/components/Layouts/Mobile.tsx @@ -1,13 +1,15 @@ -import styles from "@/styles/Fill.module.css"; - -import { font } from "@fonts"; - -export default function MobileLayout() { - return ( -
- - wallstreetlocal is not available on mobile just yet. - -
- ); -} +import styles from "@/styles/Fill.module.css"; + +import { font } from "@fonts"; + +import React from "react"; + +export default function MobileLayout(): JSX.Element { + return ( +
+ + wallstreetlocal is not available on mobile just yet. + +
+ ); +} \ No newline at end of file diff --git a/frontend/components/Loading/Loading.jsx b/frontend/components/Loading/Loading.tsx similarity index 67% rename from frontend/components/Loading/Loading.jsx rename to frontend/components/Loading/Loading.tsx index ca034ccf..8f316ea9 100644 --- a/frontend/components/Loading/Loading.jsx +++ b/frontend/components/Loading/Loading.tsx @@ -1,12 +1,16 @@ -import styles from "./Loading.module.css"; -import LoadingSVG from "@/public/static/loading.svg"; - -const Loading = (props) => { - return ( -
- -
- ); -}; - -export default Loading; +import styles from "./Loading.module.css"; +import LoadingSVG from "@/public/static/loading.svg"; + +interface LoadingProps { + className?: string; +} + +const Loading: React.FC = (props) => { + return ( +
+ +
+ ); +}; + +export default Loading; \ No newline at end of file diff --git a/frontend/components/Navigation/Navigation.jsx b/frontend/components/Navigation/Navigation.tsx similarity index 84% rename from frontend/components/Navigation/Navigation.jsx rename to frontend/components/Navigation/Navigation.tsx index ac6c06af..58cfb6fa 100644 --- a/frontend/components/Navigation/Navigation.jsx +++ b/frontend/components/Navigation/Navigation.tsx @@ -1,68 +1,79 @@ -import styles from "./Navigation.module.css"; - -import Link from "next/link"; -import { font } from "@fonts"; - -import Search from "components/Search/Button/Search"; -import Bar from "components/Bar/Bar"; - -const Item = ({ link, text, tab }) => ( -
  • - - {text} - -
  • -); - -const server = process.env.NEXT_PUBLIC_SERVER; -const Navigation = (props) => { - const variant = props.variant || null; - return ( - <> - - - - ); -}; - -export default Navigation; +import styles from "./Navigation.module.css"; + +import Link from "next/link"; +import { font } from "@fonts"; + +import Search from "components/Search/Button/Search"; +import Bar from "components/Bar/Bar"; + +interface ItemProps { + link: string; + text: string; + tab?: boolean; +} + +const Item: React.FC = ({ link, text, tab }) => ( +
  • + + {text} + +
  • +); + +const server = process.env.NEXT_PUBLIC_SERVER; + +interface NavigationProps { + variant?: "home" | string; +} + +const Navigation: React.FC = (props) => { + const variant = props.variant || null; + return ( + <> + + + + ); +}; + +export default Navigation; \ No newline at end of file diff --git a/frontend/components/Progress/Building/Building.jsx b/frontend/components/Progress/Building/Building.tsx similarity index 82% rename from frontend/components/Progress/Building/Building.jsx rename to frontend/components/Progress/Building/Building.tsx index e8d57bdc..580f49aa 100644 --- a/frontend/components/Progress/Building/Building.jsx +++ b/frontend/components/Progress/Building/Building.tsx @@ -5,7 +5,11 @@ import { fontLight } from "@fonts"; import Loading from "components/Loading/Loading"; -const Building = (props) => { +interface BuildingProps { + cik: string; +} + +const Building: React.FC = (props) => { return (
    @@ -22,4 +26,4 @@ const Building = (props) => { ); }; -export default Building; +export default Building; \ No newline at end of file diff --git a/frontend/components/Progress/Console/Console.jsx b/frontend/components/Progress/Console/Console.tsx similarity index 77% rename from frontend/components/Progress/Console/Console.jsx rename to frontend/components/Progress/Console/Console.tsx index 2e256e47..356c11b0 100644 --- a/frontend/components/Progress/Console/Console.jsx +++ b/frontend/components/Progress/Console/Console.tsx @@ -1,20 +1,24 @@ import styles from "../Progress.module.css"; import { useEffect, useRef } from "react"; - import { fontLight } from "@fonts"; - import useEllipsis from "components/Hooks/useEllipsis"; import Loading from "components/Loading/Loading"; -const Console = (props) => { +interface ConsoleProps { + logs: string[]; + stall?: boolean; + loading?: boolean; +} + +const Console: React.FC = (props) => { const logs = props.logs; const stall = props.stall || true; const loading = props.loading || false; const { ellipsis } = useEllipsis(); - const ref = useRef(null); + const ref = useRef(null); useEffect(() => { - ref.current.scrollIntoView({ behavior: "smooth", block: "end" }); + ref.current?.scrollIntoView({ behavior: "smooth", block: "end" }); }, [logs]); return ( @@ -38,4 +42,4 @@ const Console = (props) => { ); }; -export default Console; +export default Console; \ No newline at end of file diff --git a/frontend/components/Progress/Estimation/Estimation.jsx b/frontend/components/Progress/Estimation/Estimation.tsx similarity index 88% rename from frontend/components/Progress/Estimation/Estimation.jsx rename to frontend/components/Progress/Estimation/Estimation.tsx index f045df9d..1e684447 100644 --- a/frontend/components/Progress/Estimation/Estimation.jsx +++ b/frontend/components/Progress/Estimation/Estimation.tsx @@ -1,22 +1,18 @@ import styles from "./Estimation.module.css"; - import { useEffect, useState } from "react"; - import axios from "axios"; import useSWR from "swr"; - import { fontLight } from "@fonts"; - import useEllipsis from "components/Hooks/useEllipsis"; import useInterval from "components/Hooks/useInterval"; -const fetcher = (url, cik) => +const fetcher = (url: string, cik: string): Promise => axios .get(url, { params: { cik: cik } }) .then((r) => r.data) .catch((e) => console.log(e)); -const secondsToDhms = (seconds) => { +const secondsToDhms = (seconds: number): string => { seconds = Number(seconds); const d = Math.floor(seconds / (3600 * 24)); @@ -32,7 +28,18 @@ const secondsToDhms = (seconds) => { }; const server = process.env.NEXT_PUBLIC_SERVER; -const Estimation = (props) => { + +interface EstimationProps { + cik: string; +} + +interface TimeState { + confirmed: number; + estimated: number; + status: number; +} + +const Estimation = (props: EstimationProps): JSX.Element => { const cik = props.cik; const { data, @@ -45,7 +52,7 @@ const Estimation = (props) => { refreshInterval: 10000, } ); - const [time, setTime] = useState({ + const [time, setTime] = useState({ confirmed: 0, estimated: 0, status: 2, @@ -56,6 +63,7 @@ const Estimation = (props) => { useInterval(() => { setTime({ ...time, estimated: time.estimated - 1 }); }, 1000); + useEffect(() => { if (data) { const estimation = data?.time; @@ -122,4 +130,4 @@ const Estimation = (props) => { ); }; -export default Estimation; +export default Estimation; \ No newline at end of file diff --git a/frontend/components/Progress/Progress.jsx b/frontend/components/Progress/Progress.tsx similarity index 65% rename from frontend/components/Progress/Progress.jsx rename to frontend/components/Progress/Progress.tsx index f0972680..45ca7140 100644 --- a/frontend/components/Progress/Progress.jsx +++ b/frontend/components/Progress/Progress.tsx @@ -1,160 +1,149 @@ -import styles from "./Progress.module.css"; -import { useEffect, useReducer, useState } from "react"; - -import { font } from "@fonts"; - -import axios from "axios"; -import useSWR from "swr"; - -import Redirect from "components/Filer/Redirect"; -import Source from "components/Source/Source"; -import Estimation from "./Estimation/Estimation"; -import Console from "./Console/Console"; - -const server = process.env.NEXT_PUBLIC_SERVER; -const logFetcher = (url, cik, start) => - axios - .get(url, { - params: { cik: cik, start: start }, - }) - .then((res) => { - return { ...res.data, status: res.status }; - }) - .then((data) => data) - .catch((e) => { - const status = e.response.status; - const error = new Error(e.data.message); - - error.status = status; - throw error; - }); - -const Progress = (props) => { - // const [host, setHost] = useState("localhost:3000"); - // useEffect(() => { - // setHost(window.location.host); - // }, [host]); - - // const [logs, pushLog] = useReducer( - // (prev, next) => [...prev, ...next], - // ["Initializing..."] - // ); - - // useSWRSubscription( - // `ws://${host}/api/filers/logs?cik=${props.cik}`, - // (key, { next }) => { - // const socket = new WebSocket(key); - // socket.addEventListener("message", ({ data }) => { - // pushLog(data.split("\n")); - - // if (data.includes("Finished")) { - // return () => socket.close(); - // } - - // return next(null, data); - // }); - // socket.addEventListener("error", (event) => next(event.error)); - // socket.addEventListener("close", () => { - // setTimeout(() => { - // window.location.reload(); - // }, 10 * 1000); - // }); - // return () => socket.close(); - // } - // ); - const cik = props.cik; - const name = props.name || null; - const persist = props.persist || false; - const [log, addLogs] = useReducer( - (prev, next) => { - if (next.length === 0) { - return prev; - } - - const logs = [...prev.logs, ...next]; - const length = logs.length; - if (length > 100) { - logs.shift(); - } - return { - logs: logs, - count: logs.length, - wait: false, - }; - }, - { logs: ["Initializing, this may take a while..."], count: 0 } - ); - const [wait, setWait] = useState(false); - const [stop, setStop] = useState(false); - - const { - data, - isLoading: loading, - error, - } = useSWR( - wait || stop ? null : [server + "/filers/logs", cik, log.count], - ([url, cik, start]) => logFetcher(url, cik, start), - { refreshInterval: 10 * 1000 } - ); - - useEffect(() => { - if (data) { - switch (data.status) { - case 200: - addLogs(data.logs || []); - - if (persist == false) { - addLogs(["Filer finished initial load, reloading the page."]); - setTimeout(() => { - setStop(true); - }, 5 * 1000); - } - - break; - case 201: - addLogs(data.logs || []); - addLogs(["Filer finished up, reloading the page."]); - setStop(true); - case 202: - addLogs(data.logs || []); - break; - } - } - }, [data]); - - if (error) { - switch (error.status) { - case 503: - setWait(true); - setTimeout(() => { - setWait(false), 15 * 1000; - }); - break; - case 404: - addLogs(["Logs not found, try reloading the page."]); - break; - } - } - - return ( - <> - {stop ? : null} -
    -
    -
    - Building Filer - -
    -
    - {name ? {name} : null} -
    -
    - -
    - {/* View stocks continuously. */} - {/*persist ? null : */} - - ); -}; - -export default Progress; +import styles from "./Progress.module.css"; +import { useEffect, useReducer, useState } from "react"; + +import { font } from "@fonts"; + +import axios from "axios"; +import useSWR from "swr"; + +import Redirect from "components/Filer/Redirect"; +import Source from "components/Source/Source"; +import Estimation from "./Estimation/Estimation"; +import Console from "./Console/Console"; + +const server = process.env.NEXT_PUBLIC_SERVER; + +interface LogFetcherResponse { + data: any; + status: number; + logs?: string[]; +} + +const logFetcher = (url: string, cik: string, start: number): Promise => + axios + .get(url, { + params: { cik: cik, start: start }, + }) + .then((res) => { + return { ...res.data, status: res.status }; + }) + .then((data) => data) + .catch((e) => { + const status = e.response.status; + const error = new Error(e.data.message); + + (error as any).status = status; + throw error; + }); + +interface ProgressProps { + cik: string; + name?: string; + persist?: boolean; +} + +interface LogState { + logs: string[]; + count: number; + wait: boolean; +} + +const Progress: React.FC = (props) => { + const cik = props.cik; + const name = props.name || null; + const persist = props.persist || false; + const [log, addLogs] = useReducer( + (prev: LogState, next: string[]) => { + if (next.length === 0) { + return prev; + } + + const logs = [...prev.logs, ...next]; + const length = logs.length; + if (length > 100) { + logs.shift(); + } + return { + logs: logs, + count: logs.length, + wait: false, + }; + }, + { logs: ["Initializing, this may take a while..."], count: 0, wait: false } + ); + const [wait, setWait] = useState(false); + const [stop, setStop] = useState(false); + + const { + data, + isLoading: loading, + error, + } = useSWR( + wait || stop ? null : [server + "/filers/logs", cik, log.count], + ([url, cik, start]) => logFetcher(url, cik, start), + { refreshInterval: 10 * 1000 } + ); + + useEffect(() => { + if (data) { + switch (data.status) { + case 200: + addLogs(data.logs || []); + + if (persist == false) { + addLogs(["Filer finished initial load, reloading the page."]); + setTimeout(() => { + setStop(true); + }, 5 * 1000); + } + + break; + case 201: + addLogs(data.logs || []); + addLogs(["Filer finished up, reloading the page."]); + setStop(true); + case 202: + addLogs(data.logs || []); + break; + } + } + }, [data]); + + useEffect(() => { + if (error) { + switch ((error as any).status) { + case 503: + setWait(true); + setTimeout(() => { + setWait(false), 15 * 1000; + }); + break; + case 404: + addLogs(["Logs not found, try reloading the page."]); + break; + } + } + }, [error]); + + return ( + <> + {stop ? : null} +
    +
    +
    + Building Filer + +
    +
    + {name ? {name} : null} +
    +
    + +
    + {/* View stocks continuously. */} + {/*persist ? null : */} + + ); +}; + +export default Progress; \ No newline at end of file diff --git a/frontend/components/Progress/Reload/Reload.jsx b/frontend/components/Progress/Reload/Reload.tsx similarity index 83% rename from frontend/components/Progress/Reload/Reload.jsx rename to frontend/components/Progress/Reload/Reload.tsx index 302fef2b..3f11fb7a 100644 --- a/frontend/components/Progress/Reload/Reload.jsx +++ b/frontend/components/Progress/Reload/Reload.tsx @@ -1,12 +1,14 @@ import styles from "./Reload.module.css"; import { useEffect } from "react"; - import { useRouter } from "next/router"; import { fontLight } from "@fonts"; - import useEllipsis from "components/Hooks/useEllipsis"; -const Reload = (props) => { +interface ReloadProps { + delay?: number; +} + +const Reload: React.FC = (props) => { const router = useRouter(); const { ellipsis } = useEllipsis(); useEffect(() => { @@ -24,4 +26,4 @@ const Reload = (props) => { ); }; -export default Reload; +export default Reload; \ No newline at end of file diff --git a/frontend/components/Recommended/Recommended.jsx b/frontend/components/Recommended/Recommended.tsx similarity index 75% rename from frontend/components/Recommended/Recommended.jsx rename to frontend/components/Recommended/Recommended.tsx index 5c275ba3..3c1e5cf9 100644 --- a/frontend/components/Recommended/Recommended.jsx +++ b/frontend/components/Recommended/Recommended.tsx @@ -1,43 +1,51 @@ import styles from "./Recommended.module.css"; import { useEffect, useState } from "react"; - import axios from "axios"; - import Link from "next/link"; - import { font } from "@fonts"; - import { convertTitle } from "components/Filer/Info"; const server = process.env.NEXT_PUBLIC_SERVER; -const Recommended = (props) => { + +interface Filer { + cik: string; + name: string; + title?: string; +} + +interface RecommendedProps { + variant?: "default" | "homepage"; + className?: string; +} + +const Recommended: React.FC = (props) => { const variant = props.variant || "default"; const [show, setShow] = useState(false); - const [topFilers, setTopFilers] = useState([]); - const [searchedFilers, setSearchedFilers] = useState([]); + const [topFilers, setTopFilers] = useState([]); + const [searchedFilers, setSearchedFilers] = useState([]); + useEffect(() => { - topFilers == [] + topFilers.length === 0 ? null : axios .get(server + "/filers/top") .then((r) => r.data) - .then((data) => setTopFilers(data.filers || null)); - searchedFilers == [] + .then((data) => setTopFilers(data.filers || [])); + + searchedFilers.length === 0 ? null : axios .get(server + "/filers/searched") .then((r) => r.data) - .then((data) => setSearchedFilers(data.filers || null)); + .then((data) => setSearchedFilers(data.filers || [])); - window.addEventListener( - "scroll", - () => { - setShow(true); - }, - true - ); - return () => window.removeEventListener("scroll", () => {}, true); - }, []); + const handleScroll = () => { + setShow(true); + }; + + window.addEventListener("scroll", handleScroll, true); + return () => window.removeEventListener("scroll", handleScroll, true); + }, [topFilers, searchedFilers]); return (
    {
    ); }; -export default Recommended; + +export default Recommended; \ No newline at end of file diff --git a/frontend/components/Search/Button/Search.jsx b/frontend/components/Search/Button/Search.tsx similarity index 92% rename from frontend/components/Search/Button/Search.jsx rename to frontend/components/Search/Button/Search.tsx index 730f4b62..e6086274 100644 --- a/frontend/components/Search/Button/Search.jsx +++ b/frontend/components/Search/Button/Search.tsx @@ -1,156 +1,169 @@ -import styles from "./Search.module.css"; -import { useEffect, useState, useRef, useReducer } from "react"; - -import axios from "axios"; -import useSWR from "swr"; - -import Link from "next/link"; -import { font } from "@fonts"; - -import SearchIcon from "@/public/static/search.svg"; - -const server = process.env.NEXT_PUBLIC_SERVER; -const fetcher = (url, query, limit) => - axios - .get(url, { params: { q: query, limit } }) - .then((res) => res.data) - .catch((e) => console.error(e)); - -const Search = () => { - const [input, setInput] = useReducer( - (prev, next) => { - return { ...prev, ...next }; - }, - { - results: [], - search: "", - focus: false, - } - ); - const [show, setShow] = useState(false); - - const limit = 10; - const { data } = useSWR( - input.search ? [server + "/filers/search", input.search, limit] : null, - ([url, query, limit]) => fetcher(url, query, limit), - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - } - ); - useEffect(() => { - if (data) { - setInput({ results: data.results }); - } else { - setInput({ results: [] }); - } - }, [data]); - - return ( - <> - - {show ? ( - <> -
    setShow(false)} - /> -
    - setInput({ search: e.target.value })} - onFocus={() => setInput({ focus: true })} - onBlur={() => setInput({ focus: false })} - autoFocus - /> -
    - {input.search && input.results.length ? ( -
      - {input.results.map((result) => { - return ( -
    • - setShow(false)} - > -
      - - {result.name.toUpperCase()}{" "} - {result.tickers.length == 0 - ? "" - : `(${result.tickers.join(", ")})`} - - - CIK{result.cik.padStart(10, "0")} - -
      - -
    • - ); - })} -
    - ) : null} - - ) : null} - - ); - - // return ( - //
    - //
    - // setSearchInput(e.target.value)} - // onFocus={() => setIsFocused(true)} - // onBlur={() => setIsFocused(false)} - // /> - //
    - //
    - // { - //
      - // {results.map((result) => { - // return ( - //
    • - // - //
      - // - // {result.name.toUpperCase()}{" "} - // {result.tickers.length == 0 - // ? "" - // : `(${result.tickers.join(", ")})`} - // - // - // CIK{result.cik.padStart(10, "0")} - // - //
      - // - //
    • - // ); - // })} - //
    - // } - //
    - //
    - // ); -}; - -export default Search; +import styles from "./Search.module.css"; +import { useEffect, useState, useRef, useReducer } from "react"; + +import axios from "axios"; +import useSWR from "swr"; + +import Link from "next/link"; +import { font } from "@fonts"; + +import SearchIcon from "@/public/static/search.svg"; + +const server = process.env.NEXT_PUBLIC_SERVER; + +type Result = { + cik: string; + name: string; + tickers: string[]; +}; + +type InputState = { + results: Result[]; + search: string; + focus: boolean; +}; + +const fetcher = (url: string, query: string, limit: number) => + axios + .get(url, { params: { q: query, limit } }) + .then((res) => res.data) + .catch((e) => console.error(e)); + +const Search = () => { + const [input, setInput] = useReducer( + (prev: InputState, next: Partial) => { + return { ...prev, ...next }; + }, + { + results: [], + search: "", + focus: false, + } + ); + const [show, setShow] = useState(false); + + const limit = 10; + const { data } = useSWR( + input.search ? [server + "/filers/search", input.search, limit] : null, + ([url, query, limit]: [string, string, number]) => fetcher(url, query, limit), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); + useEffect(() => { + if (data) { + setInput({ results: data.results }); + } else { + setInput({ results: [] }); + } + }, [data]); + + return ( + <> + + {show ? ( + <> +
    setShow(false)} + /> +
    + setInput({ search: e.target.value })} + onFocus={() => setInput({ focus: true })} + onBlur={() => setInput({ focus: false })} + autoFocus + /> +
    + {input.search && input.results.length ? ( +
      + {input.results.map((result) => { + return ( +
    • + setShow(false)} + > +
      + + {result.name.toUpperCase()}{" "} + {result.tickers.length == 0 + ? "" + : `(${result.tickers.join(", ")})`} + + + CIK{result.cik.padStart(10, "0")} + +
      + +
    • + ); + })} +
    + ) : null} + + ) : null} + + ); + + // return ( + //
    + //
    + // setSearchInput(e.target.value)} + // onFocus={() => setIsFocused(true)} + // onBlur={() => setIsFocused(false)} + // /> + //
    + //
    + // { + //
      + // {results.map((result) => { + // return ( + //
    • + // + //
      + // + // {result.name.toUpperCase()}{" "} + // {result.tickers.length == 0 + // ? "" + // : `(${result.tickers.join(", ")})`} + // + // + // CIK{result.cik.padStart(10, "0")} + // + //
      + // + //
    • + // ); + // })} + //
    + // } + //
    + //
    + // ); +}; + +export default Search; \ No newline at end of file diff --git a/frontend/components/Search/Homepage/Search.jsx b/frontend/components/Search/Homepage/Search.tsx similarity index 83% rename from frontend/components/Search/Homepage/Search.jsx rename to frontend/components/Search/Homepage/Search.tsx index 4cd735b2..d63c26b4 100644 --- a/frontend/components/Search/Homepage/Search.jsx +++ b/frontend/components/Search/Homepage/Search.tsx @@ -1,100 +1,109 @@ -import styles from "./Search.module.css"; -import { useEffect, useReducer, useState } from "react"; - -import axios from "axios"; -import useSWR from "swr"; - -import Link from "next/link"; -import { font } from "@fonts"; - -const server = process.env.NEXT_PUBLIC_SERVER; -const fetcher = (url, query, limit) => - axios - .get(url, { params: { q: query, limit } }) - .then((res) => res.data) - .catch((e) => console.error(e)); - -const Search = () => { - const [input, setInput] = useReducer( - (prev, next) => { - return { ...prev, ...next }; - }, - { - search: "", - results: [], - focus: false, - } - ); - - const limit = 5; - const { data } = useSWR( - input.search ? [server + "/filers/search", input.search, limit] : null, - ([url, query, limit]) => fetcher(url, query, limit), - { - revalidateOnFocus: false, - revalidateOnReconnect: false, - } - ); - useEffect(() => { - if (data) { - setInput({ results: data.results }); - } else { - setInput({ results: [] }); - } - }, [data]); - - return ( -
    -
    - setInput({ search: e.target.value })} - onFocus={() => setInput({ focus: true })} - onBlur={() => setInput({ focus: false })} - /> -
    -
    - - { -
      - {input.results.map((result) => { - return ( -
    • - -
      - - {result.name.toUpperCase()}{" "} - {result.tickers.length == 0 - ? "" - : `(${result.tickers.join(", ")})`} - - - CIK{result.cik.padStart(10, "0")} - -
      - -
    • - ); - })} -
    - } -
    - -
    - ); -}; - -export default Search; +import styles from "./Search.module.css"; +import { useEffect, useReducer, useState } from "react"; +import axios from "axios"; +import useSWR from "swr"; +import Link from "next/link"; +import { font } from "@fonts"; + +const server: string | undefined = process.env.NEXT_PUBLIC_SERVER; + +interface Result { + cik: string; + name: string; + tickers: string[]; +} + +const fetcher = (url: string, query: string, limit: number) => + axios + .get(url, { params: { q: query, limit } }) + .then((res) => res.data) + .catch((e) => console.error(e)); + +interface InputState { + search: string; + results: Result[]; + focus: boolean; +} + +const Search = () => { + const [input, setInput] = useReducer( + (prev: InputState, next: Partial) => { + return { ...prev, ...next }; + }, + { + search: "", + results: [], + focus: false, + } + ); + + const limit = 5; + const { data } = useSWR( + input.search ? [server + "/filers/search", input.search, limit] : null, + ([url, query, limit]: [string, string, number]) => fetcher(url, query, limit), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); + useEffect(() => { + if (data) { + setInput({ results: data.results }); + } else { + setInput({ results: [] }); + } + }, [data]); + + return ( +
    +
    + setInput({ search: e.target.value })} + onFocus={() => setInput({ focus: true })} + onBlur={() => setInput({ focus: false })} + /> +
    +
    + { +
      + {input.results.map((result: Result) => { + return ( +
    • + +
      + + {result.name.toUpperCase()}{" "} + {result.tickers.length == 0 + ? "" + : `(${result.tickers.join(", ")})`} + + + CIK{result.cik.padStart(10, "0")} + +
      + +
    • + ); + })} +
    + } +
    +
    + ); +}; + +export default Search; \ No newline at end of file diff --git a/frontend/components/Source/Source.jsx b/frontend/components/Source/Source.tsx similarity index 69% rename from frontend/components/Source/Source.jsx rename to frontend/components/Source/Source.tsx index ddd75541..c4fec512 100644 --- a/frontend/components/Source/Source.jsx +++ b/frontend/components/Source/Source.tsx @@ -2,14 +2,25 @@ import styles from "./Source.module.css"; import SourceIcon from "@/public/static/contact.svg"; -const Source = (props) => { +import React from "react"; + +interface SourceProps { + cik?: string; + color?: "dark" | "light"; + link?: string; + width?: string; + marginLeft?: string; + className?: string; +} + +const Source: React.FC = (props) => { const cik = props.cik || null; const color = props.color || "dark"; const link = props.link || (cik ? "https://www.sec.gov/cgi-bin/browse-edgar?" + - new URLSearchParams({ CIK: cik.padStart(10, 0) }) + new URLSearchParams({ CIK: cik.padStart(10, "0") }) : null); const width = props.width || "20px"; const marginLeft = props.marginLeft || ""; @@ -28,4 +39,4 @@ const Source = (props) => { ); }; -export default Source; +export default Source; \ No newline at end of file diff --git a/frontend/components/Table/Header/Header.jsx b/frontend/components/Table/Header/Header.tsx similarity index 79% rename from frontend/components/Table/Header/Header.jsx rename to frontend/components/Table/Header/Header.tsx index ab6831e2..402398ec 100644 --- a/frontend/components/Table/Header/Header.jsx +++ b/frontend/components/Table/Header/Header.tsx @@ -1,55 +1,70 @@ -import styles from "./Header.module.css"; -import tableStyles from "../Table.module.css"; - -import { font } from "@fonts"; - -import Sort from "./sort.svg"; - -const Header = (props) => { - const headers = props.headers; - const sort = props.sort; - const reverse = props.reverse; - const activateHeader = (accessor, direction) => - props.activate(accessor, direction); - - return ( -
    - {headers - .filter((h) => h.active) - .map((h) => { - return ( - - ); - })} - - ); -}; - -export default Header; +import styles from "./Header.module.css"; +import tableStyles from "../Table.module.css"; + +import { font } from "@fonts"; + +import Sort from "./sort.svg"; + +import React from "react"; + +interface HeaderProps { + headers: Header[]; + sort: string; + reverse: boolean; + activate: (accessor: string, direction: boolean) => void; +} + +interface Header { + display: string; + sort: string; + active: boolean; +} + +const Header: React.FC = (props) => { + const headers = props.headers; + const sort = props.sort; + const reverse = props.reverse; + const activateHeader = (accessor: string, direction: boolean) => + props.activate(accessor, direction); + + return ( + + {headers + .filter((h) => h.active) + .map((h) => { + return ( + + ); + })} + + ); +}; + +export default Header; \ No newline at end of file diff --git a/frontend/components/Table/Pagination/Count/Count.jsx b/frontend/components/Table/Pagination/Count/Count.tsx similarity index 66% rename from frontend/components/Table/Pagination/Count/Count.jsx rename to frontend/components/Table/Pagination/Count/Count.tsx index 8c42ef39..6e9138c2 100644 --- a/frontend/components/Table/Pagination/Count/Count.jsx +++ b/frontend/components/Table/Pagination/Count/Count.tsx @@ -1,11 +1,21 @@ import styles from "../Pagination.module.css"; -import { useEffect, useState } from "react"; - +import { useEffect, useState, ChangeEvent, FocusEvent, KeyboardEvent } from "react"; import { font } from "@fonts"; -const Count = (props) => { +interface Pagination { + count: number; + limit: number; + offset: number; +} + +interface CountProps { + pagination: Pagination; + skip: (offset: number) => void; +} + +const Count: React.FC = (props) => { const pagination = props.pagination; - const setOffset = (o) => props.skip(o); + const setOffset = (o: number) => props.skip(o); const totalPageCount = Math.ceil(pagination.count / pagination.limit); const realPageCount = Math.floor(pagination.offset / pagination.limit) + 1; @@ -23,8 +33,8 @@ const Count = (props) => { } setFocus(false); }; - const handleChange = (e) => - isNaN(e.target.value) ? null : setPageCount(Number(e.target.value)); + const handleChange = (e: ChangeEvent) => + isNaN(Number(e.target.value)) ? null : setPageCount(Number(e.target.value)); return (
    @@ -38,7 +48,7 @@ const Count = (props) => { onChange={(e) => handleChange(e)} type="text" value={focus ? pageCount : realPageCount} - onKeyDown={(e) => (e.key === "Enter" ? e.target.blur() : null)} + onKeyDown={(e: KeyboardEvent) => (e.key === "Enter" ? e.currentTarget.blur() : null)} /> of {totalPageCount} @@ -47,4 +57,4 @@ const Count = (props) => { ); }; -export default Count; +export default Count; \ No newline at end of file diff --git a/frontend/components/Table/Pagination/Limit/Limit.jsx b/frontend/components/Table/Pagination/Limit/Limit.tsx similarity index 60% rename from frontend/components/Table/Pagination/Limit/Limit.jsx rename to frontend/components/Table/Pagination/Limit/Limit.tsx index 618a437f..ab1c7f50 100644 --- a/frontend/components/Table/Pagination/Limit/Limit.jsx +++ b/frontend/components/Table/Pagination/Limit/Limit.tsx @@ -1,11 +1,21 @@ import styles from "../Pagination.module.css"; -import { useEffect, useState } from "react"; +import { useEffect, useState, ChangeEvent, FocusEvent, KeyboardEvent } from "react"; import { font } from "@fonts"; -const Limit = (props) => { +interface PaginationProps { + pagination: Pagination; + paginate: (p: number) => void; +} + +interface Pagination { + count: number; + limit: number; +} + +const Limit = (props: PaginationProps) => { const pagination = props.pagination; - const setPagination = (p) => props.paginate(p); + const setPagination = (p: number) => props.paginate(p); const [focus, setFocus] = useState(false); const [paginationLimit, setPaginationLimit] = useState(100); @@ -16,7 +26,7 @@ const Limit = (props) => { setPagination(pagination.count); setFocus(false); } - }, []); + }, [pagination.count, setPagination]); const handleBlur = () => { if (paginationLimit > 0) { @@ -24,8 +34,8 @@ const Limit = (props) => { } setFocus(false); }; - const handleChange = (e) => - isNaN(e.target.value) ? null : setPaginationLimit(Number(e.target.value)); + const handleChange = (e: ChangeEvent) => + isNaN(e.target.value as any) ? null : setPaginationLimit(Number(e.target.value)); return (
    @@ -36,7 +46,7 @@ const Limit = (props) => { onChange={(e) => handleChange(e)} type="text" value={focus ? paginationLimit : pagination.limit} - onKeyDown={(e) => (e.key === "Enter" ? e.target.blur() : null)} + onKeyDown={(e: KeyboardEvent) => (e.key === "Enter" ? (e.target as HTMLInputElement).blur() : null)} /> of {pagination.count} @@ -45,4 +55,4 @@ const Limit = (props) => { ); }; -export default Limit; +export default Limit; \ No newline at end of file diff --git a/frontend/components/Table/Pagination/Pagination.jsx b/frontend/components/Table/Pagination/Pagination.tsx similarity index 73% rename from frontend/components/Table/Pagination/Pagination.jsx rename to frontend/components/Table/Pagination/Pagination.tsx index 986564b6..77923582 100644 --- a/frontend/components/Table/Pagination/Pagination.jsx +++ b/frontend/components/Table/Pagination/Pagination.tsx @@ -6,11 +6,23 @@ import RightIcon from "@/public/static/left.svg"; import Count from "./Count/Count"; import Limit from "./Limit/Limit"; -const Pagination = (props) => { +interface PaginationProps { + pagination: PaginationData; + paginate: (page: number) => void; + skip: (offset: number) => void; +} + +interface PaginationData { + offset: number; + limit: number; + count: number; +} + +const Pagination: React.FC = (props) => { const pagination = props.pagination; - const paginate = (p) => props.paginate(p); - const skip = (o) => props.skip(o); + const paginate = (p: number) => props.paginate(p); + const skip = (o: number) => props.skip(o); const leftOffset = Number(pagination.offset - pagination.limit); const rightOffset = Number(pagination.offset + pagination.limit); @@ -37,4 +49,4 @@ const Pagination = (props) => { ) : null; }; -export default Pagination; +export default Pagination; \ No newline at end of file diff --git a/frontend/components/Table/Row/Row.jsx b/frontend/components/Table/Row/Row.tsx similarity index 75% rename from frontend/components/Table/Row/Row.jsx rename to frontend/components/Table/Row/Row.tsx index 67d66dbe..4a207aa5 100644 --- a/frontend/components/Table/Row/Row.jsx +++ b/frontend/components/Table/Row/Row.tsx @@ -1,34 +1,50 @@ -import tableStyles from "../Table.module.css"; - -import { font } from "@fonts"; - -const Row = (props) => { - const item = props.item; - const headers = props.headers; - return ( -
    - {headers - .filter((h) => h.active) - .map((h) => { - const display = item[h.accessor]; - return ( - - ); - })} - - ); -}; - -export default Row; +import tableStyles from "../Table.module.css"; + +import { font } from "@fonts"; + +interface Header { + accessor: string; + display: string; + active: boolean; +} + +interface Item { + cusip: string; + [key: string]: any; +} + +interface RowProps { + item: Item; + headers: Header[]; +} + +const Row: React.FC = (props) => { + const item = props.item; + const headers = props.headers; + return ( + + {headers + .filter((h) => h.active) + .map((h) => { + const display = item[h.accessor]; + return ( + + ); + })} + + ); +}; + +export default Row; \ No newline at end of file diff --git a/frontend/components/Table/Table.jsx b/frontend/components/Table/Table.tsx similarity index 68% rename from frontend/components/Table/Table.jsx rename to frontend/components/Table/Table.tsx index cd8c4d89..58c44c14 100644 --- a/frontend/components/Table/Table.jsx +++ b/frontend/components/Table/Table.tsx @@ -6,18 +6,30 @@ import Row from "./Row/Row"; import Header from "./Header/Header"; import Pagination from "./Pagination/Pagination"; -const Table = (props) => { +type TableProps = { + items: Array<{ id: string; [key: string]: any }>; + loading?: boolean; + headers: Array; + sort: string; + reverse: boolean; + activate: (accessor: string, direction: boolean) => void; + pagination: { sold: boolean; na: boolean; [key: string]: any }; + paginate: (p: number) => void; + skip: (o: number) => void; +}; + +const Table: React.FC = (props) => { const items = props.items; const loading = props.loading || false; const headers = props.headers; const sort = props.sort; const reverse = props.reverse; - const activate = (accessor, direction) => props.activate(accessor, direction); + const activate = (accessor: string, direction: boolean) => props.activate(accessor, direction); const pagination = props.pagination; - const paginate = (p) => props.paginate(p); - const skip = (o) => props.skip(o); + const paginate = (p: number) => props.paginate(p); + const skip = (o: number) => props.skip(o); return ( <> @@ -51,4 +63,4 @@ const Table = (props) => { ); }; -export default Table; +export default Table; \ No newline at end of file diff --git a/frontend/components/Tabs/Tabs.jsx b/frontend/components/Tabs/Tabs.tsx similarity index 74% rename from frontend/components/Tabs/Tabs.jsx rename to frontend/components/Tabs/Tabs.tsx index a8beb474..6a1bca08 100644 --- a/frontend/components/Tabs/Tabs.jsx +++ b/frontend/components/Tabs/Tabs.tsx @@ -7,14 +7,20 @@ import { selectTab, setTab } from "@/redux/filerSlice"; import { font, fontLight } from "@fonts"; -const Tab = (props) => { - const id = props.id; - const tab = props.tab; - const index = props.index + 1; - const handleTab = props.handleTab; +import React from "react"; - const length = props.length; - const hint = props.hint || null; +interface TabProps { + id: string; + tab: string; + index: number; + handleTab: (id: string) => void; + length: number; + hint?: string | null; + title: string; +} + +const Tab: React.FC = (props) => { + const { id, tab, index, handleTab, length, hint, title } = props; return (
    { ].join(" ")} onClick={() => handleTab(id)} > - {props.title} + {title}
    {hint ? ( @@ -38,18 +44,24 @@ const Tab = (props) => { ); }; -const tabs = [ +interface TabData { + title: string; + hint: string; + id: string; +} + +const tabs: TabData[] = [ { title: "Stocks", hint: "Table", id: "stocks" }, // { title: "Charts", hint: "Graphs", id: "charts" }, { title: "Filings", hint: "Comparisons", id: "filings" }, ]; -const Tabs = () => { +const Tabs: React.FC = () => { const tab = useSelector(selectTab); const dispatch = useDispatch(); const router = useRouter(); - const handleTab = (value) => { + const handleTab = (value: string) => { router.query.tab = value; router.push(router); dispatch(setTab(value)); @@ -73,4 +85,4 @@ const Tabs = () => { ); }; -export default Tabs; +export default Tabs; \ No newline at end of file diff --git a/frontend/components/Tip/Tip.jsx b/frontend/components/Tip/Tip.tsx similarity index 69% rename from frontend/components/Tip/Tip.jsx rename to frontend/components/Tip/Tip.tsx index 398644cc..be0e3678 100644 --- a/frontend/components/Tip/Tip.jsx +++ b/frontend/components/Tip/Tip.tsx @@ -2,7 +2,12 @@ import styles from "./Tip.module.css"; import { fontLight } from "@fonts"; -const Tip = (props) => { +interface TipProps { + top?: number | null; + text: string; +} + +const Tip: React.FC = (props) => { const top = props.top || null; return ( { ); }; -export default Tip; +export default Tip; \ No newline at end of file diff --git a/frontend/components/Unavailable/Unavailable.jsx b/frontend/components/Unavailable/Unavailable.tsx similarity index 80% rename from frontend/components/Unavailable/Unavailable.jsx rename to frontend/components/Unavailable/Unavailable.tsx index 6329db46..202246ce 100644 --- a/frontend/components/Unavailable/Unavailable.jsx +++ b/frontend/components/Unavailable/Unavailable.tsx @@ -6,7 +6,15 @@ import { font } from "@fonts"; import useEllipsis from "components/Hooks/useEllipsis"; -const Unavailable = (props) => { +import React from "react"; + +type UnavailableProps = { + type?: "stocks" | "loading"; + cik?: string; + text?: string; +}; + +const Unavailable: React.FC = (props) => { const type = props.type || "stocks"; const cik = type == "stocks" ? props.cik : null; const text = props.text || null; @@ -25,7 +33,7 @@ const Unavailable = (props) => { { ); }; -export default Unavailable; +export default Unavailable; \ No newline at end of file diff --git a/frontend/components/fonts/index.js b/frontend/components/fonts/index.js deleted file mode 100644 index 30b4f487..00000000 --- a/frontend/components/fonts/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Inter } from "@next/font/google"; - -const font = Inter({ weight: "800", subsets: ["latin"] }); -const fontBold = Inter({ weight: "900", subsets: ["latin"] }); -const fontLight = Inter({ weight: "700", subsets: ["latin"] }); - -export { font, fontBold, fontLight }; diff --git a/frontend/components/fonts/index.ts b/frontend/components/fonts/index.ts new file mode 100644 index 00000000..42aadb10 --- /dev/null +++ b/frontend/components/fonts/index.ts @@ -0,0 +1,15 @@ +import { Inter } from "@next/font/google"; + +type FontWeight = "700" | "800" | "900"; + +interface FontOptions { + weight: FontWeight; + subsets: string[]; + className: string; +} + +const font: FontOptions = Inter({ weight: "800", subsets: ["latin"] }); +const fontBold: FontOptions = Inter({ weight: "900", subsets: ["latin"] }); +const fontLight: FontOptions = Inter({ weight: "700", subsets: ["latin"] }); + +export { font, fontBold, fontLight }; \ No newline at end of file diff --git a/frontend/next.config.js b/frontend/next.config.mjs similarity index 67% rename from frontend/next.config.js rename to frontend/next.config.mjs index 58863662..58804fbb 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.mjs @@ -1,32 +1,33 @@ -/** @type {import('next').NextConfig} */ - -const nextConfig = { - reactStrictMode: true, - webpack(config) { - const fileLoaderRule = config.module.rules.find((rule) => - rule.test?.test?.(".svg") - ); - - config.module.rules.push( - { - ...fileLoaderRule, - test: /\.svg$/i, - resourceQuery: /url/, - }, - { - test: /\.svg$/i, - issuer: /\.[jt]sx?$/, - resourceQuery: { not: /url/ }, - use: ["@svgr/webpack"], - } - ); - fileLoaderRule.exclude = /\.svg$/i; - return config; - }, - env: { - NEXT_PUBLIC_SERVER: "https://content.wallstreetlocal.com", - }, - output: "standalone", -}; - -module.exports = nextConfig; +const nextConfig = { + reactStrictMode: true, + webpack(config) { + const fileLoaderRule = config.module?.rules?.find((rule) => + rule.test?.test?.(".svg") + ); + + config.module?.rules?.push( + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, + }, + { + test: /\.svg$/i, + issuer: /\.[jt]sx?$/, + resourceQuery: { not: /url/ }, + use: ["@svgr/webpack"], + } + ); + if (fileLoaderRule) { + fileLoaderRule.exclude = /\.svg$/i; + } + return config; + }, + env: { + NEXT_PUBLIC_SERVER: "https://content.wallstreetlocal.com", + }, + output: "standalone", + reactStrictMode: true, +}; + +export default nextConfig; diff --git a/frontend/package.json b/frontend/package.json index 856eead7..a8bb4a87 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "dev": "next dev", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "type-check": "tsc --noEmit" }, "dependencies": { "@dnd-kit/core": "^6.0.8", @@ -39,10 +40,13 @@ "swr": "^2.1.1" }, "devDependencies": { + "@types/node": "^18.11.18", "@types/react": "18.0.28", "@vercel/analytics": "^1.1.1", "eslint": "8.34.0", - "eslint-config-next": "13.1.6" + "eslint-config-next": "13.1.6", + "typescript": "^4.9.4", + "url-loader": "^4.1.1" }, "description": "This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).", "main": "next.config.js", @@ -56,4 +60,4 @@ "url": "https://github.com/leftmove/whale-project/issues" }, "homepage": "https://github.com/leftmove/whale-project#readme" -} +} \ No newline at end of file diff --git a/frontend/pages/_app.js b/frontend/pages/_app.js deleted file mode 100644 index f776283c..00000000 --- a/frontend/pages/_app.js +++ /dev/null @@ -1,20 +0,0 @@ -import "@/styles/globals.css"; - -import Layout from "components/Layouts/Layout"; - -import { Inter } from "@next/font/google"; - -const font = Inter({ weight: "800", subsets: ["latin"] }); -const fontBold = Inter({ weight: "900", subsets: ["latin"] }); -const fontLight = Inter({ weight: "700", subsets: ["latin"] }); - -function App(props) { - const getLayout = - props.Component.getLayout || - (() => {}); - - return getLayout(props); -} - -export default App; -export { font, fontBold, fontLight }; \ No newline at end of file diff --git a/frontend/pages/_app.tsx b/frontend/pages/_app.tsx new file mode 100644 index 00000000..efb00294 --- /dev/null +++ b/frontend/pages/_app.tsx @@ -0,0 +1,18 @@ +import "@/styles/globals.css"; + +import Layout from "components/Layouts/Layout"; + +interface AppProps { + Component: React.ComponentType & { getLayout?: (props: any) => JSX.Element }; + pageProps: any; +} + +function App(props: AppProps): JSX.Element { + const getLayout = + props.Component.getLayout || + (() => {}); + + return getLayout(props); +} + +export default App; \ No newline at end of file diff --git a/frontend/pages/_document.js b/frontend/pages/_document.tsx similarity index 69% rename from frontend/pages/_document.js rename to frontend/pages/_document.tsx index af632100..54be7681 100644 --- a/frontend/pages/_document.js +++ b/frontend/pages/_document.tsx @@ -1,13 +1,14 @@ -import { Html, Head, Main, NextScript } from "next/document"; - -export default function Document() { - return ( - - - -
    - - - - ); -} +import { Html, Head, Main, NextScript } from "next/document"; +import { FC } from "react"; +const Document: FC = () => { + return ( + + + +
    + + + + ); +}; +export default Document; \ No newline at end of file diff --git a/frontend/pages/about/contact.jsx b/frontend/pages/about/contact.tsx similarity index 95% rename from frontend/pages/about/contact.jsx rename to frontend/pages/about/contact.tsx index d9c4b37b..254d143a 100644 --- a/frontend/pages/about/contact.jsx +++ b/frontend/pages/about/contact.tsx @@ -1,49 +1,49 @@ -import styles from "@/styles/Contact.module.css"; - -import Head from "next/head"; -import { font } from "@fonts"; - -import MailSVG from "@/images/envelope.svg"; -import DiscordSVG from "@/images/discord.svg"; -import LinkedInSVG from "@/images/linkedin.svg"; - -export default function Contact() { - return ( - <> - - wallstreetlocal | Contact - -
    -

    Donation and Links

    -
    -
    - {/* Info */} -
    - - - 100anonyo@gmail.com - -
    -
    - - zipped1 -
    -
    - - - anonyo-noor-272540249 - -
    -
    - -
    -
    - - ); -} +import styles from "@/styles/Contact.module.css"; + +import Head from "next/head"; +import { font } from "@fonts"; + +import MailSVG from "@/images/envelope.svg"; +import DiscordSVG from "@/images/discord.svg"; +import LinkedInSVG from "@/images/linkedin.svg"; + +export default function Contact(): JSX.Element { + return ( + <> + + wallstreetlocal | Contact + +
    +

    Donation and Links

    +
    +
    + {/* Info */} +
    + + + 100anonyo@gmail.com + +
    +
    + + zipped1 +
    +
    + + + anonyo-noor-272540249 + +
    +
    + +
    +
    + + ); +} \ No newline at end of file diff --git a/frontend/pages/about/resources.jsx b/frontend/pages/about/resources.tsx similarity index 96% rename from frontend/pages/about/resources.jsx rename to frontend/pages/about/resources.tsx index 9c4111a7..49c33651 100644 --- a/frontend/pages/about/resources.jsx +++ b/frontend/pages/about/resources.tsx @@ -1,194 +1,193 @@ -import styles from "@/styles/Resources.module.css"; - -import Head from "next/head"; -import Link from "next/link"; -import { font } from "@fonts"; - -export default function Resources() { - return ( - <> - - wallstreetlocal | Resources - {" "} -
    -
    -

    - Resources -

    - - All data used is publicly available via the SEC. The following - contains useful links and information about SEC registered - companies. - -
    -
    -
    - - - - - (Add 13F to the filing category field to find companies that have - filed 13F filings) - -
    -
    - - - - - (Bulk data via the SEC) - -
    -
    - - - - - (Bulk data via the SEC) - -
    -
    - - - - - (Bulk data via the SEC) - -
    -
    - - - - - (Explanations for different form types) - -
    -
    - - - - - (Search database for companies, created by wallstreetlocal, - formatted in BSON) - -
    -
    - - - - - (Gist listing popular filers, taken from various sources. This is - where the popular filers page gets its list from.) - -
    -
    - - - - - (Gist listing top filers, taken from various sources. This is - where the top filers page gets its list from.) - -
    -
    - - While not all data collected and formatted by wallstreetlocal is - available by bulk because of data costs, you can download resources - for individual filers by visiting their respective pages. - -
    - - ); -} +import styles from "@/styles/Resources.module.css"; +import Head from "next/head"; +import Link from "next/link"; +import { font } from "@fonts"; +import React from "react"; +export default function Resources(): JSX.Element { + return ( + <> + + wallstreetlocal | Resources + +
    +
    +

    + Resources +

    + + All data used is publicly available via the SEC. The following + contains useful links and information about SEC registered + companies. + +
    +
    +
    + + + + + (Add 13F to the filing category field to find companies that have + filed 13F filings) + +
    +
    + + + + + (Bulk data via the SEC) + +
    +
    + + + + + (Bulk data via the SEC) + +
    +
    + + + + + (Bulk data via the SEC) + +
    +
    + + + + + (Explanations for different form types) + +
    +
    + + + + + (Search database for companies, created by wallstreetlocal, + formatted in BSON) + +
    +
    + + + + + (Gist listing popular filers, taken from various sources. This is + where the popular filers page gets its list from.) + +
    +
    + + + + + (Gist listing top filers, taken from various sources. This is + where the top filers page gets its list from.) + +
    +
    + + While not all data collected and formatted by wallstreetlocal is + available by bulk because of data costs, you can download resources + for individual filers by visiting their respective pages. + +
    + + ); +} \ No newline at end of file diff --git a/frontend/pages/filers/[cik].jsx b/frontend/pages/filers/[cik].tsx similarity index 81% rename from frontend/pages/filers/[cik].jsx rename to frontend/pages/filers/[cik].tsx index ce401e99..a2428cc5 100644 --- a/frontend/pages/filers/[cik].jsx +++ b/frontend/pages/filers/[cik].tsx @@ -1,100 +1,100 @@ -import axios from "axios"; - -import { Provider } from "react-redux"; -import { wrapper } from "@/redux/store"; - -import Layout from "components/Layouts/Layout"; -import Info from "components/Filer/Info"; -import Other from "components/Filer/Other"; -import Building from "components/Filer/Building"; - -const Filer = (props) => { - const query = props.query; - const cik = props.cik; - - const continuous = props.continuous; - const persist = props.persist; - const tab = props.tab; - - console.log(cik); - - if (query.building || persist) { - return ; - } - - if (query.ok || query.continuous || continuous) { - return ; - } - - if (query.error) { - return ; - } -}; - -const server = process.env.NEXT_PUBLIC_SERVER; -export async function getServerSideProps(context) { - const cik = context.query.cik || null; - const continuous = context.query.continuous || null; - const persist = context.query.persist || null; - const tab = context.query.tab || "stocks"; - - const query = { - ok: false, - building: false, - continuous: false, - error: false, - }; - - await axios - .get(server + "/filers/query", { - params: { cik }, - validateStatus: (status) => status < 500, - }) - .then((r) => { - switch (r?.status) { - case 302: - query.continuous = true; - break; - case 201: - case 409: - query.building = true; - break; - case 200: - query.ok = true; - break; - default: - query.error = true; - break; - } - }) - .catch((e) => { - console.error(e); - query.error = true; - }); - - return { - props: { - query, - cik, - tab, - persist, - continuous, - }, - }; -} - -Filer.getLayout = function getLayout({ Component, ...rest }) { - const { store, props } = wrapper.useWrappedStore(rest); - const { cik } = props.pageProps; - const reduxStore = { ...store, filer: { ...store.filer, cik: cik } }; - - return ( - - - - - - ); -}; - -export default Filer; +import axios from "axios"; +import { Provider } from "react-redux"; +import { wrapper } from "@/redux/store"; +import Layout from "components/Layouts/Layout"; +import Info from "components/Filer/Info"; +import Other from "components/Filer/Other"; +import Building from "components/Filer/Building"; +import { GetServerSideProps } from "next"; +import { FC } from "react"; +interface Query { + ok: boolean; + building: boolean; + continuous: boolean; + error: boolean; +} +interface FilerProps { + query: Query; + cik: string | null; + continuous: boolean | null; + persist: boolean | null; + tab: string; +} +const Filer: FC = (props) => { + const query = props.query; + const cik = props.cik; + const continuous = props.continuous; + const persist = props.persist; + const tab = props.tab; + + if (query.building || persist) { + return ; + } + if (query.ok || query.continuous || continuous) { + return ; + } + if (query.error) { + return ; + } +}; +const server = process.env.NEXT_PUBLIC_SERVER; +export const getServerSideProps: GetServerSideProps = async (context) => { + const cik = context.query.cik || null; + const continuous = context.query.continuous || null; + const persist = context.query.persist || null; + const tab = context.query.tab || "stocks"; + const query: Query = { + ok: false, + building: false, + continuous: false, + error: false, + }; + await axios + .get(server + "/filers/query", { + params: { cik }, + validateStatus: (status) => status < 500, + }) + .then((r) => { + switch (r?.status) { + case 302: + query.continuous = true; + break; + case 201: + case 409: + query.building = true; + break; + case 200: + query.ok = true; + break; + default: + query.error = true; + break; + } + }) + .catch((e) => { + console.error(e); + query.error = true; + }); + return { + props: { + query, + cik, + tab, + persist, + continuous, + }, + }; +}; +Filer.getLayout = function getLayout({ Component, ...rest }) { + const { store, props } = wrapper.useWrappedStore(rest); + const { cik } = props.pageProps; + const reduxStore = { ...store, filer: { ...store.filer, cik: cik } }; + return ( + + + + + + ); +}; +export default Filer; \ No newline at end of file diff --git a/frontend/pages/index.jsx b/frontend/pages/index.tsx similarity index 95% rename from frontend/pages/index.jsx rename to frontend/pages/index.tsx index 83666030..a4de2e98 100644 --- a/frontend/pages/index.jsx +++ b/frontend/pages/index.tsx @@ -1,176 +1,180 @@ -"use server"; - -import styles from "@/styles/Home.module.css"; - -import axios from "axios"; - -import Head from "next/head"; -import Image from "next/image"; - -import { font, fontLight, fontBold } from "@fonts"; - -import Layout from "components/Layouts/Home"; -import Search from "components/Search/Homepage/Search"; -import Recommended from "components/Recommended/Recommended"; -import Health from "components/Health/Health"; -import Hero from "@/images/hero.jpg"; -import FolderIcon from "@/images/folder.svg"; -import FileIcon from "@/images/file.svg"; -import BookIcon from "@/images/book.svg"; - -export default function Home(props) { - return ( - <> - - - wallstreetlocal | Advice from the world's biggest investors - - - - - - -
    -
    - -
    -
    - -
    - - Nothing to search? See filers sorted by popularity and value in - the top right corner. - - - Thousands of filings from the world's biggest investors. - - - Wall Street's stock portfolio, for free. - -
    -
    - -
    -
    - - Explore historical stock data, directly from the SEC. - -

    - The Securities and Exchange Commission (SEC) keeps record of every - company in the United States. Companies whose holdings surpass $100 - million though, are required to file a special type of form: the 13F - form. This form, filed quarterly, discloses the filer's holdings, - providing transparency into their investment activities and allowing - the public and other market participants to monitor them. -

    -

    - The problem though, is that these holdings are often cumbersome to - access, and valuable analysis is often hidden behind a paywall. - Through wallstreetlocal, the SEC's 13F filers become more - accessible and open. -

    - {/*
    -
    - -
    - - Stock Data - - - Stocks from over 20 years, matched with external data to create - accurate, consistent, and useful analysis. - -
    -
    -
    - -
    - - SEC Filings - - - Filings directly from the SEC, served in an accessible format. - Over 20 years of coverage. - -
    -
    -
    - -
    - - Free Forever - - - The entire backlog of the SEC, free and without quotas. - -
    -
    -
    */} -
    - - ); -} - -const server = process.env.NEXT_PUBLIC_SERVER; -export async function getServerSideProps() { - const health = await axios - .get(server + "/health") - .then((r) => r.status === 200) - .then(() => true) - .catch(() => false); - return { - props: { - health, - }, - }; -} - -Home.getLayout = ({ Component, pageProps }) => ( - - - -); +"use server"; + +import styles from "@/styles/Home.module.css"; + +import axios from "axios"; + +import Head from "next/head"; +import Image from "next/image"; + +import { font, fontLight, fontBold } from "@fonts"; + +import Layout from "components/Layouts/Home"; +import Search from "components/Search/Homepage/Search"; +import Recommended from "components/Recommended/Recommended"; +import Health from "components/Health/Health"; +import Hero from "@/images/hero.jpg"; +import FolderIcon from "@/images/folder.svg"; +import FileIcon from "@/images/file.svg"; +import BookIcon from "@/images/book.svg"; + +interface HomeProps { + health: boolean; +} + +export default function Home(props: HomeProps) { + return ( + <> + + + wallstreetlocal | Advice from the world's biggest investors + + + + + + +
    +
    + +
    +
    + +
    + + Nothing to search? See filers sorted by popularity and value in + the top right corner. + + + Thousands of filings from the world's biggest investors. + + + Wall Street's stock portfolio, for free. + +
    +
    + +
    +
    + + Explore historical stock data, directly from the SEC. + +

    + The Securities and Exchange Commission (SEC) keeps record of every + company in the United States. Companies whose holdings surpass $100 + million though, are required to file a special type of form: the 13F + form. This form, filed quarterly, discloses the filer's holdings, + providing transparency into their investment activities and allowing + the public and other market participants to monitor them. +

    +

    + The problem though, is that these holdings are often cumbersome to + access, and valuable analysis is often hidden behind a paywall. + Through wallstreetlocal, the SEC's 13F filers become more + accessible and open. +

    + {/*
    +
    + +
    + + Stock Data + + + Stocks from over 20 years, matched with external data to create + accurate, consistent, and useful analysis. + +
    +
    +
    + +
    + + SEC Filings + + + Filings directly from the SEC, served in an accessible format. + Over 20 years of coverage. + +
    +
    +
    + +
    + + Free Forever + + + The entire backlog of the SEC, free and without quotas. + +
    +
    +
    */} +
    + + ); +} + +const server = process.env.NEXT_PUBLIC_SERVER; +export async function getServerSideProps() { + const health = await axios + .get(server + "/health") + .then((r) => r.status === 200) + .then(() => true) + .catch(() => false); + return { + props: { + health, + }, + }; +} + +Home.getLayout = ({ Component, pageProps }: { Component: any; pageProps: any }) => ( + + + +); \ No newline at end of file diff --git a/frontend/pages/recommended/searched.jsx b/frontend/pages/recommended/searched.tsx similarity index 91% rename from frontend/pages/recommended/searched.jsx rename to frontend/pages/recommended/searched.tsx index 5e77d7cc..b9a0e11e 100644 --- a/frontend/pages/recommended/searched.jsx +++ b/frontend/pages/recommended/searched.tsx @@ -8,14 +8,31 @@ import axios from "axios"; import { font } from "@fonts"; import { convertTitle } from "components/Filer/Info"; -const headers = [ +interface Header { + display: string; + accessor: string; +} + +interface Filer { + name: string; + cik: string; + market_value: string; + date: string; + [key: string]: any; +} + +interface SearchedProps { + filers: Filer[]; +} + +const headers: Header[] = [ { display: "Name", accessor: "name" }, { display: "CIK", accessor: "cik" }, { display: "Assets Under Management", accessor: "market_value" }, { display: "Last Updated", accessor: "date" }, ]; -const Searched = (props) => { +const Searched: React.FC = (props) => { return ( <> @@ -112,4 +129,4 @@ export async function getServerSideProps() { }; } -export default Searched; +export default Searched; \ No newline at end of file diff --git a/frontend/pages/recommended/top.jsx b/frontend/pages/recommended/top.tsx similarity index 92% rename from frontend/pages/recommended/top.jsx rename to frontend/pages/recommended/top.tsx index ff759693..157a24c0 100644 --- a/frontend/pages/recommended/top.jsx +++ b/frontend/pages/recommended/top.tsx @@ -8,14 +8,31 @@ import axios from "axios"; import { font } from "@fonts"; import { convertTitle } from "components/Filer/Info"; -const headers = [ +interface Header { + display: string; + accessor: string; +} + +interface Filer { + name: string; + cik: string; + market_value: string; + date: string; + [key: string]: any; +} + +interface TopProps { + filers: Filer[]; +} + +const headers: Header[] = [ { display: "Name", accessor: "name" }, { display: "CIK", accessor: "cik" }, { display: "Assets Under Management", accessor: "market_value" }, { display: "Last Updated", accessor: "date" }, ]; -const Top = (props) => { +const Top: React.FC = (props) => { return ( <> @@ -122,4 +139,4 @@ export async function getServerSideProps() { }; } -export default Top; +export default Top; \ No newline at end of file diff --git a/frontend/redux/filerSlice.js b/frontend/redux/filerSlice.ts similarity index 77% rename from frontend/redux/filerSlice.js rename to frontend/redux/filerSlice.ts index d91fb74d..749077b5 100644 --- a/frontend/redux/filerSlice.js +++ b/frontend/redux/filerSlice.ts @@ -1,659 +1,667 @@ -import { createSlice, createSelector } from "@reduxjs/toolkit"; -import { HYDRATE } from "next-redux-wrapper"; - -const initialDate = new Date(); -const initialHeaders = [ - { - display: "Ticker", - sort: "ticker", - accessor: "ticker_str", - active: true, - tooltip: - "This is a unique series of letters assigned to a security for trading purposes", - }, - { - display: "Name", - sort: "name", - accessor: "name", - active: false, - tooltip: "The name of the stock.", - }, - { - display: "Class", - sort: "class", - accessor: "class", - active: false, - tooltip: - "This refers to the rights of a stockholder, including things like voting and dividends.", - }, - { - display: "Sector", - sort: "sector", - accessor: "sector", - active: false, - tooltip: "The broader industry category to which the stock belongs.", - }, - { - display: "CUSIP", - sort: "cusip", - accessor: "cusip", - active: false, - tooltip: - "A unique identifier assigned to each registered security in the United States and Canada.", - }, - { - display: "Shares Held", - sort: "shares_held", - accessor: "shares_held_str", - active: false, - tooltip: "The number of shares held, or the principal amount of the stock.", - }, - { - display: "Market Value", - sort: "market_value", - accessor: "market_value_str", - active: true, - tooltip: "The value for the shares of the stock the filer owns.", - }, - { - display: "% Portfolio", - sort: "portfolio_percent", - accessor: "portfolio_str", - active: true, - tooltip: - "The value of this stock's shares divided by the total value of the portfolio, expressed in percent. ( Value of Shares / Value of Portfolio )", - }, - { - display: "% Ownership", - sort: "ownership_percent", - accessor: "ownership_str", - active: false, - tooltip: - "The number of shares owned, divided by the current total of shares outstanding, expressed in percent. This value is only accurate the most value of shares outstanding. ( Shares Owned / Shares Existing )", - }, - { - display: "Sold Date", - sort: "sold_time", - accessor: "sold_str", - active: false, - tooltip: - "The date the stock was sold, taken by retrieving the report date of the last SEC filing said stock showed up on. This is only accurate up to the quarter.", - }, - { - display: "Buy Date", - sort: "buy_time", - accessor: "buy_str", - active: false, - tooltip: - "The date the stock was bought, according to the report date of the first SEC filing it appeared on. This is only accurate up to the quarter, and only the most recent bought date is shown.", - }, - { - display: "Price Paid", - sort: "buy_price", - accessor: "buy_price_str", - active: true, - tooltip: - "The price paid for the stock, estimated by taking a close price most near the quarter from which the stock was first reported.", - }, - { - display: "Recent Price", - sort: "recent_price", - accessor: "recent_price_str", - active: true, - tooltip: - "The recent price of the stock. This may be a couple days delayed.", - }, - { - display: "% Gain", - sort: "gain_percent", - accessor: "gain_str", - active: true, - tooltip: - "The price paid for the stock subtracted from the recent price, and then divided by price paid, expressed in percent. ( ( Recent Price - Price Paid ) / Price Paid )", - }, - { - display: "Industry", - sort: "industry", - accessor: "industry", - active: false, - tooltip: - "The specific sector or category of the economy in which the stock's company operates.", - }, - { - display: "Report Date", - sort: "report", - accessor: "report_str", - active: false, - tooltip: - "The report date listed on the SEC filing this stock was taken from.", - }, -]; -const initialComparisons = initialHeaders.map((h) => { - switch (h.sort) { - case "buy_price": - return { ...h, active: false }; - case "recent_price": - return { ...h, active: false }; - case "gain_percent": - return { ...h, active: false }; - default: - return h; - } -}); -const initialSort = { - sort: "ticker", - type: "string", - set: true, - na: false, - sold: false, - reverse: true, - pagination: 100, - limit: 100, - count: 0, - offset: 0, -}; -const initialState = { - cik: "", - value: [], - headers: initialHeaders, - tab: "stocks", - sort: initialSort, - filings: [], - timeline: { - comparisons: [ - { - type: "primary", - access: "", - filing: { - time: 0, - date: "", - }, - report: { - time: 0, - date: "", - }, - headers: initialComparisons, - sort: initialSort, - stocks: [], - }, - { - type: "secondary", - access: "", - filingTime: 0, - reportTime: 0, - filingDate: "", - reportDate: "", - headers: initialComparisons, - sort: initialSort, - stocks: [], - }, - ], - open: false, - }, - difference: { - headers: initialComparisons, - sort: initialSort, - stocks: [], - }, - dates: [ - { - year: initialDate.getFullYear(), - month: initialDate.getMonth(), - day: initialDate.getDate(), - timestamp: initialDate.getTime() / 1000, - open: false, - accessor: initialDate.toLocaleDateString(), - }, - ], -}; - -export const filerSlice = createSlice({ - name: "filer", - initialState, - reducers: { - setCik(state, action) { - Object.keys(initialState).map((k) => { - state[k] = initialState[k]; - }); - state.cik = action.payload; - - return state; - }, - setTab(state, action) { - const payload = action.payload; - state.tab = payload; - - return state; - }, - activateHeader(state, action) { - const headers = state.headers; - const payload = action.payload; - state.headers = headers.map((h) => - h.accessor === payload ? { ...h, active: !h.active } : h - ); - - return state; - }, - sortHeader(state, action) { - const payload = action.payload; - let type = "string"; - switch (payload.sort) { - case "name": - case "sector": - case "industry": - case "class": - case "cusip": - type = "string"; - break; - case "shares_held": - case "market_value": - case "portfolio_percent": - case "ownership_percent": - case "gain_percent": - case "recent_price": - case "buy_price": - case "report": - case "buy": - case "sold_time": - type = "number"; - break; - case "buy": - case "report": - case "sold_time": - type = "date"; - break; - default: - type = typeof payload.sort === "number" ? "number" : "string"; - break; - } - state.sort = { ...state.sort, ...payload, type: type }; - return state; - }, - sortActive(state) { - const sort = state.sort; - const set = sort.set; - state.sort = { ...sort, set: !set }; - - return state; - }, - addHeader(state, action) { - const payload = action.payload; - const headers = state.headers; - - headers.push({ - ...payload, - active: true, - }); - - state.headers = headers; - return state; - }, - editHeader(state, action) { - const payload = action.payload; - const headers = state.headers.map((h) => - h.accessor === payload.accessor ? { ...h, display: payload.display } : h - ); - - state.headers = headers; - return state; - }, - removeHeader(state, action) { - const payload = action.payload; - const headers = state.headers.filter((h) => h.accessor !== payload); - - state.headers = headers; - return state; - }, - setHeaders(state, action) { - const payload = action.payload; - state.headers = payload; - - return state; - }, - sortSold(state) { - const sort = state.sort; - const sold = sort.sold; - state.sort = { ...sort, sold: !sold }; - - return state; - }, - sortNa(state) { - const sort = state.sort; - const na = sort.na; - state.sort = { ...sort, na: !na }; - - return state; - }, - setStocks(state, action) { - const stocks = action.payload; - state.value = stocks; - return state; - }, - updateStocks(state, action) { - const payload = action.payload; - const field = payload.field; - const values = payload.values; - - let stocks = state.value; - stocks = stocks.map((stock) => { - const cusip = stock.cusip; - const value = values[cusip]; - - return { ...stock, [field]: value }; - }); - - state.value = stocks; - return state; - }, - addDate(state, action) { - const dates = state.dates; - dates.push(action.payload); - state.dates = dates; - - return state; - }, - removeDate(state, action) { - const accessor = action.payload; - const dates = state.dates; - state.dates = dates.filter((date) => date.accessor !== accessor); - return state; - }, - editDate(state, action) { - const payload = action.payload; - const dates = state.dates.map((date) => { - if (payload.accessor === date.accessor) { - let newDate = new Date(date.year, date.month, date.day); - switch (payload.type) { - case "year": - newDate.setFullYear(payload.value); - break; - case "month": - newDate.setMonth(payload.value); - break; - case "day": - newDate.setDate(payload.value); - break; - case "date": - newDate = new Date(payload.value); - break; - default: - break; - } - return { - year: newDate.getFullYear(), - month: newDate.getMonth(), - day: newDate.getDate(), - timestamp: newDate.getTime() / 1000, - open: true, - accessor: date.accessor, - }; - } else return date; - }); - state.dates = dates; - return state; - }, - updateDates(state, action) { - const payload = action.payload; - - // const over = payload.over; - // const accessor = payload.accessor; - // const active = dates.find((d) => d.accessor === accessor); - - // const activeIndex = dates.findIndex((d) => d.accessor === accessor); - // const overIndex = dates.findIndex((d) => d.accessor === over); - - // arrayMove() - - // dates.splice(activeIndex, 1); - // dates.splice(overIndex, 0, active); - - state.dates = payload; - return state; - }, - openDate(state, action) { - const payload = action.payload; - const accessor = payload.accessor; - const dates = state.dates.map((date) => - date.accessor === accessor ? { ...date, open: payload.open } : date - ); - state.dates = dates; - return state; - }, - newDate(state) { - const dates = state.dates; - const latestDate = dates.at(-1) || { - year: initialDate.getFullYear(), - month: initialDate.getMonth(), - day: initialDate.getDate(), - timestamp: initialDate.getTime() / 1000, - open: false, - accessor: initialDate.toLocaleDateString(), - }; - const newDate = new Date(latestDate.accessor); - newDate.setDate(newDate.getDate() + 1); - - dates.push({ - year: newDate.getFullYear(), - month: newDate.getMonth(), - day: newDate.getDate(), - timestamp: newDate.getTime() / 1000, - open: false, - accessor: newDate.toLocaleDateString(), - }); - - state.dates = dates; - return state; - }, - setPagination(state, action) { - state.sort.pagination = action.payload; - return state; - }, - setCount(state, action) { - const sort = state.sort; - const payload = action.payload; - const pagination = sort.pagination; - - if (pagination < 0) { - state.sort.pagination = payload > 100 ? 100 : payload; - } - - state.sort.count = payload; - return state; - }, - setFilingCount(state, action) { - const payload = action.payload; - const type = payload.type; - const count = payload.count; - - const comparisons = state.timeline.comparisons.map((c) => - c.type === type - ? { - ...c, - sort: { ...c.sort, pagination: count > 100 ? 100 : count, count }, - } - : c - ); - - state.timeline.comparisons = comparisons; - return state; - }, - setOffset(state, action) { - const payload = action.payload; - if (payload >= 0) { - state.sort.offset = Number(payload); - } - return state; - }, - setFilings(state, action) { - const payload = action.payload; - - state.filings = payload; - return state; - }, - setPrimary(state, action) { - const payload = action.payload; - - const comparisons = state.timeline.comparisons; - const primary = comparisons[0]; - state.timeline.comparisons[0] = { ...primary, ...payload }; - - return state; - }, - setSecondary(state, action) { - const payload = action.payload; - - const comparisons = state.timeline.comparisons; - const secondary = comparisons[1]; - state.timeline.comparisons[1] = { ...secondary, ...payload }; - - return state; - }, - setComparison(state, action) { - const payload = action.payload; - const type = payload.type; - const access = payload.access; - - const filing = state.filings.find((f) => f.access_number == access); - const filingDate = new Date(filing.filing_date * 1000); - const reportDate = new Date(filing.report_date * 1000); - const filingTime = filingDate.getTime() / 1000; - const reportTime = reportDate.getTime(); - const filingStr = filingDate.toLocaleDateString(); - const reportStr = reportDate.toLocaleDateString(); - const marketValue = new Intl.NumberFormat().format(filing.market_value); - const filingStocks = filing.stocks; - - const comparisonIndex = state.timeline.comparisons.findIndex( - (c) => c.type == type - ); - const comparison = state.timeline.comparisons[comparisonIndex]; - - state.timeline.comparisons[comparisonIndex] = { - ...comparison, - access, - filing: { - time: filingTime, - date: filingStr, - }, - report: { - time: reportTime, - date: reportStr, - }, - stocks: filingStocks, - value: marketValue, - }; - return state; - }, - setOpen(state) { - const open = state.timeline.open; - - state.timeline.open = !open; - return state; - }, - editComparison(state, action) { - const payload = action.payload; - const type = payload.type; - - const comparison = payload; - const comparisons = state.timeline.comparisons.map((c) => { - return c.type == type ? { ...c, ...comparison } : c; - }); - - state.timeline.comparisons = comparisons; - return state; - }, - editSort(state, action) { - const payload = action.payload; - const type = payload.type; - delete payload.type; - - const comparisons = state.timeline.comparisons.map((c) => { - return c.type == type ? { ...c, sort: { ...c.sort, ...payload } } : c; - }); - - state.timeline.comparisons = comparisons; - return state; - }, - editDifference(state, action) { - const payload = action.payload; - const difference = state.difference; - - state.difference = { ...difference, ...payload }; - return state; - }, - - [HYDRATE]: (state, action) => { - return { - ...state, - ...action.payload, - }; - }, - }, -}); - -export const selectCik = (state) => state.filer.cik; -export const selectTab = (state) => state.filer.tab; -export const selectSort = (state) => state.filer.sort; -export const selectActive = (state) => state.filer.sort.set; -export const selectSold = (state) => state.filer.sort.sold; -export const selectNa = (state) => state.filer.sort.na; -export const selectHeaders = (state) => state.filer.headers; -export const selectStocks = createSelector( - [(state) => state.filer.value], - (stocks) => stocks -); -export const selectDates = createSelector( - [(state) => state.filer.dates], - (dates) => - dates.map((d) => { - return { ...d, id: d.accessor }; - }) -); -export const selectPagination = createSelector( - [(state) => state.filer.sort], - (sort) => { - return { limit: sort.pagination, offset: sort.offset, count: sort.count }; - } -); -export const selectTimeline = (state) => state.filer.timeline; -export const selectFilings = (state) => state.filer.filings; -export const selectPrimary = (state) => state.filer.timeline.comparisons[0]; -export const selectSecondary = (state) => state.filer.timeline.comparisons[1]; -export const selectDifference = (state) => state.filer.difference; - -export const { - setCik, - setTab, - activateHeader, - addHeader, - editHeader, - sortHeader, - sortActive, - sortSold, - sortNa, - updateStocks, - setStocks, - sortStocks, - addDate, - setHeaders, - removeHeader, - removeDate, - editDate, - openDate, - updateDates, - newDate, - setPagination, - setCount, - setFilingCount, - setOffset, - setPrimary, - setSecondary, - setFilings, - editComparison, - editSort, - setComparison, - setOpen, - editDifference, -} = filerSlice.actions; - -export default filerSlice.reducer; +import { createSlice, createSelector, PayloadAction } from "@reduxjs/toolkit"; +import { HYDRATE } from "next-redux-wrapper"; +const initialDate: Date = new Date(); +interface Header { + display: string; + sort: string; + accessor: string; + active: boolean; + tooltip: string; +} +const initialHeaders: Header[] = [ + { + display: "Ticker", + sort: "ticker", + accessor: "ticker_str", + active: true, + tooltip: + "This is a unique series of letters assigned to a security for trading purposes", + }, + { + display: "Name", + sort: "name", + accessor: "name", + active: false, + tooltip: "The name of the stock.", + }, + { + display: "Class", + sort: "class", + accessor: "class", + active: false, + tooltip: + "This refers to the rights of a stockholder, including things like voting and dividends.", + }, + { + display: "Sector", + sort: "sector", + accessor: "sector", + active: false, + tooltip: "The broader industry category to which the stock belongs.", + }, + { + display: "CUSIP", + sort: "cusip", + accessor: "cusip", + active: false, + tooltip: + "A unique identifier assigned to each registered security in the United States and Canada.", + }, + { + display: "Shares Held", + sort: "shares_held", + accessor: "shares_held_str", + active: false, + tooltip: "The number of shares held, or the principal amount of the stock.", + }, + { + display: "Market Value", + sort: "market_value", + accessor: "market_value_str", + active: true, + tooltip: "The value for the shares of the stock the filer owns.", + }, + { + display: "% Portfolio", + sort: "portfolio_percent", + accessor: "portfolio_str", + active: true, + tooltip: + "The value of this stock's shares divided by the total value of the portfolio, expressed in percent. ( Value of Shares / Value of Portfolio )", + }, + { + display: "% Ownership", + sort: "ownership_percent", + accessor: "ownership_str", + active: false, + tooltip: + "The number of shares owned, divided by the current total of shares outstanding, expressed in percent. This value is only accurate the most value of shares outstanding. ( Shares Owned / Shares Existing )", + }, + { + display: "Sold Date", + sort: "sold_time", + accessor: "sold_str", + active: false, + tooltip: + "The date the stock was sold, taken by retrieving the report date of the last SEC filing said stock showed up on. This is only accurate up to the quarter.", + }, + { + display: "Buy Date", + sort: "buy_time", + accessor: "buy_str", + active: false, + tooltip: + "The date the stock was bought, according to the report date of the first SEC filing it appeared on. This is only accurate up to the quarter, and only the most recent bought date is shown.", + }, + { + display: "Price Paid", + sort: "buy_price", + accessor: "buy_price_str", + active: true, + tooltip: + "The price paid for the stock, estimated by taking a close price most near the quarter from which the stock was first reported.", + }, + { + display: "Recent Price", + sort: "recent_price", + accessor: "recent_price_str", + active: true, + tooltip: + "The recent price of the stock. This may be a couple days delayed.", + }, + { + display: "% Gain", + sort: "gain_percent", + accessor: "gain_str", + active: true, + tooltip: + "The price paid for the stock subtracted from the recent price, and then divided by price paid, expressed in percent. ( ( Recent Price - Price Paid ) / Price Paid )", + }, + { + display: "Industry", + sort: "industry", + accessor: "industry", + active: false, + tooltip: + "The specific sector or category of the economy in which the stock's company operates.", + }, + { + display: "Report Date", + sort: "report", + accessor: "report_str", + active: false, + tooltip: + "The report date listed on the SEC filing this stock was taken from.", + }, +]; +const initialComparisons: Header[] = initialHeaders.map((h) => { + switch (h.sort) { + case "buy_price": + return { ...h, active: false }; + case "recent_price": + return { ...h, active: false }; + case "gain_percent": + return { ...h, active: false }; + default: + return h; + } +}); +interface Sort { + sort: string; + type: string; + set: boolean; + na: boolean; + sold: boolean; + reverse: boolean; + pagination: number; + limit: number; + count: number; + offset: number; +} +const initialSort: Sort = { + sort: "ticker", + type: "string", + set: true, + na: false, + sold: false, + reverse: true, + pagination: 100, + limit: 100, + count: 0, + offset: 0, +}; +interface Filing { + time: number; + date: string; +} +interface Comparison { + type: string; + access: string; + filing: Filing; + report: Filing; + headers: Header[]; + sort: Sort; + stocks: any[]; +} +interface Timeline { + comparisons: Comparison[]; + open: boolean; +} +interface DateState { + year: number; + month: number; + day: number; + timestamp: number; + open: boolean; + accessor: string; +} +interface State { + cik: string; + value: any[]; + headers: Header[]; + tab: string; + sort: Sort; + filings: any[]; + timeline: Timeline; + difference: { + headers: Header[]; + sort: Sort; + stocks: any[]; + }; + dates: DateState[]; +} +const initialState: State = { + cik: "", + value: [], + headers: initialHeaders, + tab: "stocks", + sort: initialSort, + filings: [], + timeline: { + comparisons: [ + { + type: "primary", + access: "", + filing: { + time: 0, + date: "", + }, + report: { + time: 0, + date: "", + }, + headers: initialComparisons, + sort: initialSort, + stocks: [], + }, + { + type: "secondary", + access: "", + filing: { + time: 0, + date: "", + }, + report: { + time: 0, + date: "", + }, + headers: initialComparisons, + sort: initialSort, + stocks: [], + }, + ], + open: false, + }, + difference: { + headers: initialComparisons, + sort: initialSort, + stocks: [], + }, + dates: [ + { + year: initialDate.getFullYear(), + month: initialDate.getMonth(), + day: initialDate.getDate(), + timestamp: initialDate.getTime() / 1000, + open: false, + accessor: initialDate.toLocaleDateString(), + }, + ], +}; +export const filerSlice = createSlice({ + name: "filer", + initialState, + reducers: { + setCik(state, action: PayloadAction) { + Object.keys(initialState).map((k) => { + state[k] = initialState[k]; + }); + state.cik = action.payload; + return state; + }, + setTab(state, action: PayloadAction) { + const payload = action.payload; + state.tab = payload; + return state; + }, + activateHeader(state, action: PayloadAction) { + const headers = state.headers; + const payload = action.payload; + state.headers = headers.map((h) => + h.accessor === payload ? { ...h, active: !h.active } : h + ); + return state; + }, + sortHeader(state, action: PayloadAction<{ sort: string }>) { + const payload = action.payload; + let type = "string"; + switch (payload.sort) { + case "name": + case "sector": + case "industry": + case "class": + case "cusip": + type = "string"; + break; + case "shares_held": + case "market_value": + case "portfolio_percent": + case "ownership_percent": + case "gain_percent": + case "recent_price": + case "buy_price": + case "report": + case "buy": + case "sold_time": + type = "number"; + break; + case "buy": + case "report": + case "sold_time": + type = "date"; + break; + default: + type = typeof payload.sort === "number" ? "number" : "string"; + break; + } + state.sort = { ...state.sort, ...payload, type: type }; + return state; + }, + sortActive(state) { + const sort = state.sort; + const set = sort.set; + state.sort = { ...sort, set: !set }; + return state; + }, + addHeader(state, action: PayloadAction
    ) { + const payload = action.payload; + const headers = state.headers; + headers.push({ + ...payload, + active: true, + }); + state.headers = headers; + return state; + }, + editHeader(state, action: PayloadAction<{ accessor: string; display: string }>) { + const payload = action.payload; + const headers = state.headers.map((h) => + h.accessor === payload.accessor ? { ...h, display: payload.display } : h + ); + state.headers = headers; + return state; + }, + removeHeader(state, action: PayloadAction) { + const payload = action.payload; + const headers = state.headers.filter((h) => h.accessor !== payload); + state.headers = headers; + return state; + }, + setHeaders(state, action: PayloadAction) { + const payload = action.payload; + state.headers = payload; + return state; + }, + sortSold(state) { + const sort = state.sort; + const sold = sort.sold; + state.sort = { ...sort, sold: !sold }; + return state; + }, + sortNa(state) { + const sort = state.sort; + const na = sort.na; + state.sort = { ...sort, na: !na }; + return state; + }, + setStocks(state, action: PayloadAction) { + const stocks = action.payload; + state.value = stocks; + return state; + }, + updateStocks(state, action: PayloadAction<{ field: string; values: Record }>) { + const payload = action.payload; + const field = payload.field; + const values = payload.values; + let stocks = state.value; + stocks = stocks.map((stock) => { + const cusip = stock.cusip; + const value = values[cusip]; + return { ...stock, [field]: value }; + }); + state.value = stocks; + return state; + }, + addDate(state, action: PayloadAction) { + const dates = state.dates; + dates.push(action.payload); + state.dates = dates; + return state; + }, + removeDate(state, action: PayloadAction) { + const accessor = action.payload; + const dates = state.dates; + state.dates = dates.filter((date) => date.accessor !== accessor); + return state; + }, + editDate(state, action: PayloadAction<{ accessor: string; type: string; value: any }>) { + const payload = action.payload; + const dates = state.dates.map((date) => { + if (payload.accessor === date.accessor) { + let newDate = new Date(date.year, date.month, date.day); + switch (payload.type) { + case "year": + newDate.setFullYear(payload.value); + break; + case "month": + newDate.setMonth(payload.value); + break; + case "day": + newDate.setDate(payload.value); + break; + case "date": + newDate = new Date(payload.value); + break; + default: + break; + } + return { + year: newDate.getFullYear(), + month: newDate.getMonth(), + day: newDate.getDate(), + timestamp: newDate.getTime() / 1000, + open: true, + accessor: date.accessor, + }; + } else return date; + }); + state.dates = dates; + return state; + }, + updateDates(state, action: PayloadAction) { + const payload = action.payload; + state.dates = payload; + return state; + }, + openDate(state, action: PayloadAction<{ accessor: string; open: boolean }>) { + const payload = action.payload; + const accessor = payload.accessor; + const dates = state.dates.map((date) => + date.accessor === accessor ? { ...date, open: payload.open } : date + ); + state.dates = dates; + return state; + }, + newDate(state) { + const dates = state.dates; + const latestDate = dates.at(-1) || { + year: initialDate.getFullYear(), + month: initialDate.getMonth(), + day: initialDate.getDate(), + timestamp: initialDate.getTime() / 1000, + open: false, + accessor: initialDate.toLocaleDateString(), + }; + const newDate = new Date(latestDate.accessor); + newDate.setDate(newDate.getDate() + 1); + dates.push({ + year: newDate.getFullYear(), + month: newDate.getMonth(), + day: newDate.getDate(), + timestamp: newDate.getTime() / 1000, + open: false, + accessor: newDate.toLocaleDateString(), + }); + state.dates = dates; + return state; + }, + setPagination(state, action: PayloadAction) { + state.sort.pagination = action.payload; + return state; + }, + setCount(state, action: PayloadAction) { + const sort = state.sort; + const payload = action.payload; + const pagination = sort.pagination; + if (pagination < 0) { + state.sort.pagination = payload > 100 ? 100 : payload; + } + state.sort.count = payload; + return state; + }, + setFilingCount(state, action: PayloadAction<{ type: string; count: number }>) { + const payload = action.payload; + const type = payload.type; + const count = payload.count; + const comparisons = state.timeline.comparisons.map((c) => + c.type === type + ? { + ...c, + sort: { ...c.sort, pagination: count > 100 ? 100 : count, count }, + } + : c + ); + state.timeline.comparisons = comparisons; + return state; + }, + setOffset(state, action: PayloadAction) { + const payload = action.payload; + if (payload >= 0) { + state.sort.offset = Number(payload); + } + return state; + }, + setFilings(state, action: PayloadAction) { + const payload = action.payload; + state.filings = payload; + return state; + }, + setPrimary(state, action: PayloadAction>) { + const payload = action.payload; + const comparisons = state.timeline.comparisons; + const primary = comparisons[0]; + state.timeline.comparisons[0] = { ...primary, ...payload }; + return state; + }, + setSecondary(state, action: PayloadAction>) { + const payload = action.payload; + const comparisons = state.timeline.comparisons; + const secondary = comparisons[1]; + state.timeline.comparisons[1] = { ...secondary, ...payload }; + return state; + }, + setComparison(state, action: PayloadAction<{ type: string; access: string }>) { + const payload = action.payload; + const type = payload.type; + const access = payload.access; + const filing = state.filings.find((f) => f.access_number == access); + const filingDate = new Date(filing.filing_date * 1000); + const reportDate = new Date(filing.report_date * 1000); + const filingTime = filingDate.getTime() / 1000; + const reportTime = reportDate.getTime(); + const filingStr = filingDate.toLocaleDateString(); + const reportStr = reportDate.toLocaleDateString(); + const marketValue = new Intl.NumberFormat().format(filing.market_value); + const filingStocks = filing.stocks; + const comparisonIndex = state.timeline.comparisons.findIndex( + (c) => c.type == type + ); + const comparison = state.timeline.comparisons[comparisonIndex]; + state.timeline.comparisons[comparisonIndex] = { + ...comparison, + access, + filing: { + time: filingTime, + date: filingStr, + }, + report: { + time: reportTime, + date: reportStr, + }, + stocks: filingStocks, + value: marketValue, + }; + return state; + }, + setOpen(state) { + const open = state.timeline.open; + state.timeline.open = !open; + return state; + }, + editComparison(state, action: PayloadAction>) { + const payload = action.payload; + const type = payload.type; + const comparison = payload; + const comparisons = state.timeline.comparisons.map((c) => { + return c.type == type ? { ...c, ...comparison } : c; + }); + state.timeline.comparisons = comparisons; + return state; + }, + editSort(state, action: PayloadAction<{ type: string; [key: string]: any }>) { + const payload = action.payload; + const type = payload.type; + delete payload.type; + const comparisons = state.timeline.comparisons.map((c) => { + return c.type == type ? { ...c, sort: { ...c.sort, ...payload } } : c; + }); + state.timeline.comparisons = comparisons; + return state; + }, + editDifference(state, action: PayloadAction>) { + const payload = action.payload; + const difference = state.difference; + state.difference = { ...difference, ...payload }; + return state; + }, + [HYDRATE]: (state, action: PayloadAction) => { + return { + ...state, + ...action.payload, + }; + }, + }, +}); +export const selectCik = (state: { filer: State }) => state.filer.cik; +export const selectTab = (state: { filer: State }) => state.filer.tab; +export const selectSort = (state: { filer: State }) => state.filer.sort; +export const selectActive = (state: { filer: State }) => state.filer.sort.set; +export const selectSold = (state: { filer: State }) => state.filer.sort.sold; +export const selectNa = (state: { filer: State }) => state.filer.sort.na; +export const selectHeaders = (state: { filer: State }) => state.filer.headers; +export const selectStocks = createSelector( + [(state: { filer: State }) => state.filer.value], + (stocks) => stocks +); +export const selectDates = createSelector( + [(state: { filer: State }) => state.filer.dates], + (dates) => + dates.map((d) => { + return { ...d, id: d.accessor }; + }) +); +export const selectPagination = createSelector( + [(state: { filer: State }) => state.filer.sort], + (sort) => { + return { limit: sort.pagination, offset: sort.offset, count: sort.count }; + } +); +export const selectTimeline = (state: { filer: State }) => state.filer.timeline; +export const selectFilings = (state: { filer: State }) => state.filer.filings; +export const selectPrimary = (state: { filer: State }) => state.filer.timeline.comparisons[0]; +export const selectSecondary = (state: { filer: State }) => state.filer.timeline.comparisons[1]; +export const selectDifference = (state: { filer: State }) => state.filer.difference; +export const { + setCik, + setTab, + activateHeader, + addHeader, + editHeader, + sortHeader, + sortActive, + sortSold, + sortNa, + updateStocks, + setStocks, + addDate, + setHeaders, + removeHeader, + removeDate, + editDate, + openDate, + updateDates, + newDate, + setPagination, + setCount, + setFilingCount, + setOffset, + setPrimary, + setSecondary, + setFilings, + editComparison, + editSort, + setComparison, + setOpen, + editDifference, +} = filerSlice.actions; +export default filerSlice.reducer; \ No newline at end of file diff --git a/frontend/redux/store.js b/frontend/redux/store.js deleted file mode 100644 index d6f57e3b..00000000 --- a/frontend/redux/store.js +++ /dev/null @@ -1,18 +0,0 @@ -import { configureStore } from "@reduxjs/toolkit"; -import { createWrapper } from "next-redux-wrapper"; - -import { filerSlice } from "./filerSlice"; - -const makeStore = () => - configureStore({ - reducer: { - [filerSlice.name]: filerSlice.reducer, - }, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware({ - serializableCheck: false, - }), - devTools: true, - }); - -export const wrapper = createWrapper(makeStore); diff --git a/frontend/redux/store.ts b/frontend/redux/store.ts new file mode 100644 index 00000000..036f028d --- /dev/null +++ b/frontend/redux/store.ts @@ -0,0 +1,19 @@ +import { configureStore, EnhancedStore } from "@reduxjs/toolkit"; +import { createWrapper, Context, MakeStore } from "next-redux-wrapper"; +import { AnyAction, MiddlewareArray } from "@reduxjs/toolkit"; +import { filerSlice } from "./filerSlice"; +interface StoreState { + [key: string]: any; +} +const makeStore: MakeStore> = (context: Context) => + configureStore({ + reducer: { + [filerSlice.name]: filerSlice.reducer, + }, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: false, + }) as MiddlewareArray, + devTools: true, + }); +export const wrapper = createWrapper>(makeStore); \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 00000000..f9cfebeb --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "incremental": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": ".", + "paths": { + "@/*": ["./*"], + "@/images/*": ["public/static/*"], + "@fonts": ["components/fonts"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], + "exclude": ["node_modules"] +}
    - - -
    + + +
    - {display} -
    + {display} +