feat: initial commit.
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Fresh build directory
|
||||||
|
_fresh/
|
||||||
|
# npm + other dependencies
|
||||||
|
node_modules/
|
||||||
|
vendor/
|
||||||
|
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"denoland.vscode-deno",
|
||||||
|
"bradlc.vscode-tailwindcss"
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+20
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"deno.enable": true,
|
||||||
|
"deno.lint": true,
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
},
|
||||||
|
"[javascriptreact]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "denoland.vscode-deno"
|
||||||
|
},
|
||||||
|
"files.associations": {
|
||||||
|
"*.css": "tailwindcss"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Fresh project
|
||||||
|
|
||||||
|
Your new Fresh project is ready to go. You can follow the Fresh "Getting
|
||||||
|
Started" guide here: https://fresh.deno.dev/docs/getting-started
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
Make sure to install Deno:
|
||||||
|
https://docs.deno.com/runtime/getting_started/installation
|
||||||
|
|
||||||
|
Then start the project in development mode:
|
||||||
|
|
||||||
|
```
|
||||||
|
deno task dev
|
||||||
|
```
|
||||||
|
|
||||||
|
This will watch the project directory and restart as necessary.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export async function backendSignIn(
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
// Simulate 1 second fetch
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const ok = email === "asdf@asdf" && password === "asdfasdf";
|
||||||
|
setTimeout(() => resolve(ok), 1000);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Import CSS files here for hot module reloading to work.
|
||||||
|
import "./assets/styles.css";
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { ComponentChildren } from "preact";
|
||||||
|
|
||||||
|
export interface ButtonProps {
|
||||||
|
id?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
children?: ComponentChildren;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button(props: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
class="px-2 py-1 border-gray-500 border-2 rounded-sm bg-white hover:bg-gray-200 transition-colors"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
const links = [
|
||||||
|
{ href: "/", label: "Home" },
|
||||||
|
{ href: "/sign-in", label: "Sign in" },
|
||||||
|
{ href: "/sign-up", label: "Sign up" },
|
||||||
|
{ href: "/dashboard", label: "Dashboard" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Header() {
|
||||||
|
return (
|
||||||
|
<div class="navbar bg-base-100 shadow-sm">
|
||||||
|
<div class="flex-1">
|
||||||
|
<a class="btn btn-ghost text-xl">daisyUI</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-none">
|
||||||
|
<ul class="menu menu-horizontal px-1">
|
||||||
|
{links.map((l) => (
|
||||||
|
<li key={l.href}>
|
||||||
|
<a href={l.href}>{l.label}</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export interface Props {
|
||||||
|
email: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignInForm(props: Props) {
|
||||||
|
return (
|
||||||
|
<form method="post" class="flex flex-col gap-4">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
class="input w-full"
|
||||||
|
placeholder="E-Mail"
|
||||||
|
autocomplete="email"
|
||||||
|
value={props.email}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
class="input w-full"
|
||||||
|
placeholder="Password"
|
||||||
|
autocomplete="current-password"
|
||||||
|
/>
|
||||||
|
{props.error && (
|
||||||
|
<div class="alert alert-error">
|
||||||
|
{props.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button type="submit" class="btn btn-primary self-start">Sign up</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{
|
||||||
|
"nodeModulesDir": "manual",
|
||||||
|
"tasks": {
|
||||||
|
"check": "deno fmt --check . && deno lint . && deno check",
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"start": "deno serve -A _fresh/server.js",
|
||||||
|
"update": "deno run -A -r jsr:@fresh/update ."
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"rules": {
|
||||||
|
"tags": [
|
||||||
|
"fresh",
|
||||||
|
"recommended"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"**/_fresh/*"
|
||||||
|
],
|
||||||
|
"imports": {
|
||||||
|
"@/": "./",
|
||||||
|
"@std/http": "jsr:@std/http@^1.1.3",
|
||||||
|
"daisyui": "npm:daisyui@^5.7.32",
|
||||||
|
"fresh": "jsr:@fresh/core@^2.3.3",
|
||||||
|
"preact": "npm:preact@^10.29.1",
|
||||||
|
"@preact/signals": "npm:@preact/signals@^2.9.0",
|
||||||
|
"@fresh/plugin-vite": "jsr:@fresh/plugin-vite@^1.1.2",
|
||||||
|
"vite": "npm:vite@^7.1.3",
|
||||||
|
"@types/babel__core": "npm:@types/babel__core@^7.20.5",
|
||||||
|
"tailwindcss": "npm:tailwindcss@^4.1.10",
|
||||||
|
"@tailwindcss/vite": "npm:@tailwindcss/vite@^4.1.12",
|
||||||
|
"zod": "npm:zod@^4.6.1"
|
||||||
|
},
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.asynciterable",
|
||||||
|
"dom.iterable",
|
||||||
|
"deno.ns"
|
||||||
|
],
|
||||||
|
"jsx": "precompile",
|
||||||
|
"jsxImportSource": "preact",
|
||||||
|
"jsxPrecompileSkipElements": [
|
||||||
|
"a",
|
||||||
|
"img",
|
||||||
|
"source",
|
||||||
|
"body",
|
||||||
|
"html",
|
||||||
|
"head",
|
||||||
|
"title",
|
||||||
|
"meta",
|
||||||
|
"script",
|
||||||
|
"link",
|
||||||
|
"style",
|
||||||
|
"base",
|
||||||
|
"noscript",
|
||||||
|
"template"
|
||||||
|
],
|
||||||
|
"types": [
|
||||||
|
"vite/client"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 id="decrement" onClick={() => props.count.value -= 1}>-1</Button>
|
||||||
|
<p class="text-3xl tabular-nums">{props.count}</p>
|
||||||
|
<Button id="increment" onClick={() => props.count.value += 1}>+1</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { App, staticFiles } from "fresh";
|
||||||
|
import { define, type State } from "./utils.ts";
|
||||||
|
|
||||||
|
export const app = new App<State>();
|
||||||
|
|
||||||
|
app.use(staticFiles());
|
||||||
|
|
||||||
|
// Pass a shared value from a middleware
|
||||||
|
app.use(async (ctx) => {
|
||||||
|
ctx.state.shared = "hello";
|
||||||
|
return await ctx.next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// this is the same as the /api/:name route defined via a file. feel free to delete this!
|
||||||
|
app.get("/api2/:name", (ctx) => {
|
||||||
|
const name = ctx.params.name;
|
||||||
|
return new Response(
|
||||||
|
`Hello, ${name.charAt(0).toUpperCase() + name.slice(1)}!`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// this can also be defined via a file. feel free to delete this!
|
||||||
|
const exampleLoggerMiddleware = define.middleware((ctx) => {
|
||||||
|
console.log(`${ctx.req.method} ${ctx.req.url}`);
|
||||||
|
return ctx.next();
|
||||||
|
});
|
||||||
|
app.use(exampleLoggerMiddleware);
|
||||||
|
|
||||||
|
// Include file-system based routes here
|
||||||
|
app.fsRoutes();
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import Header from "../components/Header.tsx";
|
||||||
|
import { define } from "../utils.ts";
|
||||||
|
|
||||||
|
export default define.page(function App({ Component }) {
|
||||||
|
return (
|
||||||
|
<html data-theme="light">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Deno Fresh Dummy</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<Header />
|
||||||
|
<Component />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { define } from "../../utils.ts";
|
||||||
|
|
||||||
|
export const handler = define.handlers({
|
||||||
|
GET(ctx) {
|
||||||
|
const name = ctx.params.name;
|
||||||
|
return new Response(
|
||||||
|
`Hello, ${name.charAt(0).toUpperCase() + name.slice(1)}!`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { define } from "../utils.ts";
|
||||||
|
|
||||||
|
export default define.page(function Dashboard(ctx) {
|
||||||
|
return <h1>Dashboard</h1>;
|
||||||
|
});
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { useSignal } from "@preact/signals";
|
||||||
|
import { Head } from "fresh/runtime";
|
||||||
|
import Counter from "../islands/Counter.tsx";
|
||||||
|
import { define } from "../utils.ts";
|
||||||
|
|
||||||
|
export default define.page(function Home(ctx) {
|
||||||
|
const count = useSignal(3);
|
||||||
|
|
||||||
|
console.log("Shared value " + ctx.state.shared);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="px-4 py-8 mx-auto fresh-gradient">
|
||||||
|
<Head>
|
||||||
|
<title>Fresh counter</title>
|
||||||
|
</Head>
|
||||||
|
<div class="max-w-3xl 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">Welcome to Fresh</h1>
|
||||||
|
<p class="my-4">
|
||||||
|
Try updating this message in the
|
||||||
|
<code class="mx-2">./routes/index.tsx</code> file, and refresh.
|
||||||
|
</p>
|
||||||
|
<Counter count={count} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { getCookies, setCookie } from "@std/http/cookie";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { backendSignIn } from "../backend/sign-in.ts";
|
||||||
|
import { SignInForm } from "../components/SignInForm.tsx";
|
||||||
|
import { define } from "../utils.ts";
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z.string(),
|
||||||
|
password: z.string().min(8),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const handler = define.handlers({
|
||||||
|
GET(ctx) {
|
||||||
|
const cookies = getCookies(ctx.req.headers);
|
||||||
|
const authCookie = cookies["auth"];
|
||||||
|
if (authCookie === "ok") {
|
||||||
|
const url = new URL(ctx.req.url);
|
||||||
|
url.pathname = "/dashboard";
|
||||||
|
return Response.redirect(url, 303);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
email: "",
|
||||||
|
error: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async POST(ctx) {
|
||||||
|
const formData = await ctx.req.formData();
|
||||||
|
const request = loginSchema.safeParse({
|
||||||
|
email: formData.get("email"),
|
||||||
|
password: formData.get("password"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!request.success) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
email: formData.get("email") as string,
|
||||||
|
error: request.error.issues[0].message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { email, password } = request.data;
|
||||||
|
|
||||||
|
// Super secure authorization method
|
||||||
|
if (!await backendSignIn(email, password)) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(ctx.req.url);
|
||||||
|
const headers = new Headers();
|
||||||
|
setCookie(headers, {
|
||||||
|
name: "auth",
|
||||||
|
value: "ok",
|
||||||
|
maxAge: 120,
|
||||||
|
sameSite: "Lax",
|
||||||
|
domain: url.hostname,
|
||||||
|
path: "/",
|
||||||
|
secure: true,
|
||||||
|
});
|
||||||
|
url.pathname = "/dashboard";
|
||||||
|
headers.set("location", url.toString());
|
||||||
|
return new Response(null, {
|
||||||
|
status: 303,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default define.page<typeof handler>(function SignInPage(ctx) {
|
||||||
|
const email = ctx.data.email || "";
|
||||||
|
const error = ctx.data.error;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="container mx-auto p-4 flex flex-col gap-4">
|
||||||
|
<h1>Sign in</h1>
|
||||||
|
<SignInForm email={email} error={error} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { define } from "../utils.ts";
|
||||||
|
|
||||||
|
export default define.page(function SignUpPage(ctx) {
|
||||||
|
return <div></div>;
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,19 @@
|
|||||||
|
<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.1 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
import { createDefine } from "fresh";
|
||||||
|
|
||||||
|
// This specifies the type of "ctx.state" which is used to share
|
||||||
|
// data among middlewares, layouts and routes.
|
||||||
|
export interface State {
|
||||||
|
shared: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const define = createDefine<State>();
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { fresh } from "@fresh/plugin-vite";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [fresh(), tailwindcss()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user