feat: add fresh

This commit is contained in:
monoid 2023-07-23 00:32:54 +09:00
parent c678f7e14a
commit a6635ccee6
24 changed files with 554 additions and 1 deletions

12
components/Button.tsx Normal file
View File

@ -0,0 +1,12 @@
import { JSX } from "preact";
import { IS_BROWSER } from "$fresh/runtime.ts";
export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
return (
<button
{...props}
disabled={!IS_BROWSER || props.disabled}
class="px-2 py-1 border-gray-500 border-2 rounded bg-white hover:bg-gray-200 transition-colors"
/>
);
}

30
deno.json Normal file
View File

@ -0,0 +1,30 @@
{
"lock": false,
"tasks": {
"start": "deno run -A --watch=static/,routes/ dev.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
},
"lint": {
"rules": {
"tags": [
"fresh",
"recommended"
]
}
},
"imports": {
"$fresh/": "https://deno.land/x/fresh@1.3.1/",
"preact": "https://esm.sh/preact@10.15.1",
"preact/": "https://esm.sh/preact@10.15.1/",
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
"twind": "https://esm.sh/twind@0.16.19",
"twind/": "https://esm.sh/twind@0.16.19/",
"$std/": "https://deno.land/std@0.193.0/"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}

5
dev.ts Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env -S deno run -A --watch=static/,routes/
import dev from "$fresh/dev.ts";
await dev(import.meta.url, "./main.ts");

38
fresh.gen.ts Normal file
View File

@ -0,0 +1,38 @@
// DO NOT EDIT. This file is generated by fresh.
// This file SHOULD be checked into source version control.
// This file is automatically updated during development when running `dev.ts`.
import * as $0 from "./routes/_404.tsx";
import * as $1 from "./routes/_app.tsx";
import * as $2 from "./routes/api/joke.ts";
import * as $3 from "./routes/api/kosdaq.ts";
import * as $4 from "./routes/api/kospi.ts";
import * as $5 from "./routes/api/pages/[name].ts";
import * as $6 from "./routes/api/pages/index.ts";
import * as $7 from "./routes/greet/[name].tsx";
import * as $8 from "./routes/index.tsx";
import * as $9 from "./routes/pages/[name].tsx";
import * as $$0 from "./islands/Counter.tsx";
import * as $$1 from "./islands/StockList.tsx";
const manifest = {
routes: {
"./routes/_404.tsx": $0,
"./routes/_app.tsx": $1,
"./routes/api/joke.ts": $2,
"./routes/api/kosdaq.ts": $3,
"./routes/api/kospi.ts": $4,
"./routes/api/pages/[name].ts": $5,
"./routes/api/pages/index.ts": $6,
"./routes/greet/[name].tsx": $7,
"./routes/index.tsx": $8,
"./routes/pages/[name].tsx": $9,
},
islands: {
"./islands/Counter.tsx": $$0,
"./islands/StockList.tsx": $$1,
},
baseUrl: import.meta.url,
};
export default manifest;

16
islands/Counter.tsx Normal file
View File

@ -0,0 +1,16 @@
import type { Signal } from "@preact/signals";
import { Button } from "../components/Button.tsx";
interface CounterProps {
count: Signal<number>;
}
export default function Counter(props: CounterProps) {
return (
<div class="flex gap-8 py-6">
<Button onClick={() => props.count.value -= 1}>-1</Button>
<p class="text-3xl">{props.count}</p>
<Button onClick={() => props.count.value += 1}>+1</Button>
</div>
);
}

126
islands/StockList.tsx Normal file
View File

@ -0,0 +1,126 @@
import { Button } from "../components/Button.tsx";
import { useEffect } from "preact/hooks";
import { ComponentChildren } from "preact";
import { Signal, useSignal } from "@preact/signals";
interface StockProps {
pageName: string;
}
interface ToggleButtonProps {
children?: ComponentChildren;
}
function ToggleButton(props: ToggleButtonProps) {
return (
<Button {...props}>
</Button>
);
}
type QueryStatus<T> = {
type: "loading";
} | {
type: "complete";
data: T;
} | {
type: "error";
err: Error;
};
const cacheMap = new Map<string, unknown>();
function useQuery<T>(url: string): Signal<QueryStatus<T>> {
const state = useSignal({
type: "loading",
} as QueryStatus<T>);
useEffect(() => {
if (!cacheMap.has(url)) {
(async () => {
try {
const res = await fetch(url);
const data = await res.json();
cacheMap.set(url, data);
state.value = {
type: "complete",
data: data,
};
} catch (err) {
state.value = {
type: "error",
err: err,
};
}
})();
} else {
state.value = {
type: "complete",
data: cacheMap.get(url) as T,
};
}
},[]);
return state;
}
interface Coperation {
Name: string;
Code: string;
Sector: string;
Product: string;
ListingDay: string;
ClosingMonth: string;
Representative: string;
Homepage: string;
AddressArea: string;
LastUpdate: string;
}
interface PageCorpsInfo {
name: string;
description: string;
corpListByDate: Record<string, Coperation[]>;
}
function StockList({data}: {data: PageCorpsInfo}){
console.log("data")
const keys = Object.keys(data.corpListByDate).sort().reverse().slice(0,5).reverse();
//const rows = data.corpListbyDate;
return <div class="flex">
{keys.map((x,i)=>{
const rows = data.corpListByDate[x];
return <div key={x}>
{rows.map(row=>{
return <div>
{row.Name}
</div>
})}
</div>
})}
</div>
}
export default function StockListUI(props: StockProps) {
const sig = useQuery<PageCorpsInfo>("/api/pages/" + props.pageName);
return (
<div class="my-2">
<div class="flex gap-2">
<ToggleButton>Kospi</ToggleButton>
<ToggleButton>Kosdaq</ToggleButton>
<ToggleButton>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={sig.value.data}></StockList>
}
</div>}
</div>
</div>
);
}

15
main.ts Normal file
View File

@ -0,0 +1,15 @@
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import "$std/dotenv/load.ts";
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "./twind.config.ts";
await start(manifest, { plugins: [twindPlugin(twindConfig)] });

46
pages.ts Normal file
View File

@ -0,0 +1,46 @@
import { parse } from "https://deno.land/std@0.195.0/yaml/mod.ts";
import { join, fromFileUrl } from "https://deno.land/std@0.193.0/path/mod.ts";
export const PAGES_PATH = join(fromFileUrl(import.meta.url), "../pages.yaml");
export interface PageDescription {
name: string;
description: string;
}
async function readPagesDescription() {
const pagesText = await Deno.readTextFile(PAGES_PATH);
const pages = parse(pagesText) as PageDescription[];
return pages;
}
function watchFile(
path: string,
callback: () => void | Promise<void>,
) {
const watcherRef = Deno.watchFs(path);
(async () => {
for await (const event of watcherRef) {
if (event.kind == "modify") {
await callback();
}
}
})();
Deno.addSignalListener("SIGINT", () => {
watcherRef.close();
});
}
let pages_meta: PageDescription[] = [];
let mtime = 0;
export async function get_pages_meta(): Promise<[PageDescription[],number]>{
if (pages_meta) {
pages_meta = await readPagesDescription();
mtime = Date.now();
watchFile(PAGES_PATH, async () => {
pages_meta = await readPagesDescription();
mtime = Date.now();
});
}
return [pages_meta, mtime];
}

View File

@ -58,6 +58,6 @@
- name: 240일 증가 - name: 240일 증가
description: 240일선이 증가하는 것. description: 240일선이 증가하는 것.
- name: 볼린저 밴드 25 - name: 볼린저 밴드 25
description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시. 시장 상황이 않 좋으면 평균 59개' description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시.'
- name: 양봉사이20일선 - name: 양봉사이20일선
description: Open과 Close 사이 20일 선 description: Open과 Close 사이 20일 선

28
routes/_404.tsx Normal file
View File

@ -0,0 +1,28 @@
import { Head } from "$fresh/runtime.ts";
export default function Error404() {
return (
<>
<Head>
<title>404 - Page not found</title>
</Head>
<div class="px-4 py-8 mx-auto bg-[#86efac]">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img
class="my-6"
src="/logo.svg"
width="128"
height="128"
alt="the fresh logo: a sliced lemon dripping with juice"
/>
<h1 class="text-4xl font-bold">404 - Page not found</h1>
<p class="my-4">
The page you were looking for doesn't exist.
</p>
<a href="/" class="underline">Go back home</a>
</div>
</div>
</>
);
}

9
routes/_app.tsx Normal file
View File

@ -0,0 +1,9 @@
import { AppProps } from "$fresh/server.ts";
export default function App({ Component }: AppProps) {
return (
<>
<Component />
</>
);
}

21
routes/api/joke.ts Normal file
View File

@ -0,0 +1,21 @@
import { HandlerContext } from "$fresh/server.ts";
// Jokes courtesy of https://punsandoneliners.com/randomness/programmer-jokes/
const JOKES = [
"Why do Java developers often wear glasses? They can't C#.",
"A SQL query walks into a bar, goes up to two tables and says “can I join you?”",
"Wasn't hard to crack Forrest Gump's password. 1forrest1.",
"I love pressing the F5 key. It's refreshing.",
"Called IT support and a chap from Australia came to fix my network connection. I asked “Do you come from a LAN down under?”",
"There are 10 types of people in the world. Those who understand binary and those who don't.",
"Why are assembly programmers often wet? They work below C level.",
"My favourite computer based band is the Black IPs.",
"What programme do you use to predict the music tastes of former US presidential candidates? An Al Gore Rhythm.",
"An SEO expert walked into a bar, pub, inn, tavern, hostelry, public house.",
];
export const handler = (_req: Request, _ctx: HandlerContext): Response => {
const randomIndex = Math.floor(Math.random() * JOKES.length);
const body = JOKES[randomIndex];
return new Response(body);
};

17
routes/api/kosdaq.ts Normal file
View File

@ -0,0 +1,17 @@
import { Handlers } from "$fresh/server.ts";
import {DB} from "https://deno.land/x/sqlite/mod.ts";
export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const db = new DB("stock.db");
const conn = db.query("SELECT Code,Name FROM KOSDAQ");
const body = conn.map(row=>({
code: row[0],
name: row[1]
}))
return new Response(JSON.stringify(body), {headers});
},
}

17
routes/api/kospi.ts Normal file
View File

@ -0,0 +1,17 @@
import { Handlers } from "$fresh/server.ts";
import {DB} from "https://deno.land/x/sqlite/mod.ts";
export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const db = new DB("stock.db");
const conn = db.query("SELECT Code,Name FROM KOSPI");
const body = conn.map(row=>({
code: row[0],
name: row[1]
}))
return new Response(JSON.stringify(body), {headers});
},
}

View File

@ -0,0 +1,40 @@
import { Handlers } from "$fresh/server.ts";
import { Status, STATUS_TEXT } from "https://deno.land/std@0.195.0/http/mod.ts";
import { fromFileUrl, join } from "$std/path/mod.ts";
export const handler: Handlers = {
async GET(req, ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const path = join(fromFileUrl(import.meta.url), "../../../../dist", `${ctx.params.name}.json`);
console.log("path : ",path)
let stat;
try {
stat = await Deno.stat(path);
}
catch(err){
if(err instanceof Deno.errors.NotFound){
return await ctx.renderNotFound();
}
else {
throw err;
}
}
const mtime = stat.mtime ?? new Date(0);
const body = await Deno.readTextFile(path);
headers.set("last-modified", mtime.toUTCString());
const ifModifiedSinceValue = req.headers.get("if-modified-since");
if ( ifModifiedSinceValue &&
mtime.getTime() != new Date(ifModifiedSinceValue).getTime()
){
return new Response(null, {
status: Status.NotModified,
statusText: STATUS_TEXT[Status.NotModified]
})
}
return new Response(body, {headers});
},
};

24
routes/api/pages/index.ts Normal file
View File

@ -0,0 +1,24 @@
import { Handlers } from "$fresh/server.ts";
import { get_pages_meta } from "../../../pages.ts";
import { Status, STATUS_TEXT } from "https://deno.land/std@0.195.0/http/mod.ts";
export const handler: Handlers = {
async GET(req, _ctx): Promise<Response> {
const headers = new Headers({
"content-type": "application/json"
});
const [body, mtime] = await get_pages_meta();
headers.set("last-modified", new Date(mtime).toUTCString());
console.log("aaa");
const ifModifiedSinceValue = req.headers.get("if-modified-since");
if ( ifModifiedSinceValue &&
mtime != new Date(ifModifiedSinceValue).getTime()
){
return new Response(null, {
status: Status.NotModified,
statusText: STATUS_TEXT[Status.NotModified]
})
}
return new Response(JSON.stringify(body), {headers});
},
};

5
routes/greet/[name].tsx Normal file
View File

@ -0,0 +1,5 @@
import { PageProps } from "$fresh/server.ts";
export default function Greet(props: PageProps) {
return <div>Hello {props.params.name}</div>;
}

48
routes/index.tsx Normal file
View File

@ -0,0 +1,48 @@
import { Head } from "$fresh/runtime.ts";
import { useSignal } from "@preact/signals";
import {Button} from "../components/Button.tsx";
import { PageDescription, get_pages_meta } from "../pages.ts";
import { Handlers, PageProps } from "$fresh/server.ts";
export const handler: Handlers = {
async GET(_req, ctx){
const [pages,_] = await get_pages_meta();
return await ctx.render(pages);
}
}
export default function Home({data}: PageProps<PageDescription[]>) {
const count = useSignal(3);
return (
<>
<Head>
<title>stock-front</title>
</Head>
<div class="px-4 py-8 mx-auto bg-[#86efac]">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img
class="my-6"
src="/logo.svg"
width="128"
height="128"
alt="the fresh logo: a sliced lemon dripping with juice"
/>
<h1 class="text-4xl font-bold">Stock</h1>
<div class="my-4">
<ul>
{
data.map(x=><li class="my-2">
<a class="p-2 block hover:bg-gray-300 bg-white rounded" href={`/pages/${x.name}`}>
{x.name}
</a>
</li>
)
}
</ul>
</div>
</div>
</div>
</>
);
}

25
routes/pages/[name].tsx Normal file
View File

@ -0,0 +1,25 @@
import { PageProps } from "$fresh/server.ts";
import { Head } from "$fresh/runtime.ts";
import StockList from "../../islands/StockList.tsx";
export default function Pages(props: PageProps) {
return <>
<Head>
<title>Stock: {props.params.name}</title>
</Head>
<div class="px-4 py-8 mx-auto bg-[#86efac]">
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
<img
class="my-6"
src="/stockgraph.svg"
width="128"
height="128"
alt="stock graph"
/>
<h1>{props.params.name}</h1>
<StockList pageName={props.params.name}></StockList>
</div>
</div>
</>
}

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

6
static/logo.svg Normal file
View File

@ -0,0 +1,6 @@
<svg width="40" height="40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M34.092 8.845C38.929 20.652 34.092 27 30 30.5c1 3.5-2.986 4.222-4.5 2.5-4.457 1.537-13.512 1.487-20-5C2 24.5 4.73 16.714 14 11.5c8-4.5 16-7 20.092-2.655Z" fill="#FFDB1E"/>
<path d="M14 11.5c6.848-4.497 15.025-6.38 18.368-3.47C37.5 12.5 21.5 22.612 15.5 25c-6.5 2.587-3 8.5-6.5 8.5-3 0-2.5-4-5.183-7.75C2.232 23.535 6.16 16.648 14 11.5Z" fill="#fff" stroke="#FFDB1E"/>
<path d="M28.535 8.772c4.645 1.25-.365 5.695-4.303 8.536-3.732 2.692-6.606 4.21-7.923 4.83-.366.173-1.617-2.252-1.617-1 0 .417-.7 2.238-.934 2.326-1.365.512-4.223 1.29-5.835 1.29-3.491 0-1.923-4.754 3.014-9.122.892-.789 1.478-.645 2.283-.645-.537-.773-.534-.917.403-1.546C17.79 10.64 23 8.77 25.212 8.42c.366.014.82.35.82.629.41-.14 2.095-.388 2.503-.278Z" fill="#FFE600"/>
<path d="M14.297 16.49c.985-.747 1.644-1.01 2.099-2.526.566.121.841-.08 1.29-.701.324.466 1.657.608 2.453.701-.715.451-1.057.852-1.452 2.106-1.464-.611-3.167-.302-4.39.42Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

13
static/stockgraph.svg Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
// 16pxls (c) by Paul mackenzie <paul@whatspauldoing.com>
//
// 16pxls is licensed under a
// Creative Commons Attribution-ShareAlike 4.0 International License.
//
// You should have received a copy of the license along with this
// work. If not, see <http://creativecommons.org/licenses/by-sa/4.0/>.
-->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14h16v2H0v-2zm8.5-8l4-4H11V0h5v5h-2V3.5L9.5 8l-1 1-2-2-5 5L0 10.5 6.5 4 8 5.5l.5.5z" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 590 B

7
tailwind.config.js Normal file
View File

@ -0,0 +1,7 @@
export default {
content: ["./**/*.{html,tsx}"],
theme:{
extend:{},
},
plugins: [],
}

5
twind.config.ts Normal file
View File

@ -0,0 +1,5 @@
import { Options } from "$fresh/plugins/twind.ts";
export default {
selfURL: import.meta.url,
} as Options;