230 lines
5.9 KiB
TypeScript
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,
|
|
}],
|
|
};
|
|
}
|
|
}
|