Stock/islands/StockList.tsx
2023-07-28 17:52:07 +09:00

230 lines
5.9 KiB
TypeScript

import { Button } from "../components/Button.tsx";
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
import { ComponentChildren } from "preact";
import { Signal, useSignal } from "@preact/signals";
import { IS_BROWSER } from "$fresh/runtime.ts";
import { mapValues } from "$std/collections/map_values.ts";
import { useAsync } from "../util/util.ts";
import {
Coperation,
CorpSimple,
fetchKosdaqList,
fetchKospiList,
fetchPageInfo,
PageCorpsInfo,
} from "../util/api.ts";
interface StockProps {
pageName: string;
}
interface ToggleButtonProps {
disabled?: boolean;
toggle: Signal<boolean>;
children?: ComponentChildren;
}
function ToggleButton(props: ToggleButtonProps) {
const { disabled, toggle, ...rest } = props;
return (
<button
{...rest}
disabled={!IS_BROWSER || disabled}
onClick={() => toggle.value = !toggle.value}
class={"px-2 py-1 border-2 rounded transition-colors" + (
toggle.value
? "border-gray-500 bg-white hover:bg-gray-200"
: "border-gray-200 bg-gray-800 hover:bg-gray-500 text-white"
)}
/>
);
}
function StockListByDate(
{ prevSet, rows, name }: {
prevSet: Set<string>;
rows: Coperation[];
name: string;
},
) {
const lastCount = useRef(rows.length);
const curCount = rows.length;
const parent = useRef<HTMLDivElement>(null);
const controller = useRef<
{
isEnabled: () => boolean;
disable: () => void;
enable: () => void;
} | undefined
>();
useEffect(() => {
(async () => {
console.log("animation mount on ", name);
const { default: autoAnimate } = await import(
"https://esm.sh/@formkit/auto-animate@0.7.0"
);
if (parent.current) {
const cntr = autoAnimate(parent.current);
controller.current = cntr;
}
})();
}, [parent]);
useLayoutEffect(() => {
if (controller.current) {
if (Math.abs(curCount - lastCount.current) > 200) {
console.log("disable animation", curCount, "from", lastCount.current);
controller.current.disable();
} else {
console.log("enable animation", curCount, "from", lastCount.current);
controller.current.enable();
}
lastCount.current = curCount;
}
}, [parent, rows]);
return (
<div ref={parent}>
<h2 class="text-lg">{name}</h2>
{rows.map((row) => {
const firstOccur = !prevSet.has(row.Code);
return (
<div
key={row.Code}
class={[
"bg-white",
firstOccur ? "text-[#ff5454] underline" : "text-black",
].join(" ")}
>
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
{row.Name}
</a>
</div>
);
})}
</div>
);
}
function StockList({ data }: { data: PageCorpsInfo }) {
console.log("data");
const corpListByDate = data.corpListByDate;
const keys = Object.keys(corpListByDate).sort().reverse().slice(0, 5)
.reverse();
const sets = keys.map((x) => new Set(corpListByDate[x].map((y) => y.Code)));
//const rows = data.corpListbyDate;
return (
<div class="flex">
{keys.map((x, i) => {
const prevSet = i == 0 ? new Set<string>() : sets[i - 1];
const rows = corpListByDate[x];
return (
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows} />
);
})}
</div>
);
}
type FilterInfoOption = {
list: {
items: CorpSimple[];
include: boolean;
}[];
otherwise: boolean;
};
function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
const checkMap = new Map<string, boolean>();
for (const l of filterList.list) {
for (const i of l.items) {
checkMap.set(i.code, l.include);
}
}
return info.filter((x) => {
const v = checkMap.get(x.Code);
if (v === undefined) {
return filterList.otherwise;
} else {
return v;
}
});
}
export default function StockListUI(props: StockProps) {
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(() =>
Promise.all([
fetchPageInfo(props.pageName),
fetchKospiList(),
fetchKosdaqList(),
])
);
const viewKospi = useSignal(true);
const viewKosdaq = useSignal(false);
const viewOtherwise = useSignal(false);
return (
<div class="my-2">
<div class="flex gap-2">
<ToggleButton toggle={viewKospi}>Kospi</ToggleButton>
<ToggleButton toggle={viewKosdaq}>Kosdaq</ToggleButton>
<ToggleButton toggle={viewOtherwise}>Otherwise</ToggleButton>
</div>
<div class="flex gap-8 py-6 flex-col">
{sig.value.type == "loading"
? (new Array(20).fill(0).map((_) => (
<div class="animate-pulse bg-gray-300 p-2"></div>
)))
: (
<div>
{sig.value.type == "error"
? (
<div>
<p>File Loading Failed</p>
</div>
)
: (
<StockList
data={applyFilter(
sig.value.data[0],
sig.value.data[1],
sig.value.data[2],
)}
/>
)}
</div>
)}
</div>
</div>
);
function applyFilter(
data: PageCorpsInfo,
kospi: CorpSimple[],
kosdaq: CorpSimple[],
): PageCorpsInfo {
const filter = getFilters(kospi, kosdaq);
return {
name: data.name,
description: data.description,
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[]) => {
return filterInfo(it, filter);
}),
};
}
function getFilters(
kospi: CorpSimple[],
kosdaq: CorpSimple[],
): FilterInfoOption {
return {
otherwise: viewOtherwise.value,
list: [{
include: viewKospi.value,
items: kospi,
}, {
include: viewKosdaq.value,
items: kosdaq,
}],
};
}
}