Initial commit
This commit is contained in:
commit
3d33e72f42
11
README.md
Normal file
11
README.md
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# fresh project
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Start the project:
|
||||||
|
|
||||||
|
```
|
||||||
|
deno task start
|
||||||
|
```
|
||||||
|
|
||||||
|
This will watch the project directory and restart as necessary.
|
12
components/Button.tsx
Normal file
12
components/Button.tsx
Normal 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-100 2) hover:bg-gray-200"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
10
deno.json
Normal file
10
deno.json
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"tasks": {
|
||||||
|
"start": "deno run -A --watch=static/,routes/ dev.ts"
|
||||||
|
},
|
||||||
|
"importMap": "./import_map.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact"
|
||||||
|
}
|
||||||
|
}
|
5
dev.ts
Executable file
5
dev.ts
Executable 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");
|
44
fresh.gen.ts
Normal file
44
fresh.gen.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// 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 config from "./deno.json" assert { type: "json" };
|
||||||
|
import * as $0 from "./routes/[name].tsx";
|
||||||
|
import * as $1 from "./routes/_404.tsx";
|
||||||
|
import * as $2 from "./routes/_500.tsx";
|
||||||
|
import * as $3 from "./routes/_middleware.ts";
|
||||||
|
import * as $4 from "./routes/about.tsx";
|
||||||
|
import * as $5 from "./routes/api/joke.ts";
|
||||||
|
import * as $6 from "./routes/api/random-uuid.ts";
|
||||||
|
import * as $7 from "./routes/countdown.tsx";
|
||||||
|
import * as $8 from "./routes/github/[username].tsx";
|
||||||
|
import * as $9 from "./routes/index.tsx";
|
||||||
|
import * as $10 from "./routes/route-config-example.tsx";
|
||||||
|
import * as $11 from "./routes/search.tsx";
|
||||||
|
import * as $$0 from "./islands/Countdown.tsx";
|
||||||
|
import * as $$1 from "./islands/Counter.tsx";
|
||||||
|
|
||||||
|
const manifest = {
|
||||||
|
routes: {
|
||||||
|
"./routes/[name].tsx": $0,
|
||||||
|
"./routes/_404.tsx": $1,
|
||||||
|
"./routes/_500.tsx": $2,
|
||||||
|
"./routes/_middleware.ts": $3,
|
||||||
|
"./routes/about.tsx": $4,
|
||||||
|
"./routes/api/joke.ts": $5,
|
||||||
|
"./routes/api/random-uuid.ts": $6,
|
||||||
|
"./routes/countdown.tsx": $7,
|
||||||
|
"./routes/github/[username].tsx": $8,
|
||||||
|
"./routes/index.tsx": $9,
|
||||||
|
"./routes/route-config-example.tsx": $10,
|
||||||
|
"./routes/search.tsx": $11,
|
||||||
|
},
|
||||||
|
islands: {
|
||||||
|
"./islands/Countdown.tsx": $$0,
|
||||||
|
"./islands/Counter.tsx": $$1,
|
||||||
|
},
|
||||||
|
baseUrl: import.meta.url,
|
||||||
|
config,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default manifest;
|
12
import_map.json
Normal file
12
import_map.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"$fresh/": "https://deno.land/x/fresh@1.1.1/",
|
||||||
|
"preact": "https://esm.sh/preact@10.11.0",
|
||||||
|
"preact/": "https://esm.sh/preact@10.11.0/",
|
||||||
|
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@5.2.4",
|
||||||
|
"@preact/signals": "https://esm.sh/*@preact/signals@1.0.3",
|
||||||
|
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.0.1",
|
||||||
|
"twind": "https://esm.sh/twind@0.16.17",
|
||||||
|
"twind/": "https://esm.sh/twind@0.16.17/"
|
||||||
|
}
|
||||||
|
}
|
27
islands/Countdown.tsx
Normal file
27
islands/Countdown.tsx
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
const timeFmt = new Intl.RelativeTimeFormat("en-US");
|
||||||
|
|
||||||
|
export default function Countdown(props: { target: string }) {
|
||||||
|
const target = new Date(props.target);
|
||||||
|
const [now, setNow] = useState(new Date());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setNow((now) => {
|
||||||
|
if (now > target) {
|
||||||
|
clearInterval(timer);
|
||||||
|
}
|
||||||
|
return new Date();
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [props.target]);
|
||||||
|
|
||||||
|
if (now > target) {
|
||||||
|
return <span>🎉</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secondsLeft = Math.ceil((target.getTime() - now.getTime()) / 1000);
|
||||||
|
return <span>{timeFmt.format(secondsLeft, "seconds")}</span>;
|
||||||
|
}
|
17
islands/Counter.tsx
Normal file
17
islands/Counter.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { Button } from "../components/Button.tsx";
|
||||||
|
|
||||||
|
interface CounterProps {
|
||||||
|
start: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Counter(props: CounterProps) {
|
||||||
|
const [count, setCount] = useState(props.start);
|
||||||
|
return (
|
||||||
|
<div class="flex gap-2 w-full">
|
||||||
|
<p class="flex-grow-1 font-bold text-xl">{count}</p>
|
||||||
|
<Button onClick={() => setCount(count - 1)}>-1</Button>
|
||||||
|
<Button onClick={() => setCount(count + 1)}>+1</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
13
main.ts
Normal file
13
main.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
/// <reference no-default-lib="true" />
|
||||||
|
/// <reference lib="dom" />
|
||||||
|
/// <reference lib="dom.iterable" />
|
||||||
|
/// <reference lib="dom.asynciterable" />
|
||||||
|
/// <reference lib="deno.ns" />
|
||||||
|
|
||||||
|
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)] });
|
5
routes/[name].tsx
Normal file
5
routes/[name].tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { PageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export default function Greet(props: PageProps) {
|
||||||
|
return <div>Hello {props.params.name}</div>;
|
||||||
|
}
|
5
routes/_404.tsx
Normal file
5
routes/_404.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { UnknownPageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export default function NotFoundPage({ url }: UnknownPageProps) {
|
||||||
|
return <p>404 not found: {url.pathname}</p>;
|
||||||
|
}
|
5
routes/_500.tsx
Normal file
5
routes/_500.tsx
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { ErrorPageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export default function Error500Page({ error }: ErrorPageProps) {
|
||||||
|
return <p>500 internal error: {(error as Error).message}</p>;
|
||||||
|
}
|
15
routes/_middleware.ts
Normal file
15
routes/_middleware.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { MiddlewareHandlerContext } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handler(
|
||||||
|
_: Request,
|
||||||
|
ctx: MiddlewareHandlerContext<State>,
|
||||||
|
) {
|
||||||
|
ctx.state.data = "myData";
|
||||||
|
const resp = await ctx.next();
|
||||||
|
resp.headers.set("server", "fresh server");
|
||||||
|
return resp;
|
||||||
|
}
|
8
routes/about.tsx
Normal file
8
routes/about.tsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>About</h1>
|
||||||
|
<p>This is the about page.</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
21
routes/api/joke.ts
Normal file
21
routes/api/joke.ts
Normal 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);
|
||||||
|
};
|
10
routes/api/random-uuid.ts
Normal file
10
routes/api/random-uuid.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Handlers } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export const handler: Handlers = {
|
||||||
|
GET(_: Request) {
|
||||||
|
const uuid = crypto.randomUUID();
|
||||||
|
return new Response(JSON.stringify(uuid), {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
11
routes/countdown.tsx
Normal file
11
routes/countdown.tsx
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import Countdown from "../islands/Countdown.tsx";
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
const date = new Date();
|
||||||
|
date.setSeconds(date.getSeconds() + 10);
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
The big event is happening <Countdown target={date.toISOString()} />.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
33
routes/github/[username].tsx
Normal file
33
routes/github/[username].tsx
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
login: string;
|
||||||
|
name: string;
|
||||||
|
avatar_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<User | null> = {
|
||||||
|
async GET(_, ctx) {
|
||||||
|
const { username } = ctx.params;
|
||||||
|
const resp = await fetch(`https://api.github.com/users/${username}`);
|
||||||
|
if (resp.status === 404) {
|
||||||
|
return ctx.render(null);
|
||||||
|
}
|
||||||
|
const user: User = await resp.json();
|
||||||
|
return ctx.render(user);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page({ data }: PageProps<User | null>) {
|
||||||
|
if (!data) {
|
||||||
|
return <h1>User not found</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<img src={data.avatar_url} width={64} height={64} />
|
||||||
|
<h1>{data.name}</h1>
|
||||||
|
<p>{data.login}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
18
routes/index.tsx
Normal file
18
routes/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import Counter from "../islands/Counter.tsx";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<div class="p-4 mx-auto max-w-screen-md">
|
||||||
|
<img
|
||||||
|
src="/logo.svg"
|
||||||
|
class="w-32 h-32"
|
||||||
|
alt="the fresh logo: a sliced lemon dripping with juice"
|
||||||
|
/>
|
||||||
|
<p class="my-6">
|
||||||
|
Welcome to `fresh`. Try updating this message in the ./routes/index.tsx
|
||||||
|
file, and refresh.
|
||||||
|
</p>
|
||||||
|
<Counter start={3} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
14
routes/route-config-example.tsx
Normal file
14
routes/route-config-example.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { RouteConfig } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
export const config: RouteConfig = {
|
||||||
|
routeOverride: "/route-override-example",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<h1>RouteConfigExample</h1>
|
||||||
|
<p>This is the RouteConfigExample page. It should only be accessible from /route-override-example</p>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
32
routes/search.tsx
Normal file
32
routes/search.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
|
|
||||||
|
const NAMES = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Frank"];
|
||||||
|
|
||||||
|
interface Data {
|
||||||
|
results: string[];
|
||||||
|
query: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<Data> = {
|
||||||
|
GET(req, ctx) {
|
||||||
|
const url = new URL(req.url);
|
||||||
|
const query = url.searchParams.get("q") || "";
|
||||||
|
const results = NAMES.filter((name) => name.includes(query));
|
||||||
|
return ctx.render({ results, query });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page({ data }: PageProps<Data>) {
|
||||||
|
const { results, query } = data;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<input type="text" name="q" value={query} />
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
<ul>
|
||||||
|
{results.map((name) => <li key={name}>{name}</li>)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
6
static/logo.svg
Normal file
6
static/logo.svg
Normal 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 KiB |
5
twind.config.ts
Normal file
5
twind.config.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { Options } from "$fresh/plugins/twind.ts";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
selfURL: import.meta.url,
|
||||||
|
} as Options;
|
Loading…
Reference in a new issue